package com.ficat.easyble.scan;

import android.annotation.TargetApi;
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.ScanRecord;
import android.bluetooth.le.ScanResult;
import android.bluetooth.le.ScanSettings;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.ParcelUuid;
import android.text.TextUtils;

import com.ficat.easyble.BleDevice;
import com.ficat.easyble.BleReceiver;
import com.ficat.easyble.Logger;

import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

public class BleScanner implements BleScan<BleScanCallback>, BleReceiver.BluetoothStateChangedListener {
    protected BluetoothAdapter mBluetoothAdapter;
    private BluetoothAdapter.LeScanCallback mLeScanCallback;//sdk<21 uses this scan callback
    private ScanCallback mScanCallback;//SDK>=21 uses this scan callback
    private BleScanCallback mBleScanCallback;//all sdk version uses this scan callback
    private BluetoothLeScanner mBluetoothLeScanner;
    private ScanSettings mScanSettings;
    private int mScanPeriod = 12000;//scan period, default 10s
    private String mDeviceName;
    private String mDeviceAddress;
    private UUID[] mServiceUuids;
    private volatile boolean mScanning;
    private Handler mHandler;
    private Runnable mScanTimeoutRunnable = new Runnable() {
        @Override
        public void run() {
            stopScan();
        }
    };

    public BleScanner() {
        mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
        mHandler = new Handler(Looper.getMainLooper());
        //register BluetoothStateChangedListener
        BleReceiver.getInstance().registerBluetoothStateChangedListener(this);
    }

    @Override
    public synchronized void startScan(int scanPeriod, String scanDeviceName, String scanDeviceAddress, UUID[] scanServiceUuids, final BleScanCallback callback) {
        if (callback == null) {
            throw new IllegalArgumentException("BleScanCallback is null");
        }
        mBleScanCallback = callback;
        if (mScanning) {
            mHandler.post(new Runnable() {
                @Override
                public void run() {
                    if (mBleScanCallback != null) {
                        mBleScanCallback.onStart(false, "you can't start a new scan until the previous scan is over");
                    }
                }
            });
            return;
        }

        if (scanPeriod > 0) {
            mScanPeriod = scanPeriod;
        }
        mDeviceName = scanDeviceName;
        mDeviceAddress = scanDeviceAddress;
        mServiceUuids = scanServiceUuids;

        boolean scanStart;
        if (sdkVersionLowerThan21()) {
            scanStart = scanByOldApi();
        } else {
            scanStart = scanByNewApi();
        }
        if (scanStart) {
            mScanning = true;
            mHandler.post(new Runnable() {
                @Override
                public void run() {
                    if (mBleScanCallback != null) {
                        mBleScanCallback.onStart(true, "scan begin success");
                    }
                }
            });
            mHandler.postDelayed(mScanTimeoutRunnable, mScanPeriod);
        } else {
            mHandler.post(new Runnable() {
                @Override
                public void run() {
                    if (mBleScanCallback != null) {
                        mBleScanCallback.onStart(false,
                                "scan begin fail,bluetooth is closed or other unknown reason");
                    }
                }
            });
        }
    }

    @SuppressWarnings("NewApi")
    @Override
    public synchronized void stopScan() {
        if (mBluetoothAdapter == null || !mScanning) {
            return;
        }
        if (sdkVersionLowerThan21()) {
            if (mLeScanCallback != null) {
                mBluetoothAdapter.stopLeScan(mLeScanCallback);
            }
        } else {
            //if bluetooth is close, stopScan() will throw an exception
            if (mBluetoothLeScanner != null && mBluetoothAdapter.isEnabled() && mScanCallback != null) {
                mBluetoothLeScanner.stopScan(mScanCallback);
            }
        }
        mScanning = false;
        mHandler.post(new Runnable() {
            @Override
            public void run() {
                if (mBleScanCallback != null) {
                    mBleScanCallback.onFinish();
                }
            }
        });
        mHandler.removeCallbacks(mScanTimeoutRunnable);
    }

    @Override
    public boolean isScanning() {
        return mScanning;
    }

    @Override
    public void onBluetoothStateChanged() {
        if (mBluetoothAdapter.getState() == BluetoothAdapter.STATE_OFF) {
            stopScan();
        }
    }

