/*
 * Part of Phonk http://www.phonk.io
 * A prototyping platform for Android devices
 *
 * Copyright (C) 2013 - 2017 Victor Diaz Barrales @victordiaz (Protocoder)
 * Copyright (C) 2017 - Victor Diaz Barrales @victordiaz (Phonk)
 *
 * Phonk is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Phonk is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Phonk. If not, see <http://www.gnu.org/licenses/>.
 *
 */

package io.phonk.runner.apprunner.api.network;

import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.util.Log;

import org.mozilla.javascript.NativeArray;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Set;

import io.phonk.runner.apidoc.annotation.PhonkClass;
import io.phonk.runner.apidoc.annotation.PhonkMethod;
import io.phonk.runner.apidoc.annotation.PhonkMethodParam;
import io.phonk.runner.apprunner.AppRunner;
import io.phonk.runner.apprunner.api.ProtoBase;
import io.phonk.runner.apprunner.api.common.ReturnInterface;
import io.phonk.runner.apprunner.api.common.ReturnObject;
import io.phonk.runner.apprunner.api.other.WhatIsRunningInterface;
import io.phonk.runner.base.utils.MLog;

@PhonkClass
public class PBluetoothClient extends ProtoBase implements WhatIsRunningInterface {
    private final PBluetooth mPBluetooth;
    private ReturnInterface mCallbackConnected;
    private ReturnInterface mCallbackData;

    private final static int DISCONNECTED = 0;
    private final static int CONNECTING = 1;
    private final static int CONNECTED = 2;

    private BluetoothDevice mDevice;

    private int status;


    public PBluetoothClient(PBluetooth pBluetooth, AppRunner appRunner) {
        super(appRunner);
        mPBluetooth = pBluetooth;
        status = DISCONNECTED;
    }

    public PBluetoothClient onConnected(ReturnInterface callbackOnConnected) {
        mCallbackConnected = callbackOnConnected;
        return this;
    }

    public void onNewData(ReturnInterface callbackfn) {
        mCallbackData = callbackfn;
    }

    @PhonkMethod(description = "Connects to a bluetooth device using a popup with the available devices", example = "")
    @PhonkMethodParam(params = {"function(name, macAddress, strength)"})
    public void connectSerialUsingMenu() {
        final NativeArray nativeArray = mPBluetooth.getBondedDevices();
        String[] arrayStrings = new String[nativeArray.size()];
        for (int i = 0; i < nativeArray.size(); i++) {
            ReturnObject o = (ReturnObject) nativeArray.get(i, null);
            arrayStrings[i] = o.get("name") + " " + o.get("mac");
            MLog.d(TAG, "bt " + arrayStrings[i]);
        }

        getAppRunner().pUi.popup().title("Connect to device").choice(arrayStrings).onAction(r -> {
            int id = (int) r.get("answerId");
            ReturnObject o = (ReturnObject) nativeArray.get(id);
            String mac = (String) o.get("mac");
            MLog.d(TAG, "bt address " + " " + id + " " + mac);

            connectSerial(mac);
        }).show();
    }


    @PhonkMethod(description = "Connect to mContext bluetooth device using the mac address", example = "")
    @PhonkMethodParam(params = {"mac", "function(data)"})
    public void connectSerial(String mac) {
        BluetoothDevice device = mPBluetooth.getAdapter().getRemoteDevice(mac);
        tryToConnect(device);
    }

    @PhonkMethod(description = "Connect to mContext bluetooth device using mContext name", example = "")
    @PhonkMethodParam(params = {"name, function(data)"})
    public PBluetoothClient connectSerialByName(String name) {
        Set<BluetoothDevice> pairedDevices = mPBluetooth.getAdapter().getBondedDevices();
        if (pairedDevices.size() > 0) {
            for (BluetoothDevice device : pairedDevices) {
                if (device.getName().equals(name)) {
                    tryToConnect(device);

                    break;
                }
            }
        }
        return this;
    }

