package hr.fer.labust.diverapp;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothServerSocket;
import android.bluetooth.BluetoothSocket;
import android.os.Handler;
import android.os.Message;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

public class BluetoothService {
    private final UUID mUUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
    private final String mServerName;

    private final BluetoothAdapter mBluetoothAdapter;
    private final Handler mMainHandler;

    private HashMap<BluetoothDevice, RemoteDevice> mConnectedDevices;
    private ArrayList<String> mSendChatMessagesList;
    private ArrayList<String> mSendCommandMessagesList;

    private ServerThread mSecuredServerThread = null;
    private ServerThread mInsecuredServerThread = null;
    private boolean mServerRunning = false;

    public static final int STATE_CHANGED = 0;

    public static final int STATE_NONE = 1;
    public static final int STATE_CONNECTING = 2;
    public static final int STATE_CONNECTED = 3;
    public static final int STATE_DISCONNECTED = 4;
    public static final int LOST_CONNECTION = 5;

    public static final int MESSAGE_SENT = 6;
    public static final int MESSAGE_RECEIVED = 7;

    public static final int START_DISCOVERY = 8;
    public static final int CANCEL_DISCOVERY = 9;
    public static final int SERVER_STATE_CHANGED = 10;

    BluetoothService(Handler handler, String serverName) {
        mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
        mMainHandler = handler;
        mServerName = serverName;

        mConnectedDevices = new HashMap<>();
        mSendCommandMessagesList = new ArrayList<>();
        mSendChatMessagesList = new ArrayList<>();
    }

    public static boolean isAvailable() {
        if (BluetoothAdapter.getDefaultAdapter() == null) {
            return false;
        }
        else {
            return true;
        }
    }

    public boolean isEnabled() {
        return mBluetoothAdapter.isEnabled();
    }

    public boolean isDiscoverable() {
        if (mBluetoothAdapter.getScanMode() == BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
            return true;
        }
        else {
            return false;
        }
    }

    public void startDiscovery() {
        mBluetoothAdapter.startDiscovery();
    }

    public void cancelDiscovery() {
        mBluetoothAdapter.cancelDiscovery();
    }

    public void enable() {
        if (!mBluetoothAdapter.isEnabled()) {
            mBluetoothAdapter.enable();
        }
    }

    public void disable() {
        disconnectAll();
        mBluetoothAdapter.disable();
    }

    public Set<BluetoothDevice> getBondedDevices() {
        return mBluetoothAdapter.getBondedDevices();
    }

    public synchronized Set<BluetoothDevice> getConnectedDevices() {
        return mConnectedDevices.keySet();
    }

    public String getName() {
        return mBluetoothAdapter.getName();
    }

    public void connect(BluetoothDevice device, boolean secure) {
        RemoteDevice remoteDevice = new RemoteDevice(device, mHandler, mMainHandler);
        remoteDevice.connect(mUUID, secure);
        mConnectedDevices.put(device, remoteDevice);
    }

    public void connected(BluetoothSocket socket) {
        BluetoothDevice device = socket.getRemoteDevice();

        RemoteDevice remoteDevice = new RemoteDevice(device, mHandler, mMainHandler);
        remoteDevice.connected(socket);
        mConnectedDevices.put(device, remoteDevice);
    }

    public boolean isConnected(BluetoothDevice device) {
        return mConnectedDevices.containsKey(device);
    }

    public int getState(BluetoothDevice device) {
        if (mConnectedDevices.containsKey(device)) {
            return mConnectedDevices.get(device).getState();
        }

        return BluetoothService.STATE_DISCONNECTED;
    }

    public void disconnect(BluetoothDevice device) {
        if (mConnectedDevices.containsKey(device)) {
            RemoteDevice remoteDevice = mConnectedDevices.get(device);
            remoteDevice.disconnect();
            mConnectedDevices.remove(device);
        }
    }

    public void disconnectAll() {
        for (Map.Entry<BluetoothDevice, RemoteDevice> entry: mConnectedDevices.entrySet()) {
            entry.getValue().disconnect();
        }

        mConnectedDevices.clear();
    }

    public void send (byte[] buffer, BluetoothDevice device) {
        if (mConnectedDevices.containsKey(device)) {
            mConnectedDevices.get(device).send(buffer);
        }
    }

    public void send (String message, BluetoothDevice device) {
        if (mConnectedDevices.containsKey(device)) {
            mConnectedDevices.get(device).send(message);
        }
    }

