/**

 Copyright (C) 2015, Roman P., dev.roman [at] gmail

 This program is free software; you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation; either version 3 of the License, or
 (at your option) any later version.

 This program 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 General Public License
 along with this program. If not, see http://www.gnu.org/licenses/

 */

package com.rp.podemu;

import android.app.Activity;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothServerSocket;
import android.bluetooth.BluetoothSocket;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Handler;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Set;
import java.util.UUID;

/**
 * Created by rp on 07/22/2017.
 */

public class SerialInterface_BT extends SerialInterface_Common implements SerialInterface
{
    private BluetoothDevice btDevice;
    private static Context baseContext;
    private Activity baseActivity;
    private static SharedPreferences sharedPref;


    public final static String BTDEV_NAME_DEFAULT = "BT device not set";
    public final static String BTDEV_ADDRESS_DEFAULT = "unknown";
    public final static UUID BTDEV_UUID[]=
            { UUID.fromString("00001101-0000-1000-8000-00805F9B34FB")//,
            //  UUID.fromString("0000ffe0-0000-1000-8000-00805f9b34fb"),
            //  UUID.fromString("0000ffe1-0000-1000-8000-00805f9b34fb")
            };
    public static int btdev_uuid_idx = 0;
    private static final int REQUEST_DISCOVERABLE_BT = 0x1e;
    private static final int BUFFER_SIZE = 1024;

    // Constants that indicate the current connection state
    public static final int STATE_NONE = 0;
    public static final int STATE_SEARCHING = 1;
    public static final int STATE_CONNECTING = 2;
    public static final int STATE_CONNECTED = 3;
    public static final int STATE_LOST = 4;
    public static final int STATE_FAILED = 5;

    private final BluetoothServerSocket btServerSocket;
    private final BluetoothAdapter btAdapter;
    //private BluetoothSocket btSocket;
    private int btState;
    private Handler mHandler = null;

    //private AcceptThread mAcceptThread;
    private ConnectThread mConnectThread;
    private ConnectedThread mConnectedThread;

    private boolean isBtAskingDiscoverability = false;
    private static SerialInterface_BT serialInterfaceBTInstance = null;

    // timeout after which interface will be closed if not connected (ms)
    public final static int BT_CONNECT_TIMEOUT = 1500;

    private static SharedPreferences getSharedPref(Context context)
    {
        if(sharedPref == null)
            sharedPref = context.getSharedPreferences("PODEMU_PREFS", Context.MODE_PRIVATE);
        return sharedPref;
    }


    public static synchronized SerialInterface_BT getInstance(Context context)
    {
        baseContext = context;
        return getInstance();
    }

    public static synchronized SerialInterface_BT getInstance()
    {
        int bluetoothEnabled=getSharedPref(baseContext).getInt("bluetoothEnabled", 0);
        if(bluetoothEnabled==0)
        {
            serialInterfaceBTInstance = null;
        }
        else if(serialInterfaceBTInstance == null)
        {
            PodEmuLog.debug("SIBT: initializing new instance");
            serialInterfaceBTInstance = new SerialInterface_BT();
        }

        return serialInterfaceBTInstance;
    }

    public void setHandler(Handler handler)
    {
        mHandler = handler;
    }



    public SerialInterface_BT ()
    {
        btAdapter = BluetoothAdapter.getDefaultAdapter();
        if (btAdapter == null)
        {
            // Device does not support Bluetooth
            PodEmuLog.debug("SIBT: device does not support Bluetooth.");
            btServerSocket = null;
            return;
        }
        if (!btAdapter.isEnabled())
        {
            // Bluetooth is still disabled :(
            PodEmuLog.debug("SIBT: Bluetooth is disabled. Not trying to turn it on though...");
            btServerSocket = null;
            return;
        }

        // Use a temporary object that is later assigned to mmServerSocket
        // because mmServerSocket is final.
        BluetoothServerSocket tmp = null;
        try
        {
            PodEmuLog.debug("SIBT: SerialInterface_BT connecting with btdev_uuid_idx=0");
            tmp = btAdapter.listenUsingRfcommWithServiceRecord(getName(), BTDEV_UUID[0]);
        }
        catch (IOException e)
        {
            PodEmuLog.debug("SIBT: Socket's listen() method failed");
            PodEmuLog.printStackTrace(e);

        }
        btServerSocket = tmp;
        btState = STATE_NONE;
    }

