/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2010-2013 Oracle and/or its affiliates. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License.  You can
 * obtain a copy of the License at
 * http://glassfish.java.net/public/CDDL+GPL_1_1.html
 * or packager/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at packager/legal/LICENSE.txt.
 *
 * GPL Classpath Exception:
 * Oracle designates this particular file as subject to the "Classpath"
 * exception as provided by Oracle in the GPL Version 2 section of the License
 * file that accompanied this code.
 *
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 *
 * Contributor(s):
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */
package com.sun.jersey.api.client;

import com.sun.jersey.api.client.config.ClientConfig;
import com.sun.jersey.api.client.config.DefaultClientConfig;
import com.sun.jersey.api.client.filter.ClientFilter;
import com.sun.jersey.api.client.filter.Filterable;
import com.sun.jersey.client.impl.CopyOnWriteHashMap;
import com.sun.jersey.client.proxy.ViewProxy;
import com.sun.jersey.client.proxy.ViewProxyProvider;
import com.sun.jersey.client.urlconnection.URLConnectionClientHandler;
import com.sun.jersey.core.spi.component.ComponentContext;
import com.sun.jersey.core.spi.component.ComponentInjector;
import com.sun.jersey.core.spi.component.ComponentScope;
import com.sun.jersey.core.spi.component.ProviderFactory;
import com.sun.jersey.core.spi.component.ProviderServices;
import com.sun.jersey.core.spi.component.ioc.IoCComponentProcessor;
import com.sun.jersey.core.spi.component.ioc.IoCComponentProcessorFactory;
import com.sun.jersey.core.spi.component.ioc.IoCComponentProcessorFactoryInitializer;
import com.sun.jersey.core.spi.component.ioc.IoCComponentProviderFactory;
import com.sun.jersey.core.spi.component.ioc.IoCProviderFactory;
import com.sun.jersey.core.spi.factory.ContextResolverFactory;
import com.sun.jersey.core.spi.factory.InjectableProviderFactory;
import com.sun.jersey.core.spi.factory.MessageBodyFactory;
import com.sun.jersey.core.util.FeaturesAndProperties;
import com.sun.jersey.core.util.LazyVal;
import com.sun.jersey.spi.MessageBodyWorkers;
import com.sun.jersey.spi.inject.ClientSide;
import com.sun.jersey.spi.inject.Errors;
import com.sun.jersey.spi.inject.Injectable;
import com.sun.jersey.spi.inject.InjectableProvider;
import com.sun.jersey.spi.inject.SingletonTypeInjectableProvider;
import com.sun.jersey.spi.service.ServiceFinder;

import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.ext.ContextResolver;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.MessageBodyReader;
import javax.ws.rs.ext.MessageBodyWriter;
import javax.ws.rs.ext.Providers;
import java.lang.annotation.Annotation;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.net.HttpURLConnection;
import java.net.URI;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * The main class for creating {@link WebResource} instances and configuring
 * the properties of connections and requests.
 * <p>
 * {@link ClientFilter} instances may be added to the client for filtering
 * requests and responses, including those of {@link WebResource} instances
 * created from the client.
 * <p>
 * A client may be configured by passing a {@link ClientConfig} instance to
 * the appropriate constructor.
 * <p>
 * Methods to create instances of {@link WebResource} are thread-safe. Methods
 * that modify configuration and or filters are not guaranteed to be
 * thread-safe.
 * <p>
 * The creation of a <code>Client</code> instance is an expensive operation and
 * the instance may make use of and retain many resources. It is therefore
 * recommended that a <code>Client</code> instance is reused for the creation of
 * {@link WebResource} instances that require the same configuration settings.
 * <p>
 * A client may integrate with an IoC framework by passing a
 * {@link IoCComponentProviderFactory} instance to the appropriate constructor.
 *
 * @author Paul.Sandoz@Sun.Com
 */
public class Client extends Filterable implements ClientHandler {

    private static final Logger LOGGER = Logger.getLogger(Client.class.getName());

    private ProviderFactory componentProviderFactory;

    private Providers providers;

    private boolean destroyed = false;

    private LazyVal<ExecutorService> executorService;

    private CopyOnWriteHashMap<String, Object> properties;

