/*
 * (C) Copyright 2015 by fr3ts0n <[email protected]>
 *
 * 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 2 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, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston,
 * MA 02111-1307 USA
 */

package com.fr3ts0n.ecu.gui.androbd;

import android.annotation.SuppressLint;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.content.Context;
import android.os.Handler;
import android.os.ParcelUuid;

import com.fr3ts0n.prot.StreamHandler;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.util.UUID;
import java.util.logging.Level;

/**
 * This class does all the work for setting up and managing Bluetooth
 * connections with other devices. It has a thread that listens for incoming
 * connections, a thread for connecting with a device, and a thread for
 * performing data transmissions when connected.
 */
@SuppressLint("NewApi")
public class BtCommService extends CommService
{
	
	private BtConnectThread mBtConnectThread;
	private BtWorkerThread mBtWorkerThread;
	/** communication stream handler */
	private final StreamHandler ser = new StreamHandler();
	
	
	/**
	 * Constructor. Prepares a new Bluetooth Communication session.
	 *
	 * @param context The UI Activity Context
	 * @param handler A Handler to send messages back to the UI Activity
	 */
	BtCommService(Context context, Handler handler)
	{
		super(context, handler);

		// Always cancel discovery because it will slow down a connection
		// Member fields
		BluetoothAdapter mAdapter = BluetoothAdapter.getDefaultAdapter();
		mAdapter.cancelDiscovery();
		
		// set up protocol handlers
		elm.addTelegramWriter(ser);
		ser.setMessageHandler(elm);
	}

	/**
	 * Start the chat service. Specifically start AcceptThread to begin a session
	 * in listening (server) mode. Called by the Activity onResume()
	 */
	@Override
	public synchronized void start()
	{
		log.log(Level.FINE, "start");

		// Cancel any thread attempting to make a connection
		if (mBtConnectThread != null)
		{
			mBtConnectThread.cancel();
			mBtConnectThread = null;
		}

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

		setState(STATE.LISTEN);
	}

	/**
	 * start connection to specified device
	 *
	 * @param device The device to connect
	 * @param secure Socket Security type - Secure (true) , Insecure (false)
	 */
	@Override
	public synchronized void connect(Object device, boolean secure)
	{
		log.log(Level.FINE, "connect to: " + device);

		// Cancel any thread attempting to make a connection
		if (mState == STATE.CONNECTING)
		{
			if (mBtConnectThread != null)
			{
				mBtConnectThread.cancel();
				mBtConnectThread = null;
			}
		}

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

		setState(STATE.CONNECTING);

		// Start the thread to connect with the given device
		mBtConnectThread = new BtConnectThread((BluetoothDevice)device, secure);
		mBtConnectThread.start();
	}

	/**
	 * Start the BtWorkerThread to begin managing a Bluetooth connection
	 *
	 * @param socket The BluetoothSocket on which the connection was made
	 * @param device The BluetoothDevice that has been connected
	 */
	private synchronized void connected(BluetoothSocket socket, BluetoothDevice
		                                                            device, final String socketType)
	{
		log.log(Level.FINE, "connected, Socket Type:" + socketType);

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

		// Cancel any thread currently running a connection
		if (mBtWorkerThread != null)
		{
			mBtWorkerThread.cancel();
			mBtWorkerThread = null;
		}
		// Start the thread to manage the connection and perform transmissions
		mBtWorkerThread = new BtWorkerThread(socket, socketType);
		mBtWorkerThread.start();

        // we are connected -> signal connection established
        connectionEstablished(device.getName());
    }

	/**
	 * Stop all threads
	 */
	@Override
	public synchronized void stop()
	{
		log.log(Level.FINE, "stop");
		elm.removeTelegramWriter(ser);

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

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

		setState(STATE.OFFLINE);
	}

	/**
	 * Write to the BtWorkerThread in an un-synchronized manner
	 *
	 * @param out The bytes to write
	 * @see BtWorkerThread#write(byte[])
	 */
	@Override
	public synchronized void write(byte[] out)
	{
		// Perform the write un-synchronized
		mBtWorkerThread.write(out);
	}

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