    public synchronized boolean init(Context context)
    {
        boolean podEmuBTFound = false;
        baseContext = context;

        if(context instanceof MainActivity)
        {
            setHandler(((MainActivity) context).mHandler);
        }

        btdev_uuid_idx = (btdev_uuid_idx+1) % BTDEV_UUID.length;

        PodEmuLog.debug("SIBT: Bluetooth initialization started with btdev_uuid_idx=" + btdev_uuid_idx);
        ensureDiscoverable();

        if(mConnectedThread!=null)
        {
            PodEmuLog.debug("SIBT: mConnectedThread active. Aborting init.");
            return false;
        }

        if(mConnectThread!=null)
        {
            PodEmuLog.debug("SIBT: mConnectThread active. Aborting init.");
            return false;
        }

        /*
        if(mAcceptThread!=null)
        {
            PodEmuLog.debug("SIBT: mAcceptThread active. Aborting init.");
            return false;
        }
        */

        if(btAdapter == null)
        {
            PodEmuLog.error("SIBT: mBluetoothAdapter is not initialized. Aborting...");
            return false;
        }
        if(btServerSocket == null)
        {
            PodEmuLog.error("SIBT: mmServerSocket is not initialized. Aborting...");
            return false;
        }

        Set<BluetoothDevice> pairedDevices = btAdapter.getBondedDevices();

        if (pairedDevices.size() > 0)
        {
            // There are paired devices. Get the name and address of each paired device.
            for (BluetoothDevice device : pairedDevices)
            {
                String deviceName = device.getName();
                String deviceHardwareAddress = device.getAddress(); // MAC address

                PodEmuLog.debugVerbose("SIBT: paired device found: " + deviceName + " (" + deviceHardwareAddress + ")");

                if(deviceName.equals(getName()))
                {
                    podEmuBTFound = true;
                    btDevice = device;
                }

            }
        }

        if(!podEmuBTFound)
        {
            PodEmuLog.debug("SIBT: bluetooth device not found. Exiting...");
            return false;
        }

        PodEmuLog.debug("SIBT: bluetooth device found: " + btDevice.getName() + " (" + btDevice.getAddress() + "). Trying to connect...");

        //BluetoothDevice actualBtDevice = btAdapter.getRemoteDevice(btDevice.getAddress());
        connect(btDevice);

        /*
        // give background threads time to actually establish the connection
        try
        {
            Thread.sleep(BT_CONNECT_TIMEOUT + 150);
        }
        catch(java.lang.InterruptedException e)
        {
            // do nothing

        }
    */
        try
        {
            wait();
        }
        catch(InterruptedException e)
        {
            PodEmuLog.debug("SIBT: connection wait interrupted.");
        }

        PodEmuLog.debug("SIBT: init finishing with status: " + isConnected());

        return isConnected();
    }

    public int write(byte[] buffer, int numBytes)
    {
        // Create temporary object
        ConnectedThread r;
        // Synchronize a copy of the ConnectedThread
        synchronized (this)
        {
            if (btState != STATE_CONNECTED) return -1;
            r = mConnectedThread;
        }
        // Perform the write asynchronously
        r.write(buffer, numBytes);

        return numBytes;
    }

    public int read(byte[] buffer)
    {
        // Create temporary object
        ConnectedThread r;
        // Synchronize a copy of the ConnectedThread
        synchronized (this)
        {
            if (btState != STATE_CONNECTED) return -1;
            r = mConnectedThread;
        }
        return r.read(buffer);
    }

