import React from 'react';
import { Permission, PermissionsAndroid, Platform } from 'react-native';
import { BleError, BleErrorCode, BleManager, Device, State } from 'react-native-ble-plx';
import LocalLogger from '../bluetooth/LocalLogger';
import {
  createErrorFromCode,
  ErrorTranslations,
  SFPToolboxError,
  SFPToolboxErrorCode,
  userMessageOfError,
  createErrorFromBleError,
} from '../error/error';
import Mutex from '../mutex/Mutex';
import {
  BluetoothState,
  DEVICE_NAME_PREFIX,
  MINIMUM_SIGNAL_STRENGTH,
  RECONNECT_TIMEOUT_MS,
} from './constants';
import { BluetoothContext, BluetoothContextType, TimeOuts } from './context';
import { makeTimeLimited } from './promise';
import { filterIdProbe } from './utils';
import { generateAdditionalInformation, formatErrorForLog } from '../error/utils';

interface BlueProviderTranslations {
  unavailableState: string;
  unauthorized: string;
  poweredOff: string;
  permissionsTitle: string;
  permissionsMessage: string;
  permissionsNeutral: string;
  permissionsPositive: string;
  permissionsNegative: string;
}

interface Props {
  onDisconnected?: (err: any) => void;
  mockProbe?: boolean;
  translations: BlueProviderTranslations;
  timeOuts: TimeOuts;
  errorTranslations?: ErrorTranslations;
  children: React.ReactNode | JSX.Element;
}

export class BluetoothContextProvider extends React.PureComponent<Props, BluetoothContextType> {
  manager: BleManager;

  constructor(props: Props) {
    super(props);

    this.manager = new BleManager();

    this.state = {
      startScan: this.startScan.bind(this),
      stopScan: this.stopScan.bind(this),
      connect: this.connect.bind(this),
      reconnect: this.reconnect.bind(this),
      unlink: this.unlink.bind(this),
      autoConnect: this.autoConnect.bind(this),
      setTargetDevice: this.setTargetDevice.bind(this),
      setBatteryPercentage: this.setBatteryPercentage.bind(this),
      getConnectedDeviceSerialNumber: this.getConnectedDeviceSerialNumber.bind(this),
      setFirmware: this.setFirmware.bind(this),
      error2Message: this.error2Message.bind(this),
      scannedDevices: [],
      connectionError: null,
      scanError: null,
      status: BluetoothState.IDLE,
      bluetoothState: props.mockProbe ? BluetoothState.CONNECTED : BluetoothState.IDLE,
      connectedDevice: props.mockProbe
        ? {
            name: 'Sonda_41234132',
            version: '1.1.1',
          }
        : null,
      targetDevice: null,
      mutex: new Mutex(),
      batteryPercentage: props.mockProbe ? 85 : null,
      version: props.mockProbe ? '1.1.1' : '',
      batteryLoading: props.mockProbe ? false : true,
      UNKNOWN_FIRMWARE_VERSION: 'Desconocida',
      mockProbe: props.mockProbe ?? false,
      timeOuts: props.timeOuts,
      isConnected: this.isConnected.bind(this),
      manager: this.manager,
      isBluetoothUnavailable: this.isBluetoothUnavailable.bind(this),
    };

    if (!props.mockProbe) {
      this.subscribeStateChanges();
      this.manager.state().then(this.bluetoothStateUpdater);
    }
  }

  componentWillUnmount() {
    (this.manager as any).destroyed = true;
    this.manager.destroy();
  }

  setBatteryPercentage(aBatteryPercentage) {
    this.setState(prevState => ({
      batteryPercentage: aBatteryPercentage ?? prevState.batteryPercentage,
      batteryLoading: false,
    }));
  }

  subscribeStateChanges() {
    this.manager.onStateChange(this.bluetoothStateUpdater);
  }

  connectionError(error) {
    this.setState({ connectionError: error });
  }

  bluetoothState(state) {
    this.setState({ bluetoothState: state });
  }

  setFirmware(version) {
    this.setState({ version });
  }

  getConnectedDeviceSerialNumber() {
    if (this.state.connectedDevice == null) {
      if (this.state.targetDevice) {
        return filterIdProbe(this.state.targetDevice.name);
      }

      return null;
    }

    return filterIdProbe(this.state.connectedDevice.name);
  }

