package net.athletic.capacitortcp;

import android.os.AsyncTask;
import android.os.Build;
import android.util.Base64;
import android.util.Log;

import com.getcapacitor.JSArray;
import com.getcapacitor.JSObject;
import com.getcapacitor.NativePlugin;
import com.getcapacitor.Plugin;
import com.getcapacitor.PluginCall;
import com.getcapacitor.PluginMethod;

import org.json.JSONException;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@NativePlugin
public class CapacitorTcp extends Plugin {
    private static final String LOG_TAG = "CapacitorTCP";
    private Map<Integer, TcpSocket> sockets = new ConcurrentHashMap<Integer, TcpSocket>();
    private int nextSocket = 0;

    @PluginMethod
    public void echo(PluginCall call) {
        String value = call.getString("value");

        JSObject ret = new JSObject();
        ret.put("value", value);
        call.success(ret);
    }

    @PluginMethod()
    public void create(PluginCall call) {
        try {
            JSObject properties = call.getObject("properties");
            TcpSocket socket = new TcpSocket(nextSocket++, properties);
            sockets.put(Integer.valueOf(socket.getSocketId()), socket);
            JSObject ret = new JSObject();
            ret.put("socketId", socket.getSocketId());
            ret.put("ipv4", socket.ipv4Address.getHostAddress());
            String ipv6 = socket.ipv6Address.getHostAddress();
            int ip6InterfaceIndex = ipv6.indexOf("%");
            if(ip6InterfaceIndex > 0) {
                ret.put("ipv6", ipv6.substring(0, ip6InterfaceIndex));
            }
            else
            {
                ret.put("ipv6", ipv6);
            }

            call.success(ret);

        } catch (Exception e) {
            call.reject("create error", e);
        }
    }

    private TcpSocket obtainSocket(int socketId) throws Exception {
        TcpSocket socket = sockets.get(Integer.valueOf(socketId));
        if (socket == null) {
            throw new Exception("No socket with socketId " + socketId);
        }
        return socket;
    }

    @PluginMethod()
    public void update(PluginCall call) {
        try {
            int socketId = call.getInt("socketId");
            JSObject properties = call.getObject("properties");
            TcpSocket socket = obtainSocket(socketId);
            socket.setProperties(properties);
            call.success();
        } catch (Exception e) {
            call.error(e.getMessage());
        }
    }

    @PluginMethod()
    public void setPaused(PluginCall call) {
        int socketId = call.getInt("socketId");
        boolean paused = call.getBoolean("paused");
        try {
            TcpSocket socket = obtainSocket(socketId);
            socket.setPaused(paused);
            call.success();
        } catch (Exception e) {
            call.error(e.getMessage());
        }
    }

    @PluginMethod()
    public void setKeepAlive(PluginCall call) {
        int socketId = call.getInt("socketId");
        boolean enable = call.getBoolean("enable");
        int delay = call.getInt("delay");
        try {
            TcpSocket socket = obtainSocket(socketId);
            socket.setKeepAlive(enable);
            call.success();
        } catch (Exception e) {
            call.error(e.getMessage());
        }
    }

    @PluginMethod()
    public void setNoDelay(PluginCall call) {
        int socketId = call.getInt("socketId");
        boolean noDelay = call.getBoolean("noDelay");
        try {
            TcpSocket socket = obtainSocket(socketId);
            socket.setNoDelay(noDelay);

            call.success();
        } catch (Exception e) {
            call.error(e.getMessage());
        }
    }

    @PluginMethod()
    public void connect(PluginCall call) {
        int socketId = call.getInt("socketId");
        String peerAddress = call.getString("peerAddress");
        int peerPort = call.getInt("peerPort");
        try {
            TcpSocket socket = obtainSocket(socketId);
            socket.connect(InetAddress.getByName(peerAddress), peerPort);

            call.success();
        } catch (Exception e) {
            call.error(e.getMessage());
        }
    }

    @PluginMethod()
    public void disconnect(PluginCall call) {
        int socketId = call.getInt("socketId");
        try {
            TcpSocket socket = obtainSocket(socketId);
            socket.disconnect();

            call.success();
        } catch (Exception e) {
            call.error(e.getMessage());
        }
    }

    @PluginMethod()
    public void secure(PluginCall call) {
        // TODO: Implement
        try {
            call.success();
        } catch (Exception e) {
            call.error(e.getMessage());
        }
    }

    @PluginMethod()
    public void send(PluginCall call) {
        int socketId = call.getInt("socketId");
        String data = call.getString("data");
        try {
            TcpSocket socket = obtainSocket(socketId);
            socket.sendMessage(data);

            call.success();
        } catch (Exception e) {
            call.error(e.getMessage());
        }
    }

