/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 *
 */
package org.apache.polygene.library.rest.client.api;

import java.lang.reflect.ParameterizedType;
import java.util.HashMap;
import java.util.Map;
import org.apache.polygene.api.injection.scope.Uses;
import org.apache.polygene.api.util.Classes;
import org.apache.polygene.library.rest.client.spi.NullResponseHandler;
import org.apache.polygene.library.rest.client.spi.ResponseHandler;
import org.apache.polygene.library.rest.client.spi.ResultHandler;
import org.apache.polygene.library.rest.common.Resource;
import org.apache.polygene.library.rest.common.link.Link;
import org.apache.polygene.library.rest.common.link.LinksUtil;
import org.restlet.Request;
import org.restlet.Response;
import org.restlet.data.ChallengeResponse;
import org.restlet.data.ChallengeScheme;
import org.restlet.data.Method;
import org.restlet.data.Reference;
import org.restlet.data.Status;
import org.restlet.representation.EmptyRepresentation;
import org.restlet.resource.ResourceException;
import org.restlet.security.User;

/**
 * Client-side context resources
 */
public class ContextResourceClient
{
    @Uses
    private ContextResourceClientFactory contextResourceFactory;

    @Uses
    private Reference reference;

    private Resource resource;

    // Response handlers
    private ResponseHandler errorHandler = NullResponseHandler.INSTANCE;
    private ResponseHandler resourceHandler = NullResponseHandler.INSTANCE;
    private ResponseHandler deleteHandler = NullResponseHandler.INSTANCE;
    private Map<String, ResponseHandler> queryHandlers = new HashMap<>();
    private Map<String, ResponseHandler> commandHandlers = new HashMap<>();
    private Map<String, ResponseHandler> processingErrorHandlers = new HashMap<>();

    // DSL for registering rules
    public ContextResourceClient onError(ResponseHandler handler)
    {
        errorHandler = handler;
        return this;
    }

    public <T> ContextResourceClient onResource( final ResultHandler<T> handler)
    {
        resourceHandler = new ResponseHandler()
        {
            @Override
            public HandlerCommand handleResponse( Response response, ContextResourceClient client )
            {
                final Class<T> resultType = (Class<T>) Classes.RAW_CLASS.apply(( (ParameterizedType) handler.getClass().getGenericInterfaces()[ 0 ] ).getActualTypeArguments()[0]);
                T result = contextResourceFactory.readResponse( response, resultType );

                if (result instanceof Resource)
                {
                    resource = (Resource) result;
                }

                return handler.handleResult( result, client );
            }
        };
        return this;
    }

    public ContextResourceClient onQuery( String relation, ResponseHandler handler )
    {
        queryHandlers.put( relation, handler );
        return this;
    }

    public <T> ContextResourceClient onQuery( String relation, final ResultHandler<T> handler
    )
    {
        final Class<T> resultType = (Class<T>) Classes.RAW_CLASS.apply(( (ParameterizedType) handler.getClass().getGenericInterfaces()[ 0 ] ).getActualTypeArguments()[0]);

        queryHandlers.put( relation,  new ResponseHandler()
        {
            @Override
            public HandlerCommand handleResponse( Response response, ContextResourceClient client )
            {
                T result = contextResourceFactory.readResponse( response, resultType );
                return handler.handleResult( result, client );
            }
        });

        return this;
    }

    public ContextResourceClient onCommand( String relation, ResponseHandler handler )
    {
        commandHandlers.put( relation, handler);
        return this;
    }

    public <T> ContextResourceClient onCommand( String relation, final ResultHandler<T> handler )
    {
        final Class<T> resultType = (Class<T>) Classes.RAW_CLASS.apply(( (ParameterizedType) handler.getClass().getGenericInterfaces()[ 0 ] ).getActualTypeArguments()[0]);

        commandHandlers.put( relation,  new ResponseHandler()
        {
            @Override
            public HandlerCommand handleResponse( Response response, ContextResourceClient client )
            {
                T result = contextResourceFactory.readResponse( response, resultType );
                return handler.handleResult( result, client );
            }
        });

        return this;
    }

