/*
 * Copyright (C) 2015 Neo Visionaries Inc.
 *
 * Licensed 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 com.neovisionaries.ws.client;


import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import javax.net.SocketFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;


/**
 * Proxy settings.
 *
 * <p>
 * If a proxy server's host name is set (= if {@link #getHost()}
 * returns a non-null value), a socket factory that creates a
 * socket to communicate with the proxy server is selected based
 * on the settings of this {@code ProxySettings} instance. The
 * following is the concrete flow to select a socket factory.
 * </p>
 *
 * <blockquote>
 * <ol>
 * <li>
 *   If {@link #isSecure()} returns {@code true},
 *   <ol type="i">
 *     <li>
 *       If an {@link SSLContext} instance has been set by {@link
 *       #setSSLContext(SSLContext)}, the value returned from {@link
 *       SSLContext#getSocketFactory()} method of the instance is used.
 *     <li>
 *       Otherwise, if an {@link SSLSocketFactory} instance has been
 *       set by {@link #setSSLSocketFactory(SSLSocketFactory)}, the
 *       instance is used.
 *     <li>
 *       Otherwise, the value returned from {@link SSLSocketFactory#getDefault()}
 *       is used.
 *   </ol>
 * <li>
 *   Otherwise (= {@link #isSecure()} returns {@code false}),
 *   <ol type="i">
 *     <li>
 *       If a {@link SocketFactory} instance has been set by {@link
 *       #setSocketFactory(SocketFactory)}, the instance is used.
 *     <li>
 *       Otherwise, the value returned from {@link SocketFactory#getDefault()}
 *       is used.
 *   </ol>
 * </ol>
 * </blockquote>
 *
 * <p>
 * Note that the current implementation supports only Basic Authentication
 * for authentication at the proxy server.
 * </p>
 *
 * @see WebSocketFactory#getProxySettings()
 *
 * @since 1.3
 */
public class ProxySettings
{
    private final WebSocketFactory mWebSocketFactory;
    private final Map<String, List<String>> mHeaders;
    private final SocketFactorySettings mSocketFactorySettings;
    private boolean mSecure;
    private String mHost;
    private int mPort;
    private String mId;
    private String mPassword;


    ProxySettings(WebSocketFactory factory)
    {
        mWebSocketFactory = factory;
        mHeaders = new TreeMap<String, List<String>>(String.CASE_INSENSITIVE_ORDER);
        mSocketFactorySettings = new SocketFactorySettings();

        reset();
    }


    /**
     * Get the associated {@link WebSocketFactory} instance.
     */
    public WebSocketFactory getWebSocketFactory()
    {
        return mWebSocketFactory;
    }


    /**
     * Reset the proxy settings. To be concrete, parameter values are
     * set as shown below.
     *
     * <blockquote>
     * <table border="1" cellpadding="5" style="border-collapse: collapse;">
     *   <thead>
     *     <tr>
     *       <th>Name</th>
     *       <th>Value</th>
     *       <th>Description</th>
     *     </tr>
     *   </thead>
     *   <tbody>
     *     <tr>
     *       <td>Secure</td>
     *       <td>{@code false}</td>
     *       <td>Use TLS to connect to the proxy server or not.</td>
     *     </tr>
     *     <tr>
     *       <td>Host</td>
     *       <td>{@code null}</td>
     *       <td>The host name of the proxy server.</td>
     *     </tr>
     *     <tr>
     *       <td>Port</td>
     *       <td>{@code -1}</td>
     *       <td>The port number of the proxy server.</td>
     *     </tr>
     *     <tr>
     *       <td>ID</td>
     *       <td>{@code null}</td>
     *       <td>The ID for authentication at the proxy server.</td>
     *     </tr>
     *     <tr>
     *       <td>Password</td>
     *       <td>{@code null}</td>
     *       <td>The password for authentication at the proxy server.</td>
     *     </tr>
     *     <tr>
     *       <td>Headers</td>
     *       <td>Cleared</td>
     *       <td>Additional HTTP headers passed to the proxy server.</td>
     *     </tr>
     *   </tbody>
     * </table>
     * </blockquote>
     *
     * @return
     *         {@code this} object.
     */
    public ProxySettings reset()
    {
        mSecure   = false;
        mHost     = null;
        mPort     = -1;
        mId       = null;
        mPassword = null;
        mHeaders.clear();

        return this;
    }


    /**
     * Check whether use of TLS is enabled or disabled.
     *
     * @return
     *         {@code true} if TLS is used in the communication with
     *         the proxy server.
     */
    public boolean isSecure()
    {
        return mSecure;
    }


    /**
     * Enable or disable use of TLS.
     *
     * @param secure
     *         {@code true} to use TLS in the communication with
     *         the proxy server.
     *
     * @return
     *         {@code this} object.
     */
    public ProxySettings setSecure(boolean secure)
    {
        mSecure = secure;

        return this;
    }