    private Set<ViewProxyProvider> vpps;

    private MessageBodyFactory workers;

    private static class ContextInjectableProvider<T> extends
            SingletonTypeInjectableProvider<Context, T> {

        ContextInjectableProvider(Type type, T instance) {
            super(type, instance);
        }
    }

    /**
     * Create a new client instance.
     *
     */
    public Client() {
        this(createDefaultClientHander(), new DefaultClientConfig(), null);
    }

    /**
     * Create a new client instance.
     *
     * @param root the root client handler for dispatching a request and
     *        returning a response.
     */
    public Client(ClientHandler root) {
        this(root, new DefaultClientConfig(), null);
    }

    /**
     * Create a new client instance with a client configuration.
     *
     * @param root the root client handler for dispatching a request and
     *        returning a response.
     * @param config the client configuration.
     */
    public Client(ClientHandler root, ClientConfig config) {
        this(root, config, null);
    }

    /**
     * Create a new instance with a client configuration and a
     * component provider.
     *
     * @param root the root client handler for dispatching a request and
     *        returning a response.
     * @param config the client configuration.
     * @param provider the IoC component provider factory.
     */
    public Client(final ClientHandler root, final ClientConfig config,
            final IoCComponentProviderFactory provider) {
        // Defer instantiation of root to component provider
        super(root);

        Errors.processWithErrors(new Errors.Closure<Void>() {
            @Override
            public Void f() {
                Errors.setReportMissingDependentFieldOrMethod(false);
                init(root, config, provider);
                return null;
            }
        });
    }