		BtConnectThread(BluetoothDevice device, boolean secure)
		{
			mmDevice = device;
			BluetoothSocket tmp = null;
			mSocketType = secure ? "Secure" : "Insecure";

			// Modified to work with SPP Devices
			final UUID SPP_UUID = UUID
				.fromString("00001101-0000-1000-8000-00805F9B34FB");
			
			// Get a BluetoothSocket for a connection with the
			// given BluetoothDevice
			try
			{
				if (secure)
				{
					tmp = device.createRfcommSocketToServiceRecord(SPP_UUID);
				} else
				{
					tmp = device.createInsecureRfcommSocketToServiceRecord(SPP_UUID);
				}
			} catch (IOException e)
			{
				log.log(Level.SEVERE, "Socket Type: " + mSocketType + "create() failed", e);
			}
			mmSocket = tmp;

			logSocketUuids(mmSocket, "BT socket");
		}
		
		/**
		 * Log supported UUIDs of specified BluetoothSocket
		 * @param socket Socket to log
		 * @param msg Message to prepend the UUIDs
		 */
		private void logSocketUuids(BluetoothSocket socket, String msg)
		{
			if(log.isLoggable(Level.INFO))
			{
				StringBuilder message = new StringBuilder(msg);
				// dump supported UUID's
				message.append(" - UUIDs:");
				ParcelUuid[] uuids = socket.getRemoteDevice().getUuids();
				if(uuids != null)
				{
					for (ParcelUuid uuid: uuids)
					{
						message.append(uuid.getUuid().toString()).append(",");
					}
				}
				else
				{
					message.append("NONE (Invalid BT implementation)");
				}
				log.log(Level.INFO, message.toString());
			}
		}
		
		public void run()
		{
			log.log(Level.INFO, "BEGIN mBtConnectThread SocketType:" + mSocketType);

			// Make a connection to the BluetoothSocket
			try
			{
				log.log(Level.FINE, "Connect BT socket");

				// This is a blocking call and will only return on a
				// successful connection or an exception
				mmSocket.connect();
			}
			catch (IOException e)
			{
				log.log(Level.FINE, e.getMessage());
				cancel();
				
				log.log(Level.INFO, "Fallback attempt to create RfComm socket");
				BluetoothSocket sockFallback;
				Class<?> clazz = mmSocket.getRemoteDevice().getClass();
				Class<?>[] paramTypes = new Class<?>[]{Integer.TYPE};
				try {
					//noinspection JavaReflectionMemberAccess
					Method m = clazz.getMethod("createRfcommSocket", paramTypes);
					Object[] params = new Object[]{1};
					sockFallback = (BluetoothSocket) m.invoke(mmSocket.getRemoteDevice(), params);
					mmSocket = sockFallback;
					
					logSocketUuids(mmSocket, "Fallback socket");
					
					// connect fallback socket
					mmSocket.connect();
				}
				catch (Exception e2)
				{
					log.log(Level.SEVERE, e2.getMessage());
					connectionFailed();
					return;
				}
			}

			// Reset the BtConnectThread because we're done
			synchronized (BtCommService.this)
			{
				mBtConnectThread = null;
			}

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

		synchronized void cancel()
		{
			try
			{
				log.log(Level.INFO, "Closing BT socket");
				mmSocket.close();
			} catch (IOException e)
			{
				log.log(Level.SEVERE, e.getMessage());
			}
		}
	}

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

		BtWorkerThread(BluetoothSocket socket, String socketType)
		{
			log.log(Level.FINE, "create BtWorkerThread: " + socketType);
			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)
			{
				log.log(Level.SEVERE, "temp sockets not created", e);
			}

			mmInStream = tmpIn;
			mmOutStream = tmpOut;
			// set streams
			ser.setStreams(mmInStream, mmOutStream);
		}

		/**
		 * run the main communication loop
		 */
		public void run()
		{
			log.log(Level.INFO, "BEGIN mBtWorkerThread");
			try
			{
				// run the communication thread
				ser.run();
			} catch (Exception ex)
			{
				// Intentionally ignore
                log.log(Level.SEVERE, "Comm thread aborted", ex);
			}
			connectionLost();
		}

		/**
		 * Write to the connected OutStream.
		 *
		 * @param buffer The bytes to write
		 */
		synchronized void write(byte[] buffer)
		{
			ser.writeTelegram(new String(buffer).toCharArray());
		}
		
		synchronized void cancel()
		{
			try
			{
				log.log(Level.INFO, "Closing BT socket");
				mmSocket.close();
			} catch (IOException e)
			{
				log.log(Level.SEVERE, e.getMessage());
			}
		}

	}
}