    /**
     * Get the host name of the proxy server.
     *
     * <p>
     * The default value is {@code null}. If this method returns
     * a non-null value, it is used as the proxy server.
     * </p>
     *
     * @return
     *         The host name of the proxy server.
     */
    public String getHost()
    {
        return mHost;
    }


    /**
     * Set the host name of the proxy server.
     *
     * <p>
     * If a non-null value is set, it is used as the proxy server.
     * </p>
     *
     * @param host
     *         The host name of the proxy server.
     *
     * @return
     *         {@code this} object.
     */
    public ProxySettings setHost(String host)
    {
        mHost = host;

        return this;
    }


    /**
     * Get the port number of the proxy server.
     *
     * <p>
     * The default value is {@code -1}. {@code -1} means that
     * the default port number ({@code 80} for non-secure
     * connections and {@code 443} for secure connections)
     * should be used.
     * </p>
     *
     * @return
     *         The port number of the proxy server.
     */
    public int getPort()
    {
        return mPort;
    }


    /**
     * Set the port number of the proxy server.
     *
     * <p>
     * If {@code -1} is set, the default port number ({@code 80}
     * for non-secure connections and {@code 443} for secure
     * connections) is used.
     * </p>
     *
     * @param port
     *         The port number of the proxy server.
     *
     * @return
     *         {@code this} object.
     */
    public ProxySettings setPort(int port)
    {
        mPort = port;

        return this;
    }


    /**
     * Get the ID for authentication at the proxy server.
     *
     * <p>
     * The default value is {@code null}. If this method returns
     * a non-null value, it is used as the ID for authentication
     * at the proxy server. To be concrete, the value is used to
     * generate the value of <code><a href=
     * "http://tools.ietf.org/html/rfc2616#section-14.34"
     * >Proxy-Authorization</a></code> header.
     * </p>
     *
     * @return
     *         The ID for authentication at the proxy server.
     */
    public String getId()
    {
        return mId;
    }


    /**
     * Set the ID for authentication at the proxy server.
     *
     * <p>
     * If a non-null value is set, it is used as the ID for
     * authentication at the proxy server. To be concrete, the
     * value is used to generate the value of <code><a href=
     * "http://tools.ietf.org/html/rfc2616#section-14.34"
     * >Proxy-Authorization</a></code> header.
     * </p>
     *
     * @param id
     *         The ID for authentication at the proxy server.
     *
     * @return
     *         {@code this} object.
     */
    public ProxySettings setId(String id)
    {
        mId = id;

        return this;
    }


    /**
     * Get the password for authentication at the proxy server.
     *
     * @return
     *         The password for authentication at the proxy server.
     */
    public String getPassword()
    {
        return mPassword;
    }


    /**
     * Set the password for authentication at the proxy server.
     *
     * @param password
     *         The password for authentication at the proxy server.
     *
     * @return
     *         {@code this} object.
     */
    public ProxySettings setPassword(String password)
    {
        mPassword = password;

        return this;
    }


    /**
     * Set credentials for authentication at the proxy server.
     * This method is an alias of {@link #setId(String) setId}{@code
     * (id).}{@link #setPassword(String) setPassword}{@code
     * (password)}.
     *
     * @param id
     *         The ID.
     *
     * @param password
     *         The password.
     *
     * @return
     *         {@code this} object.
     */
    public ProxySettings setCredentials(String id, String password)
    {
        return setId(id).setPassword(password);
    }


    /**
     * Set the proxy server by a URI. See the description of
     * {@link #setServer(URI)} about how the parameters are updated.
     *
     * @param uri
     *         The URI of the proxy server. If {@code null} is given,
     *         none of the parameters are updated.
     *
     * @return
     *         {@code this} object.
     *
     * @throws IllegalArgumentException
     *         Failed to convert the given string to a {@link URI} instance.
     */
    public ProxySettings setServer(String uri)
    {
        if (uri == null)
        {
            return this;
        }

        return setServer(URI.create(uri));
    }


    /**
     * Set the proxy server by a URL. See the description of
     * {@link #setServer(URI)} about how the parameters are updated.
     *
     * @param url
     *         The URL of the proxy server. If {@code null} is given,
     *         none of the parameters are updated.
     *
     * @return
     *         {@code this} object.
     *
     * @throws IllegalArgumentException
     *         Failed to convert the given URL to a {@link URI} instance.
     */
    public ProxySettings setServer(URL url)
    {
        if (url == null)
        {
            return this;
        }

        try
        {
            return setServer(url.toURI());
        }
        catch (URISyntaxException e)
        {
            throw new IllegalArgumentException(e);
        }
    }


