/*
 * Copyright (c) 2017 Oracle and/or its affiliates.  All rights reserved.
 *
 * This software is dual-licensed to you under the MIT License (MIT) and
 * the Universal Permissive License (UPL).  See the LICENSE file in the root
 * directory for license terms.  You may choose either license, or both.
 */
package com.oracle.iot.sample.obd;

import android.annotation.TargetApi;
import android.app.Activity;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothSocket;
import android.bluetooth.le.BluetoothLeScanner;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanResult;
import android.bluetooth.le.ScanSettings;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
import android.util.Log;
import android.os.AsyncTask;

import java.io.IOException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

/**
 * This class acts as a manager of Bluetooth connections to the OBD device.
 * It has helper functions for scanning and finding the bluetooth device and providing BLE callbacks for clients.
 */
public class BluetoothCommManager {

    private static final String TAG = BluetoothCommManager.class.getName();

    private BluetoothManager mBluetoothManager;
    private BluetoothAdapter mBluetoothAdapter;
    private BluetoothGatt mBluetoothGatt;
    private ObdSensor.IObdDeviceInterface deviceInterface;
    private String mBluetoothDeviceAddress;
    public Activity activity;

    private static final int STATE_DISCONNECTED = 0;
    private static final int STATE_CONNECTING = 1;
    private static final int STATE_CONNECTED = 2;
    private int mConnectionState = STATE_DISCONNECTED;
    private static final UUID BT_OBD_SERIAL_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
    private static final String FREEMATICS_RXTX_UUID = "0000ffe1-0000-1000-8000-00805f9b34fb";

    private static BluetoothCommManager instance = null;

    private BluetoothCommManager() {
    }

    public static BluetoothCommManager getInstance() {
        if (instance == null) {
            instance = new BluetoothCommManager();
        }
        return instance;
    }

    private final static int REQUEST_ENABLE_BT = 1;

    public void setActivity(Activity activity) {
        this.activity = activity;
    }

