import {
  EventSubscription,
  NativeModules,
} from 'react-native';
import {
  BleScanCallbackType,
  BleScanMatchCount,
  BleScanMatchMode,
  BleScanMode,
  BleState,
  ConnectOptions,
  ConnectionPriority,
  CompanionScanOptions,
  Peripheral,
  PeripheralInfo,
  ScanOptions,
  StartOptions,
} from './types';
export * from './types';

// @ts-expect-error This applies the turbo module version only when turbo is enabled for backwards compatibility.
const isTurboModuleEnabled = global?.__turboModuleProxy != null;

const BleManagerModule = isTurboModuleEnabled
  ? require('./NativeBleManager').default
  : NativeModules.BleManager;

class BleManager {
  constructor() {
    if (!BleManagerModule) {
      throw new Error('BleManagerModule not found');
    }
    this.isPeripheralConnected = this.isPeripheralConnected.bind(this);
  }

  /**
   *
   * @param peripheralId
   * @param serviceUUID
   * @param characteristicUUID
   * @returns data as an array of numbers (which can be converted back to a Uint8Array (ByteArray) using something like [Buffer.from()](https://github.com/feross/buffer))
   */
  read(peripheralId: string, serviceUUID: string, characteristicUUID: string) {
    return new Promise<number[]>((fulfill, reject) => {
      BleManagerModule.read(
        peripheralId,
        serviceUUID,
        characteristicUUID,
        (error: string | null, data: number[]) => {
          if (error) {
            reject(error);
          } else {
            fulfill(data);
          }
        }
      );
    });
  }

  /**
   *
   * @param peripheralId
   * @param serviceUUID
   * @param characteristicUUID
   * @param descriptorUUID
   * @returns data as an array of numbers (which can be converted back to a Uint8Array (ByteArray) using something like [Buffer.from()](https://github.com/feross/buffer))
   */
  readDescriptor(
    peripheralId: string,
    serviceUUID: string,
    characteristicUUID: string,
    descriptorUUID: string
  ) {
    return new Promise<number[]>((fulfill, reject) => {
      BleManagerModule.readDescriptor(
        peripheralId,
        serviceUUID,
        characteristicUUID,
        descriptorUUID,
        (error: string | null, data: number[]) => {
          if (error) {
            reject(error);
          } else {
            fulfill(data);
          }
        }
      );
    });
  }

  /**
   *
   * @param peripheralId
   * @param serviceUUID
   * @param characteristicUUID
   * @param descriptorUUID
   * @param data data to write as an array of numbers (which can be converted from a Uint8Array (ByteArray) using something like [Buffer.toJSON().data](https://github.com/feross/buffer))
   * @returns
   */
  writeDescriptor(
    peripheralId: string,
    serviceUUID: string,
    characteristicUUID: string,
    descriptorUUID: string,
    data: number[]
  ) {
    return new Promise<void>((fulfill, reject) => {
      BleManagerModule.writeDescriptor(
        peripheralId,
        serviceUUID,
        characteristicUUID,
        descriptorUUID,
        data,
        (error: string | null) => {
          if (error) {
            reject(error);
          } else {
            fulfill();
          }
        }
      );
    });
  }

  /**
   *
   * @param peripheralId
   * @returns a promise resolving with the updated RSSI (`number`) if it succeeds.
   */
  readRSSI(peripheralId: string) {
    return new Promise<number>((fulfill, reject) => {
      BleManagerModule.readRSSI(
        peripheralId,
        (error: string | null, rssi: number) => {
          if (error) {
            reject(error);
          } else {
            fulfill(rssi);
          }
        }
      );
    });
  }

  /**
   * [Android only]
   * @param peripheralId
   * @returns a promise that resolves to a boolean indicating if gatt was successfully refreshed or not.
   */
  refreshCache(peripheralId: string) {
    return new Promise<boolean>((fulfill, reject) => {
      BleManagerModule.refreshCache(
        peripheralId,
        (error: string | null, result: boolean) => {
          if (error) {
            reject(error);
          } else {
            fulfill(result);
          }
        }
      );
    });
  }

