package temple.multiplayer.net.bluetooth.service;

import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothServerSocket;
import android.bluetooth.BluetoothSocket;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;

import temple.multiplayer.net.bluetooth.device.BluetoothCommunicationThread;
import temple.multiplayer.net.common.service.ServiceMessageKeys;
import temple.multiplayer.net.common.service.ServiceMessageType;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * Created by stephan on 22-5-2015.
 */
public class BluetoothServerService extends AbstractBluetoothService {
    private static final String TAG = BluetoothServerService.class.getSimpleName();

    private AcceptThread _secureAcceptThread;
    private AcceptThread _insecureAcceptThread;
    private Map<String, BluetoothCommunicationThread> _communicationThreads = new HashMap<>();
    private Boolean _isStopping = false;

    /**
     * @param handler A Handler to send messages back to the UI Activity
     */
    public BluetoothServerService(Handler handler) {
        super(handler);
    }

    @Override
    public synchronized void start() {
        super.start();

        // Start the thread to listen on a BluetoothServerSocket
        if (_secureAcceptThread == null) {
            _secureAcceptThread = new AcceptThread(true);
            _secureAcceptThread.start();
        }
        if (_insecureAcceptThread == null) {
            _insecureAcceptThread = new AcceptThread(false);
            _insecureAcceptThread.start();
        }

        setState(STATE_LISTEN);
    }

    @Override
    public synchronized void stop() {
        super.stop();

        stopListening();

        cancelCommunicationThreads();
    }

    public synchronized void stopListening () {
        if (_secureAcceptThread != null) {
            _secureAcceptThread.cancel();
            _secureAcceptThread = null;
        }

        if (_insecureAcceptThread != null) {
            _insecureAcceptThread.cancel();
            _insecureAcceptThread = null;
        }
    }

    public void write(String deviceAddress, byte[] out) {
        // Create temporary object
        BluetoothCommunicationThread communicationThread;

        // Synchronize a copy of all communcation threads
        synchronized (this) {
            communicationThread = _communicationThreads.get(deviceAddress);
        }
        // Perform the write unsynchronized
        if(communicationThread != null) {
            communicationThread.write(out);
        }
    }

    private synchronized void accepted(BluetoothSocket socket, BluetoothDevice device, String socketType) {
        if (_debug) Log.d(TAG, "accepted, Socket Type:" + socketType);

        // Start the thread to manage the connection and perform transmissions
        createCommunicationThread(socket, device);

        // Send the name of the connected device back to the UI Activity
        Bundle bundle = new Bundle();
        bundle.putString(ServiceMessageKeys.DEVICE_NAME, device.getName());
        bundle.putString(ServiceMessageKeys.DEVICE_ADDRESS, device.getAddress());
        sendMessage(ServiceMessageType.MESSAGE_DEVICE_ADDED, bundle);
    }

    @Override
    protected void connectionLost(BluetoothDevice device) {
        if(_isStopping) return;

        super.connectionLost(device);

		BluetoothCommunicationThread communicationThread = _communicationThreads.get(device.getAddress());
		if (communicationThread != null) {
			cancelCommunicationThread(communicationThread);

			_communicationThreads.remove(device.getAddress());
		}
	}

    protected void cancelCommunicationThreads() {
        _isStopping = true;

        for (Map.Entry<String, BluetoothCommunicationThread> entry : _communicationThreads.entrySet()) {
            BluetoothCommunicationThread communicationThread = entry.getValue();

            cancelCommunicationThread(communicationThread);
        }

        _communicationThreads.clear();

        _isStopping = false;
    }

    @Override
    protected BluetoothCommunicationThread createCommunicationThread(BluetoothSocket socket, BluetoothDevice device) {
        BluetoothCommunicationThread communicationThread = super.createCommunicationThread(socket, device);

        _communicationThreads.put(device.getAddress(), communicationThread);

        return communicationThread;
    }

    /**
     * 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 {
        private final BluetoothServerSocket _serverSocket;
        private final String _socketType;
        private boolean _isListening;

        public AcceptThread(boolean secure) {
            _socketType = secure ? "Secure" : "Insecure";

            // Create a new listening server socket
            BluetoothServerSocket serverSocket = null;
            try {
                if (secure) {
                    if (_debug) Log.d(TAG, "AcceptThread: started listening secure, uuid = " + _secureUuid + ", spd name = " + _secureSPDName);
                    serverSocket = _adapter.listenUsingRfcommWithServiceRecord(_secureSPDName, _secureUuid);
                } else {
                    if (_debug) Log.d(TAG, "AcceptThread: started listening insecure, uuid = " + _insecureUuid + ", spd name = " + _insecureSPDName);
                    serverSocket = _adapter.listenUsingInsecureRfcommWithServiceRecord(_insecureSPDName, _insecureUuid);
                }
            } catch (IOException e) {
                Log.e(TAG, "AcceptThread: listen failed");
                e.printStackTrace();
            }

            _serverSocket = serverSocket;

            _isListening = true;
        }

        public void run() {
            if (_debug) Log.i(TAG, "run: START AcceptThread " + this);

            setName("AcceptThread " + _socketType);

            BluetoothSocket socket = null;

            // Listen to the server socket
            while (_isListening) {
                try {
                    // This is a blocking call and will only return on a
                    // successful connection or an exception
                    socket = _serverSocket.accept();
                    if (_debug) Log.d(TAG, "run: accepted socket for device " + socket.getRemoteDevice().getName());
                } catch (IOException e) {
                    if (_isListening) {
                        Log.e(TAG, "run: accept failed");
                        e.printStackTrace();
                    }
                }

                // If a connection was accepted
                if (socket != null) {
                    if (_debug) Log.d(TAG, "run: socket found, _isListening = " + _isListening);

                    synchronized (BluetoothServerService.this) {
                        switch (_state) {
                            case STATE_LISTEN:
                            case STATE_CONNECTING:
                                // Situation normal. Start the connected thread.
                                accepted(socket, socket.getRemoteDevice(), _socketType);
                                break;
                            case STATE_IDLE:
                            case STATE_CONNECTED:
                                // Either not ready or already connected. Terminate new socket.
                                try {
                                    socket.close();
                                } catch (IOException e) {
                                    Log.e(TAG, "Could not close unwanted socket", e);
                                }
                                break;
                        }
                    }

                    socket = null;
                }
            }

            if (_debug) Log.i(TAG, "run: END " + this);
        }

        public void cancel() {
            if (_debug) Log.d(TAG, "cancel: " + this);

            _isListening = false;

            try {
                _serverSocket.close();

                if (_debug) Log.d(TAG, "cancel: server socket closed");
            } catch (IOException e) {
                Log.e(TAG, "cancel: close failed");
                e.printStackTrace();
            }
        }
    }
}