  error2Message(error: SFPToolboxError): string {
    return userMessageOfError(error, this.props.errorTranslations);
  }

  async unlink() {
    if (!this.state.mockProbe) {
      try {
        if (this.state.connectedDevice != null) {
          await this.state.connectedDevice.cancelConnection();
        }
      } catch (error) {
        if (
          ![BleErrorCode.DeviceDisconnected, BleErrorCode.DeviceNotConnected].includes(
            (error as BleError)?.errorCode,
          )
        ) {
          throw error;
        }
      }
    }

    this.setState({
      targetDevice: null,
      connectedDevice: null,
      batteryPercentage: null,
      version: '',
      connectionError: null,
      scannedDevices: [],
      batteryLoading: false,
      bluetoothState: BluetoothState.IDLE,
    });
  }

  isConnected() {
    return this.state.bluetoothState === BluetoothState.CONNECTED;
  }
  isBluetoothUnavailable() {
    return this.state.bluetoothState === BluetoothState.UNAVAILABLE;
  }

  bluetoothStateUpdater = state => {
    switch (state) {
      case State.Unsupported: {
        return this.bluetoothState(BluetoothState.UNAVAILABLE);
      }
      case State.Unauthorized: {
        return this.bluetoothState(BluetoothState.UNAVAILABLE);
      }
      case State.PoweredOff: {
        return this.bluetoothState(BluetoothState.UNAVAILABLE);
      }
      case State.PoweredOn: {
        this.setState({
          scanError: null,
        });
        return this.bluetoothState(BluetoothState.IDLE);
      }
    }
  };

  status(status) {
    this.setState({ status });
  }

  cleanScan() {
    this.setState({ scanError: null, scannedDevices: [] });
  }

  requestScanPermissions = async () => {
    const permissionsToAsk = [PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION as Permission].concat(
      Platform.Version >= 31
        ? [
            PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT as Permission,
            PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN as Permission,
          ]
        : [],
    );

    const response = await PermissionsAndroid.requestMultiple(permissionsToAsk);

    if (permissionsToAsk.some(permission => response[permission] !== 'granted')) {
      throw createErrorFromCode(SFPToolboxErrorCode.InsufficientPermissions);
    }
  };

  startScan() {
    this.cleanScan();
    const onError = error => {
      this.setState({
        scanError: error,
        scannedDevices: [],
        bluetoothState: BluetoothState.IDLE,
      });
    };
    this.bluetoothState(BluetoothState.SCANNING);

    this.requestScanPermissions()
      .then(() => {
        this.manager.startDeviceScan(null, null, (bleError, device) => {
          if (bleError || device == null) {
            const error = createErrorFromBleError(bleError as BleError);
            LocalLogger.log(`SCAN_DEVICE_ERROR: ${formatErrorForLog(error)}`);
            return onError(error);
          }

          if (
            device.name?.startsWith(DEVICE_NAME_PREFIX) &&
            device.rssi &&
            device.rssi > MINIMUM_SIGNAL_STRENGTH
          ) {
            this.setState((prevState: BluetoothContextType) => {
              const previousScannedDevices = prevState.scannedDevices;

              if (!previousScannedDevices.find(deviceInList => deviceInList.id === device.id)) {
                return {
                  scannedDevices: [...prevState.scannedDevices, device],
                };
              } else {
                return prevState;
              }
            });
          }
        });
      })
      .catch(onError);
  }

  stopScan() {
    this.cleanScan();
    this.bluetoothState(BluetoothState.IDLE);
    this.manager.stopDeviceScan();
  }

  setTargetDevice(targetDevice: { name: string; id: string }) {
    this.setState({
      targetDevice,
    });
  }