  /**
   *
   * @param peripheralId
   * @param serviceUUIDs [iOS only] optional filter of services to retrieve.
   * @returns
   */
  retrieveServices(peripheralId: string, serviceUUIDs: string[] = []) {
    return new Promise<PeripheralInfo>((fulfill, reject) => {
      BleManagerModule.retrieveServices(
        peripheralId,
        serviceUUIDs,
        (error: string | null, peripheral: PeripheralInfo) => {
          if (error) {
            reject(error);
          } else {
            fulfill(peripheral);
          }
        }
      );
    });
  }

  /**
   *
   * @param peripheralId
   * @param serviceUUID
   * @param characteristicUUID
   * @param data data to write as an array of numbers (which can be converted from a Uint8Array (ByteArray) using something like [Buffer.toJSON().data](https://github.com/feross/buffer))
   * @param maxByteSize optional, defaults to 20
   * @returns
   */
  write(
    peripheralId: string,
    serviceUUID: string,
    characteristicUUID: string,
    data: number[],
    maxByteSize: number = 20
  ) {
    return new Promise<void>((fulfill, reject) => {
      BleManagerModule.write(
        peripheralId,
        serviceUUID,
        characteristicUUID,
        data,
        maxByteSize,
        (error: string | null) => {
          if (error) {
            reject(error);
          } else {
            fulfill();
          }
        }
      );
    });
  }

  /**
   *
   * @param peripheralId
   * @param serviceUUID
   * @param characteristicUUID
   * @param data data to write as an array of numbers (which can be converted from a Uint8Array (ByteArray) using something like [Buffer.toJSON().data](https://github.com/feross/buffer))
   * @param maxByteSize optional, defaults to 20
   * @param queueSleepTime optional, defaults to 10. Only useful if data length is greater than maxByteSize.
   * @returns
   */
  writeWithoutResponse(
    peripheralId: string,
    serviceUUID: string,
    characteristicUUID: string,
    data: number[],
    maxByteSize: number = 20,
    queueSleepTime: number = 10
  ) {
    return new Promise<void>((fulfill, reject) => {
      BleManagerModule.writeWithoutResponse(
        peripheralId,
        serviceUUID,
        characteristicUUID,
        data,
        maxByteSize,
        queueSleepTime,
        (error: string | null) => {
          if (error) {
            reject(error);
          } else {
            fulfill();
          }
        }
      );
    });
  }

  connect(peripheralId: string, options?: ConnectOptions) {
    return new Promise<void>((fulfill, reject) => {
      if (!options) {
        options = {};
      }
      BleManagerModule.connect(
        peripheralId,
        options,
        (error: string | null) => {
          if (error) {
            reject(error);
          } else {
            fulfill();
          }
        }
      );
    });
  }

  /**
   * [Android only]
   * @param peripheralId
   * @param peripheralPin optional. will be used to auto-bond if possible.
   * @returns
   */
  createBond(peripheralId: string, peripheralPin: string | null = null) {
    return new Promise<void>((fulfill, reject) => {
      BleManagerModule.createBond(
        peripheralId,
        peripheralPin,
        (error: string | null) => {
          if (error) {
            reject(error);
          } else {
            fulfill();
          }
        }
      );
    });
  }

  /**
   * [Android only]
   * @param peripheralId
   * @returns
   */
  removeBond(peripheralId: string) {
    return new Promise<void>((fulfill, reject) => {
      BleManagerModule.removeBond(peripheralId, (error: string | null) => {
        if (error) {
          reject(error);
        } else {
          fulfill();
        }
      });
    });
  }

  /**
   *
   * @param peripheralId
   * @param force [Android only] defaults to true.
   * @returns
   */
  disconnect(peripheralId: string, force: boolean = true) {
    return new Promise<void>((fulfill, reject) => {
      BleManagerModule.disconnect(
        peripheralId,
        force,
        (error: string | null) => {
          if (error) {
            reject(error);
          } else {
            fulfill();
          }
        }
      );
    });
  }

  startNotification(
    peripheralId: string,
    serviceUUID: string,
    characteristicUUID: string
  ) {
    return new Promise<void>((fulfill, reject) => {
      BleManagerModule.startNotification(
        peripheralId,
        serviceUUID,
        characteristicUUID,
        (error: string | null) => {
          if (error) {
            reject(error);
          } else {
            fulfill();
          }
        }
      );
    });
  }