    private void init(ClientHandler root, ClientConfig config,
            IoCComponentProviderFactory provider) {

        final Object threadpoolSize = config.getProperties().get(ClientConfig.PROPERTY_THREADPOOL_SIZE);

        this.executorService = new LazyVal<ExecutorService>() {
                @Override
                protected ExecutorService instance() {
                    if(threadpoolSize != null && threadpoolSize instanceof Integer && (Integer)threadpoolSize > 0) {
                        return Executors.newFixedThreadPool((Integer) threadpoolSize);
                    } else {
                        return Executors.newCachedThreadPool();
                    }
                }
            };

        Class<?>[] components = ServiceFinder.find("jersey-client-components").toClassArray();
        if (components.length > 0) {
            if (LOGGER.isLoggable(Level.INFO)) {
                StringBuilder b = new StringBuilder();
                b.append("Adding the following classes declared in META-INF/services/jersey-client-components to the client configuration:");
                for (Class c : components)
                        b.append('\n').append("  ").append(c);
                LOGGER.log(Level.INFO, b.toString());
            }

            config = new ComponentsClientConfig(config, components);
        }

        final InjectableProviderFactory injectableFactory = new InjectableProviderFactory();

        getProperties().putAll(config.getProperties());

        if (provider != null) {
            if (provider instanceof IoCComponentProcessorFactoryInitializer) {
                IoCComponentProcessorFactoryInitializer i = (IoCComponentProcessorFactoryInitializer)provider;
                i.init(new ComponentProcessorFactoryImpl(injectableFactory));
            }
        }

        // Set up the component provider factory
        this.componentProviderFactory = (provider == null)
                ? new ProviderFactory(injectableFactory)
                : new IoCProviderFactory(injectableFactory, provider);

        ProviderServices providerServices = new ProviderServices(
                ClientSide.class,
                this.componentProviderFactory,
                config.getClasses(),
                config.getSingletons());

        // Get the set of WebResourceProxy
        vpps = providerServices.getServices(ViewProxyProvider.class);

        // Allow injection of features and properties
        injectableFactory.add(new ContextInjectableProvider<FeaturesAndProperties>(
                FeaturesAndProperties.class, config));

        // Allow injection of client config
        injectableFactory.add(new ContextInjectableProvider<ClientConfig>(
                ClientConfig.class, config));

        // Allow injection of client
        injectableFactory.add(new ContextInjectableProvider<Client>(
                Client.class, this));

        injectableFactory.configure(providerServices);

        // Obtain all context resolvers
        final ContextResolverFactory crf = new ContextResolverFactory();

        // Obtain all message body readers/writers
        final MessageBodyFactory bodyContext = new MessageBodyFactory(providerServices,
                config.getFeature(FeaturesAndProperties.FEATURE_PRE_1_4_PROVIDER_PRECEDENCE));
        workers = bodyContext;
        // Allow injection of message body context
        injectableFactory.add(new ContextInjectableProvider<MessageBodyWorkers>(
                MessageBodyWorkers.class, bodyContext));

        // Injection of Providers
        this.providers = new Providers() {
            @Override
            public <T> MessageBodyReader<T> getMessageBodyReader(Class<T> c, Type t,
                    Annotation[] as, MediaType m) {
                return bodyContext.getMessageBodyReader(c, t, as, m);
            }

            @Override
            public <T> MessageBodyWriter<T> getMessageBodyWriter(Class<T> c, Type t,
                    Annotation[] as, MediaType m) {
                return bodyContext.getMessageBodyWriter(c, t, as, m);
            }

            @Override
            public <T extends Throwable> ExceptionMapper<T> getExceptionMapper(Class<T> c) {
                throw new IllegalArgumentException("This method is not supported on the client side");
            }

            @Override
            public <T> ContextResolver<T> getContextResolver(Class<T> ct, MediaType m) {
                return crf.resolve(ct, m);
            }
        };
        injectableFactory.add(
                new ContextInjectableProvider<Providers>(
                Providers.class, this.providers));

        injectableFactory.add(new InjectableProvider<Context, Type>() {
            @Override
            public ComponentScope getScope() {
                return ComponentScope.Singleton;
            }

            @Override
            public Injectable<Injectable> getInjectable(ComponentContext ic, Context a, Type c) {
                if (c instanceof ParameterizedType) {
                    ParameterizedType pt = (ParameterizedType)c;
                    if (pt.getRawType() == Injectable.class) {
                        if (pt.getActualTypeArguments().length == 1) {
                            final Injectable<?> i = injectableFactory.getInjectable(
                                    a.annotationType(),
                                    ic,
                                    a,
                                    pt.getActualTypeArguments()[0],
                                    ComponentScope.PERREQUEST_UNDEFINED_SINGLETON);
                            if (i == null)
                                return null;
                            return new Injectable<Injectable>() {
                                @Override
                                public Injectable getValue() {
                                    return i;
                                }
                            };
                        }
                    }
                }

                return null;
            }
        });

        // Initiate context resolvers
        crf.init(providerServices, injectableFactory);

        // Initiate message body readers/writers
        bodyContext.init();


        // Inject on all components
        Errors.setReportMissingDependentFieldOrMethod(true);
        componentProviderFactory.injectOnAllComponents();
        componentProviderFactory.injectOnProviderInstances(config.getSingletons());
        componentProviderFactory.injectOnProviderInstance(root);
    }

    private class ComponentProcessorFactoryImpl implements IoCComponentProcessorFactory {
        private final InjectableProviderFactory injectableFactory;

        ComponentProcessorFactoryImpl(InjectableProviderFactory injectableFactory) {
            this.injectableFactory = injectableFactory;
        }

        @Override
        public ComponentScope getScope(Class c) {
            return ComponentScope.Singleton;
        }

        @Override
        public IoCComponentProcessor get(Class c, ComponentScope scope) {
            final ComponentInjector ci = new ComponentInjector(injectableFactory, c);
            return new IoCComponentProcessor() {

                @Override
                public void preConstruct() {
                }

                @Override
                public void postConstruct(Object o) {
                    ci.inject(o);
                }
            };
        }
    }

    /**
     * Destroy the client. Any system resources associated with the client
     * will be cleaned up.
     * <p>
     * This method must be called when there are not responses pending otherwise
     * undefined behavior will occur.
     * <p>
     * The client must not be reused after this method is called otherwise
     * undefined behavior will occur.
     */
    public void destroy() {
        if (!destroyed) {
            componentProviderFactory.destroy();
            destroyed = true;
        }
    }

    /**
     * Defer to {@link #destroy() }
     */
    @Override
    @SuppressWarnings("FinalizeDeclaration")
    protected void finalize() throws Throwable {
        destroy();
        super.finalize();
    }

