package com.stonem.sockets;

import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.WritableMap;

import android.util.Log;
import android.os.AsyncTask;
import android.support.annotation.Nullable;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.InetSocketAddress;
import java.net.Socket;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;

/**
 * Created by David Stoneham on 2017-08-03.
 */

public class SocketClient {
    public Socket clientSocket;


    private final String eTag = "REACT-NATIVE-SOCKETS";
    private final String event_closed = "socketClient_closed";
    private final String event_data = "socketClient_data";
    private final String event_error = "socketClient_error";
    private final String event_timeout = "socketClient_timeout";
    private final String event_connect = "socketClient_connected";
    private int timeout;
    private String dstAddress;
    private int dstPort;
    private ReactContext mReactContext;
    private boolean isOpen = false;
    private boolean reconnectOnClose = false;
    private int reconnectDelay = 500;
    private int maxReconnectAttempts = -1;
    private int reconnectAttempts = 0;
    private boolean userDidClose = false;
    private boolean isFirstConnect = true;
    private BufferedInputStream bufferedInput;
    private boolean readingStream = false;
    private final byte EOT = 0x04;

    SocketClient(ReadableMap params, ReactContext reactContext) {
        //String addr, int port, boolean autoReconnect
        mReactContext = reactContext;
        dstAddress = params.getString("address");
        dstPort = params.getInt("port");
        if (params.hasKey("timeout")) {
            timeout = params.getInt("timeout");
        } else {
            timeout = 60000;
        }
        if (params.hasKey("reconnect")) {
            reconnectOnClose = params.getBoolean("reconnect");
        }
        if (params.hasKey("maxReconnectAttempts")) {
            maxReconnectAttempts = params.getInt("maxReconnectAttempts");
        }
        if (params.hasKey("reconnectDelay")) {
            reconnectDelay = params.getInt("reconnectDelay");
        }

        Thread socketClientThread = new Thread(new SocketClientThread());
        socketClientThread.start();
    }

    public void disconnect(boolean wasUser) {
        try {
            if (clientSocket != null) {
                userDidClose = wasUser;
                isOpen = false;
                clientSocket.close();
                clientSocket = null;
                Log.d(eTag, "client closed");
            }
        } catch (IOException e) {
            handleIOException(e);
        }
    }

    private void sendEvent(ReactContext reactContext, String eventName, @Nullable WritableMap params) {
        reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(eventName, params);
    }

    protected void write(String message) {
        new AsyncTask<String, Void, Void>() {
            @Override
            protected void onPreExecute() {
                super.onPreExecute();
            }

            @Override
            protected Void doInBackground(String... params) {
                try {
                    String message = params[0];
                    OutputStream outputStream = clientSocket.getOutputStream();
                    PrintStream printStream = new PrintStream(outputStream);
                    printStream.print(message + (char) EOT);
                    printStream.flush();
                    //debug log
                    Log.d(eTag, "client sent message: " + message);
                } catch (IOException e) {
                    handleIOException(e);
                }
                return null;
            }

            protected void onPostExecute(Void dummy) {
            }
        }.execute(message);
    }

    public void onDestroy() {
        if (clientSocket != null) {
            try {
                clientSocket.close();
            } catch (IOException e) {
                Log.e(eTag, "Client Destroy IOException", e);
            }
        }
    }

    private class SocketClientThread extends Thread {
        @Override
        public void run() {
            while (isFirstConnect || (!userDidClose && reconnectOnClose)) {
                try {
                    if (connectSocket()) {
                        watchIncoming();
                        reconnectAttempts = 0;
                    } else {
                        reconnectAttempts++;
                    }
                    isFirstConnect = false;
                    if (maxReconnectAttempts == -1 || maxReconnectAttempts < reconnectAttempts) {
                        Thread.sleep(reconnectDelay);
                    } else {
                        reconnectOnClose = false;
                    }
                } catch (InterruptedException e) {
                    //debug log
                    Log.e(eTag, "Client InterruptedException", e);
                }
            }
        }
    }

    private boolean connectSocket() {
        try {
            int connectionTimeout = 1000;
            clientSocket = new Socket();
            clientSocket.connect(new InetSocketAddress(dstAddress, dstPort), connectionTimeout);
            clientSocket.setSoTimeout(timeout);
            isOpen = true;

            WritableMap eventParams = Arguments.createMap();
            sendEvent(mReactContext, event_connect, eventParams);
            return true;
        } catch (UnknownHostException e) {
            handleUnknownHostException(e);
        } catch (IOException e) {
            handleIOException(e);
        }
        return false;
    }

    private void watchIncoming() {
        try {
            String data = "";
            InputStream inputStream = clientSocket.getInputStream();
            while (isOpen) {
                int incomingByte = inputStream.read();

                if (incomingByte == -1) {
                    //debug log
                    Log.v(eTag, "Client disconnected");
                    isOpen = false;
                    //emit event
                    WritableMap eventParams = Arguments.createMap();
                    eventParams.putString("data", data);
                    sendEvent(mReactContext, event_closed, eventParams);
                } else if (incomingByte == EOT) {
                    //debug log
                    Log.d(eTag, "client received message: " + data);
                    //emit event
                    WritableMap eventParams = Arguments.createMap();
                    eventParams.putString("data", data);
                    sendEvent(mReactContext, event_data, eventParams);
                    //clear incoming
                    data = "";
                } else {
                    data += (char) incomingByte;
                }
            }
        } catch (SocketTimeoutException e) {
            handleSocketTimeoutException(e);
        } catch (IOException e) {
            handleIOException(e);
        }
    }

    private void handleIOException(IOException e) {
        //debug log
        Log.e(eTag, "Client IOException", e);
        //emit event
        String message = e.getMessage();
        WritableMap eventParams = Arguments.createMap();
        eventParams.putString("error", message);
        if (message.equals("Socket closed")) {
            isOpen = false;
            sendEvent(mReactContext, event_closed, eventParams);
        } else {
            sendEvent(mReactContext, event_error, eventParams);
        }
    }

    private void handleUnknownHostException(UnknownHostException e) {
        //debug log
        Log.e(eTag, "Client UnknownHostException", e);
        //emit event
        String message = e.getMessage();
        WritableMap eventParams = Arguments.createMap();
        eventParams.putString("error", e.getMessage());
        sendEvent(mReactContext, event_error, eventParams);
    }

    private void handleSocketTimeoutException(SocketTimeoutException e) {
        //debug log
        Log.e(eTag, "Client SocketTimeoutException", e);
        //emit event
        String message = e.getMessage();
        WritableMap eventParams = Arguments.createMap();
        eventParams.putString("error", e.getMessage());
        sendEvent(mReactContext, event_timeout, eventParams);
    }

}