/*
 * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file is distributed
 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
 * express or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */

package software.amazon.freertos.amazonfreertossdk;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.le.BluetoothLeScanner;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanFilter;
import android.bluetooth.le.ScanResult;
import android.bluetooth.le.ScanSettings;
import android.content.Context;
import android.os.Handler;
import android.os.HandlerThread;

import java.security.KeyStore;
import java.util.Arrays;

import android.os.ParcelUuid;
import android.util.Log;

import com.amazonaws.auth.AWSCredentialsProvider;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import lombok.NonNull;

import static software.amazon.freertos.amazonfreertossdk.AmazonFreeRTOSConstants.*;

public class AmazonFreeRTOSManager {

    private static final String TAG = "AmazonFreeRTOSManager";
    private Context mContext;

    private Handler mScanHandler;
    private HandlerThread mScanHandlerThread;
    private boolean mScanning = false;

    private BluetoothAdapter mBluetoothAdapter;
    private BluetoothLeScanner mBluetoothLeScanner;
    private List<ScanFilter> mScanFilters = Arrays.asList(
            new ScanFilter.Builder().setServiceUuid(
                    new ParcelUuid(UUID.fromString(UUID_AmazonFreeRTOS))).build());

    private Map<String, AmazonFreeRTOSDevice> mAFreeRTOSDevices = new HashMap<>();

    private BleScanResultCallback mBleScanResultCallback;

    /**
     * Construct an AmazonFreeRTOSManager instance.
     *
     * @param context          The app context. Should be passed in by the app that creates a new instance
     *                         of AmazonFreeRTOSManager.
     * @param bluetoothAdapter BluetoothAdaptor passed in by the app.
     */
    public AmazonFreeRTOSManager(Context context, BluetoothAdapter bluetoothAdapter) {
        mContext = context;
        mBluetoothAdapter = bluetoothAdapter;
    }

    /**
     * Setting the criteria for which exact the BLE devices to scan for. This overrides the default
     * mScanFilters which is by default set to scan for UUID_AmazonFreeRTOS.
     *
     * @param filters The list of ScanFilter for BLE devices.
     */
    public void setScanFilters(List<ScanFilter> filters) {
        mScanFilters = filters;
    }

    /**
     * Start scanning of nearby BLE devices. It filters the scan result only with AmazonFreeRTOS
     * service UUID unless setScanFilters was explicitly called. It keeps scanning for a period of
     * AmazonFreeRTOSConstants.class#SCAN_PERIOD ms, then stops the scanning automatically.
     * The scan result is passed back through the BleScanResultCallback. If at the time of calling
     * this API, there's already an ongoing scanning, then this will return immediately without
     * starting another scan.
     *
     * @param scanResultCallback The callback to notify the calling app of the scanning result. The
     *                           callback will be triggered, every time it finds a BLE device
     *                           nearby that meets the ScanFilter criteria.
     */
    public void startScanDevices(final BleScanResultCallback scanResultCallback) {
        startScanDevices(scanResultCallback, SCAN_PERIOD);
    }

    /**
     * Start scanning nearby BLE devices for a total duration of scanDuration milliseconds.
     *
     * @param scanResultCallback The callback to notify the calling app of the scanning result.
     * @param scanDuration       The duration of scanning. Keep scanning if 0.
     */
    public void startScanDevices(final BleScanResultCallback scanResultCallback, long scanDuration) {
        if (scanResultCallback == null) {
            throw new IllegalArgumentException("BleScanResultCallback is null");
        }
        mBleScanResultCallback = scanResultCallback;
        if (mBluetoothAdapter != null) {
            mBluetoothLeScanner = mBluetoothAdapter.getBluetoothLeScanner();
            if (mScanHandlerThread == null) {
                mScanHandlerThread = new HandlerThread("ScanBleDeviceThread");
                mScanHandlerThread.start();
                mScanHandler = new Handler(mScanHandlerThread.getLooper());
            }
            scanLeDevice(scanDuration);
        } else {
            Log.e(TAG, "BluetoothAdaptor is null, please enable bluetooth.");
        }
    }