  async reconnect(deviceName) {
    this.setState({
      bluetoothState: BluetoothState.SCANNING,
    });

    return this.requestScanPermissions()
      .then(async () => {
        return makeTimeLimited(
          new Promise((resolve, reject) => {
            this.manager.startDeviceScan(null, null, (error, device) => {
              if (error) {
                reject(error);
              }

              if (device?.name === deviceName) {
                resolve(device);
              }
            });
          }),
          this.props.timeOuts.reconnectInMs || RECONNECT_TIMEOUT_MS,
        );
      })
      .finally(() => {
        this.manager.stopDeviceScan();
      })
      .catch(async bleError => {
        const error = createErrorFromBleError(bleError);
        LocalLogger.log(
          `RECONNECTION_WITH_PROBE_ERROR: ${formatErrorForLog(
            error,
          )}\n${await generateAdditionalInformation()}`,
        );

        this.setState({
          bluetoothState: BluetoothState.IDLE,
          connectionError: error,
        });

        throw error;
      })
      .then(device => {
        return this.connect(device);
      });
  }

  onDisconnected = error => {
    this.setState({
      connectedDevice: null,
      batteryPercentage: null,
      version: '',
      connectionError: createErrorFromCode(BleErrorCode.DeviceDisconnected),
      scannedDevices: [],
      batteryLoading: false,
      bluetoothState: BluetoothState.IDLE,
    });

    this.props.onDisconnected?.(error);
  };

  async autoConnect(): Promise<Device> {
    this.setState({
      connectionError: null,
    });

    if (this.isBluetoothUnavailable()) {
      const error = createErrorFromCode(SFPToolboxErrorCode.BluetoothNotAvailable);

      LocalLogger.log(
        `AUTOCONNECTION_WITH_PROBE_BLUETOOTH_ERROR: ${formatErrorForLog(
          error,
        )}\n${await generateAdditionalInformation()}`,
      );
      this.connectionError(error);
      throw error;
    }
    this.setState({
      bluetoothState: BluetoothState.CONNECTING,
      connectionError: null,
    });

    const aDeviceId = this.state.targetDevice?.id;

    if (!aDeviceId) {
      throw new Error('Device id not found');
    }

    return this.manager
      .connectToDevice(aDeviceId, {
        timeout: this.props.timeOuts.reconnectInMs || RECONNECT_TIMEOUT_MS,
      })
      .then(device => {
        return device.discoverAllServicesAndCharacteristics();
      })
      .then(device => {
        if (!device.name) {
          device.name = this.state.targetDevice?.name ?? null;
        }
        this.setState({
          connectedDevice: device,
          bluetoothState: BluetoothState.CONNECTED,
          batteryLoading: true,
          mutex: new Mutex(),
          connectionError: null,
        });

        device.onDisconnected(this.onDisconnected);

        return device;
      })
      .catch(async bleError => {
        const error = createErrorFromBleError(bleError);
        LocalLogger.log(
          `AUTOCONNECTION_WITH_PROBE_ERROR: ${formatErrorForLog(
            error,
          )}\n${await generateAdditionalInformation()}`,
        );

        this.setState({
          bluetoothState: BluetoothState.IDLE,
          connectionError: error,
        });

        throw error;
      });
  }

  connect(aDevice: Device, onSetTargetDevice?: Function): Promise<Device> {
    const targetDevice = { name: aDevice.name as string, id: aDevice.id };
    this.manager.stopDeviceScan();
    this.setState({
      bluetoothState: BluetoothState.CONNECTING,
      connectionError: null,
      targetDevice,
    });
    onSetTargetDevice?.(targetDevice);

    return aDevice
      .connect()
      .then(device => {
        return device.discoverAllServicesAndCharacteristics();
      })
      .then(device => {
        this.setState({
          connectedDevice: device,
          bluetoothState: BluetoothState.CONNECTED,
          batteryLoading: true,
          mutex: new Mutex(),
        });

        device.onDisconnected(this.onDisconnected);

        return device;
      })
      .catch(async bleError => {
        const error = createErrorFromBleError(bleError);
        LocalLogger.log(
          `CONNECTION_WITH_PROBE_ERROR: ${formatErrorForLog(
            error,
          )}\n${await generateAdditionalInformation()}`,
        );

        this.setState({
          bluetoothState: BluetoothState.IDLE,
          connectionError: error,
        });

        throw error;
      });
  }

  render() {
    const { children } = this.props;

    return <BluetoothContext.Provider value={this.state}>{children}</BluetoothContext.Provider>;
  }
}

export default BluetoothContextProvider;
