/*
 BleDeviceDetector
 Copyright (c) 2015 NTT DOCOMO,INC.
 Released under the MIT license
 http://opensource.org/licenses/mit-license.php
 */
package org.deviceconnect.android.deviceplugin.heartrate.ble;

import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.os.Build;
import android.os.Handler;
import android.util.Log;

import org.deviceconnect.android.deviceplugin.heartrate.BuildConfig;
import org.deviceconnect.android.deviceplugin.heartrate.ble.adapter.NewBleDeviceAdapterImpl;
import org.deviceconnect.android.deviceplugin.heartrate.ble.adapter.OldBleDeviceAdapterImpl;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

/**
 * This class to detect the BLE device.
 *
 * @author NTT DOCOMO, INC.
 */
public class BleDeviceDetector {
    /**
     * Tag for debugging.
     */
    private static final String TAG = "Ble";

    /**
     * Instance of ScheduledExecutorService.
     */
    private ScheduledExecutorService mExecutor = Executors.newSingleThreadScheduledExecutor();

    /**
     * ScheduledFuture of scan timer.
     */
    private ScheduledFuture<?> mScanTimerFuture;

    /**
     * Defines a delay 1 second at first execution.
     */
    private static final long SCAN_FIRST_WAIT_PERIOD = 1000;

    /**
     * Defines a period 10 seconds between successive executions.
     */
    private static final long SCAN_WAIT_PERIOD = 10 * 1000;

    /**
     * Stops scanning after 1 second.
     */
    private static final long SCAN_PERIOD = 2000;

    private Context mContext;
    private BleDeviceAdapter mBleAdapter;

    private Handler mHandler = new Handler();
    private boolean mScanning;
    private BleDeviceDiscoveryListener mListener;
    private List<BluetoothDevice> mDevices = new CopyOnWriteArrayList<>();

    private BleDeviceAdapterFactory mFactory;