  /**
   * [Android only]
   * @param peripheralId
   * @param serviceUUID
   * @param characteristicUUID
   * @param buffer
   * @returns
   */
  startNotificationUseBuffer(
    peripheralId: string,
    serviceUUID: string,
    characteristicUUID: string,
    buffer: number
  ) {
    return new Promise<void>((fulfill, reject) => {
      BleManagerModule.startNotificationUseBuffer(
        peripheralId,
        serviceUUID,
        characteristicUUID,
        buffer,
        (error: string | null) => {
          if (error) {
            reject(error);
          } else {
            fulfill();
          }
        }
      );
    });
  }

  stopNotification(
    peripheralId: string,
    serviceUUID: string,
    characteristicUUID: string
  ) {
    return new Promise<void>((fulfill, reject) => {
      BleManagerModule.stopNotification(
        peripheralId,
        serviceUUID,
        characteristicUUID,
        (error: string | null) => {
          if (error) {
            reject(error);
          } else {
            fulfill();
          }
        }
      );
    });
  }

  checkState() {
    return new Promise<BleState>((fulfill, _) => {
      BleManagerModule.checkState((state: BleState) => {
        fulfill(state);
      });
    });
  }

  start(options?: StartOptions) {
    return new Promise<void>((fulfill, reject) => {
      if (options == null) {
        options = {};
      }
      BleManagerModule.start(options, (error: string | null) => {
        if (error) {
          reject(error);
        } else {
          fulfill();
        }
      });
    });
  }

  /**
   *
   * @param serviceUUIDs
   * @param seconds amount of seconds to scan. if set to 0 or less, will scan until you call stopScan() or the OS stops the scan (background etc).
   * @param allowDuplicates [iOS only]
   * @param scanningOptions optional map of properties to fine-tune scan behavior, see DOCS.
   * @returns
   */
  scan(
    serviceUUIDs: string[],
    seconds: number,
    allowDuplicates?: boolean,
    scanningOptions: ScanOptions = {}
  ) {
    return new Promise<void>((fulfill, reject) => {
      if (allowDuplicates == null) {
        allowDuplicates = false;
      }

      // (ANDROID) Match as many advertisement per filter as hw could allow
      // depends on current capability and availability of the resources in hw.
      if (scanningOptions.numberOfMatches == null) {
        scanningOptions.numberOfMatches = BleScanMatchCount.MaxAdvertisements;
      }

      // (ANDROID) Defaults to MATCH_MODE_AGGRESSIVE
      if (scanningOptions.matchMode == null) {
        scanningOptions.matchMode = BleScanMatchMode.Aggressive;
      }

      // (ANDROID) Defaults to SCAN_MODE_LOW_POWER
      if (scanningOptions.scanMode == null) {
        scanningOptions.scanMode = BleScanMode.LowPower;
      }

      // (ANDROID) Defaults to CALLBACK_TYPE_ALL_MATCHES
      // WARN: sometimes, setting a scanSetting instead of leaving it untouched might result in unexpected behaviors.
      // https://github.com/dariuszseweryn/RxAndroidBle/issues/462
      if (scanningOptions.callbackType == null) {
        scanningOptions.callbackType = BleScanCallbackType.AllMatches;
      }

      // (ANDROID) Defaults to 0ms (report results immediately).
      if (scanningOptions.reportDelay == null) {
        scanningOptions.reportDelay = 0;
      }

      // In Android ScanFilter used to restrict search to devices with a specific advertising name.
      // https://developer.android.com/reference/android/bluetooth/le/ScanFilter.Builder#setDeviceName(java.lang.String)
      // In iOS, this is a whole word match, not a partial search.
      if (!scanningOptions.exactAdvertisingName) {
        delete scanningOptions.exactAdvertisingName;
      } else {
        if (typeof scanningOptions.exactAdvertisingName === 'string') {
          scanningOptions.exactAdvertisingName = [
            scanningOptions.exactAdvertisingName,
          ];
        }
      }

      BleManagerModule.scan(
        serviceUUIDs,
        seconds,
        allowDuplicates,
        scanningOptions,
        (error: string | null) => {
          if (error) {
            reject(error);
          } else {
            fulfill();
          }
        }
      );
    });
  }

  stopScan() {
    return new Promise<void>((fulfill, reject) => {
      BleManagerModule.stopScan((error: string | null) => {
        if (error) {
          reject(error);
        } else {
          fulfill();
        }
      });
    });
  }