    @PhonkMethod(description = "Send bluetooth serial message", example = "")
    @PhonkMethodParam(params = {"string"})
    public void send(String string) {
        mConnectedThread.write(string.getBytes());
    }

    @PhonkMethod(description = "Send bluetooth serial message", example = "")
    @PhonkMethodParam(params = {"int"})
    public void sendBytes(byte[] bytes) {
        mConnectedThread.write(bytes);
    }

    @PhonkMethod(description = "Disconnect the bluetooth", example = "")
    @PhonkMethodParam(params = {""})
    public void disconnect() {
        if (mConnectingThread != null) {
            mConnectingThread.cancel();
            mConnectingThread = null;
        }
        if (mConnectedThread != null) {
            mConnectedThread.shutdown();
            mConnectedThread.cancel();
            mConnectedThread = null;
        }
        changeStatus(DISCONNECTED, mDevice);
    }

    @PhonkMethod(description = "Enable/Disable the bluetooth adapter", example = "")
    @PhonkMethodParam(params = {"boolean"})
    public boolean isConnected() {
        return status == CONNECTED;
    }

    @Override
    public void __stop() {
        disconnect();
    }


    /***********************************************************************************
     * IMPL
     */


    private ConnectingThread mConnectingThread;
    private ConnectedThread mConnectedThread;


