package com.adafruit.bluefruit.le.connect.ble.central;

import android.os.ParcelUuid;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.util.Log;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.ListIterator;

import no.nordicsemi.android.support.v18.scanner.BluetoothLeScannerCompat;
import no.nordicsemi.android.support.v18.scanner.ScanCallback;
import no.nordicsemi.android.support.v18.scanner.ScanFilter;
import no.nordicsemi.android.support.v18.scanner.ScanRecord;
import no.nordicsemi.android.support.v18.scanner.ScanResult;
import no.nordicsemi.android.support.v18.scanner.ScanSettings;

public class BleScanner {
    // Config
    private static final int kScanReportDelay = 500;     // in milliseconds

    // Constants
    private final static String TAG = BleScanner.class.getSimpleName();

    // Singleton
    private static BleScanner mInstance = null;

    // Interfaces
    public interface BleScannerListener {
        void onScanPeripheralsUpdated(List<BlePeripheral> scanResults);

        void onScanPeripheralsFailed(int errorCode);

        void onScanStatusChanged(boolean isScanning);
    }

    // Data
    private WeakReference<BleScannerListener> mWeakListener;
    private List<BlePeripheral> mPeripheralScanResults = new ArrayList<>();
    private List<ScanFilter> mScanFilters = new ArrayList<>();
    private boolean mIsScanning;

    private ScanCallback mScanCallback = new ScanCallback() {
        public void onScanResult(int callbackType, @NonNull ScanResult result) {
            peripheralDiscovered(result);

            BleScannerListener listener = mWeakListener.get();
            if (listener != null) {
                listener.onScanPeripheralsUpdated(mPeripheralScanResults);
            }
        }

        public void onBatchScanResults(List<ScanResult> results) {
            for (ScanResult result : results) {
                peripheralDiscovered(result);
            }

            BleScannerListener listener = mWeakListener.get();
            if (listener != null) {
                //Log.d(TAG, "mPeripheralScanResults: " + mPeripheralScanResults.size());
                listener.onScanPeripheralsUpdated(mPeripheralScanResults);
            }
        }

        public void onScanFailed(int errorCode) {
            BleScannerListener listener = mWeakListener.get();
            if (listener != null) {
                listener.onScanPeripheralsFailed(errorCode);
            }
            stop();
        }
    };

    // region Setup
    public static BleScanner getInstance() {
        if (mInstance == null) {
            mInstance = new BleScanner();
        }
        return mInstance;
    }

    private BleScanner() {
        mIsScanning = false;
    }

    public BleScannerListener getListener() {
        return mWeakListener.get();
    }

    public void setListener(BleScannerListener listener) {
        mWeakListener = new WeakReference<>(listener);
    }

    // endregion

    // region Properties
    public boolean isScanning() {
        return mIsScanning;
    }

    public @NonNull
    List<BlePeripheral> getConnectedPeripherals() {
        List<BlePeripheral> connectedPeripherals = new ArrayList<>();
        for (BlePeripheral blePeripheral : mPeripheralScanResults) {
            final int state = blePeripheral.getConnectionState();
            if (state == BlePeripheral.STATE_CONNECTED) {
                connectedPeripherals.add(blePeripheral);
            }
        }

        return connectedPeripherals;
    }

    @SuppressWarnings("WeakerAccess")
    public @NonNull
    List<BlePeripheral> getConnectedOrConnectingPeripherals() {
        List<BlePeripheral> connectedPeripherals = new ArrayList<>();
        for (BlePeripheral blePeripheral : mPeripheralScanResults) {
            final int state = blePeripheral.getConnectionState();
            if (state == BlePeripheral.STATE_CONNECTED || state == BlePeripheral.STATE_CONNECTING) {
                connectedPeripherals.add(blePeripheral);
            }
        }

        return connectedPeripherals;
    }

    public void disconnectFromAll() {
        List<BlePeripheral> connectedPeriperals = getConnectedOrConnectingPeripherals();

        Log.d(TAG, "disconnectFromAll. Number of connected: " + connectedPeriperals.size());
        for (BlePeripheral blePeripheral : connectedPeriperals) {
            blePeripheral.disconnect();
        }
    }

    @Nullable
    public BlePeripheral getPeripheralWithIdentifier(@Nullable String identifier) {
        if (identifier == null) {
            return null;
        }

        int i = 0;
        BlePeripheral foundPeripheral = null;
        while (i < mPeripheralScanResults.size() && foundPeripheral == null) {
            BlePeripheral blePeripheral = mPeripheralScanResults.get(i);
            if (blePeripheral.getIdentifier().equals(identifier)) {
                foundPeripheral = blePeripheral;
            }
            i++;
        }

        return foundPeripheral;
    }

    // endregion

    // region Actions
    public void start() {
        startWithFilters(null);
    }

    @SuppressWarnings("unused")
    public void startFilteringServiceUuid(ParcelUuid uuid) {

        List<ScanFilter> scanFilters = new ArrayList<>();
        if (uuid != null) {
            scanFilters.add(new ScanFilter.Builder().setServiceUuid(uuid).build());
        }

        startWithFilters(scanFilters);
    }