  /**
   * [Android only] triggers an ENABLE_REQUEST intent to the end-user to enable bluetooth.
   * @returns
   */
  enableBluetooth() {
    return new Promise<void>((fulfill, reject) => {
      BleManagerModule.enableBluetooth((error: string | null) => {
        if (error) {
          reject(error);
        } else {
          fulfill();
        }
      });
    });
  }

  /**
   *
   * @param serviceUUIDs [optional] not used on android, optional on ios.
   * @returns
   */
  getConnectedPeripherals(serviceUUIDs: string[] = []) {
    return new Promise<Peripheral[]>((fulfill, reject) => {
      BleManagerModule.getConnectedPeripherals(
        serviceUUIDs,
        (error: string | null, result: Peripheral[] | null) => {
          if (error) {
            reject(error);
          } else {
            if (result) {
              fulfill(result);
            } else {
              fulfill([]);
            }
          }
        }
      );
    });
  }

  /**
   * [Android only]
   * @returns
   */
  getBondedPeripherals() {
    return new Promise<Peripheral[]>((fulfill, reject) => {
      BleManagerModule.getBondedPeripherals(
        (error: string | null, result: Peripheral[] | null) => {
          if (error) {
            reject(error);
          } else {
            if (result) {
              fulfill(result);
            } else {
              fulfill([]);
            }
          }
        }
      );
    });
  }

  getDiscoveredPeripherals() {
    return new Promise<Peripheral[]>((fulfill, reject) => {
      BleManagerModule.getDiscoveredPeripherals(
        (error: string | null, result: Peripheral[] | null) => {
          if (error) {
            reject(error);
          } else {
            if (result) {
              fulfill(result);
            } else {
              fulfill([]);
            }
          }
        }
      );
    });
  }

  /**
   * [Android only]
   * @param peripheralId
   * @returns
   */
  removePeripheral(peripheralId: string) {
    return new Promise<void>((fulfill, reject) => {
      BleManagerModule.removePeripheral(
        peripheralId,
        (error: string | null) => {
          if (error) {
            reject(error);
          } else {
            fulfill();
          }
        }
      );
    });
  }

  /**
   * @param peripheralId
   * @param serviceUUIDs [optional] not used on android, optional on ios.
   * @returns
   */
  isPeripheralConnected(peripheralId: string, serviceUUIDs: string[] = []) {
    return this.getConnectedPeripherals(serviceUUIDs).then((result) => {
      if (result.find((p) => p.id === peripheralId)) {
        return true;
      } else {
        return false;
      }
    });
  }

  /**
   * @param peripheralId
   * @param serviceUUIDs [optional] not used on android, optional on ios.
   * @returns
   */
  isScanning() {
    return new Promise<boolean>((fulfill, reject) => {
      BleManagerModule.isScanning((error: string | null, status: boolean) => {
        if (error) {
          reject(error);
        } else {
          fulfill(status);
        }
      });
    });
  }

  /**
   * [Android only, API 21+]
   * @param peripheralId
   * @param connectionPriority
   * @returns a promise that resolves with a boolean indicating of the connection priority was changed successfully, or rejects with an error message.
   */
  requestConnectionPriority(
    peripheralId: string,
    connectionPriority: ConnectionPriority
  ) {
    return new Promise<boolean>((fulfill, reject) => {
      BleManagerModule.requestConnectionPriority(
        peripheralId,
        connectionPriority,
        (error: string | null, status: boolean) => {
          if (error) {
            reject(error);
          } else {
            fulfill(status);
          }
        }
      );
    });
  }

  /**
   * [Android only, API 21+]
   * @param peripheralId
   * @param mtu size to be requested, in bytes.
   * @returns a promise resolving with the negotiated MTU if it succeeded. Beware that it might not be the one requested due to device's BLE limitations on both side of the negotiation.
   */
  requestMTU(peripheralId: string, mtu: number) {
    return new Promise<number>((fulfill, reject) => {
      BleManagerModule.requestMTU(
        peripheralId,
        mtu,
        (error: string | null, mtu: number) => {
          if (error) {
            reject(error);
          } else {
            fulfill(mtu);
          }
        }
      );
    });
  }