    public boolean isBluetoothAvailable() {
        // For API level 18 and above, get a reference to BluetoothAdapter through
        PackageManager pm = activity.getPackageManager();
        boolean hasBluetooth = pm.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH) ||
                pm.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE);
        return hasBluetooth;
    }

    /**
     * Initializes a reference to the local Bluetooth adapter.
     *
     * @return Return true if the initialization is successful.
     */
    public boolean initialize() {
        boolean isInitialized = false;
        mBluetoothManager = (BluetoothManager) activity.getSystemService(Context.BLUETOOTH_SERVICE);
        if (mBluetoothManager == null) {
            Log.e(TAG, "Unable to initialize BluetoothManager.");
        } else {
            mBluetoothAdapter = mBluetoothManager.getAdapter();
            if (mBluetoothAdapter == null) {
                Log.e(TAG, "Unable to obtain a BluetoothAdapter.");
            } else if (!mBluetoothAdapter.isEnabled()) {
                isInitialized = mBluetoothAdapter.enable();
                Log.e(TAG, "Bluetooth Adapater initialized?." + isInitialized);
            } else {
                isInitialized = true;
            }
        }
        return isInitialized;
    }

    public boolean connect(final BluetoothDevice bluetoothDevice, ObdSensor.IObdDeviceInterface deviceInterface) {

        this.deviceInterface = deviceInterface;
        String address = bluetoothDevice.getAddress();

        // Previously connected device.  Try to reconnect.
        if (mBluetoothDeviceAddress != null && address.equals(mBluetoothDeviceAddress) && mBluetoothGatt != null) {
            Log.d(TAG, "Trying to use an existing mBluetoothGatt for connection.");
            if (mBluetoothGatt.connect()) {
                mConnectionState = STATE_CONNECTING;
                return true;
            } else {
                return false;
            }
        }

        // We want to directly connect to the device, so we are setting the autoConnect
        // parameter to false.
        mBluetoothGatt = bluetoothDevice.connectGatt(activity, true, mGattCallback);
        Log.d(TAG, "Trying to create a new connection.");
        mBluetoothDeviceAddress = address;
        mConnectionState = STATE_CONNECTING;
        return true;
    }

    private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
        @Override
        public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
            if (newState == BluetoothProfile.STATE_CONNECTED) {
                mConnectionState = STATE_CONNECTED;
                Log.i(TAG, "Connected to GATT server.");
                // Attempts to discover services after successful connection.
                //Log.i(TAG, "Attempting to start service discovery:" +
                //        mBluetoothGatt.discoverServices());
                mBluetoothGatt.discoverServices();

            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                mConnectionState = STATE_DISCONNECTED;
                Log.i(TAG, "Disconnected from GATT server.");
            }
        }

        @Override
        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
            if (status == BluetoothGatt.GATT_SUCCESS) {
                //broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED);
                //deviceInterface.gattServicesDiscovered();
                displayGattServices(getSupportedGattServices());
            } else {
                Log.w(TAG, "onServicesDiscovered received: " + status);
            }
        }

        @Override
        public void onCharacteristicRead(BluetoothGatt gatt,
                                         BluetoothGattCharacteristic characteristic,
                                         int status) {
            if (deviceInterface != null && status == BluetoothGatt.GATT_SUCCESS) {
                //broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic);
                byte data[] = characteristic.getValue();
                if (data != null && data.length > 0) {
                    String strData = new String(data);
                    deviceInterface.obdDataReceived(strData);
                }
            }
        }

        @Override
        public void onCharacteristicChanged(BluetoothGatt gatt,
                                            BluetoothGattCharacteristic characteristic) {
            if (deviceInterface != null) {
                byte data[] = characteristic.getValue();
                if (data != null && data.length > 0) {
                    String strData = new String(data);
                    deviceInterface.obdDataReceived(strData);
                }
            }
        }
    };

    public void disconnect() {
        if (mConnectionState == STATE_CONNECTED) {
            mBluetoothGatt.disconnect();
        }
    }

    private void displayGattServices(List<BluetoothGattService> gattServices) {
        if (gattServices == null) return;
        String uuid = null;
        ArrayList<ArrayList<BluetoothGattCharacteristic>> mGattCharacteristics = new ArrayList<ArrayList<BluetoothGattCharacteristic>>();

        // Loops through available GATT Services.
        for (BluetoothGattService gattService : gattServices) {
            uuid = gattService.getUuid().toString();
            List<BluetoothGattCharacteristic> gattCharacteristics = gattService.getCharacteristics();

            // Loops through available Characteristics.
            for (BluetoothGattCharacteristic gattCharacteristic : gattCharacteristics) {
                uuid = gattCharacteristic.getUuid().toString();
                if (uuid.equals(FREEMATICS_RXTX_UUID)) {
                    setCharacteristicNotification(gattCharacteristic, true);
                    readCharacteristic(gattCharacteristic);
                    break;
                }
            }
        }
    }

    /**
     * Request a read on a given {@code BluetoothGattCharacteristic}. The read result is reported
     * asynchronously through the {@code BluetoothGattCallback#onCharacteristicRead(android.bluetooth.BluetoothGatt, android.bluetooth.BluetoothGattCharacteristic, int)}
     * callback.
     *
     * @param characteristic The characteristic to read from.
     */
    public void readCharacteristic(BluetoothGattCharacteristic characteristic) {
        if (mBluetoothAdapter == null || mBluetoothGatt == null) {
            Log.w(TAG, "BluetoothAdapter not initialized");
            return;
        }
        mBluetoothGatt.readCharacteristic(characteristic);
    }

    /**
     * Request a read on a given {@code BluetoothGattCharacteristic}. The read result is reported
     * asynchronously through the {@code BluetoothGattCallback#onCharacteristicRead(android.bluetooth.BluetoothGatt, android.bluetooth.BluetoothGattCharacteristic, int)}
     * callback.
     *
     * @param characteristic The characteristic to read from.
     */
    public void writeCharacteristic(BluetoothGattCharacteristic characteristic, byte data[]) {
        if (mBluetoothAdapter == null || mBluetoothGatt == null) {
            Log.w(TAG, "BluetoothAdapter not initialized");
            return;
        }
        boolean isSuccess = characteristic.setValue(data);
        if (isSuccess) {
            isSuccess = mBluetoothGatt.writeCharacteristic(characteristic);
            if (isSuccess) {
                Log.w(TAG, "writeCharacteristic success");
            }
        }
    }

    /**
     * Enables or disables notification on a give characteristic.
     *
     * @param characteristic Characteristic to act on.
     * @param enabled        If true, enable notification.  False otherwise.
     */
    public void setCharacteristicNotification(BluetoothGattCharacteristic characteristic,
                                              boolean enabled) {
        if (mBluetoothAdapter == null || mBluetoothGatt == null) {
            Log.w(TAG, "BluetoothAdapter not initialized");
            return;
        }
        mBluetoothGatt.setCharacteristicNotification(characteristic, enabled);

    }

    /**
     * Retrieves a list of supported GATT services on the connected device. This should be
     * invoked only after {@code BluetoothGatt#discoverServices()} completes successfully.
     *
     * @return A {@code List} of supported services.
     */
    public List<BluetoothGattService> getSupportedGattServices() {
        if (mBluetoothGatt == null) return null;

        return mBluetoothGatt.getServices();
    }

    /**
     * Instantiates a BluetoothSocket for the remote device and connects it.
     *
     * @param bluetoothDevice The remote device to connect to over se
     * @return The BluetoothSocket
     * @throws IOException
     */
    public BluetoothSocket connectBtSerial(BluetoothDevice bluetoothDevice) throws IOException {
        BluetoothSocket sock = null;
        BluetoothSocket sockFallback = null;
        boolean isConnected = false;

        Log.d(TAG, "Starting Bluetooth connection..");
        try {
            sock = bluetoothDevice.createRfcommSocketToServiceRecord(BT_OBD_SERIAL_UUID);
            sock.connect();
        } catch (Exception e1) {
            Log.e(TAG, "There was an error while establishing Bluetooth connection. Falling back..", e1);
            Class<?> clazz = sock.getRemoteDevice().getClass();
            Class<?>[] paramTypes = new Class<?>[]{Integer.TYPE};
            try {
                Method m = clazz.getMethod("createRfcommSocket", paramTypes);
                Object[] params = new Object[]{Integer.valueOf(1)};
                sockFallback = (BluetoothSocket) m.invoke(sock.getRemoteDevice(), params);
                sockFallback.connect();
                sock = sockFallback;
                isConnected = true;
            } catch (Exception e2) {
                Log.e(TAG, "Couldn't fallback while establishing Bluetooth connection.", e2);
                throw new IOException(e2.getMessage());
            }
        }
        return sock;
    }

    private static final String FREEMATICS_ONE_DEVICE_NAME = "FREEMATICS_ONE";
    IObdScanResult scanResult;
    boolean isScanning = false;
    ScanTask scanTask = null;


    /**
     * This class provides the Async implementation for BLE scanning to find the Freematics data logger.
     */
    class ScanTask extends AsyncTask<Context, String, String> {

        BluetoothLeScanner bluetoothLeScanner = null;
        private ScanCallback scanCallback = null;
        private BluetoothAdapter.LeScanCallback mLeScanCallback = null;

        private void deviceFound(BluetoothDevice device) {
            if (isScanning) {
                String devname = device.getName();
                if (devname != null && devname.equalsIgnoreCase(FREEMATICS_ONE_DEVICE_NAME)) {
                    isScanning = false;
                    Log.d(TAG,"BLE device found:" + devname);
                    if (scanResult != null) {
                        scanResult.deviceFound(ObdSensor.ObdDeviceType.FREEMATICS, device);
                    }
                }
            }
        }

        @Override
        protected String doInBackground(Context... contexts) {
            // Check if Android vesrion Lollipop or higher for new API.
            if (Build.VERSION.SDK_INT >= 21) {
                bluetoothLeScanner = mBluetoothAdapter.getBluetoothLeScanner();
                scanCallback  = new ScanCallback() {
                    @Override
                    public void onScanResult(int callbackType, ScanResult result) {
                        BluetoothDevice btDevice = result.getDevice();
                        super.onScanResult(callbackType, result);
                        deviceFound(btDevice);
                    }
                };
                bluetoothLeScanner.startScan(scanCallback);
            } else {
                mLeScanCallback =  new BluetoothAdapter.LeScanCallback() {
                    @Override
                    public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
                        deviceFound(device);
                    }
                };
                mBluetoothAdapter.startLeScan(mLeScanCallback);
            }
            return null;
        }

        private void stopScan() {
            if (Build.VERSION.SDK_INT >= 21) {
                if (scanCallback != null) {
                    bluetoothLeScanner.stopScan(scanCallback);
                }
            } else {
                if (mLeScanCallback != null) {
                    mBluetoothAdapter.stopLeScan(mLeScanCallback);
                }
            }
        }
    }

    public void scanForFreematicsDevice(IObdScanResult obdScanResult) {
        this.scanResult = obdScanResult;
        isScanning = true;
        scanTask = new ScanTask();
        scanTask.execute();
    }

    public void stopScanning() {
        isScanning = false;
        if (scanTask != null) {
            scanTask.stopScan();
        }
    }

    public interface IObdScanResult {
        public void deviceFound(ObdSensor.ObdDeviceType type, BluetoothDevice device);
        public void  scanFinished();
    }
}