/**
 * 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.cxf.jaxrs.client.spec;

import java.net.URI;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.ws.rs.ProcessingException;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.Invocation.Builder;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.Configurable;
import javax.ws.rs.core.Configuration;
import javax.ws.rs.core.Feature;
import javax.ws.rs.core.Link;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.UriBuilder;

import org.apache.cxf.common.util.ClassHelper;
import org.apache.cxf.common.util.SystemPropertyAction;
import org.apache.cxf.configuration.jsse.TLSClientParameters;
import org.apache.cxf.jaxrs.client.AbstractClient;
import org.apache.cxf.jaxrs.client.ClientConfiguration;
import org.apache.cxf.jaxrs.client.ClientProviderFactory;
import org.apache.cxf.jaxrs.client.JAXRSClientFactoryBean;
import org.apache.cxf.jaxrs.client.WebClient;
import org.apache.cxf.jaxrs.model.FilterProviderInfo;
import org.apache.cxf.message.Message;
import org.apache.cxf.transport.https.SSLUtils;

public class ClientImpl implements Client {
    static final String HTTP_CONNECTION_TIMEOUT_PROP = "http.connection.timeout";
    static final String HTTP_RECEIVE_TIMEOUT_PROP = "http.receive.timeout";
    private static final String HTTP_PROXY_SERVER_PROP = "http.proxy.server.uri";
    private static final String HTTP_PROXY_SERVER_PORT_PROP = "http.proxy.server.port";
    private static final String HTTP_AUTOREDIRECT_PROP = "http.autoredirect";
    private static final String HTTP_MAINTAIN_SESSION_PROP = "http.maintain.session";
    private static final String HTTP_RESPONSE_AUTOCLOSE_PROP = "http.response.stream.auto.close";
    private static final String THREAD_SAFE_CLIENT_PROP = "thread.safe.client";
    private static final String THREAD_SAFE_CLIENT_STATE_CLEANUP_PROP = "thread.safe.client.state.cleanup.period";
    private static final Boolean DEFAULT_THREAD_SAFETY_CLIENT_STATUS;
    private static final Integer THREAD_SAFE_CLIENT_STATE_CLEANUP_PERIOD;
    static  {
        DEFAULT_THREAD_SAFETY_CLIENT_STATUS =
            Boolean.parseBoolean(SystemPropertyAction.getPropertyOrNull(THREAD_SAFE_CLIENT_PROP));
        THREAD_SAFE_CLIENT_STATE_CLEANUP_PERIOD =
            getIntValue(SystemPropertyAction.getPropertyOrNull(THREAD_SAFE_CLIENT_STATE_CLEANUP_PROP));
    }

    private Configurable<Client> configImpl;
    private TLSConfiguration secConfig;
    private boolean closed;
    private Set<WebClient> baseClients =
        Collections.synchronizedSet(Collections.newSetFromMap(new WeakHashMap<WebClient, Boolean>()));

    public ClientImpl(Configuration config,
                      TLSConfiguration secConfig) {
        configImpl = new ClientConfigurableImpl<Client>(this, config);
        this.secConfig = secConfig;
    }

    @Override
    public void close() {
        if (!closed) {
            synchronized (baseClients) {
                for (WebClient wc : baseClients) {
                    wc.close();
                }
            }
            baseClients = null;
            closed = true;
        }

    }

    @Override
    public Builder invocation(Link link) {
        checkNull(link);
        checkClosed();
        Builder builder = target(link.getUriBuilder()).request();
        String type = link.getType();
        if (type != null) {
            builder.accept(type);
        }
        return builder;
    }

    @Override
    public WebTarget target(UriBuilder builder) {
        checkNull(builder);
        checkClosed();

        return new WebTargetImpl(builder, getConfiguration());
    }


    @Override
    public WebTarget target(String address) {
        checkNull(address);
        if (address.isEmpty()) {
            address = "/";
        }
        return target(UriBuilder.fromUri(address));
    }

    @Override
    public WebTarget target(Link link) {
        checkNull(link);
        return target(link.getUriBuilder());
    }

    @Override
    public WebTarget target(URI uri) {
        checkNull(uri);
        return target(UriBuilder.fromUri(uri));
    }

    private void checkNull(Object... target) {
        for (Object o : target) {
            if (o == null) {
                throw new NullPointerException("Value is null");
            }
        }
    }

    @Override
    public HostnameVerifier getHostnameVerifier() {
        checkClosed();
        return secConfig.getTlsClientParams().getHostnameVerifier();
    }

    @Override
    public SSLContext getSslContext() {
        checkClosed();
        if (secConfig.getSslContext() != null) {
            return secConfig.getSslContext();
        } else if (secConfig.getTlsClientParams().getTrustManagers() != null) {
            try {
                return SSLUtils.getSSLContext(secConfig.getTlsClientParams());
            } catch (Exception ex) {
                throw new ProcessingException(ex);
            }
        } else {
            return null;
        }
    }

    private void checkClosed() {
        if (closed) {
            throw new IllegalStateException("client is closed");
        }
    }

    @Override
    public Configuration getConfiguration() {
        checkClosed();
        return configImpl.getConfiguration();
    }

    @Override
    public Client property(String name, Object value) {
        checkClosed();
        return configImpl.property(name, value);
    }

    @Override
    public Client register(Class<?> cls) {
        checkClosed();
        return configImpl.register(cls);
    }

    @Override
    public Client register(Object object) {
        checkClosed();
        return configImpl.register(object);
    }

    @Override
    public Client register(Class<?> cls, int index) {
        checkClosed();
        return configImpl.register(cls, index);
    }

    @Override
    public Client register(Class<?> cls, Class<?>... contracts) {
        checkClosed();
        return configImpl.register(cls, contracts);
    }

    @Override
    public Client register(Class<?> cls, Map<Class<?>, Integer> map) {
        checkClosed();
        return configImpl.register(cls, map);
    }

    @Override
    public Client register(Object object, int index) {
        checkClosed();
        return configImpl.register(object, index);
    }

    @Override
    public Client register(Object object, Class<?>... contracts) {
        checkClosed();
        return configImpl.register(object, contracts);
    }

    @Override
    public Client register(Object object, Map<Class<?>, Integer> map) {
        checkClosed();
        return configImpl.register(object, map);
    }

    public class WebTargetImpl implements WebTarget {
        private Configurable<WebTarget> configImpl;
        private UriBuilder uriBuilder;
        private WebClient targetClient;


        public WebTargetImpl(UriBuilder uriBuilder,
                             Configuration config) {
            this(uriBuilder, config, null);
        }

        public WebTargetImpl(UriBuilder uriBuilder,
                             Configuration config,
                             WebClient targetClient) {
            this.configImpl = new ClientConfigurableImpl<WebTarget>(this, config);
            this.uriBuilder = uriBuilder.clone();
            this.targetClient = targetClient;
        }

        public WebClient getWebClient() {
            return this.targetClient;
        }

        @Override
        public Builder request() {
            checkClosed();
            Map<String, Object> configProps = getConfiguration().getProperties();

            initTargetClientIfNeeded(configProps);

            ClientProviderFactory pf =
                ClientProviderFactory.getInstance(WebClient.getConfig(targetClient).getEndpoint());
            List<Object> providers = new LinkedList<Object>();
            List<org.apache.cxf.feature.Feature> cxfFeatures =
                new LinkedList<org.apache.cxf.feature.Feature>();
            Configuration cfg = configImpl.getConfiguration();
            for (Object p : cfg.getInstances()) {
                if (p instanceof org.apache.cxf.feature.Feature) {
                    cxfFeatures.add((org.apache.cxf.feature.Feature)p);
                } else if (!(p instanceof Feature)) {
                    Map<Class<?>, Integer> contracts = cfg.getContracts(p.getClass());
                    if (contracts == null || contracts.isEmpty()) {
                        providers.add(p);
                    } else {
                        final Class<?> providerCls = ClassHelper.getRealClass(pf.getBus(), p);
                        providers.add(new FilterProviderInfo<Object>(p.getClass(),
                            providerCls, p, pf.getBus(), contracts));
                    }
                }
            }

            pf.setUserProviders(providers);
            ClientConfiguration clientCfg = WebClient.getConfig(targetClient);

            clientCfg.getRequestContext().putAll(configProps);
            clientCfg.getRequestContext().put(Client.class.getName(), ClientImpl.this);
            clientCfg.getRequestContext().put(Configuration.class.getName(),
                                                                      getConfiguration());

            // Response auto-close
            Boolean responseAutoClose = getBooleanValue(configProps.get(HTTP_RESPONSE_AUTOCLOSE_PROP));
            if (responseAutoClose != null) {
                clientCfg.getResponseContext().put("response.stream.auto.close", responseAutoClose);
            }
            // TLS
            TLSClientParameters tlsParams = secConfig.getTlsClientParams();
            if (tlsParams.getSSLSocketFactory() != null
                || tlsParams.getTrustManagers() != null
                || tlsParams.getHostnameVerifier() != null) {
                clientCfg.getHttpConduit().setTlsClientParameters(tlsParams);
            }
            // Executor for the asynchronous calls
            Object executorServiceProp = configProps.get(AbstractClient.EXECUTOR_SERVICE_PROPERTY);
            if (executorServiceProp != null) {
                clientCfg.getResponseContext().put(AbstractClient.EXECUTOR_SERVICE_PROPERTY, executorServiceProp);
            }
            setConnectionProperties(configProps, clientCfg);
            // CXF Features
            for (org.apache.cxf.feature.Feature cxfFeature : cxfFeatures) {
                cxfFeature.initialize(clientCfg, clientCfg.getBus());
            }
            // Start building the invocation
            return new InvocationBuilderImpl(WebClient.fromClient(targetClient),
                                             getConfiguration());
        }
        private void setConnectionProperties(Map<String, Object> configProps, ClientConfiguration clientCfg) {
            Long connTimeOutValue = getLongValue(configProps.get(HTTP_CONNECTION_TIMEOUT_PROP));
            if (connTimeOutValue != null) {
                clientCfg.getHttpConduit().getClient().setConnectionTimeout(connTimeOutValue);
            }
            Long recTimeOutValue = getLongValue(configProps.get(HTTP_RECEIVE_TIMEOUT_PROP));
            if (recTimeOutValue != null) {
                clientCfg.getHttpConduit().getClient().setReceiveTimeout(recTimeOutValue);
            }
            Object proxyServerValue = configProps.get(HTTP_PROXY_SERVER_PROP);
            if (proxyServerValue != null) {
                clientCfg.getHttpConduit().getClient().setProxyServer((String)proxyServerValue);
            }
            Integer proxyServerPortValue = getIntValue(configProps.get(HTTP_PROXY_SERVER_PORT_PROP));
            if (proxyServerPortValue != null) {
                clientCfg.getHttpConduit().getClient().setProxyServerPort(proxyServerPortValue);
            }
            Boolean autoRedirectValue = getBooleanValue(configProps.get(HTTP_AUTOREDIRECT_PROP));
            if (autoRedirectValue != null) {
                clientCfg.getHttpConduit().getClient().setAutoRedirect(autoRedirectValue);
            }
            Boolean mantainSessionValue = getBooleanValue(configProps.get(HTTP_MAINTAIN_SESSION_PROP));
            if (mantainSessionValue != null) {
                clientCfg.getRequestContext().put(Message.MAINTAIN_SESSION, mantainSessionValue);
            }
        }

        private void initTargetClientIfNeeded(Map<String, Object> configProps) {
            URI uri = uriBuilder.build();
            if (targetClient == null) {
                JAXRSClientFactoryBean bean = new JAXRSClientFactoryBean();
                bean.setAddress(uri.toString());
                Boolean threadSafe = getBooleanValue(configProps.get(THREAD_SAFE_CLIENT_PROP));
                if (threadSafe == null) {
                    threadSafe = DEFAULT_THREAD_SAFETY_CLIENT_STATUS;
                }
                bean.setThreadSafe(threadSafe);
                if (threadSafe) {
                    Integer cleanupPeriod = getIntValue(configProps.get(THREAD_SAFE_CLIENT_STATE_CLEANUP_PROP));
                    if (cleanupPeriod == null) {
                        cleanupPeriod = THREAD_SAFE_CLIENT_STATE_CLEANUP_PERIOD;
                    }
                    if (cleanupPeriod != null) {
                        bean.setSecondsToKeepState(cleanupPeriod);
                    }
                }
                targetClient = bean.createWebClient();
                ClientImpl.this.baseClients.add(targetClient);
            } else if (!targetClient.getCurrentURI().equals(uri)) {
                targetClient.to(uri.toString(), false);
            }
        }

        @Override
        public Builder request(String... accept) {
            return request().accept(accept);
        }

        @Override
        public Builder request(MediaType... accept) {
            return request().accept(accept);
        }

        @Override
        public URI getUri() {
            checkClosed();
            return uriBuilder.build();
        }

        @Override
        public UriBuilder getUriBuilder() {
            checkClosed();
            return uriBuilder.clone();
        }

        @Override
        public WebTarget path(String path) {
            checkClosed();
            checkNull(path);
            return newWebTarget(getUriBuilder().path(path));
        }

        @Override
        public WebTarget queryParam(String name, Object... values) {
            checkClosed();
            checkNullValues(name, values);
            UriBuilder thebuilder = getUriBuilder();
            if (values == null || values.length == 1 && values[0] == null) {
                thebuilder.replaceQueryParam(name, (Object[])null);
            } else {
                thebuilder.queryParam(name, values);
            }
            return newWebTarget(thebuilder);
        }

        @Override
        public WebTarget matrixParam(String name, Object... values) {
            checkClosed();
            checkNullValues(name, values);

            UriBuilder thebuilder = getUriBuilder();
            if (values == null || values.length == 1 && values[0] == null) {
                thebuilder.replaceMatrixParam(name, (Object[])null);
            } else {
                thebuilder.matrixParam(name, values);
            }
            return newWebTarget(thebuilder);
        }

        @Override
        public WebTarget resolveTemplate(String name, Object value) {
            return resolveTemplate(name, value, true);
        }

        @Override
        public WebTarget resolveTemplate(String name, Object value, boolean encodeSlash) {
            checkClosed();
            checkNull(name, value);
            return newWebTarget(getUriBuilder().resolveTemplate(name, value, encodeSlash));
        }

        @Override
        public WebTarget resolveTemplateFromEncoded(String name, Object value) {
            checkNull(name, value);
            return newWebTarget(getUriBuilder().resolveTemplateFromEncoded(name, value));
        }

        @Override
        public WebTarget resolveTemplates(Map<String, Object> templatesMap) {
            return resolveTemplates(templatesMap, true);
        }

        @Override
        public WebTarget resolveTemplates(Map<String, Object> templatesMap, boolean encodeSlash) {
            checkClosed();
            checkNullMap(templatesMap);

            if (templatesMap.isEmpty()) {
                return this;
            }
            return newWebTarget(getUriBuilder().resolveTemplates(templatesMap, encodeSlash));
        }

        @Override
        public WebTarget resolveTemplatesFromEncoded(Map<String, Object> templatesMap) {
            checkClosed();
            checkNullMap(templatesMap);
            if (templatesMap.isEmpty()) {
                return this;
            }
            return newWebTarget(getUriBuilder().resolveTemplatesFromEncoded(templatesMap));
        }

        private WebTarget newWebTarget(UriBuilder newBuilder) {
            WebClient newClient;
            if (targetClient != null) {
                newClient = WebClient.fromClient(targetClient);
            } else {
                newClient = null;
            }
            return new WebTargetImpl(newBuilder, getConfiguration(), newClient);
        }

        @Override
        public Configuration getConfiguration() {
            checkClosed();
            return configImpl.getConfiguration();
        }

        @Override
        public WebTarget property(String name, Object value) {
            checkClosed();
            return configImpl.property(name, value);
        }

        @Override
        public WebTarget register(Class<?> cls) {
            checkClosed();
            return configImpl.register(cls);
        }

        @Override
        public WebTarget register(Object object) {
            checkClosed();
            return configImpl.register(object);
        }

        @Override
        public WebTarget register(Class<?> cls, int index) {
            checkClosed();
            return configImpl.register(cls, index);
        }

        @Override
        public WebTarget register(Class<?> cls, Class<?>... contracts) {
            checkClosed();
            return configImpl.register(cls, contracts);
        }

        @Override
        public WebTarget register(Class<?> cls, Map<Class<?>, Integer> map) {
            checkClosed();
            return configImpl.register(cls, map);
        }

        @Override
        public WebTarget register(Object object, int index) {
            checkClosed();
            return configImpl.register(object, index);
        }

        @Override
        public WebTarget register(Object object, Class<?>... contracts) {
            checkClosed();
            return configImpl.register(object, contracts);
        }

        @Override
        public WebTarget register(Object object, Map<Class<?>, Integer> map) {
            checkClosed();
            return configImpl.register(object, map);
        }

        private void checkNullValues(Object name, Object... values) {
            checkNull(name);
            if (values != null && values.length > 1) {
                checkNull(values);
            }
        }

        private void checkNullMap(Map<String, Object> templatesMap) {
            checkNull(templatesMap);
            checkNull(templatesMap.keySet().toArray());
            checkNull(templatesMap.values().toArray());
        }
    }
    private static Long getLongValue(Object o) {
        return o instanceof Long ? (Long)o
            : o instanceof String ? Long.valueOf(o.toString())
            : o instanceof Integer ? ((Integer)o).longValue() : null;
    }
    private static Integer getIntValue(Object o) {
        return o instanceof Integer ? (Integer)o : o instanceof String ? Integer.valueOf(o.toString()) : null;
    }
    private static Boolean getBooleanValue(Object o) {
        return o instanceof Boolean ? (Boolean)o : o instanceof String ? Boolean.valueOf(o.toString()) : null;
    }
}