    @PluginMethod()
    public void close(PluginCall call) {
        try {
            int socketId = call.getInt("socketId");
            try {
                TcpSocket socket = obtainSocket(socketId);
                socket.disconnect(); // TODO: Different from disconnect?

                call.success();
            } catch (Exception e) {
                call.error(e.getMessage());
            }
        } catch (Exception e) {
            call.error(e.getMessage());
        }
    }

    @PluginMethod()
    public void getInfo(PluginCall call) {
        try {
            int socketId = call.getInt("socketId");
            TcpSocket socket = obtainSocket(socketId);
            call.success(socket.getInfo());
        } catch (Exception e) {
            call.error(e.getMessage());
        }
    }

    @PluginMethod()
    public void getSockets(PluginCall call) {
        try {
            JSArray results = new JSArray();
            for (TcpSocket socket : sockets.values()) {
                results.put(socket.getInfo());
            }
            JSObject ret = new JSObject();
            ret.put("sockets", results);
            call.success(ret);
        } catch (Exception e) {
            call.error(e.getMessage());
        }
    }

    private InetAddress getIPAddress(boolean useIPv4) {
        try {
            List<NetworkInterface> interfaces = Collections.list(NetworkInterface.getNetworkInterfaces());
            for (NetworkInterface intf : interfaces) {
                List<InetAddress> addrs = Collections.list(intf.getInetAddresses());
                if (addrs.size() < 2) continue;
                for (InetAddress addr : addrs) {
                    if (!addr.isLoopbackAddress()) {
                        String sAddr = addr.getHostAddress();
                        //boolean isIPv4 = InetAddressUtils.isIPv4Address(sAddr);
                        boolean isIPv4 = sAddr.indexOf(':') < 0;
                        if (useIPv4) {
                            if (isIPv4)
                                return addr;
                            //return sAddr;
                        } else {
                            if (!isIPv4) {
                                int delim = sAddr.indexOf('%'); // drop ip6 zone suffix
                                return addr;
                                //return delim<0 ? sAddr.toUpperCase() : sAddr.substring(0, delim).toUpperCase();
                            }
                        }
                    }
                }
            }
        } catch (Exception ignored) {
        } // for now eat exceptions
        return InetAddress.getLoopbackAddress();
    }

    private NetworkInterface getNetworkInterface() {
        try {
            List<NetworkInterface> interfaces = Collections.list(NetworkInterface.getNetworkInterfaces());
            for (NetworkInterface intf : interfaces) {
                List<InetAddress> addrs = Collections.list(intf.getInetAddresses());
                if (addrs.size() < 2) continue;
                if (addrs.get(0).isLoopbackAddress()) continue;
                return intf;
            }
        } catch (Exception ignored) {
        } // for now eat exceptions
        return null;
    }

    // Based on http://www.myandroidsolutions.com/2013/03/31/android-tcp-connection-enhanced/#.XxhfuShKiUl
    private class TcpClient {
        InetAddress peerAddress;
        int peerPort;
        Socket client;
        // message to send to the server
        private String mServerMessage;
        // sends message received notifications
        private OnMessageReceived mMessageListener = null;
        // while this is true, the server will continue running
        private boolean mRun = false;
        // used to send messages
        private PrintWriter mBufferOut;
        // used to read messages from the server
        private BufferedReader mBufferIn;
        // Is this socket still connected?
        private boolean connected;

        TcpClient(InetAddress peerAddress, int peerPort) throws JSONException, IOException {
            this.peerAddress = peerAddress;
            this.peerPort = peerPort;
        }

        void setListener(OnMessageReceived listener) {
            mMessageListener = listener;
        }

        public void run() throws IOException {
            Log.i("TCP init socket", "run" + peerAddress.toString() + String.valueOf(peerPort));
            mRun = true;

            try {
                client = new Socket(peerAddress, peerPort);
                this.connected = true;
                Log.i("TCP init socket", "runa" + peerAddress.toString() + String.valueOf(peerPort));
                try {
                    //sends the message to the server
                    mBufferOut = new PrintWriter(new BufferedWriter(new OutputStreamWriter(client.getOutputStream())), true);

                    //receives the message which the server sends back
                    mBufferIn = new BufferedReader(new InputStreamReader(client.getInputStream()));

                    //in this while the client listens for the messages sent by the server
                    while (mRun) {

                        mServerMessage = mBufferIn.readLine();

                        if (mServerMessage != null && mMessageListener != null) {
                            //call the method messageReceived from MyActivity class
                            mMessageListener.messageReceived(mServerMessage);
                        }

                        if (mServerMessage == null) {
                            // Means that the peer has closed connection.
                            stop();
                            client.close();
                            connected = false;
                        }

                    }

                    Log.e("RESPONSE FROM SERVER", "S: Received Message: '" + mServerMessage + "'");
                } catch (Exception e) {

                    Log.e("TCP", "S: Error", e);

                } finally {
                    //the socket must be closed. It is not possible to reconnect to this socket
                    // after it is closed, which means a new socket instance has to be created.
                    client.close();
                    this.connected = false;
                }
            } catch (Exception e) {
                Log.e("TCP", "C: Error", e);
            }
        }

