//
//  ========================================================================
//  Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd.
//  ------------------------------------------------------------------------
//  All rights reserved. This program and the accompanying materials
//  are made available under the terms of the Eclipse Public License v1.0
//  and Apache License v2.0 which accompanies this distribution.
//
//      The Eclipse Public License is available at
//      http://www.eclipse.org/legal/epl-v10.html
//
//      The Apache License v2.0 is available at
//      http://www.opensource.org/licenses/apache2.0.php
//
//  You may elect to redistribute this code under either of these licenses.
//  ========================================================================
//

package org.eclipse.jetty.websocket;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ProtocolException;
import java.net.SocketAddress;
import java.net.URI;
import java.nio.channels.ByteChannel;
import java.nio.channels.SocketChannel;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.log.Logger;

import android.text.TextUtils;


/* ------------------------------------------------------------ */
/**
 * <p>{@link WebSocketClient} allows to create multiple connections to multiple destinations
 * that can speak the websocket protocol.</p>
 * <p>When creating websocket connections, {@link WebSocketClient} accepts a {@link WebSocket}
 * object (to receive events from the server), and returns a {@link WebSocket.Connection} to
 * send data to the server.</p>
 * <p>Example usage is as follows:</p>
 * <pre>
 *   WebSocketClientFactory factory = new WebSocketClientFactory();
 *   factory.start();
 *
 *   WebSocketClient client = factory.newWebSocketClient();
 *   // Configure the client
 *
 *   WebSocket.Connection connection = client.open(new URI("ws://127.0.0.1:8080/"), new WebSocket.OnTextMessage()
 *   {
 *     public void onOpen(Connection connection)
 *     {
 *       // open notification
 *     }
 *
 *     public void onClose(int closeCode, String message)
 *     {
 *       // close notification
 *     }
 *
 *     public void onMessage(String data)
 *     {
 *       // handle incoming message
 *     }
 *   }).get(5, TimeUnit.SECONDS);
 *
 *   connection.sendMessage("Hello World");
 * </pre>
 *
 * modified by KNOWLEDGECODE
 */
public class WebSocketClient
{
    private final static Logger __log = org.eclipse.jetty.util.log.Log.getLogger(WebSocketClient.class.getName());

    private final WebSocketClientFactory _factory;
    private final Map<String,String> _cookies=new ConcurrentHashMap<String, String>();
    private final List<Extension> _extensions=new CopyOnWriteArrayList<Extension>();
    private String _origin;
    private String _protocol;
    private String _agent;
    private int _maxIdleTime=-1;
    private int _maxTextMessageSize=-1;
    private int _maxBinaryMessageSize=-1;
    private MaskGen _maskGen;
    private SocketAddress _bindAddress;

    /* ------------------------------------------------------------ */
    /**
     * <p>Creates a WebSocketClient with shared WebSocketClientFactory.</p>
     *
     * @param factory the shared {@link WebSocketClientFactory}
     */
    public WebSocketClient(WebSocketClientFactory factory)
    {
        _factory=factory;
        _maskGen=_factory.getMaskGen();
    }

    /* ------------------------------------------------------------ */
    /**
     * @return The WebSocketClientFactory this client was created with.
     */
    public WebSocketClientFactory getFactory()
    {
        return _factory;
    }

    /* ------------------------------------------------------------ */
    /**
     * @return The maxIdleTime in ms for connections opened by this client,
     * or -1 if the default from {@link WebSocketClientFactory#getSelectorManager()} is used.
     * @see #setMaxIdleTime(int)
     */
    public int getMaxIdleTime()
    {
        return _maxIdleTime;
    }

    /* ------------------------------------------------------------ */
    /**
     * @param maxIdleTime The max idle time in ms for connections opened by this client
     * @see #getMaxIdleTime()
     */
    public void setMaxIdleTime(int maxIdleTime)
    {
        _maxIdleTime=maxIdleTime;
    }

    /* ------------------------------------------------------------ */
    /**
     * @return The subprotocol string for connections opened by this client.
     * @see #setProtocol(String)
     */
    public String getProtocol()
    {
        return _protocol;
    }