    /**
     * Get the {@link Providers} utilized by the client.
     *
     * @return the {@link Providers} utilized by the client.
     */
    public Providers getProviders() {
        return providers;
    }

    /**
     * Get the {@link MessageBodyWorkers} utilized by the client.
     *
     * @return the {@link MessageBodyWorkers} utilized by the client.
     */
    public MessageBodyWorkers getMessageBodyWorkers() {
        return workers;
    }

    /**
     * Create a Web resource from the client.
     *
     * @param u the URI of the resource.
     * @return the Web resource.
     */
    public WebResource resource(String u) {
        return resource(URI.create(u));
    }

    /**
     * Create a Web resource from the client.
     *
     * @param u the URI of the resource.
     * @return the Web resource.
     */
    public WebResource resource(URI u) {
        return new WebResource(this, this.properties, u);
    }

    /**
     * Create an asynchronous Web resource from the client.
     *
     * @param u the URI of the resource.
     * @return the Web resource.
     */
    public AsyncWebResource asyncResource(String u) {
        return asyncResource(URI.create(u));
    }

    /**
     * Create an asynchronous Web resource from the client.
     *
     * @param u the URI of the resource.
     * @return the Web resource.
     */
    public AsyncWebResource asyncResource(URI u) {
        return new AsyncWebResource(this, this.properties, u);
    }

    public ViewResource viewResource(String u) {
        return viewResource(URI.create(u));
    }

    public ViewResource viewResource(URI u) {
        return new ViewResource(this, u);
    }

    public AsyncViewResource asyncViewResource(String u) {
        return asyncViewResource(URI.create(u));
    }

    public AsyncViewResource asyncViewResource(URI u) {
        return new AsyncViewResource(this, u);
    }

    public <T> T view(String u, Class<T> type) {
        ViewResource vr = viewResource(u);
        return vr.get(type);
    }

    public <T> T view(URI uri, Class<T> type) {
        ViewResource vr = viewResource(uri);
        return vr.get(type);
    }

    public <T> T view(String u, T t) {
        ViewResource vr = viewResource(u);
        return vr.get(t);
    }

    public <T> T view(URI uri, T t) {
        ViewResource vr = viewResource(uri);
        return vr.get(t);
    }

    public <T> Future<T> asyncView(String u, Class<T> type) {
        AsyncViewResource vr = asyncViewResource(u);
        return vr.get(type);
    }

    public <T> Future<T> asyncView(URI uri, Class<T> type) {
        AsyncViewResource vr = asyncViewResource(uri);
        return vr.get(type);
    }

    public <T> Future<T> asyncView(String u, T t) {
        AsyncViewResource vr = asyncViewResource(u);
        return vr.get(t);
    }

    public <T> Future<T> asyncView(URI uri, T t) {
        AsyncViewResource vr = asyncViewResource(uri);
        return vr.get(t);
    }

    public <T> T view(Class<T> c, ClientResponse response) {
        return getViewProxy(c).view(c, response);
    }

    public <T> T view(T t, ClientResponse response) {
        return getViewProxy((Class<T>)t.getClass()).view(t, response);
    }

    public <T> ViewProxy<T> getViewProxy(Class<T> c) {
        for (ViewProxyProvider vpp : vpps) {
            ViewProxy<T> vp = vpp.proxy(this, c);
            if (vp != null) {
                return vp;
            }
        }
        throw new IllegalArgumentException("A view proxy is not " +
                "available for the class '" + c.getName() + "'");
    }

    /**
     * Set the {@link ExecutorService} for sending asynchronous
     * HTTP requests when no underlying asynchronous HTTP implementation is
     * utilized.
     *
     * @param es the {@link ExecutorService}.
     * @since 1.4
     */
    public void setExecutorService(ExecutorService es) {
        if (es == null)
            throw new IllegalArgumentException("ExecutorService service MUST not be null");

        this.executorService.set(es);
    }

    /**
     * Get the {@link ExecutorService} for sending asynchronous
     * HTTP requests when no underlying asynchronous HTTP implementation is
     * utilized.
     * <p>
     * By default the implementation returned
     * from {@link Executors#newCachedThreadPool() } is utilized.
     *
     * @return the {@link ExecutorService}.
     * @since 1.4
     */
    public ExecutorService getExecutorService() {
        return executorService.get();
    }

