
package tv.twitch.sdk;

//import com.crashlytics.android.Crashlytics;

import org.java_websocket.WebSocket;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.drafts.Draft;
import org.java_websocket.drafts.Draft_17;
import org.java_websocket.exceptions.WebsocketNotConnectedException;
import org.java_websocket.framing.Framedata;
import org.java_websocket.handshake.ServerHandshake;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.channels.NotYetConnectedException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.Map;
import java.util.Queue;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;

import tv.twitch.CoreErrorCode;
import tv.twitch.ErrorCode;
import tv.twitch.IWebSocket;
import tv.twitch.ResultContainer;
import tv.twitch.WebSocketMessageType;

import tv.twitch.twiglib.Logger;

/**
 * Adapted by loohill on 05/03/18
 * Last sync: 05/03/18
 * Source: twitch-apps/twitch-android/blob/master/Twitch/src/main/java/tv/twitch/android/sdk/SDKWebSocketHandler.java
 * Changes:
 *** Removed all codes for test and logger;
 *** Added our own logger;
 *** Commented Crashlytics;
 */

public class SDKWebSocketHandler implements IWebSocket {

    private final static String TAG = "Twitch_WebSocket";
    private final static int WEBSOCKET_CONNECT_TIMEOUT = 10000;

    private class SocketClient extends WebSocketClient {

        private StringBuffer mFragmentBuffer;

        public SocketClient(URI serverUri, Draft draft, Map<String, String> headers, int timeout) {
            super(serverUri, draft, headers, timeout);

            if (mPendingMessages != null) {
                synchronized (mPendingMessages) {
                    mPendingMessages.clear();
                }
            }
        }

        @Override
        public void onOpen(ServerHandshake handshakedata) { }

        @Override
        public void onMessage(String message) {
            if (mPendingMessages != null) {
                synchronized (mPendingMessages) {
                    mPendingMessages.add(message.getBytes());
                }
            }
        }

        @Override
        public void onFragment(Framedata frame) {
            if (frame == null || frame.getOpcode() == null || frame.getPayloadData() == null) {
                return;
            }

            // First frame
            if (frame.getOpcode() != Framedata.Opcode.CONTINUOUS) {
                mFragmentBuffer = new StringBuffer();
            }

            // somehow got a continuous frame without a starting frame
            if (mFragmentBuffer == null) {
                String error = "websocket received continuous frame without a starting frame, shouldn't happen";
                Logger.e(TAG, error);
//                Crashlytics.logException(new Exception(error));
                return;
            }

            // append frame to buffer
            mFragmentBuffer.append(new String(frame.getPayloadData().array()));

            // If last frame, report message
            if (frame.isFin()) {
                onMessage(mFragmentBuffer.toString());
                mFragmentBuffer = null;
            }
        }

        @Override
        public void onClose(int code, String reason, boolean remote) { }

        @Override
        public void onError(Exception ex) {
            Logger.e(TAG, "websocket ERRORED: " + ex.getMessage());
        }
    }

    private SocketClient mClient;
    private URI mUri;
    protected Queue<byte[]> mPendingMessages;

    public SDKWebSocketHandler(String url) {
        mPendingMessages = new LinkedList<>();

        try {
            mUri = new URI(url);
        } catch (URISyntaxException e) {
            Logger.e(TAG, "websocket Uri syntax exception for: " + url);
        }
    }