    public boolean sendChatMessage(byte[] buffer) {
        boolean sent = false;

        for (Map.Entry<BluetoothDevice, RemoteDevice> entry: mConnectedDevices.entrySet()) {
            if (mSendChatMessagesList.contains(entry.getValue().getName())) {
                entry.getValue().send(buffer);
                sent = true;
            }
        }

        return sent;
    }

    public boolean sendCommand(byte[] buffer) {
        boolean sent = false;

        for (Map.Entry<BluetoothDevice, RemoteDevice> entry: mConnectedDevices.entrySet()) {
            if (mSendCommandMessagesList.contains(entry.getValue().getName())) {
                entry.getValue().send(buffer);
                sent = true;
            }
        }

        return sent;
    }

    public int numberOfConnections() {
        return mConnectedDevices.size();
    }

    public void setChatFlag (String name, boolean state) {
        if (state) {
            if (!mSendChatMessagesList.contains(name)) {
                mSendChatMessagesList.add(name);
            }
        }
        else {
            if (mSendChatMessagesList.contains(name)) {
                mSendChatMessagesList.remove(name);
            }
        }
    }

    public void setCommandFlag (String name, boolean state) {
        if (state) {
            if (!mSendCommandMessagesList.contains(name)) {
                mSendCommandMessagesList.add(name);
            }
        }
        else {
            if (mSendCommandMessagesList.contains(name)) {
                mSendCommandMessagesList.remove(name);
            }
        }
    }

    public boolean getChatFlag(String name) {
        return mSendChatMessagesList.contains(name);
    }

    public boolean getCommandFlag(String name) {
        return mSendCommandMessagesList.contains(name);
    }

    public synchronized void startServer() {
        if (mInsecuredServerThread == null) {
            mInsecuredServerThread = new ServerThread(false);
            mInsecuredServerThread.start();
        }

        if (mSecuredServerThread == null) {
            mSecuredServerThread = new ServerThread(true);
            mSecuredServerThread.start();
        }

        mServerRunning = true;
    }

    public synchronized void closeServer() {
        if (mSecuredServerThread != null) {
            mSecuredServerThread.close();
            mSecuredServerThread = null;
        }

        if (mInsecuredServerThread != null) {
            mInsecuredServerThread.close();
            mInsecuredServerThread = null;
        }
        mServerRunning = false;
    }

    public boolean isServerRunning() {
        return mServerRunning;
    }

    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch(msg.what) {
                case STATE_CHANGED:
                    BluetoothDevice device = (BluetoothDevice)msg.obj;
                    if (msg.arg1 == BluetoothService.STATE_NONE) {
                        if (mConnectedDevices.containsKey(device)) {
                            RemoteDevice remoteDevice = mConnectedDevices.get(device);
                            remoteDevice.disconnect();
                            mConnectedDevices.remove(device);
                        }
                    }
                    break;

                case START_DISCOVERY:
                    startDiscovery();
                    break;

                case CANCEL_DISCOVERY:
                    cancelDiscovery();
                    break;
            }
        }
    };

    private class ServerThread extends Thread {
        private BluetoothServerSocket mServerSocket = null;
        private final boolean secured;
        private boolean createNewSocket = true;

        ServerThread (boolean secure) {
            secured = secure;
        }

        @Override
        public void run() {
            BluetoothSocket socket = null;
            createServerSocket();

            while (true) {
                socket = null;
                if (mServerSocket == null) {
                    closeServer();
                    mMainHandler.obtainMessage(SERVER_STATE_CHANGED).sendToTarget();
                    break;
                }

                try {
                    socket = mServerSocket.accept();
                }
                catch(IOException e) {
                    closeServer();
                    mMainHandler.obtainMessage(SERVER_STATE_CHANGED).sendToTarget();
                    break;
                }

                if (socket != null) {
                    connected(socket);
                }
            }
        }

        private void createServerSocket() {
            BluetoothServerSocket tempServerSocket = null;

            try {
                if (secured) {
                    tempServerSocket = mBluetoothAdapter.listenUsingRfcommWithServiceRecord(mServerName, mUUID);
                }
                else {
                    tempServerSocket = mBluetoothAdapter.listenUsingInsecureRfcommWithServiceRecord(mServerName, mUUID);
                }
            }
            catch(IOException e) {}

            mServerSocket = tempServerSocket;
        }

        public void close() {
            try {
                mServerSocket.close();
            }
            catch(IOException e) {}
        }
    }
}