    /**
     * Get the mutable property bag.
     *
     * @return the property bag.
     */
    public Map<String, Object> getProperties() {
        if (properties == null)
            properties = new CopyOnWriteHashMap<String, Object>();

        return properties;
    }

    /**
     * Set if redirection should be performed or not.
     *
     * This method is the functional equivalent to setting the property
     * {@link ClientConfig#PROPERTY_FOLLOW_REDIRECTS} on the property bag
     * returned from {@link #getProperties}
     *
     * @param redirect if true then the client will automatically redirect
     *        to the URI declared in 3xx responses.
     */
    public void setFollowRedirects(Boolean redirect) {
        getProperties().put(ClientConfig.PROPERTY_FOLLOW_REDIRECTS, redirect);
    }

    /**
     * Set the read timeout interval, in milliseconds.
     *
     * This method is the functional equivalent to setting the property
     * {@link ClientConfig#PROPERTY_READ_TIMEOUT} on the property bag
     * returned from {@link #getProperties}
     *
     * @param interval the read timeout interval. If null or 0 then
     * an interval of infinity is declared.
     */
    public void setReadTimeout(Integer interval) {
        getProperties().put(ClientConfig.PROPERTY_READ_TIMEOUT, interval);
    }

    /**
     * Set the connect timeout interval, in milliseconds.
     *
     * This method is the functional equivalent to setting the property
     * {@link ClientConfig#PROPERTY_CONNECT_TIMEOUT} on the property bag
     * returned from {@link #getProperties}
     *
     * @param interval the connect timeout interval. If null or 0 then
     * an interval of infinity is declared.
     */
    public void setConnectTimeout(Integer interval) {
        getProperties().put(ClientConfig.PROPERTY_CONNECT_TIMEOUT, interval);
    }

    /**
     * Set the client to send request entities using chunked encoding
     * with a particular chunk size.
     *
     * This method is the functional equivalent to setting the property
     * {@link ClientConfig#PROPERTY_CHUNKED_ENCODING_SIZE} on the property bag
     * returned from {@link #getProperties}
     *
     * @param chunkSize the chunked encoding size. If &lt= 0 then the default
     *        size will be used. If null then chunked encoding will not be
     *        utilized.
     */
    public void setChunkedEncodingSize(Integer chunkSize) {
        getProperties().put(ClientConfig.PROPERTY_CHUNKED_ENCODING_SIZE, chunkSize);
    }

    // ClientHandler

    @Override
    public ClientResponse handle(final ClientRequest request) throws ClientHandlerException {
        Map<String, Object> effectiveProperties =
                new HashMap<String, Object>(properties);
        effectiveProperties.put(Client.class.getName(), this);
        effectiveProperties.putAll(request.getProperties());
        request.setProperties(effectiveProperties);

        final ClientResponse response = getHeadHandler().handle(request);

        response.getProperties().put(Client.class.getName(), this);
        return response;
    }

    /**
     * Inject client-side bindings on an instance.
     *
     * @param o the instance to inject on.
     */
    public void inject(Object o) {
        componentProviderFactory.injectOnProviderInstance(o);
    }

    /**
     * Create a default client.
     *
     * @return a default client.
     */
    public static Client create() {
        return new Client(createDefaultClientHander());
    }

    /**
     * Create a default client with client configuration.
     *
     * @param cc the client configuration.
     * @return a default client.
     */
    public static Client create(ClientConfig cc) {
        return new Client(createDefaultClientHander(), cc);
    }

    /**
     * Create a default client with client configuration and component provider.
     *
     * @param cc the client configuration.
     * @param provider the IoC component provider factory.
     * @return a default client.
     */
    public static Client create(ClientConfig cc, IoCComponentProviderFactory provider) {
        return new Client(createDefaultClientHander(), cc, provider);
    }

    /**
     * Create a default client handler.
     * <p>
     * This implementation returns a client handler implementation that
     * utilizes {@link HttpURLConnection}.
     *
     * @return a default client handler.
     */
    private static ClientHandler createDefaultClientHander() {
        return new URLConnectionClientHandler();
    }
}