    /* ------------------------------------------------------------ */
    /**
     * @param protocol The subprotocol string for connections opened by this client.
     * @see #getProtocol()
     */
    public void setProtocol(String protocol)
    {
        if (!TextUtils.isEmpty(protocol))
        {
            for (String p : protocol.split(", *"))
            {
                if (!p.matches("^[0-9A-Za-z!#\\$%&'\\*\\+\\-\\.\\^_`|~]+$"))
                {
                    IllegalArgumentException e = new IllegalArgumentException("Bad protocol: " + p);
                    __log.warn(e);
                    throw e;
                }
            }
        }
        _protocol = protocol == null ? "" : protocol;
    }

    /* ------------------------------------------------------------ */
    /**
     * @return The origin URI of the client
     * @see #setOrigin(String)
     */
    public String getOrigin()
    {
        return _origin;
    }

    /* ------------------------------------------------------------ */
    /**
     * @param origin The origin URI of the client (eg "http://example.com")
     * @see #getOrigin()
     */
    public void setOrigin(String origin)
    {
        if (!TextUtils.isEmpty(origin))
        {
            if (!origin.matches("^[\\x21-\\x7E]+$"))
            {
                IllegalArgumentException e = new IllegalArgumentException("Bad Origin: " + origin);
                __log.warn(e);
                throw e;
            }
        }
        _origin = origin == null ? "" : origin;
    }

    /* ------------------------------------------------------------ */
    /**
     * <p>Returns the map of the cookies that are sent during the initial HTTP handshake
     * that upgrades to the websocket protocol.</p>
     * @return The read-write cookie map
     */
    public Map<String,String> getCookies()
    {
        return _cookies;
    }

    /* ------------------------------------------------------------ */
    /**
     * @return The list of websocket protocol extensions
     */
    public List<Extension> getExtensions()
    {
        return _extensions;
    }

    /* ------------------------------------------------------------ */
    /**
     * @return the mask generator to use, or null if not mask generator should be used
     * @see #setMaskGen(MaskGen)
     */
    public MaskGen getMaskGen()
    {
        return _maskGen;
    }

    /* ------------------------------------------------------------ */
    /**
     * @param maskGen the mask generator to use, or null if not mask generator should be used
     * @see #getMaskGen()
     */
    public void setMaskGen(MaskGen maskGen)
    {
        _maskGen = maskGen;
    }

    /* ------------------------------------------------------------ */
    /**
     * @return The initial maximum text message size (in characters) for a connection
     */
    public int getMaxTextMessageSize()
    {
        return _maxTextMessageSize;
    }

    /* ------------------------------------------------------------ */
    /**
     * Set the initial maximum text message size for a connection. This can be changed by
     * the application calling {@link WebSocket.Connection#setMaxTextMessageSize(int)}.
     * @param maxTextMessageSize The default maximum text message size (in characters) for a connection
     */
    public void setMaxTextMessageSize(int maxTextMessageSize)
    {
        _maxTextMessageSize = maxTextMessageSize;
    }

    /* ------------------------------------------------------------ */
    /**
     * @return The initial maximum binary message size (in bytes)  for a connection
     */
    public int getMaxBinaryMessageSize()
    {
        return _maxBinaryMessageSize;
    }

    /* ------------------------------------------------------------ */
    /**
     * Set the initial maximum binary message size for a connection. This can be changed by
     * the application calling {@link WebSocket.Connection#setMaxBinaryMessageSize(int)}.
     * @param maxBinaryMessageSize The default maximum binary message size (in bytes) for a connection
     */
    public void setMaxBinaryMessageSize(int maxBinaryMessageSize)
    {
        _maxBinaryMessageSize = maxBinaryMessageSize;
    }

    /* ------------------------------------------------------------ */
    /**
     * @return user-agent
     */
    public String getAgent()
    {
        return _agent;
    }

    /* ------------------------------------------------------------ */
    /**
     * @param user-agent
     */
    public void setAgent(String _agent)
    {
        if (!TextUtils.isEmpty(_agent))
        {
            if (!_agent.matches("^[\\x20-\\x7E]+$"))
            {
                IllegalArgumentException e = new IllegalArgumentException("Bad UA: " + _agent);
                __log.warn(e);
                throw e;
            }
        }
        this._agent = _agent == null ? "" : _agent;
    }