    public String readString()
    {
        // FIXME
        return "";
    }

    public String getName()
    {
        return getSharedPref(baseContext).getString("bluetoothDeviceName", SerialInterface_BT.BTDEV_NAME_DEFAULT);
    }

    public String getAddress()
    {
        return getSharedPref(baseContext).getString("bluetoothDeviceAddress", SerialInterface_BT.BTDEV_ADDRESS_DEFAULT);
    }

    public int getVID()
    {
        return 0;
    }

    public int getPID()
    {
        return 0;
    }

    public void setBaudRate(int rate)
    {
        // FIXME:
        return;
    }

    public int getBaudRate()
    {
        // FIXME
        return 57600;
    }


    public boolean isConnecting()
    {
        return false;
    }


    public boolean isConnected()
    {
        return (btState == STATE_CONNECTED);
    }

    public int getReadBufferSize()
    {
        return BUFFER_SIZE;
    }

    public synchronized void close()
    {
        PodEmuLog.debug("SIBT: close requested");
        if (mConnectThread != null)
        {
            PodEmuLog.debug("SIBT: close() resetting mConnectThread");
            mConnectThread.cancel();
            mConnectThread = null;
        }
        if (mConnectedThread != null)
        {
            PodEmuLog.debug("SIBT: close() resetting mConnectedThread");
            mConnectedThread.cancel();
            mConnectedThread = null;
        }

        /*
        if (mAcceptThread != null)
        {
            mAcceptThread.cancel();
            mAcceptThread = null;
        }
        */
        serialInterfaceBTInstance = null;

        setState(STATE_NONE);
    }

    public void restart()
    {
        close();
        init(baseContext);
    }


    static public String getStateName(int state)
    {
        switch (state)
        {
            case STATE_CONNECTED: return "CONNECTED";
            case STATE_CONNECTING: return "CONNECTING";
            case STATE_LOST: return "LOST";
            case STATE_FAILED: return "FAILED";
            case STATE_SEARCHING: return "SEARCHING";
            case STATE_NONE:
            default:
                return "NONE";

        }
    }

    /**
     * Set the current state of the chat connection
     * @param state  An integer defining the current connection state
     */
    private synchronized void setState(int state) 
    {
        PodEmuLog.debug("SIBT: setState() " + getStateName(btState) + " -> " + getStateName(state));
        btState = state;
        //mHandler.obtainMessage(MainActivity.MESSAGE_STATE_CHANGE, state, -1).sendToTarget();

        PodEmuService.communicateSerialStatusChange();
    }
    public synchronized int getState()
    {
        return btState;
    }
    
    private synchronized void ensureDiscoverable()
    {
        if(this.isBtAskingDiscoverability) return;

        //if(!btAdapter.isEnabled() )// ||
        //        ((statusBT != STATUS_DISCOVERABLE) && (statusBT != STATUS_CONNECTED)))
        if(btAdapter!=null && btAdapter.isEnabled())
        {
            PodEmuLog.debug("SIBT: Bluetooth adapter is enabled");
            if(btAdapter.getScanMode() != BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE)
            {
                PodEmuLog.debug("SIBT: Bluetooth discovery turned off. Turning on...");
                this.isBtAskingDiscoverability = true;
                Intent discoverableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
                //discoverableIntent.putExtra("android.bluetooth.adapter.extra.DISCOVERABLE_DURATION", mPC.time_to_discoverable);
            }
            else
            {
                PodEmuLog.debug("SIBT: Bluetooth discovery is on.");
                //if(mBtService == null)
                //    initService();
            }
        }
        /*else
        {
            PodEmuLog.debug("SIBT: Bluetooth adapter is not enabled");
            //if(mBtService == null)
            //    initService();
        }*/
    }

    private void connectionFailed()
    {
        setState(STATE_FAILED);
    }

    private void connectionLost()
    {
        setState(STATE_LOST);
    }