    /**
     * Constructor.
     *
     * @param context context of this application
     */
    public BleDeviceDetector(final Context context) {
        this(context, new BleDeviceAdapterFactory() {
            @Override
            public BleDeviceAdapter createAdapter(Context context) {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                    return new NewBleDeviceAdapterImpl(context);
                } else {
                    return new OldBleDeviceAdapterImpl(context);
                }
            }
        });
    }

    /**
     * Constructor.
     *
     * @param context context of this application
     * @param factory factory class
     */
    public BleDeviceDetector(final Context context, final BleDeviceAdapterFactory factory) {
        mContext = context;
        mFactory = factory;
        initialize();
    }

    /**
     * Get a context.
     *
     * @return context
     */
    public Context getContext() {
        return mContext;
    }

    /**
     * Initialize the BleDeviceDetector.
     */
    public synchronized void initialize() {
        if (mBleAdapter == null) {
            mBleAdapter = mFactory.createAdapter(mContext);
        }
    }

    /**
     * Get a state of scan.
     *
     * @return true if scanning a BLE
     */
    public boolean isScanning() {
        return mScanning;
    }

    /**
     * Return true if Bluetooth is currently enabled and ready for use.
     *
     * @return true if the local adapter is turned on
     */
    public boolean isEnabled() {
        if (mBleAdapter == null) {
            return false;
        }

        if (!BleUtils.isBLEPermission(getContext())) {
            return false;
        }

        return mBleAdapter.isEnabled();
    }

    /**
     * Sets a BluetoothDiscoveryListener.
     *
     * @param listener Notify that discovered the bluetooth device
     */
    public void setListener(final BleDeviceDiscoveryListener listener) {
        mListener = listener;
    }

    /**
     * Start Bluetooth LE scan.
     */
    public void startScan() {
        if (isEnabled()) {
            scanLeDevice(true);
        }
    }

    /**
     * Stop Bluetooth LE scan.
     */
    public void stopScan() {
        if (isEnabled()) {
            scanLeDevice(false);
        }
    }

    /**
     * Gets a BluetoothDevice from address.
     *
     * @param address bluetooth address
     * @return instance of BluetoothDevice, null if not found device
     */
    public BluetoothDevice getDevice(final String address) {
        if (mBleAdapter == null || !mBleAdapter.isEnabled()) {
            return null;
        }
        return mBleAdapter.getDevice(address);
    }

    /**
     * Gets the set of BluetoothDevice that are bonded (paired) to the local adapter.
     *
     * @return set of BluetoothDevice, or null on error
     */
    public Set<BluetoothDevice> getBondedDevices() {
        if (mBleAdapter == null || !mBleAdapter.isEnabled()) {
            return null;
        }
        return mBleAdapter.getBondedDevices();
    }

    public boolean checkBluetoothAddress(final String address) {
        return mBleAdapter.checkBluetoothAddress(address);
    }

    /**
     * Scan a BLE only once.
     * @param listener listener
     */
    public synchronized void scanLeDeviceOnce(final BleDeviceDiscoveryListener listener) {
        final List<BluetoothDevice> devices = new ArrayList<>();
        final BleDeviceAdapter.BleDeviceScanCallback callback =
                new BleDeviceAdapter.BleDeviceScanCallback() {
                    @Override
                    public void onLeScan(final BluetoothDevice device, final int rssi) {
                        devices.add(device);
                    }

                    @Override
                    public void onFail() {

                    }
                };
        mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                if (mBleAdapter.isEnabled()) {
                    mBleAdapter.stopScan(callback);
                    listener.onDiscovery(devices);
                }
            }
        }, SCAN_PERIOD);
        mBleAdapter.startScan(callback);
    }

    /**
     * Sets the scan state of BLE.
     *
     * @param enable Start the scan if enable is true. Stop the scan if enable is false
     */
    private synchronized void scanLeDevice(final boolean enable) {

        if (mBleAdapter == null || !mBleAdapter.isEnabled()) {
            return;
        }

        if (enable) {
            if (mScanning || mScanTimerFuture != null) {
                // scan have already started.
                return;
            }
            mScanning = true;
            mScanTimerFuture = mExecutor.scheduleAtFixedRate(new Runnable() {
                @Override
                public void run() {
                    // Stops scanning after a pre-defined scan period.
                    mHandler.postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            if (mBleAdapter.isEnabled()) {
                                stopBleScan();
                                notifyBluetoothDevice();
                            } else {
                                cancelScanTimer();
                            }
                        }
                    }, SCAN_PERIOD);
                    if (mBleAdapter.isEnabled()) {
                        mDevices.clear();
                        startBleScan();
                    } else {
                        cancelScanTimer();
                    }
                }
            }, SCAN_FIRST_WAIT_PERIOD, SCAN_WAIT_PERIOD, TimeUnit.MILLISECONDS);
        } else {
            mScanning = false;
            stopBleScan();
            cancelScanTimer();
        }
    }

    /**
     * Stopped the scan timer.
     */
    private synchronized void cancelScanTimer() {
        if (mScanTimerFuture != null) {
            mScanTimerFuture.cancel(true);
            mScanTimerFuture = null;
        }
    }

    /**
     * Starts a BLE scan.
     */
    private void startBleScan() {
        try {
            mBleAdapter.startScan(mScanCallback);
        } catch (Exception e) {
            // Exception occurred when the BLE state is invalid.
            if (BuildConfig.DEBUG) {
                Log.e(TAG, "", e);
            }
        }
    }

    /**
     * Stops a BLE scan.
     */
    private void stopBleScan() {
        try {
            mBleAdapter.stopScan(mScanCallback);
        } catch (Exception e) {
            // Exception occurred when the BLE state is invalid.
            if (BuildConfig.DEBUG) {
                Log.e(TAG, "", e);
            }
        }
    }

    /**
     * Notify that all bluetooth device was discovered.
     */
    private void notifyBluetoothDevice() {
        if (mListener != null) {
            mListener.onDiscovery(mDevices);
        }
    }

    /**
     * Implement the BluetoothAdapter.
     */
    private final BleDeviceAdapter.BleDeviceScanCallback mScanCallback =
            new BleDeviceAdapter.BleDeviceScanCallback() {
                @Override
                public void onLeScan(final BluetoothDevice device, final int rssi) {
                    if (!mDevices.contains(device)) {
                        mDevices.add(device);
                    }
                }
                @Override
                public void onFail() {
                    if (BuildConfig.DEBUG) {
                        Log.w(TAG, "Failed to scan the ble.");
                    }
                }
            };

    /**
     * This listener to be notified when discovered the BLE device.
     */
    public static interface BleDeviceDiscoveryListener {
        /**
         * Discovered the BLE device.
         *
         * @param devices BLE device list
         */
        void onDiscovery(List<BluetoothDevice> devices);
    }
}