    private void scanLeDevice(long duration) {
        if (mScanning) {
            Log.d(TAG, "Scanning is already in progress.");
            return;
        }
        // Stops scanning after a pre-defined scan period.
        if (duration != 0) {
            mScanHandler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    stopScanDevices();
                }
            }, duration);
        }
        Log.i(TAG, "Starting ble device scan");
        mScanning = true;

        ScanSettings scanSettings = new ScanSettings.Builder().build();
        mBluetoothLeScanner.startScan(mScanFilters, scanSettings, mScanCallback);
    }

    /**
     * Stop scanning of nearby BLE devices. If there's no ongoing BLE scanning, then it will return
     * immediately.
     */
    public void stopScanDevices() {
        if (!mScanning) {
            Log.w(TAG, "No ble device scan is currently in progress.");
            return;
        }
        Log.i(TAG, "Stopping ble device scan");
        mBluetoothLeScanner.stopScan(mScanCallback);
        mScanning = false;
    }

    private ScanCallback mScanCallback = new ScanCallback() {
        @Override
        public void onScanResult(int callbackType, ScanResult result) {
            Log.d(TAG, "Found ble device: " + result.getDevice().getAddress()
                    + " RSSI: " + result.getRssi());
            if (mBleScanResultCallback != null) {
                mBleScanResultCallback.onBleScanResult(result);
            }
        }

        @Override
        public void onScanFailed(int errorCode) {
            Log.e(TAG, "Error when scanning ble device. Error code: " + errorCode);
            if (mBleScanResultCallback != null) {
                mBleScanResultCallback.onBleScanFailed(errorCode);
            }
        }
    };

    /**
     * Connect to the BLE device, and notify the connection state via BleConnectionStatusCallback.
     *
     * @param connectionStatusCallback The callback to notify app whether the BLE connection is
     *                                 successful. Must not be null.
     * @param btDevice                 the BLE device to be connected to.
     * @param cp                       the AWSCredential used to connect to AWS IoT.
     * @param autoReconnect            auto reconnect to device after unexpected disconnect
     */
    public AmazonFreeRTOSDevice connectToDevice(@NonNull final BluetoothDevice btDevice,
                                                @NonNull final BleConnectionStatusCallback connectionStatusCallback,
                                                final AWSCredentialsProvider cp,
                                                final boolean autoReconnect) {
        AmazonFreeRTOSDevice aDevice = new AmazonFreeRTOSDevice(btDevice, mContext, cp);
        mAFreeRTOSDevices.put(btDevice.getAddress(), aDevice);
        aDevice.connect(connectionStatusCallback, autoReconnect);
        return aDevice;
    }

    /**
     * Connect to the BLE device, and notify the connection state via BleConnectionStatusCallback.
     *
     * @param connectionStatusCallback The callback to notify app whether the BLE connection is
     *                                 successful. Must not be null.
     * @param btDevice                 the BLE device to be connected to.
     * @param ks                       the KeyStore that contains certificate used to connect to AWS IoT.
     * @param autoReconnect            auto reconnect to device after unexpected disconnect
     */
    public AmazonFreeRTOSDevice connectToDevice(@NonNull final BluetoothDevice btDevice,
                                                @NonNull final BleConnectionStatusCallback connectionStatusCallback,
                                                final KeyStore ks,
                                                final boolean autoReconnect) {
        AmazonFreeRTOSDevice aDevice = new AmazonFreeRTOSDevice(btDevice, mContext, ks);
        mAFreeRTOSDevices.put(btDevice.getAddress(), aDevice);
        aDevice.connect(connectionStatusCallback, autoReconnect);
        return aDevice;
    }

    /**
     * Closing BLE connection for the AmazonFreeRTOSDevice, reset all variables, and disconnect from AWS IoT.
     *
     * @param aDevice The AmazonFreeRTOSDevice to be disconnected.
     */
    public void disconnectFromDevice(@NonNull final AmazonFreeRTOSDevice aDevice) {
        mAFreeRTOSDevices.remove(aDevice.getMBluetoothDevice().getAddress());
        aDevice.disconnect();
    }

    /**
     * Get the instance of AmazonFreeRTOSDevice given the mac address of the BLE device
     *
     * @param macAddr the mac address of the connected BLE device
     * @return the corresponding AmazonFreeRTOSDevice instance that represents the connected BLE device.
     */
    public AmazonFreeRTOSDevice getConnectedDevice(String macAddr) {
        return mAFreeRTOSDevices.get(macAddr);
    }
}