    /* ------------------------------------------------------------ */
    /**
     * <p>Opens a websocket connection to the URI and blocks until the connection is accepted or there is an error.</p>
     *
     * @param uri The URI to connect to.
     * @param websocket The {@link WebSocket} instance to handle incoming events.
     * @param maxConnectTime The interval to wait for a successful connection
     * @param units the units of the maxConnectTime
     * @return A {@link WebSocket.Connection}
     * @throws IOException if the connection fails
     * @throws InterruptedException if the thread is interrupted
     * @throws TimeoutException if the timeout elapses before the connection is completed
     * @see #open(URI, WebSocket)
     */
    public WebSocket.Connection open(URI uri, WebSocket websocket,long maxConnectTime,TimeUnit units) throws IOException, InterruptedException, TimeoutException
    {
        try
        {
            return open(uri,websocket).get(maxConnectTime,units);
        }
        catch (ExecutionException e)
        {
            Throwable cause = e.getCause();
            if (cause instanceof IOException)
                throw (IOException)cause;
            if (cause instanceof Error)
                throw (Error)cause;
            if (cause instanceof RuntimeException)
                throw (RuntimeException)cause;
            throw new RuntimeException(cause);
        }
    }

    /* ------------------------------------------------------------ */
    /**
     * <p>Asynchronously opens a websocket connection and returns a {@link Future} to obtain the connection.</p>
     * <p>The caller must call {@link Future#get(long, TimeUnit)} if they wish to impose a connect timeout on the open.</p>
     *
     * @param uri The URI to connect to.
     * @param websocket The {@link WebSocket} instance to handle incoming events.
     * @return A {@link Future} to the {@link WebSocket.Connection}
     * @throws IOException if the connection fails
     * @see #open(URI, WebSocket, long, TimeUnit)
     */
    public Future<WebSocket.Connection> open(URI uri, WebSocket websocket) throws IOException
    {
        if (!_factory.isStarted())
            throw new IllegalStateException("Factory !started");

        InetSocketAddress address = toSocketAddress(uri);

        SocketChannel channel = null;
        try
        {
            channel = SocketChannel.open();
            if (_bindAddress != null)
                channel.socket().bind(_bindAddress);
            channel.socket().setTcpNoDelay(true);

            WebSocketFuture holder = new WebSocketFuture(websocket,uri,this,channel);

            channel.configureBlocking(false);
            channel.connect(address);
            _factory.getSelectorManager().register(channel,holder);

            return holder;
        }
        catch (RuntimeException e)
        {
            // close the channel (prevent connection leak)
            IO.close(channel);

            // rethrow
            throw e;
        }
        catch(IOException e)
        {
            // close the channel (prevent connection leak)
            IO.close(channel);

            // rethrow
            throw e;
        }
    }

    public static InetSocketAddress toSocketAddress(URI uri)
    {
        String scheme = uri.getScheme();
        if (!("ws".equalsIgnoreCase(scheme) || "wss".equalsIgnoreCase(scheme)))
            throw new IllegalArgumentException("Bad WebSocket scheme: " + scheme);
        int port = uri.getPort();
        if (port == 0)
            throw new IllegalArgumentException("Bad WebSocket port: " + port);
        if (port < 0)
            port = "ws".equals(scheme) ? 80 : 443;

        return new InetSocketAddress(uri.getHost(), port);
    }

    /* ------------------------------------------------------------ */
    /** The Future Websocket Connection.
     */
    static class WebSocketFuture implements Future<WebSocket.Connection>
    {
        final WebSocket _websocket;
        final URI _uri;
        final WebSocketClient _client;
        final CountDownLatch _done = new CountDownLatch(1);
        ByteChannel _channel;
        WebSocketConnection _connection;
        Throwable _exception;

        private WebSocketFuture(WebSocket websocket, URI uri, WebSocketClient client, ByteChannel channel)
        {
            _websocket=websocket;
            _uri=uri;
            _client=client;
            _channel=channel;
        }