        public void stop() {
            mRun = false;

            if (mBufferOut != null) {
                mBufferOut.flush();
                mBufferOut.close();
            }

            mMessageListener = null;
            mBufferIn = null;
            mBufferOut = null;
            mServerMessage = null;
        }

        public void sendMessage(String message) {
            Log.i("TCP send message", message);
            if (mBufferOut != null && !mBufferOut.checkError()) {
                mBufferOut.println(message);
                mBufferOut.flush();
            }
        }
    }

    private class TcpSocket {
        private final int socketId;

        private TcpClient client;

        private boolean paused;

        private String name;
        private int bufferSize;

        private InetAddress ipv4Address;
        private InetAddress ipv6Address;

        TcpSocket(int socketId, JSObject properties) throws JSONException, IOException {
            this.socketId = socketId;
            this.ipv4Address = getIPAddress(true);
            this.ipv6Address = getIPAddress(false);

            // set socket default options
            paused = false;
            bufferSize = 4096;
            name = "";

            setProperties(properties);
        }

        int getSocketId() {
            return socketId;
        }

//        void register(Selector selector, int interestSets) throws IOException {
//            key = channel.register(selector, interestSets, this);
//        }

        void setProperties(JSObject properties) throws JSONException, SocketException {

            if (!properties.isNull("name"))
                name = properties.getString("name");

            if (!properties.isNull("bufferSize")) {
                bufferSize = properties.getInt("bufferSize");
            }
        }

        void connect(InetAddress peerAddress, int peerPort) {
            try {
                this.client = new TcpClient(peerAddress, peerPort);
                new ConnectTask(this.client).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
            } catch (Exception e) {
                Log.e("TCP", "S: Error", e);
            }
        }

        void disconnect() {
            try {
                this.client.stop();
                if (this.client.client != null) {
                    Log.i("TCP Port", String.valueOf(this.client.client.getPort()));
                    Log.i("TCP Inet Address", String.valueOf(this.client.client.getInetAddress()));
                    this.client.client.close();
                }
                // TODO: Do more?
            } catch (Exception e) {
                Log.e("TCP", "S: Error", e);
            }
        }

        void setPaused(boolean paused) {
            try {
                if (paused) {
                    this.client.stop();
                } else {
                    this.client.run();
                }
                // TODO: Do more?
            } catch (Exception e) {
                Log.e("TCP", "S: Error", e);

            }
        }

        void setKeepAlive(boolean keepAlive) {
            try {
                this.client.client.setKeepAlive(keepAlive);
            } catch (Exception e) {
                Log.e("TCP", "S: Error", e);

            }
        }

        void setNoDelay(boolean noDelay) {
            try {
                this.client.client.setTcpNoDelay(noDelay);
            } catch (Exception e) {
                Log.e("TCP", "S: Error", e);

            }
        }

        void sendMessage(String message) {
            client.sendMessage(message);
        }

        JSObject getInfo() throws JSONException {
            JSObject info = new JSObject();
            info.put("socketId", socketId);
            info.put("bufferSize", bufferSize);
            info.put("name", name);
            info.put("paused", paused);
            info.put("connected", this.client.connected);
//            if (channel.socket().getLocalAddress() != null) {
//                info.put("localAddress", channel.socket().getLocalAddress().getHostAddress());
//                info.put("localPort", channel.socket().getLocalPort());
//            }
            return info;
        }
    }

    public interface OnMessageReceived {
        public void messageReceived(String message);
    }

    public class ConnectTask extends AsyncTask<String, String, TcpClient> {
        TcpClient mTcpClient;
        ConnectTask(TcpClient mTcpClient) {
            Log.i("TCP init socket", "ConnectTask2");
            this.mTcpClient = mTcpClient;
        }
        @Override
        protected TcpClient doInBackground(String... message) {
            try {
                Log.i("TCP init socket", "ConnectTask3");
                //we create a TCPClient object
                mTcpClient.setListener(new OnMessageReceived() {
                    @Override
                    //here the messageReceived method is implemented
                    public void messageReceived(String message) {
                        Log.i("RESPONSE FROM SERVER 2", "S: Received Message: '" + message + "'");
                        //this method calls the onProgressUpdate
                        publishProgress(message);
                    }
                });
                mTcpClient.run();

                return null;
            } catch (Exception e) {
                Log.e("TCP", "S: Error", e);
            }

            return null;
        }

        @Override
        protected void onProgressUpdate(String... values) {
            super.onProgressUpdate(values);
            //response received from server
            Log.d("test", "response " + values[0]);
            //process server response here....
        }
    }
}