    /**
     * Set the proxy server by a URI. The parameters are updated as
     * described below.
     *
     * <blockquote>
     * <dl>
     *   <dt>Secure</dt>
     *   <dd><p>
     *     If the URI contains the scheme part and its value is
     *     either {@code "http"} or {@code "https"} (case-insensitive),
     *     the {@code secure} parameter is updated to {@code false}
     *     or to {@code true} accordingly. In other cases, the parameter
     *     is not updated.
     *   </p></dd>
     *   <dt>ID &amp; Password</dt>
     *   <dd><p>
     *     If the URI contains the userinfo part and the ID embedded
     *     in the userinfo part is not an empty string, the {@code
     *     id} parameter and the {@code password} parameter are updated
     *     accordingly. In other cases, the parameters are not updated.
     *   </p></dd>
     *   <dt>Host</dt>
     *   <dd><p>
     *     The {@code host} parameter is always updated by the given URI.
     *   </p></dd>
     *   <dt>Port</dt>
     *   <dd><p>
     *     The {@code port} parameter is always updated by the given URI.
     *   </p></dd>
     * </dl>
     * </blockquote>
     *
     * @param uri
     *         The URI of the proxy server. If {@code null} is given,
     *         none of the parameters is updated.
     *
     * @return
     *         {@code this} object.
     */
    public ProxySettings setServer(URI uri)
    {
        if (uri == null)
        {
            return this;
        }

        String scheme   = uri.getScheme();
        String userInfo = uri.getUserInfo();
        String host     = uri.getHost();
        int port        = uri.getPort();

        return setServer(scheme, userInfo, host, port);
    }


    private ProxySettings setServer(String scheme, String userInfo, String host, int port)
    {
        setByScheme(scheme);
        setByUserInfo(userInfo);
        mHost = host;
        mPort = port;

        return this;
    }


    private void setByScheme(String scheme)
    {
        if ("http".equalsIgnoreCase(scheme))
        {
            mSecure = false;
        }
        else if ("https".equalsIgnoreCase(scheme))
        {
            mSecure = true;
        }
    }


    private void setByUserInfo(String userInfo)
    {
        if (userInfo == null)
        {
            return;
        }

        String[] pair = userInfo.split(":", 2);
        String id;
        String pw;

        switch (pair.length)
        {
            case 2:
                id = pair[0];
                pw = pair[1];
                break;

            case 1:
                id = pair[0];
                pw = null;
                break;

            default:
                return;
        }

        if (id.length() == 0)
        {
            return;
        }

        mId       = id;
        mPassword = pw;
    }


    /**
     * Get additional HTTP headers passed to the proxy server.
     *
     * @return
     *         Additional HTTP headers passed to the proxy server.
     *         The comparator of the returned map is {@link
     *         String#CASE_INSENSITIVE_ORDER}.
     */
    public Map<String, List<String>> getHeaders()
    {
        return mHeaders;
    }


    /**
     * Add an additional HTTP header passed to the proxy server.
     *
     * @param name
     *         The name of an HTTP header (case-insensitive).
     *         If {@code null} or an empty string is given,
     *         nothing is added.
     *
     * @param value
     *         The value of the HTTP header.
     *
     * @return
     *         {@code this} object.
     */
    public ProxySettings addHeader(String name, String value)
    {
        if (name == null || name.length() == 0)
        {
            return this;
        }

        List<String> list = mHeaders.get(name);

        if (list == null)
        {
            list = new ArrayList<String>();
            mHeaders.put(name, list);
        }

        list.add(value);

        return this;
    }


    /**
     * Get the socket factory that has been set by {@link
     * #setSocketFactory(SocketFactory)}.
     *
     * @return
     *         The socket factory.
     */
    public SocketFactory getSocketFactory()
    {
        return mSocketFactorySettings.getSocketFactory();
    }


    /**
     * Set a socket factory.
     *
     * @param factory
     *         A socket factory.
     *
     * @return
     *         {@code this} instance.
     */
    public ProxySettings setSocketFactory(SocketFactory factory)
    {
        mSocketFactorySettings.setSocketFactory(factory);

        return this;
    }


    /**
     * Get the SSL socket factory that has been set by {@link
     * #setSSLSocketFactory(SSLSocketFactory)}.
     *
     * @return
     *         The SSL socket factory.
     */
    public SSLSocketFactory getSSLSocketFactory()
    {
        return mSocketFactorySettings.getSSLSocketFactory();
    }


    /**
     * Set an SSL socket factory.
     *
     * @param factory
     *         An SSL socket factory.
     *
     * @return
     *         {@code this} instance.
     */
    public ProxySettings setSSLSocketFactory(SSLSocketFactory factory)
    {
        mSocketFactorySettings.setSSLSocketFactory(factory);

        return this;
    }


    /**
     * Get the SSL context that has been set by {@link #setSSLContext(SSLContext)}.
     *
     * @return
     *         The SSL context.
     */
    public SSLContext getSSLContext()
    {
        return mSocketFactorySettings.getSSLContext();
    }


    /**
     * Set an SSL context to get a socket factory.
     *
     * @param context
     *         An SSL context.
     *
     * @return
     *         {@code this} instance.
     */
    public ProxySettings setSSLContext(SSLContext context)
    {
        mSocketFactorySettings.setSSLContext(context);

        return this;
    }


    SocketFactory selectSocketFactory()
    {
        return mSocketFactorySettings.selectSocketFactory(mSecure);
    }
}