    public synchronized void connected(BluetoothSocket socket, BluetoothDevice device)
    {
        PodEmuLog.debug("SIBT: new BT communication started");
        if (mConnectThread != null)
        {
            PodEmuLog.debug("SIBT: connected() resetting mConnectThread");
            mConnectThread.cancel();
            mConnectThread = null;
        }
        if (mConnectedThread != null)
        {
            PodEmuLog.debug("SIBT: connected() resetting mConnectedThread");
            mConnectedThread.cancel();
            mConnectedThread = null;
        }

        /*
        if (mAcceptThread != null)
        {
            mAcceptThread.cancel();
            mAcceptThread = null;
        }
        */

        mConnectedThread = new ConnectedThread(socket);
        mConnectedThread.start();


        /*
        Message msg = mHandler.obtainMessage(MainActivity.MESSAGE_DEVICE_NAME);
        Bundle bundle = new Bundle();
        bundle.putString(MainActivity.DEVICE_NAME, device.getName());
        msg.setData(bundle);
        mHandler.sendMessage(msg);
        */
        setState(STATE_CONNECTED);
        synchronized (this)
        {
            notifyAll();
        }
    }

    /**
     * Start the chat service. Specifically start AcceptThread to begin a
     * session in listening (server) mode. Called by the Activity onResume() */
    public synchronized void start()
    {
        PodEmuLog.debug("SIBT: start()");

        if (mConnectThread != null)
        {
            mConnectThread.cancel();
            PodEmuLog.debug("SIBT: start() resetting mConnectThread");
            mConnectThread = null;
            connect(btDevice);
        }

        /*
        if (mConnectedThread != null)
        {
            mConnectedThread.cancel();
            mConnectedThread = null;
        }

        if (mAcceptThread == null)
        {
            mAcceptThread = new AcceptThread();
            mAcceptThread.start();
        }


        setState(STATE_SEARCHING);

        */
    }


    public synchronized void connect(BluetoothDevice device)
    {
        PodEmuLog.debug("SIBT: trying to connect to: " + device);
        if (btState == STATE_CONNECTING)
        {
            if (mConnectThread != null)
            {
                mConnectThread.cancel();
                PodEmuLog.debug("SIBT: connect() resetting mConnectThread");
                mConnectThread = null;
            }
        }
        if (mConnectedThread != null)
        {
            mConnectedThread.cancel();
            mConnectedThread = null;
        }
        mConnectThread = new ConnectThread(device);
        //if(mConnectedThread.mmSocket != null)
        {
            mConnectThread.start();
            setState(STATE_CONNECTING);
        }
        //else
        //    this.close();
    }



    /**
     * This thread runs while listening for incoming connections. It behaves
     * like a server-side client. It runs until a connection is accepted
     * (or until cancelled).
     */

    private class AcceptThread extends Thread
    {
        // The local server socket
        private final BluetoothServerSocket mmServerSocket;

        public AcceptThread()
        {
            BluetoothServerSocket tmp = null;

            // Create a new listening server socket
            try
            {
                PodEmuLog.debug("SIBT: AcceptThread connecting with btdev_uuid_idx=" + btdev_uuid_idx);
                tmp = btAdapter.listenUsingRfcommWithServiceRecord(getName(), BTDEV_UUID[btdev_uuid_idx]);
            }
            catch (IOException e)
            {
                PodEmuLog.debug("SIBT: AcceptThread listen() failed");
                PodEmuLog.printStackTrace(e);
            }
            mmServerSocket = tmp;
        }