    // connection thread
    public synchronized void tryToConnect(BluetoothDevice device) {
        MLog.d(TAG, "connect to: " + device);

        // if trying to connect cancel any thread
        if (status == CONNECTING) {
            if (mConnectingThread != null) {
                mConnectingThread.cancel();
                mConnectingThread = null;
            }
        }

        // Cancel any thread currently running a connection
        if (mConnectedThread != null) {
            mConnectedThread.cancel();
            mConnectedThread = null;
        }

        // Start the thread to connect with the given device
        try {
            mConnectingThread = new ConnectingThread(device);
            mConnectingThread.start();
            changeStatus(CONNECTING, device);
        } catch (SecurityException e) {
            e.printStackTrace();
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }


    /**
     * This thread runs while attempting to make an outgoing connection with a
     * device. It runs straight through; the connection either succeeds or
     * fails.
     */
    private class ConnectingThread extends Thread {
        private final BluetoothSocket mmSocket;
        private final BluetoothDevice mmDevice;

        public ConnectingThread(BluetoothDevice device) throws SecurityException, NoSuchMethodException,
                IllegalArgumentException, IllegalAccessException, InvocationTargetException {
            mmDevice = device;
            BluetoothSocket tmpSocket = null;

            // Get mContext BluetoothSocket for mContext connection with the given BluetoothDevice
            try {
                tmpSocket = device.createRfcommSocketToServiceRecord(PBluetooth.UUID_SPP);
                MLog.d(TAG, "socketTmp " + tmpSocket);
            } catch (IOException e) {
                Log.e(TAG, "create socket failed, trying with new fallback", e);

                Method m = device.getClass().getMethod("createRfcommSocket", int.class);
                tmpSocket = (BluetoothSocket) m.invoke(device, 1);
                MLog.d(TAG, "socketTmp 2" + tmpSocket);
            }


            mmSocket = tmpSocket;
        }

        @Override
        public void run() {
            Log.i(TAG, "BEGIN mConnectThread");
            setName("ConnectThread");

            // Always cancel discovery because it will slow down a connection
            mPBluetooth.getAdapter().cancelDiscovery();

            // Make mContext connection to the BluetoothSocket
            try {
                // This is a blocking call and will only return on a successful connection or an exception
                mmSocket.connect();
            } catch (IOException e) {
                e.printStackTrace();

                changeStatus(DISCONNECTED, mmDevice);

                // Close the socket
                try {
                    mmSocket.close();
                } catch (IOException e2) {
                    Log.e(TAG, "unable to close() socket during connection failure", e2);
                }
                return;
            }

            // Reset the ConnectThread because we're done
            synchronized (this) {
                mConnectingThread = null;
            }

            // Start the connected thread
            connected(mmSocket, mmDevice);
        }

        public void cancel() {
            try {
                mmSocket.close();
            } catch (IOException e) {
                Log.e(TAG, "close() of connect socket failed", e);
            }
        }
    }

    // start connection thread
    public synchronized void connected(BluetoothSocket socket, BluetoothDevice device) {
        MLog.d(TAG, "connected");

        changeStatus(CONNECTED, device);
        mDevice = device;

        // Cancel the thread that completed the connection
        if (mConnectingThread != null) {
            mConnectingThread.cancel();
            mConnectingThread = null;
        }

        // Cancel any thread currently running mContext connection
        if (mConnectedThread != null) {
            mConnectedThread.cancel();
            mConnectedThread = null;
        }

        // Start the thread to manage the connection and perform transmissions
        mConnectedThread = new ConnectedThread(socket, device);
        mConnectedThread.start();
    }


    /**
     * This thread runs during a connection with a remote device. It handles all
     * incoming and outgoing transmissions.
     */
    private class ConnectedThread extends Thread {
        private final BluetoothSocket mmSocket;
        private final InputStream mmInStream;
        private final OutputStream mmOutStream;
        private final BluetoothDevice mmDevice;

        public ConnectedThread(BluetoothSocket socket, BluetoothDevice device) {
            MLog.d(TAG, "create ConnectedThread");
            mmSocket = socket;
            mmDevice = device;
            InputStream tmpIn = null;
            OutputStream tmpOut = null;

            // Get the BluetoothSocket input and output streams
            try {
                tmpIn = socket.getInputStream();
                tmpOut = socket.getOutputStream();
            } catch (IOException e) {
                Log.e(TAG, "temp sockets not created", e);
            }

            mmInStream = tmpIn;
            mmOutStream = tmpOut;
        }

        private boolean stop = false;
        private final boolean hasReadAnything = false;

        public void shutdown() {
            stop = true;
            if (!hasReadAnything) {
                return;
            }
            if (mmInStream != null) {
                try {
                    mmInStream.close();
                } catch (IOException e) {
                    Log.e(TAG, "close() of InputStream failed.");
                }
            }
        }

        @Override
        public void run() {
            Log.i(TAG, "BEGIN mConnectedThread");
            BufferedReader reader = new BufferedReader(new InputStreamReader(mmInStream));

            while (!stop) {
                try {
                    final String line = reader.readLine();
                    if (line != null) {
                        MLog.d(TAG, line);

                        if (mCallbackData != null) {
                            mHandler.post(() -> {
                                //MLog.d(TAG, "Got data: " + data);
                                ReturnObject o = new ReturnObject();
                                o.put("data", line);
                                mCallbackData.event(o);
                            });
                        }

                    }
                } catch (IOException e) {
                    Log.e(TAG, "disconnected", e);
                    changeStatus(DISCONNECTED, mmDevice);
                    break;
                }
            }
        }

        /**
         * Write to the connected OutStream.
         *
         * @param buffer The bytes to write
         */
        public void write(byte[] buffer) {
            try {
                mmOutStream.write(buffer);
            } catch (IOException e) {
                Log.e(TAG, "Exception during write", e);
            }
        }

        public void cancel() {
            try {
                mmSocket.close();
            } catch (IOException e) {
                Log.e(TAG, "close() of connect socket failed", e);
            }
        }
    }

    private void changeStatus(int status, BluetoothDevice device) {
        if (mCallbackConnected != null) {

            String returnString = "";

            switch (status) {
                case DISCONNECTED:
                    returnString = "disconnected";
                    break;

                case CONNECTING:
                    returnString = "connecting";
                    break;

                case CONNECTED:
                    returnString = "connected";
                    break;
            }

            final ReturnObject o = new ReturnObject();
            o.put("status", returnString);

            if (device != null) {
                o.put("mac", device.getAddress());
                o.put("name", device.getName());
            }

            mHandler.post(() -> mCallbackConnected.event(o));
        }
    }


}