  /**
   * [Android only, API 26+]
   *
   * @returns
   */
  getAssociatedPeripherals() {
    return new Promise<Peripheral[]>((fulfill, reject) => {
      BleManagerModule.getAssociatedPeripherals(
        (error: string | null, peripherals: Peripheral[] | null) => {
          if (error) {
            reject(error);
          } else {
            fulfill(peripherals || []);
          }
        }
      );
    });
  }

  /**
   * [Android only, API 26+]
   * @param peripheralId Peripheral to remove
   * @returns Promise that resolves once the peripheral has been removed. Rejects
   *          if no association is found.
   */
  removeAssociatedPeripheral(peripheralId: string) {
    return new Promise<void>((fulfill, reject) => {
      BleManagerModule.removeAssociatedPeripheral(
        peripheralId,
        (error: string | null) => {
          if (error !== null) {
            reject(error);
          } else {
            fulfill();
          }
        }
      );
    });
  }

  /**
   * [Android only]
   *
   * Check if current device supports companion device manager.
   *
   * @return Promise resolving to a boolean.
   */
  supportsCompanion() {
    return new Promise<boolean>((fulfill) => {
      BleManagerModule.supportsCompanion((supports: boolean) =>
        fulfill(supports)
      );
    });
  }

  /**
   * [Android only, API 26+]
   *
   * Start companion scan.
   */
  companionScan(serviceUUIDs: string[], options: CompanionScanOptions = {}) {
    return new Promise<Peripheral | null>((fulfill, reject) => {
      BleManagerModule.companionScan(
        serviceUUIDs,
        options,
        (error: string | null, peripheral: Peripheral | null) => {
          if (error) {
            reject(error);
          } else {
            fulfill(peripheral);
          }
        }
      );
    });
  }

  /**
   * [Android only]
   * @param name
   */
  setName(name: string) {
    BleManagerModule.setName(name);
  }

  /**
   * [iOS only]
   * @param peripheralId
   * @returns
   */
  getMaximumWriteValueLengthForWithoutResponse(peripheralId: string) {
    return new Promise<number>((fulfill, reject) => {
      BleManagerModule.getMaximumWriteValueLengthForWithoutResponse(
        peripheralId,
        (error: string | null, max: number) => {
          if (error) {
            reject(error);
          } else {
            fulfill(max);
          }
        }
      );
    });
  }

  /**
   * [iOS only]
   * @param peripheralId
   * @returns
   */
  getMaximumWriteValueLengthForWithResponse(peripheralId: string) {
    return new Promise<number>((fulfill, reject) => {
      BleManagerModule.getMaximumWriteValueLengthForWithResponse(
        peripheralId,
        (error: string | null, max: number) => {
          if (error) {
            reject(error);
          } else {
            fulfill(max);
          }
        }
      );
    });
  }

  onDiscoverPeripheral(callback: any): EventSubscription {
    return BleManagerModule.onDiscoverPeripheral(callback);
  }

  onStopScan(callback: any): EventSubscription {
    return BleManagerModule.onStopScan(callback);
  }

  onDidUpdateState(callback: any): EventSubscription {
    return BleManagerModule.onDidUpdateState(callback);
  }

  onCentralManagerDidUpdateState(callback: any): EventSubscription {
    return BleManagerModule.onCentralManagerDidUpdateState(callback);
  }

  onConnectPeripheral(callback: any): EventSubscription {
    return BleManagerModule.onConnectPeripheral(callback);
  }

  onDisconnectPeripheral(callback: any): EventSubscription {
    return BleManagerModule.onDisconnectPeripheral(callback);
  }

  onDidUpdateValueForCharacteristic(callback: any): EventSubscription {
    return BleManagerModule.onDidUpdateValueForCharacteristic(callback);
  }

  onPeripheralDidBond(callback: any): EventSubscription {
    return BleManagerModule.onPeripheralDidBond(callback);
  }

  onCentralManagerWillRestoreState(callback: any): EventSubscription {
    return BleManagerModule.onCentralManagerWillRestoreState(callback);
  }

  onDidUpdateNotificationStateFor(callback: any): EventSubscription {
    return BleManagerModule.onDidUpdateNotificationStateFor(callback);
  }

  onCompanionPeripheral(callback: any): EventSubscription {
    return BleManagerModule.onCompanionPeripheral(callback);
  }

  onCompanionAvailability(callback: any): EventSubscription {
    return BleManagerModule.onCompanionAvailability(callback);
  }
}

export default new BleManager();