        public void run() 
        {
            PodEmuLog.debug("SIBT: BEGIN AcceptThread " + this);
            setName("SIBT: AcceptThread");
            BluetoothSocket socket = null;

            // Listen to the server socket if we're not connected
            while (btState != STATE_CONNECTED)
            {
                try
                {
                    // This is a blocking call and will only return on a
                    // successful connection or an exception
                    socket = mmServerSocket.accept();
                    PodEmuLog.debug("SIBT: AcceptThread Connection Accepted");
                }
                catch (IOException e)
                {
                    PodEmuLog.debug("SIBT: AcceptThread accept() failed");
                    //PodEmuLog.printStackTrace(e);
                    break;
                }

                // If a connection was accepted
                if (socket != null) 
                {
                    synchronized (SerialInterface_BT.this) 
                    {
                        PodEmuLog.debug("SIBT: AcceptThread connection accepted, State: " + btState);
                        switch (btState)
                        {
                            case STATE_SEARCHING:
                            case STATE_CONNECTING:
                                // Situation normal. Start the connected thread.
                                PodEmuLog.debug("SIBT: AcceptThread starting mConnectedThread.");
                                connected(socket, socket.getRemoteDevice());
                                break;
                            case STATE_NONE:
                            case STATE_CONNECTED:
                                // Either not ready or already connected. Terminate new socket.
                                PodEmuLog.debug("SIBT: AcceptThread Either not ready or already connected. Terminate new socket.");
                                try
                                {
                                    socket.close();
                                    PodEmuLog.debug("SIBT: AcceptThread close() successfull.");
                                }
                                catch (IOException e)
                                {
                                    PodEmuLog.debug("SIBT: AcceptThread could not close unwanted socket");
                                    //PodEmuLog.printStackTrace(e);
                                }
                                break;
                        }
                    }
                }
            }

            PodEmuLog.debug("SIBT: AcceptThread END");
        }

        public void cancel()
        {
            PodEmuLog.debug("SIBT: cancel " + this);
            try
            {
                mmServerSocket.close();
            }
            catch (IOException e)
            {
                PodEmuLog.debug("SIBT: AcceptThread close() or server failed");
                //PodEmuLog.printStackTrace(e);
            }
        }
    }