    //     @RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION})
    private synchronized void startWithFilters(@Nullable List<ScanFilter> filters) {
        if (!mIsScanning) {
            BluetoothLeScannerCompat scanner = BluetoothLeScannerCompat.getScanner();
            ScanSettings settings = new ScanSettings.Builder()
                    .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).setReportDelay(kScanReportDelay)
                    .setUseHardwareBatchingIfSupported(false).build();
            mScanFilters = filters;
            try {
                scanner.startScan(mScanFilters, settings, mScanCallback);
                mIsScanning = true;
                BleScannerListener listener = mWeakListener.get();
                if (listener != null) {
                    listener.onScanStatusChanged(true);
                }
            } catch (IllegalStateException e) {     // Exception if the BT adapter is not on
                Log.d(TAG, "startWithFilters illegalStateExcpetion" + e.getMessage());
            }
        }
    }

    public synchronized void stop() {
        if (mIsScanning) {
            BluetoothLeScannerCompat scanner = BluetoothLeScannerCompat.getScanner();
            scanner.stopScan(mScanCallback);
            mIsScanning = false;
            BleScannerListener listener = mWeakListener.get();
            if (listener != null) {
                listener.onScanStatusChanged(false);
            }
        }
    }

    public synchronized void refresh() {
        stop();

        // Don't remove connnected or connecting peripherals
        for (ListIterator<BlePeripheral> listIterator = mPeripheralScanResults.listIterator(); listIterator.hasNext(); ) {
            BlePeripheral blePeripheral = listIterator.next();
            if (blePeripheral.getConnectionState() == BlePeripheral.STATE_DISCONNECTED) {
                listIterator.remove();
            }
        }

        startWithFilters(mScanFilters);

        BleScannerListener listener = mWeakListener.get();
        if (listener != null) {
            listener.onScanPeripheralsUpdated(mPeripheralScanResults);
        }
    }
    // endregion

    // region

    private void peripheralDiscovered(@NonNull ScanResult result) {
        // Check that the device was not previously found
        final String resultAddress = result.getDevice().getAddress();

        int index = 0;
        boolean found = false;
        while (index < mPeripheralScanResults.size() && !found) {
            if (mPeripheralScanResults.get(index).getIdentifier().equals(resultAddress)) {
                found = true;
            } else {
                index++;
            }
        }

        if (found) {
            // Replace existing record
            mPeripheralScanResults.get(index).replaceScanResult(result);
        } else {
            // Add mew record
            BlePeripheral blePeripheral = new BlePeripheral(result);
            mPeripheralScanResults.add(blePeripheral);
        }
    }

    // endregion

    // region Utils
    @SuppressWarnings("WeakerAccess")
    public static final int kDeviceType_Unknown = 0;
    public static final int kDeviceType_Uart = 1;
    public static final int kDeviceType_Beacon = 2;
    public static final int kDeviceType_UriBeacon = 3;

    public static int getDeviceType(@NonNull BlePeripheral blePeripheral) {
        int type = kDeviceType_Unknown;

        ScanRecord scanRecord = blePeripheral.getScanRecord();
        if (scanRecord != null) {
            byte[] advertisedData = scanRecord.getBytes();

            // Check if is an iBeacon ( 0x02, 0x01, a flag byte, 0x1A, 0xFF, manufacturer (2bytes), 0x02, 0x15)
            final boolean isBeacon = advertisedData != null && advertisedData.length > 8 && advertisedData[0] == 0x02 && advertisedData[1] == 0x01 && advertisedData[3] == 0x1A && advertisedData[4] == (byte) 0xFF && advertisedData[7] == 0x02 && advertisedData[8] == 0x15;
            if (isBeacon) {
                type = kDeviceType_Beacon;
            } else {
                // Check if is an URIBeacon
                final byte[] kUriBeaconPrefix = {0x03, 0x03, (byte) 0xD8, (byte) 0xFE};
                final boolean isUriBeacon = advertisedData != null && advertisedData.length > 7 && Arrays.equals(Arrays.copyOf(advertisedData, kUriBeaconPrefix.length), kUriBeaconPrefix) && advertisedData[5] == 0x16 && advertisedData[6] == kUriBeaconPrefix[2] && advertisedData[7] == kUriBeaconPrefix[3];

                if (isUriBeacon) {
                    type = kDeviceType_UriBeacon;
                } else {
                    // Check if Uart is contained in the uuids
                    boolean isUart = false;
                    List<ParcelUuid> serviceUuids = scanRecord.getServiceUuids();
                    if (serviceUuids != null) {
                        ParcelUuid uartUuid = ParcelUuid.fromString("6e400001-b5a3-f393-e0a9-e50e24dcca9e");
                        for (ParcelUuid serviceUuid : serviceUuids) {
                            if (serviceUuid.equals(uartUuid)) {
                                isUart = true;
                                break;
                            }
                        }
                    }

                    if (isUart) {
                        type = kDeviceType_Uart;
                    }
                }
            }
        }

        return type;
    }

    // endregion
}