package com.idevicesinc.sweetblue.compat;


import android.annotation.TargetApi;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.le.AdvertiseCallback;
import android.bluetooth.le.AdvertiseData;
import android.bluetooth.le.AdvertiseSettings;
import android.bluetooth.le.BluetoothLeAdvertiser;
import android.bluetooth.le.BluetoothLeScanner;
import android.bluetooth.le.ScanFilter;
import android.bluetooth.le.ScanSettings;
import android.os.Build;
import android.util.Log;
import com.idevicesinc.sweetblue.BleAdvertisingSettings;
import com.idevicesinc.sweetblue.BleDevice;
import com.idevicesinc.sweetblue.BleManager;
import com.idevicesinc.sweetblue.utils.Interval;
import java.util.ArrayList;
import java.util.List;


@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class L_Util
{

    private L_Util() {}


    public interface ScanCallback
    {
        void onScanResult(int callbackType, ScanResult result);
        void onBatchScanResults(List<ScanResult> results);
        void onScanFailed(int errorCode);
    }

    public interface AdvertisingCallback
    {
        void onStartSuccess(BleAdvertisingSettings settings);
        void onStartFailure(int errorCode);
    }

    public static class ScanResult
    {
        private BluetoothDevice device;
        private int rssi;
        private byte[] record;

        public BluetoothDevice getDevice() {
            return device;
        }

        public int getRssi() {
            return rssi;
        }

        public byte[] getRecord() {
            return record;
        }
    }

    private static ScanCallback m_UserScanCallback;
    private static AdvertisingCallback m_userAdvCallback;

    private static android.bluetooth.le.ScanCallback m_callback = new android.bluetooth.le.ScanCallback()
    {
        @Override public void onScanResult(int callbackType, android.bluetooth.le.ScanResult result)
        {
            if (m_UserScanCallback != null) {
                m_UserScanCallback.onScanResult(callbackType, toLScanResult(result));
            }
        }

        @Override public void onBatchScanResults(List<android.bluetooth.le.ScanResult> results)
        {
            if (m_UserScanCallback != null) {
                m_UserScanCallback.onBatchScanResults(toLScanResults(results));
            }
        }

        @Override public void onScanFailed(int errorCode)
        {
            if (m_UserScanCallback != null) {
                m_UserScanCallback.onScanFailed(errorCode);
            }
        }
    };

    private static final AdvertiseCallback m_nativeAdvertiseCallback = new AdvertiseCallback()
    {
        @Override
        public void onStartSuccess(AdvertiseSettings settingsInEffect)
        {
            if (m_userAdvCallback != null)
            {
                m_userAdvCallback.onStartSuccess(fromNativeSettings(settingsInEffect));
            }
        }

        @Override
        public void onStartFailure(int errorCode)
        {
            if (m_userAdvCallback != null)
            {
                m_userAdvCallback.onStartFailure(errorCode);
            }
        }
    };

    public static android.bluetooth.le.ScanCallback getNativeScanCallback() {
        return m_callback;
    }

    public static AdvertiseCallback getNativeAdvertisingCallback()
    {
        return m_nativeAdvertiseCallback;
    }

    static void setAdvCallback(AdvertisingCallback callback)
    {
        m_userAdvCallback = callback;
    }


    public static BleAdvertisingSettings fromNativeSettings(AdvertiseSettings settings)
    {
        return new BleAdvertisingSettings(BleAdvertisingSettings.BleAdvertisingMode.fromNative(settings.getMode()),
                BleAdvertisingSettings.BleTransmissionPower.fromNative(settings.getTxPowerLevel()),
                Interval.millis(settings.getTimeout()));
    }


    // TODO - Remove this in version 3.0
    @Deprecated
    public static boolean requestMtu(BleDevice device, int mtu) {
        return device.getNativeGatt().requestMtu(mtu);
    }

    public static boolean requestMtu(BluetoothGatt gatt, int mtu) {
        return gatt.requestMtu(mtu);
    }

    // TODO - Remove this for version 3.0
    @Deprecated
    public static boolean isAdvertisingSupportedByChipset(BleManager mgr) {
        return mgr.getNativeAdapter().isMultipleAdvertisementSupported();
    }

    public static boolean isAdvertisingSupportedByChipset(BluetoothAdapter adapter) {
        return adapter.isMultipleAdvertisementSupported();
    }

    public static BluetoothLeAdvertiser getBluetoothLeAdvertiser(BluetoothAdapter adapter)
    {
        return adapter.getBluetoothLeAdvertiser();
    }

    // TODO - Remove this for version 3.0
    @Deprecated
    public static void stopNativeScan(BleManager mgr) {
        mgr.getNativeAdapter().getBluetoothLeScanner().stopScan(m_callback);
    }

    public static void stopNativeScan(BluetoothAdapter adapter) {
        if (adapter == null)
        {
            Log.e("ScanManager", "Tried to stop the scan, but the Bluetooth Adapter instance was null!");
            return;
        }

        final BluetoothLeScanner scanner = adapter.getBluetoothLeScanner();
        if (scanner != null)
            scanner.stopScan(m_callback);
        else
            Log.w("ScanManager", "Tried to stop the scan, but the BluetoothLeScanner instance was null. This implies the scanning has stopped already.");
    }


    // TODO - Remove this for version 3.0
    @Deprecated
    public static boolean requestConnectionPriority(BleDevice device, int mode)
    {
        return requestConnectionPriority(device.getNativeGatt(), mode);
    }

    public static boolean requestConnectionPriority(BluetoothGatt gatt, int mode) {
        return gatt.requestConnectionPriority(mode);
    }

    public static void startNativeScan(BluetoothAdapter adapter, int scanMode, Interval scanReportDelay, ScanCallback listener) {

        final ScanSettings settings = buildSettings(adapter, scanMode, scanReportDelay).build();

        startScan(adapter, settings, listener);
    }

    // TODO - Remove this in version 3.0
    @Deprecated
    public static void startNativeScan(BleManager mgr, int scanMode, Interval scanReportDelay, ScanCallback listener) {

        final ScanSettings settings = buildSettings(mgr, scanMode, scanReportDelay).build();

        startScan(mgr, settings, listener);
    }

    // TODO - Remove this in version 3.0
    @Deprecated
    static ScanSettings.Builder buildSettings(BleManager mgr, int scanMode, Interval scanReportDelay) {
        final ScanSettings.Builder builder = new ScanSettings.Builder();
        builder.setScanMode(scanMode);

        if( mgr.getNativeAdapter().isOffloadedScanBatchingSupported() )
        {
            final long scanReportDelay_millis = false == Interval.isDisabled(scanReportDelay) ? scanReportDelay.millis() : 0;
            builder.setReportDelay(scanReportDelay_millis);
        }
        else
        {
            builder.setReportDelay(0);
        }
        return builder;
    }

    // TODO - Remove this in version 3.0
    @Deprecated
    static void startScan(BleManager mgr, ScanSettings scanSettings, ScanCallback listener) {
        m_UserScanCallback = listener;
        mgr.getNativeAdapter().getBluetoothLeScanner().startScan(getFilterList(), scanSettings, m_callback);
    }

    static ScanSettings.Builder buildSettings(BluetoothAdapter adapter, int scanMode, Interval scanReportDelay) {
        final ScanSettings.Builder builder = new ScanSettings.Builder();
        builder.setScanMode(scanMode);

        if( adapter.isOffloadedScanBatchingSupported() )
        {
            final long scanReportDelay_millis = false == Interval.isDisabled(scanReportDelay) ? scanReportDelay.millis() : 0;
            builder.setReportDelay(scanReportDelay_millis);
        }
        else
        {
            builder.setReportDelay(0);
        }
        return builder;
    }

    static void startScan(BluetoothAdapter adapter, ScanSettings scanSettings, ScanCallback listener) {
        m_UserScanCallback = listener;
        // Add a last ditch check to make sure the adapter isn't null before trying to start the scan.
        // We check in the task, but by the time we reach this method, it could have been shut off
        // Either the adapter, or the scanner object may be null, so we check it here
        if (adapter == null || adapter.getBluetoothLeScanner() == null)
        {
            m_callback.onScanFailed(android.bluetooth.le.ScanCallback.SCAN_FAILED_INTERNAL_ERROR);
            return;
        }
        adapter.getBluetoothLeScanner().startScan(getFilterList(), scanSettings, m_callback);
    }

    public static boolean startAdvertising(BluetoothAdapter adapter, AdvertiseSettings settings, AdvertiseData adData, AdvertisingCallback callback)
    {
        final BluetoothLeAdvertiser adv = adapter.getBluetoothLeAdvertiser();
        if (adv == null)
            return false;

        m_userAdvCallback = callback;
        adv.startAdvertising(settings, adData, m_nativeAdvertiseCallback);
        return true;
    }

    public static void stopAdvertising(BluetoothAdapter adapter)
    {
        if (adapter != null)
        {
            final BluetoothLeAdvertiser adv = adapter.getBluetoothLeAdvertiser();
            if (adv != null)
            {
                adv.stopAdvertising(m_nativeAdvertiseCallback);
            }
        }
    }




    private static List<ScanFilter> getFilterList()
    {
        // A change in Android 8.1 made it so that if you run an "unfiltered" scan, you will not receive scan results when the screen is off
        // This is a hack to ensure we still get results back when the screen is off. We may have to monitor this in the future, if they
        // start checking the filter instances themselves (right now, they simply check that the list isn't null).
        // Gating this with and SDK version check, as we know it was introduced in API 27, and we're unsure of how this will affect lower versions
        if (Build.VERSION.SDK_INT >= 27)
        {
            final ScanFilter sf = new ScanFilter.Builder().build();
            final List<ScanFilter> list = new ArrayList<>(1);
            list.add(sf);
            return list;
        }
        return null;
    }

    private static ScanResult toLScanResult(android.bluetooth.le.ScanResult result) {
        ScanResult res = new ScanResult();
        res.device = result.getDevice();
        res.rssi = result.getRssi();
        res.record = result.getScanRecord().getBytes();
        return res;
    }

    private static List<ScanResult> toLScanResults(List<android.bluetooth.le.ScanResult> results) {
        int size = results.size();
        List<ScanResult> res = new ArrayList<ScanResult>(size);
        for (int i = 0; i < size; i++) {
            res.add(toLScanResult(results.get(i)));
        }
        return res;
    }

}