    /**
     * 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 ConnectThread extends Thread
    {
        private final BluetoothSocket mmSocket;
        private final BluetoothDevice mmDevice;

        public ConnectThread(BluetoothDevice device)
        {
            mmDevice = device;
            BluetoothSocket tmp = null;

            if(!btAdapter.isEnabled())
            {
                mmSocket = null;
                this.cancel();
//                mConnectedThread.cancel();
                PodEmuService.stopService(baseContext);
                return;
            }

            // Get a BluetoothSocket for a connection with the
            // given BluetoothDevice
            try
            {
                PodEmuLog.debug("SIBT: ConnectThread connecting with btdev_uuid_idx=" + btdev_uuid_idx);
                setState(STATE_CONNECTING);
                tmp = device.createRfcommSocketToServiceRecord(BTDEV_UUID[btdev_uuid_idx]);
            }
            catch (IOException e)
            {
                PodEmuLog.debug("SIBT: ConnectThread createRfcomm() failed");
                PodEmuLog.printStackTrace(e);
            }
            mmSocket = tmp;
        }

        public void run()
        {
            PodEmuLog.debug("SIBT: BEGIN ConnectThread");
            setName("SIBT: ConnectThread");

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

            // Make a connection to the BluetoothSocket
            try
            {
                // This is a blocking call and will only return on a
                // successful connection or an exception
                PodEmuLog.debug("SIBT: ConnectThread - establishing BT connections started");
                mmSocket.connect();
            }
            catch (IOException e)
            {
                PodEmuLog.debug("SIBT: ConnectThread - connection failed");
                connectionFailed();
                // Close the socket
                try
                {
                    mmSocket.close();
                    PodEmuLog.debug("SIBT: ConnectThread - close() succesfull");
                }
                catch (IOException e2)
                {
                    PodEmuLog.debug("SIBT: ConnectThread - unable to close() socket during connection failure");
                    PodEmuLog.printStackTrace(e);
                }

                PodEmuLog.debug("SIBT: ConnectThread - trying to restart from");
                // Start the service over to restart listening mode
                SerialInterface_BT.this.start();
                return;
            }
            catch(Exception e)
            {
                PodEmuLog.debug("SIBT: ConnectThread - connection failed - BT disabled");
                connectionFailed();
                PodEmuService.stopService(baseContext);
            }


            // Reset the ConnectThread because we're done
            synchronized (SerialInterface_BT.this)
            {
                PodEmuLog.debug("SIBT: run() resetting mConnectThread");
                mConnectThread = null;
            }

            PodEmuLog.debug("SIBT: ConnectThread - BT connection established");
            // Start the connected thread
            connected(mmSocket, mmDevice);
        }

        public void cancel()
        {
            if(mmSocket == null) return;
            try
            {
                mmSocket.close();
            }
            catch (IOException e)
            {
                PodEmuLog.debug("SIBT: ConnectThread close() of connect socket failed");
                PodEmuLog.printStackTrace(e);
            }
        }
    }



    /**
     * 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;

        public ConnectedThread(BluetoothSocket socket)
        {
            PodEmuLog.debug("SIBT: ConnectedThread create ConnectedThread");
            mmSocket = socket;
            InputStream tmpIn = null;
            OutputStream tmpOut = null;

            // Get the BluetoothSocket input and output streams
            try
            {
                tmpIn = socket.getInputStream();
                tmpOut = socket.getOutputStream();
            }
            catch (IOException e)
            {
                PodEmuLog.debug("SIBT: ConnectedThread temp sockets not created");
                PodEmuLog.printStackTrace(e);
            }
            catch (Exception e)
            {
                PodEmuLog.debug("SIBT: ConnectedThread - connection failed - BT disabled");
                connectionFailed();
                PodEmuService.stopService(baseContext);
            }

            mmInStream = tmpIn;
            mmOutStream = tmpOut;
        }

        public void run()
        {
            //PodEmuLog.debug("SIBT: ConnectedThread BEGIN (EMPTY)");
            /* RPP
            byte[] buffer = new byte[1024];
            int bytes;

            // Keep listening to the InputStream while connected

            while (true)
            {
                try
                {
                    // Read from the InputStream
                    //bytes = mmInStream.read(buffer);

                    // Send the obtained bytes to the UI Activity
                    mHandler.obtainMessage(MainActivity.MESSAGE_READ, bytes, -1, buffer)
                            .sendToTarget();
                }
                catch (IOException e)
                {
                    PodEmuLog.debug("SIBT: ConnectedThread disconnected");
                    PodEmuLog.printStackTrace(e);
                    connectionLost();
                    break;
                }
            }
            */
            //PodEmuLog.debug("SIBT: ConnectedThread END (EMPTY)");
        }
        /**
         * Write to the connected OutStream.
         * @param buffer  The bytes to write
         */
        public void write(byte[] buffer, int numBytes)
        {
            try
            {
                mmOutStream.write(buffer, 0, numBytes);
            }
            catch (IOException e)
            {
                PodEmuLog.debug("SIBT: ConnectedThread Exception during write");
                PodEmuLog.printStackTrace(e);
            }
        }

        /**
         * Read from the connected InputStream.
         * @param buffer: The buffer to read to
         * @return numBytes: number of bytes read
         *
         * WARNING: this function is blocking until at least one byte is read or EOF encountered
         */
        public int read(byte[] buffer)
        {
            int numBytes = -1;

            try
            {
                numBytes = mmInStream.read(buffer,0,BUFFER_SIZE);
            }
            catch (IOException e)
            {
                PodEmuLog.debug("SIBT: ConnectedThread Exception during read");
                // PodEmuLog.printStackTrace(e);
            }
            return numBytes;
        }
        public void cancel()
        {
            try
            {
                if(mmSocket != null)  mmSocket.close();
                if(mmInStream!=null)  mmInStream.close();
                if(mmOutStream!=null) mmOutStream.close();
            }
            catch (IOException e)
            {
                PodEmuLog.debug("SIBT: ConnectedThread close() of connect socket failed");
                PodEmuLog.printStackTrace(e);
            }
        }
    }
}