    @Override
    public void destroy() {
        stopScan();
        //remove scan period delayed message
        mHandler.removeCallbacksAndMessages(null);
        //unregister BluetoothStateChangedListener
        BleReceiver.getInstance().unregisterBluetoothStateChangedListener(this);
    }

    private boolean scanByOldApi() {
        if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) {
            return false;
        }
        if (mLeScanCallback == null) {
            mLeScanCallback = new BluetoothAdapter.LeScanCallback() {
                @Override
                public void onLeScan(final BluetoothDevice device, final int rssi, final byte[] scanRecord) {
                    mHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            if (!TextUtils.isEmpty(mDeviceName) && !mDeviceName.equals(device.getName())) {
                                return;
                            }
                            if (!TextUtils.isEmpty(mDeviceAddress) && !mDeviceAddress.equals(device.getAddress())) {
                                return;
                            }
                            if (mBleScanCallback != null) {
                                mBleScanCallback.onLeScan(newBleDevice(device), rssi, scanRecord);
                            }
                        }
                    });
                }
            };
        }
        return mBluetoothAdapter.startLeScan(mServiceUuids, mLeScanCallback);
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    private boolean scanByNewApi() {
        //mBluetoothLeScanner will be null and startScan() will throw an exception if bluetooth isn't open
        if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) {
            return false;
        }
        if (mScanCallback == null) {
            mScanCallback = new ScanCallback() {
                @Override
                public void onScanResult(int callbackType, final ScanResult result) {
                    super.onScanResult(callbackType, result);
                    mHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            if (!hasResultByFilterUuids(result)) return;
                            if (mBleScanCallback == null) return;
                            byte[] scanRecord = (result.getScanRecord() == null) ? new byte[]{} : result.getScanRecord().getBytes();
                            mBleScanCallback.onLeScan(newBleDevice(result.getDevice()), result.getRssi(), scanRecord);
                        }
                    });
                }

                @Override
                public void onBatchScanResults(List<ScanResult> results) {
                    for (ScanResult sr : results) {
                        Logger.i("Batch scan results: " + sr.toString());
                    }
                }

                @Override
                public void onScanFailed(int errorCode) {
                    Logger.i("Ble scan fail: " + errorCode);
                }
            };
        }
        //note that getBluetoothLeScanner() will be null if bluetooth is closed
        if (mBluetoothLeScanner == null) {
            mBluetoothLeScanner = mBluetoothAdapter.getBluetoothLeScanner();
        }
        if (mScanSettings == null) {
            mScanSettings = new ScanSettings.Builder()
                    .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
                    .build();
        }
        List<ScanFilter> scanFilters = new ArrayList<>();
        ScanFilter filter = new ScanFilter.Builder()
                .setDeviceName(mDeviceName)
                .setDeviceAddress(mDeviceAddress)
                .build();
        scanFilters.add(filter);
        mBluetoothLeScanner.startScan(scanFilters, mScanSettings, mScanCallback);
        return true;
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    private boolean hasResultByFilterUuids(ScanResult result) {
        if (mServiceUuids == null || mServiceUuids.length <= 0) {//no filtered uuids
            return true;
        }
        ScanRecord scanRecord = result.getScanRecord();
        if (scanRecord == null) {
            return false;
        }
        List<ParcelUuid> serviceUuidList = new ArrayList<>();
        for (UUID uuid : mServiceUuids) {
            serviceUuidList.add(new ParcelUuid(uuid));
        }
        List<ParcelUuid> scanServiceUuids = result.getScanRecord().getServiceUuids();
        return scanServiceUuids != null && scanServiceUuids.containsAll(serviceUuidList);
    }

    private boolean sdkVersionLowerThan21() {
        return Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP;
    }

    private BleDevice newBleDevice(BluetoothDevice device) {
        Class<?> clasz = BleDevice.class;
        try {
            Constructor<?> constructor = clasz.getDeclaredConstructor(BluetoothDevice.class);
            constructor.setAccessible(true);
            BleDevice bleDevice = (BleDevice) constructor.newInstance(device);
            return bleDevice;
        } catch (Exception e) {
            Logger.i("encounter an exception while creating a BleDevice object by reflection: " + e.getMessage());
            return null;
        }
    }
}