    public ContextResourceClient onProcessingError( String relation, ResponseHandler handler )
    {
        processingErrorHandlers.put( relation, handler);
        return this;
    }

    public <T> ContextResourceClient onProcessingError( String relation, final ResultHandler<T> handler)
    {
        final Class<T> resultType = (Class<T>) Classes.RAW_CLASS.apply(( (ParameterizedType) handler.getClass().getGenericInterfaces()[ 0 ] ).getActualTypeArguments()[0]);

        processingErrorHandlers.put( relation,  new ResponseHandler()
        {
            @Override
            public HandlerCommand handleResponse( Response response, ContextResourceClient client )
            {
                T result = contextResourceFactory.readResponse( response, resultType );
                return handler.handleResult( result, client );
            }
        });

        return this;
    }

    public ContextResourceClient onDelete(ResponseHandler handler)
    {
        deleteHandler = handler;
        return this;
    }

    public ContextResourceClientFactory getContextResourceClientFactory()
    {
        return contextResourceFactory;
    }

    public Reference getReference()
    {
        return reference;
    }

    public Resource getResource()
    {
        return resource;
    }

    public void start()
    {
        HandlerCommand command = refresh();
        while (command != null)
            command = command.execute( this );
    }

    // Callable from HandlerCommand
    HandlerCommand refresh()
    {
        if (resourceHandler == null)
            throw new IllegalStateException( "No handler set for resources" );

        return invokeQuery( reference, null, resourceHandler, null );
    }

    HandlerCommand query( String relation, Object queryRequest, ResponseHandler handler, ResponseHandler processingErrorHandler )
    {
        return query( resource.query( relation ), queryRequest, handler, processingErrorHandler );
    }

    HandlerCommand query( Link link, Object queryRequest, ResponseHandler handler, ResponseHandler processingErrorHandler )
    {
        if (handler == null)
            handler = queryHandlers.get( link.rel().get() );

        if (handler == null)
            throw new IllegalArgumentException( "No handler set for relation "+link.rel().get() );

        if (processingErrorHandler == null)
            processingErrorHandler = processingErrorHandlers.get( link.rel().get() );

        Reference linkRef = new Reference(link.href().get());
        if (linkRef.isRelative())
            linkRef = new Reference( reference.toUri().toString() + link.href().get() );
        return invokeQuery( linkRef, queryRequest, handler, processingErrorHandler );
    }

    private HandlerCommand invokeQuery( Reference ref, Object queryRequest, ResponseHandler resourceHandler, ResponseHandler processingErrorHandler )
    {
        Request request = new Request( Method.GET, ref );

        if( queryRequest != null )
        {
            contextResourceFactory.writeRequest( request, queryRequest );
        }

        contextResourceFactory.updateQueryRequest( request );

        User user = request.getClientInfo().getUser();
        if ( user != null)
            request.setChallengeResponse( new ChallengeResponse( ChallengeScheme.HTTP_BASIC, user.getName(), user.getSecret() ) );

        Response response = new Response( request );

        contextResourceFactory.getClient().handle( request, response );

        if( response.getStatus().isSuccess() )
        {
            contextResourceFactory.updateCache( response );

            return resourceHandler.handleResponse( response, this );
        } else if (response.getStatus().isRedirection())
        {
            Reference redirectedTo = response.getLocationRef();
            return invokeQuery( redirectedTo, queryRequest, resourceHandler, processingErrorHandler );
        } else
        {
            if (response.getStatus().equals(Status.CLIENT_ERROR_UNPROCESSABLE_ENTITY) && processingErrorHandler != null)
            {
                return processingErrorHandler.handleResponse( response, this );
            } else
            {
                // TODO This needs to be expanded to allow custom handling of all the various cases
                return errorHandler.handleResponse( response, this );
            }
        }
    }