        public void onConnection(WebSocketConnection connection)
        {
            try
            {
                _client.getFactory().addConnection(connection);

                connection.getConnection().setMaxTextMessageSize(_client.getMaxTextMessageSize());
                connection.getConnection().setMaxBinaryMessageSize(_client.getMaxBinaryMessageSize());

                WebSocketConnection con;
                synchronized (this)
                {
                    if (_channel!=null)
                        _connection=connection;
                    con=_connection;
                }

                if (con!=null)
                {
                    if (_websocket instanceof WebSocket.OnFrame)
                        ((WebSocket.OnFrame)_websocket).onHandshake((WebSocket.FrameConnection)con.getConnection());

                    _websocket.onOpen(con.getConnection());
                }
            }
            finally
            {
                _done.countDown();
            }
        }

        public void handshakeFailed(Throwable ex)
        {
            try
            {
                ByteChannel channel=null;
                synchronized (this)
                {
                    if (_channel!=null)
                    {
                        channel=_channel;
                        _channel=null;
                        _exception=ex;
                    }
                }

                if (channel!=null)
                {
                    if (ex instanceof ProtocolException)
                        closeChannel(channel,WebSocketConnectionRFC6455.CLOSE_PROTOCOL,ex.getMessage());
                    else
                        closeChannel(channel,WebSocketConnectionRFC6455.CLOSE_NO_CLOSE,ex.getMessage());
                }
            }
            finally
            {
                _done.countDown();
            }
        }

        public Map<String,String> getCookies()
        {
            return _client.getCookies();
        }

        public String getProtocol()
        {
            return _client.getProtocol();
        }

        public WebSocket getWebSocket()
        {
            return _websocket;
        }

        public URI getURI()
        {
            return _uri;
        }

        public int getMaxIdleTime()
        {
            return _client.getMaxIdleTime();
        }

        public String getOrigin()
        {
            return _client.getOrigin();
        }

        public MaskGen getMaskGen()
        {
            return _client.getMaskGen();
        }

        public String getAgent()
        {
            return _client.getAgent();
        }

        public List<Extension> getExtensions() {
            return _client.getExtensions();
        }

        @Override
        public String toString()
        {
            return "[" + _uri + ","+_websocket+"]@"+hashCode();
        }

        public boolean cancel(boolean mayInterruptIfRunning)
        {
            try
            {
                ByteChannel channel=null;
                synchronized (this)
                {
                    if (_connection==null && _exception==null && _channel!=null)
                    {
                        channel=_channel;
                        _channel=null;
                    }
                }

                if (channel!=null)
                {
                    closeChannel(channel,WebSocketConnectionRFC6455.CLOSE_NO_CLOSE,"cancelled");
                    return true;
                }
                return false;
            }
            finally
            {
                _done.countDown();
            }
        }

        public boolean isCancelled()
        {
            synchronized (this)
            {
                return _channel==null && _connection==null;
            }
        }

        public boolean isDone()
        {
            synchronized (this)
            {
                return _connection!=null && _exception==null;
            }
        }

        public org.eclipse.jetty.websocket.WebSocket.Connection get() throws InterruptedException, ExecutionException
        {
            try
            {
                return get(Long.MAX_VALUE,TimeUnit.SECONDS);
            }
            catch(TimeoutException e)
            {
                throw new IllegalStateException("The universe has ended",e);
            }
        }

        public org.eclipse.jetty.websocket.WebSocket.Connection get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException,
                TimeoutException
        {
            _done.await(timeout,unit);
            ByteChannel channel=null;
            org.eclipse.jetty.websocket.WebSocket.Connection connection=null;
            Throwable exception;
            synchronized (this)
            {
                exception=_exception;
                if (_connection==null)
                {
                    exception=_exception;
                    channel=_channel;
                    _channel=null;
                }
                else
                    connection=_connection.getConnection();
            }

            if (channel!=null)
                closeChannel(channel,WebSocketConnectionRFC6455.CLOSE_NO_CLOSE,"timeout");
            if (exception!=null)
                throw new ExecutionException(exception);
            if (connection!=null)
                return connection;
            throw new TimeoutException();
        }

        private void closeChannel(ByteChannel channel,int code, String message)
        {
            try
            {
                _websocket.onClose(code,message);
            }
            catch(Exception e)
            {
                __log.warn(e);
            }

            try
            {
                channel.close();
            }
            catch(IOException e)
            {
                __log.debug(e);
            }
        }
    }
}