    @Override
    public ErrorCode connect() {
        if (connected()) {
            return CoreErrorCode.TTV_EC_SOCKET_EALREADY;
        }

        if (mUri == null) {
            return CoreErrorCode.TTV_EC_SOCKET_CONNECT_FAILED;
        }

        Logger.d(TAG, "websocket connect requested: " + mUri);

        // socket clients can't be reused, so reinstantiate for every connect

        if (mUri.getScheme().contains("wss")) {
            mClient = new SocketClient(mUri, new Draft_17(), null, WEBSOCKET_CONNECT_TIMEOUT);
            SSLContext context;
            try {
                context = SSLContext.getInstance("TLS");
                context.init(null, null, null);
            } catch (NoSuchAlgorithmException | KeyManagementException e) {
                Logger.e(TAG, "websocket error initializing SSLContext");
                return CoreErrorCode.TTV_EC_SOCKET_CONNECT_FAILED;
            }

            try {
                SSLSocketFactory factory = context.getSocketFactory();
                mClient.setSocket(factory.createSocket());
            } catch (IOException e) {
                Logger.e(TAG, "websocket error creating ssl socket");
                return CoreErrorCode.TTV_EC_SOCKET_CONNECT_FAILED;
            }

        } else {

            mClient = new SocketClient(mUri, new Draft_17(), null, WEBSOCKET_CONNECT_TIMEOUT);
        }

        try {
            if (mClient.connectBlocking()) {
                Logger.d(TAG, "websocket connected: " + mUri);
                return CoreErrorCode.TTV_EC_SUCCESS;
            }
        } catch (InterruptedException e) {
            Logger.e(TAG, "websocket interrupted exception connecting");
        }

        return CoreErrorCode.TTV_EC_SOCKET_CONNECT_FAILED;
    }

    @Override
    public ErrorCode disconnect() {
        Logger.d(TAG, "websocket disconnect requested: " + mUri);
        if (mClient != null && mClient.getReadyState() != WebSocket.READYSTATE.CLOSING && mClient.getReadyState() != WebSocket.READYSTATE.CLOSED) {
            mClient.close();
        }

        return CoreErrorCode.TTV_EC_SUCCESS;
    }

    @Override
    public ErrorCode send(WebSocketMessageType type, byte[] buffer, int length) {
        if (!connected()) {
            return CoreErrorCode.TTV_EC_SOCKET_ENOTCONN;
        }

        if (type != WebSocketMessageType.Text) {
            Logger.e(TAG, "websocket only supporting text message types");
            return CoreErrorCode.TTV_EC_UNIMPLEMENTED;
        }

        // convert buffer of length to a string and send the string
        String s = new String(Arrays.copyOfRange(buffer, 0, length));

        try {
            Logger.d(TAG, "websocket send: " + mUri + " " + s);
            mClient.send(s);
            return CoreErrorCode.TTV_EC_SUCCESS;
        } catch (NotYetConnectedException|WebsocketNotConnectedException e) {
            Logger.e(TAG, "websocket not yet connected yet exception");
        } catch (Exception e) {
            Logger.e(TAG, "websocket send exploded");
//            Crashlytics.logException(e);
        }

        disconnect();
        return CoreErrorCode.TTV_EC_SOCKET_SEND_ERROR;
    }

    @Override
    public ErrorCode recv(ResultContainer<WebSocketMessageType> type, byte[] buffer, int length, ResultContainer<Integer> received) {
        if (!connected()) {
            return CoreErrorCode.TTV_EC_SOCKET_ENOTCONN;
        }

        if (mClient != null && mPendingMessages != null) {
            synchronized (mPendingMessages) {
                byte[] s = mPendingMessages.peek();

                if (s == null) {
                    return CoreErrorCode.TTV_EC_SOCKET_EWOULDBLOCK;
                }

                if (length < s.length) {
                    return CoreErrorCode.TTV_EC_INVALID_BUFFER;
                } else {
                    type.result = WebSocketMessageType.Text;
                    received.result = s.length;
                    System.arraycopy(s, 0, buffer, 0, length);
                    mPendingMessages.remove();
                    return CoreErrorCode.TTV_EC_SUCCESS;
                }
            }
        } else {
            return CoreErrorCode.TTV_EC_SOCKET_EWOULDBLOCK;
        }

    }

    @Override
    public ErrorCode peek(ResultContainer<WebSocketMessageType> type, ResultContainer<Integer> length) {
        length.result = 0;
        type.result = WebSocketMessageType.None;

        if (mClient != null && mPendingMessages != null) {
            synchronized (mPendingMessages) {
                byte[] s = mPendingMessages.peek();
                if (s != null) {
                    length.result = s.length;
                    type.result = WebSocketMessageType.Text;
                } else {
                    return CoreErrorCode.TTV_EC_SOCKET_EWOULDBLOCK;
                }
            }
        }

        return CoreErrorCode.TTV_EC_SUCCESS;
    }

    @Override
    public boolean connected() {
        return (mClient != null && mClient.getReadyState() == WebSocket.READYSTATE.OPEN);
    }
}