    // Commands
    HandlerCommand command( Link link, Object commandRequest, ResponseHandler handler, ResponseHandler processingErrorHandler )
    {
        if (handler == null)
            handler = commandHandlers.get( link.rel().get() );

        if (processingErrorHandler == null)
            processingErrorHandler = processingErrorHandlers.get( link.rel().get() );

        // Check if we should do POST or PUT
        Method method;
        if( LinksUtil.withClass( "idempotent" ).test( link ) )
        {
            method = Method.PUT;
        }
        else
        {
            method = Method.POST;
        }

        Reference ref = new Reference( reference.toUri().toString() + link.href().get() );
        return invokeCommand( ref, method, commandRequest, handler, processingErrorHandler );
    }

    private HandlerCommand invokeCommand( Reference ref, Method method, Object requestObject, ResponseHandler responseHandler, ResponseHandler processingErrorHandler )
    {
        Request request = new Request( method, ref );

        if (requestObject == null)
            requestObject = new EmptyRepresentation();

        contextResourceFactory.writeRequest( request, requestObject );

        contextResourceFactory.updateCommandRequest( request );

        User user = request.getClientInfo().getUser();
        if ( user != null)
            request.setChallengeResponse( new ChallengeResponse( ChallengeScheme.HTTP_BASIC, user.getName(), user.getSecret() ) );

        Response response = new Response( request );
        contextResourceFactory.getClient().handle( request, response );

        try
        {
            if( response.getStatus().isSuccess() )
            {
                contextResourceFactory.updateCache( response );

                if (responseHandler != null)
                    return responseHandler.handleResponse( response, this );
            }
            else
            {
                if (response.getStatus().equals(Status.CLIENT_ERROR_UNPROCESSABLE_ENTITY) && processingErrorHandler != null)
                {
                    return processingErrorHandler.handleResponse( response, this );
                } else
                {
                    // TODO This needs to be expanded to allow custom handling of all the various cases
                    return errorHandler.handleResponse( response, this );
                }
            }

            return null; // No handler found
        }
        finally
        {
            try
            {
                response.getEntity().exhaust();
            }
            catch( Throwable e )
            {
                // Ignore
            }
        }
    }

    // Delete
    public HandlerCommand delete( ResponseHandler responseHandler, ResponseHandler processingErrorHandler )
        throws ResourceException
    {
        if (responseHandler == null)
            responseHandler = deleteHandler;

        Request request = new Request( Method.DELETE, new Reference( reference.toUri() ).toString() );
        contextResourceFactory.updateCommandRequest( request );

        int tries = 3;
        while( true )
        {
            Response response = new Response( request );
            try
            {
                contextResourceFactory.getClient().handle( request, response );
                if( !response.getStatus().isSuccess() )
                {
                    return errorHandler.handleResponse( response, this );
                }
                else
                {
                    // Reset modification date
                    contextResourceFactory.updateCache( response );

                    return responseHandler.handleResponse( response, this );
                }
            }
            catch( ResourceException e )
            {
                if( e.getStatus().equals( Status.CONNECTOR_ERROR_COMMUNICATION ) ||
                    e.getStatus().equals( Status.CONNECTOR_ERROR_CONNECTION ) )
                {
                    if( tries == 0 )
                    {
                        throw e; // Give up
                    }
                    else
                    {
                        // Try again
                        tries--;
                        continue;
                    }
                }
                else
                {
                    // Abort
                    throw e;
                }
            }
            finally
            {
                try
                {
                    response.getEntity().exhaust();
                }
                catch( Throwable e )
                {
                    // Ignore
                }
            }
        }
    }

    // Browse to other resources
    public synchronized ContextResourceClient newClient( Link link )
    {
        if( link == null )
        {
            throw new NullPointerException( "No link specified" );
        }

        return newClient( link.href().get() );
    }

    public synchronized ContextResourceClient newClient( String relativePath )
    {
        if( relativePath.startsWith( "http://" ) )
        {
            return contextResourceFactory.newClient( new Reference( relativePath ) );
        }

        Reference reference = this.reference.clone();
        if( relativePath.startsWith( "/" ) )
        {
            reference.setPath( relativePath );
        }
        else
        {
            reference.setPath( reference.getPath() + relativePath );
            reference = reference.normalize();
        }

        return contextResourceFactory.newClient( reference );
    }

    @Override
    public String toString()
    {
        return reference.toString();
    }
}