const debug = require('debug')('noble');

const NobleEventEmitter = require('./noble-event-emitter');

const Peripheral = require('./peripheral');
const Service = require('./service');
const Characteristic = require('./characteristic');
const Descriptor = require('./descriptor');

class Noble extends NobleEventEmitter {
  
  constructor (bindings) {
    super();

    this._address = 'unknown';
    this._state = 'unknown';
    
    this._initialized = false;
    this._bindings = bindings;

    this._discoveredPeripherals = new Set();
    this._peripherals = new Map();
    this._services = {};
    this._characteristics = {};
    this._descriptors = {};

    this._cleanupPeriperals();

    this.on('warning', message => console.warn(`noble: ${message}`));

    this.on('newListener', event => {
      if (event === 'stateChange' && this._initialized === false) {
        this._initialized = true;
        process.nextTick(this._initializeBindings.bind(this));
      }
    });
  }

  get state () {
    if (this._initialized === false) {
      this._initializeBindings();
    }
    return this._state;
  }

  get address () {
    return this._address;
  }

  _initializeBindings () {
    this._initialized = true;
    this._registerListeners();
    this._bindings.start();
  }

  _registerListeners () {
    this._bindings.on('stateChange', this._onStateChange.bind(this));
    this._bindings.on('addressChange', this._onAddressChange.bind(this));
    this._bindings.on('scanParametersSet', this._onScanParametersSet.bind(this));
    this._bindings.on('scanStart', this._onScanStart.bind(this));
    this._bindings.on('scanStop', this._onScanStop.bind(this));
    this._bindings.on('discover', this._onDiscover.bind(this));
    this._bindings.on('connect', this._onConnect.bind(this));
    this._bindings.on('disconnect', this._onDisconnect.bind(this));
    this._bindings.on('rssiUpdate', this._onRssiUpdate.bind(this));
    this._bindings.on('servicesDiscover', this._onServicesDiscover.bind(this));
    this._bindings.on('servicesDiscovered', this._onServicesDiscovered.bind(this));
    this._bindings.on('includedServicesDiscover', this._onIncludedServicesDiscover.bind(this));
    this._bindings.on('characteristicsDiscover', this._onCharacteristicsDiscover.bind(this));
    this._bindings.on('characteristicsDiscovered', this._onCharacteristicsDiscovered.bind(this));
    this._bindings.on('read', this._onRead.bind(this));
    this._bindings.on('write', this._onWrite.bind(this));
    this._bindings.on('broadcast', this._onBroadcast.bind(this));
    this._bindings.on('notify', this._onNotify.bind(this));
    this._bindings.on('descriptorsDiscover', this._onDescriptorsDiscover.bind(this));
    this._bindings.on('valueRead', this._onValueRead.bind(this));
    this._bindings.on('valueWrite', this._onValueWrite.bind(this));
    this._bindings.on('handleRead', this._onHandleRead.bind(this));
    this._bindings.on('handleWrite', this._onHandleWrite.bind(this));
    this._bindings.on('handleNotify', this._onHandleNotify.bind(this));
    this._bindings.on('onMtu', this._onMtu.bind(this));
  }

  _createPeripheral (uuid, address, addressType, connectable, advertisement, rssi, scannable) {
    const peripheral = new Peripheral(this, uuid, address, addressType, connectable, advertisement, rssi, scannable);

    this._peripherals.set(uuid, peripheral);
    this._services[uuid] = {};
    this._characteristics[uuid] = {};
    this._descriptors[uuid] = {};

    return peripheral;
  }

  _cleanupPeriperals (uuid = null) {
    const terminateConnection = (peripheral) => {
      if (peripheral.state === 'connecting') {
        // To unblock the async connect call
        this._onConnect(peripheral.id, new Error('cleanup'));
      }
      else if (peripheral.state !== 'disconnected') {
        this._onDisconnect(peripheral.id, 'cleanup');
      }
    };
    if (uuid) {
      const peripheral = this._peripherals.get(uuid);
      if (peripheral) {
        terminateConnection(peripheral);
      }
      this._peripherals.delete(uuid);
      this._discoveredPeripherals.delete(uuid);
      delete this._services[uuid];
      delete this._characteristics[uuid];
      delete this._descriptors[uuid];
    } else {
      this._peripherals.forEach(peripheral => terminateConnection(peripheral));
      this._peripherals.clear();
      this._discoveredPeripherals.clear();
      this._services = {};
      this._characteristics = {};
      this._descriptors = {};
    }
  }

  _onStateChange (state) {
    debug(`stateChange ${state}`);

    // If the state is poweredOff and the previous state was poweredOn, clean up the peripherals
    if (state === 'poweredOff' && this._state === 'poweredOn') {
      this._cleanupPeriperals();
    }

    this._state = state;
    this.emit('stateChange', state);
  }

  _onAddressChange (address) {
    debug(`addressChange ${address}`);
    this._address = address;
    this.emit('addressChange', address);
  }

  setScanParameters (interval, window, callback) {
    if (callback) {
      this.onceExclusive('scanParametersSet', callback);
    }
    this._bindings.setScanParameters(interval, window);
  }

  _onScanParametersSet () {
    debug('scanParametersSet');
    this.emit('scanParametersSet');
  }
  
  setAddress (address) {
    if (this._bindings.setAddress) {
      this._bindings.setAddress(address);
    } else {
      this.emit('warning', 'current binding does not implement setAddress method.');
    }
  }

  async waitForPoweredOnAsync (timeout = 10000) {
    return new Promise((resolve, reject) => {
      if (this.state === 'poweredOn') {
        resolve();
        return;
      }
      const timeoutId = setTimeout(() => {
        this.removeListener('stateChange', listener);
        reject(new Error('Timeout waiting for Noble to be powered on'));
      }, timeout);

      const listener = (state) => {
        if (state === 'poweredOn') {
          clearTimeout(timeoutId);
          resolve();
        } else {
          this.once('stateChange', listener);
        }
      };

      this.once('stateChange', listener);
    });
  }

  startScanning (serviceUuids, allowDuplicates, callback) {
    const self = this;
    const scan = (state) => {
      if (state !== 'poweredOn') {
        const boundScan = scan.bind(self);
        self.once('stateChange', boundScan);
        const error = new Error(`Could not start scanning, state is ${state} (not poweredOn)`);

        if (typeof callback === 'function') {
          self.removeListener('stateChange', boundScan);
          callback(error);
        } else {
          throw error;
        }
      } else {
        if (callback) {
          this.onceExclusive('scanStart', filterDuplicates => callback(null, filterDuplicates));
        }

        this._discoveredPeripherals.clear();
        this._allowDuplicates = allowDuplicates;

        this._bindings.startScanning(serviceUuids, allowDuplicates);
      }
    };
    // if bindings still not init, do it now
    if (this._initialized === false) {
      this.once('stateChange', scan.bind(this));
      this._initializeBindings();
    } else {
      scan.call(this, this._state);
    }
  }

  async startScanningAsync (serviceUUIDs, allowDuplicates) {
    return new Promise((resolve, reject) => {
      this.startScanning(serviceUUIDs, allowDuplicates, error => error ? reject(error) : resolve());
    });
  }

  _onScanStart (filterDuplicates) {
    debug('scanStart');
    this.emit('scanStart', filterDuplicates);
  }

  stopScanning (callback) {
    if (this._initialized === false || this._bindings === null) {
      callback(new Error('Bindings are not initialized'));
      return;
    }
    if (callback) {
      this.onceExclusive('scanStop', callback);
    }
    this._bindings.stopScanning();
  }

  async stopScanningAsync () {
    return new Promise((resolve, reject) => {
      this.stopScanning(error => error ? reject(error) : resolve());
    });
  }

  async *discoverAsync () {
    const deviceQueue = [];
    let scanning = true;
    
    // Main discover listener to add devices to the queue
    const discoverListener = peripheral => deviceQueue.push(peripheral);
    
    // State change listener
    const scanStopListener = () => scanning = false;
    
    // Set up listeners
    this.on('discover', discoverListener);
    this.once('scanStop', scanStopListener);
    
    try {
        // Start the scanning process
        await this.startScanningAsync();
        
        // Process discovered devices
        while (scanning || deviceQueue.length > 0) {
            if (deviceQueue.length > 0) {
                // If we have devices in the queue, yield them
                yield deviceQueue.shift();
            } else if (scanning) {
                // Wait for either a new device or scan stop
                await new Promise(resolve => {
                    let resolved = false;
                    let timeoutId = null;

                    const cleanup = () => {
                        if (resolved) return;
                        resolved = true;
                        this.removeListener('discover', tempDiscoverListener);
                        this.removeListener('scanStop', tempScanStopListener);
                        if (timeoutId) clearTimeout(timeoutId);
                        resolve();
                    };

                    const tempDiscoverListener = () => cleanup();
                    const tempScanStopListener = () => cleanup();

                    this.once('discover', tempDiscoverListener);
                    this.once('scanStop', tempScanStopListener);

                    // Handle race condition where a device might arrive during promise setup
                    if (deviceQueue.length > 0) {
                        cleanup();
                        return;
                    }

                    // Add a maximum wait time with proper cleanup
                    if (scanning) {
                        timeoutId = setTimeout(() => cleanup(), 1000);
                    }
                });
            }
        }
    } finally {
        // Clean up listeners
        this.removeListener('discover', discoverListener);
        this.removeListener('scanStop', scanStopListener);
        
        // Ensure scanning is stopped
        if (scanning) {
            await this.stopScanningAsync();
        }
    }
  }

  _onScanStop () {
    debug('scanStop');
    this.emit('scanStop');
  }

  reset () {
    if (typeof this._bindings.reset !== 'function') { return; }
    this._bindings.reset();
  }

  stop () {
    if (typeof this._bindings.stop !== 'function') { return; }
    this._bindings.stop();
  }

  _onDiscover (uuid, address, addressType, connectable, advertisement, rssi, scannable) {
    let peripheral = this._peripherals.get(uuid);

    if (!peripheral) {
      peripheral = this._createPeripheral(uuid, address, addressType, connectable, advertisement, rssi, scannable);
    } else {
      // "or" the advertisment data with existing
      for (const i in advertisement) {
        if (advertisement[i] !== undefined) {
          peripheral.advertisement[i] = advertisement[i];
        }
      }

      peripheral.connectable = connectable;
      peripheral.scannable = scannable;
      peripheral.rssi = rssi;
    }

    const previouslyDiscoverd = this._discoveredPeripherals.has(uuid);

    if (!previouslyDiscoverd) {
      this._discoveredPeripherals.add(uuid);
    }
    if (this._allowDuplicates || !previouslyDiscoverd || (!scannable && !connectable)) {
      this.emit('discover', peripheral);
    }
  }

  _getPeripheralId (idOrAddress) {
    let identifier;
    // Convert the peripheralId to an identifier
    if (/^[0-9A-Fa-f]+$/.test(idOrAddress) === false) {
      identifier = this._bindings.addressToId(idOrAddress);
      if (identifier === null) {
        throw new Error(`Invalid peripheral ID or Address ${idOrAddress}`);
      }
    } else {
      identifier = idOrAddress;
    }
    return identifier;
  }

  connect (idOrAddress, parameters, callback) {
    // Get the identifier for the peripheral
    const identifier = this._getPeripheralId(idOrAddress);
    // Check if callback is a function
    if (typeof callback === 'function') {
      // Add a one-time listener for this specific event
      this.onceExclusive(`connect:${identifier}`, error => callback(error, this._peripherals.get(identifier)));
    }

    // Proceed to initiate the connection
    this._bindings.connect(identifier, parameters);
  }
  
  async connectAsync (idOrAddress, parameters) {
    return new Promise((resolve, reject) => {
      this.connect(idOrAddress, parameters, (error, peripheral) => error ? reject(error) : resolve(peripheral));
    });
  }

  _onConnect (peripheralId, error) {
    const peripheral = this._peripherals.get(peripheralId);

    if (peripheral) {
      // Emit a unique connect event for the specific peripheral
      this.emit(`connect:${peripheralId}`, error);

      peripheral.state = error ? 'error' : 'connected';
      // Also emit the general 'connect' event for a peripheral
      peripheral.emit('connect', error);
    } else {
      this.emit('warning', `unknown peripheral ${peripheralId} connected!`);
    }
  }

  cancelConnect (idOrAddress, parameters) {
    // Get the identifier for the peripheral
    const identifier = this._getPeripheralId(idOrAddress);
    
    // Check if the peripheral is connecting
    const peripheral = this._peripherals.get(identifier);
    if (peripheral && peripheral.state === 'connecting') {
      peripheral.state = 'disconnected';
    }
    // Emit a unique connect event for the specific peripheral
    this.emit(`connect:${identifier}`, new Error('connection canceled!'));
    // Cancel the connection
    this._bindings.cancelConnect(identifier, parameters);
  }

  disconnect (peripheralId) {
    // Disconnect the peripheral
    this._bindings.disconnect(peripheralId);
  }

  _onDisconnect (peripheralId, reason = 'unknown') {
    const peripheral = this._peripherals.get(peripheralId);

    if (peripheral) {
      peripheral.state = 'disconnected';
      peripheral.emit('disconnect', reason);
      this.emit(`disconnect:${peripheralId}`, reason);
    } else {
      this.emit('warning', `unknown peripheral ${peripheralId} disconnected!`);
    }
  }

  updateRssi (peripheralId) {
    this._bindings.updateRssi(peripheralId);
  }

  _onRssiUpdate (peripheralId, rssi, error) {
    const peripheral = this._peripherals.get(peripheralId);

    if (peripheral) {
      peripheral.rssi = rssi;

      peripheral.emit('rssiUpdate', rssi, error);
    } else {
      this.emit('warning', `unknown peripheral ${peripheralId} RSSI update!`);
    }
  }

  /// add an array of service objects (as retrieved via the servicesDiscovered event)
  addServices (peripheralId, services) {
    const servObjs = [];

    for (let i = 0; i < services.length; i++) {
      const o = this.addService(peripheralId, services[i]);
      servObjs.push(o);
    }
    return servObjs;
  }

  /// service is a ServiceObject { uuid, startHandle, endHandle,..}
  addService (peripheralId, service) {
    const peripheral = this._peripherals.get(peripheralId);

    // pass on to lower layers (gatt)
    if (this._bindings.addService) {
      this._bindings.addService(peripheralId, service);
    }

    if (!peripheral.services) {
      peripheral.services = [];
    }
    // allocate internal service object and return
    const serv = new Service(this, peripheralId, service.uuid);

    this._services[peripheralId][service.uuid] = serv;
    this._characteristics[peripheralId][service.uuid] = {};
    this._descriptors[peripheralId][service.uuid] = {};

    peripheral.services.push(serv);

    return serv;
  }

  /// callback receiving a list of service objects from the gatt layer
  _onServicesDiscovered (peripheralId, services) {
    const peripheral = this._peripherals.get(peripheralId);

    if (peripheral) { peripheral.emit('servicesDiscovered', peripheral, services); } // pass on to higher layers
  }

  discoverServices (peripheralId, uuids) {
    this._bindings.discoverServices(peripheralId, uuids);
  }

  _onServicesDiscover (peripheralId, serviceUuids, error) {
    const peripheral = this._peripherals.get(peripheralId);

    if (peripheral) {
      const services = [];

      for (let i = 0; i < serviceUuids.length; i++) {
        const serviceUuid = serviceUuids[i];
        const service = new Service(this, peripheralId, serviceUuid);

        this._services[peripheralId][serviceUuid] = service;
        this._characteristics[peripheralId][serviceUuid] = {};
        this._descriptors[peripheralId][serviceUuid] = {};

        services.push(service);
      }

      peripheral.services = services;
      
      peripheral.emit('servicesDiscover', services, error);
    } else {
      this.emit('warning', `unknown peripheral ${peripheralId} services discover!`);
    }
  }

  discoverIncludedServices (peripheralId, serviceUuid, serviceUuids) {
    this._bindings.discoverIncludedServices(peripheralId, serviceUuid, serviceUuids);
  }

  _onIncludedServicesDiscover (peripheralId, serviceUuid, includedServiceUuids, error) {
    const service = this._services[peripheralId][serviceUuid];

    if (service) {
      service.includedServiceUuids = includedServiceUuids;

      service.emit('includedServicesDiscover', includedServiceUuids, error);
    } else {
      this.emit('warning', `unknown peripheral ${peripheralId}, ${serviceUuid} included services discover!`);
    }
  }

  /// add characteristics to the peripheral; returns an array of initialized Characteristics objects
  addCharacteristics (peripheralId, serviceUuid, characteristics) {
    // first, initialize gatt layer:
    if (this._bindings.addCharacteristics) {
      this._bindings.addCharacteristics(peripheralId, serviceUuid, characteristics);
    }

    const service = this._services[peripheralId][serviceUuid];
    if (!service) {
      this.emit('warning', `unknown service ${peripheralId}, ${serviceUuid} characteristics discover!`);
      return;
    }

    const characteristics_ = [];
    for (let i = 0; i < characteristics.length; i++) {
      const characteristicUuid = characteristics[i].uuid;

      const characteristic = new Characteristic(
        this,
        peripheralId,
        serviceUuid,
        characteristicUuid,
        characteristics[i].properties
      );

      this._characteristics[peripheralId][serviceUuid][characteristicUuid] = characteristic;
      this._descriptors[peripheralId][serviceUuid][characteristicUuid] = {};

      characteristics_.push(characteristic);
    }
    service.characteristics = characteristics_;
    return characteristics_;
  }

  _onCharacteristicsDiscovered (peripheralId, serviceUuid, characteristics) {
    const service = this._services[peripheralId][serviceUuid];

    service.emit('characteristicsDiscovered', characteristics);
  }

  discoverCharacteristics (peripheralId, serviceUuid, characteristicUuids) {
    this._bindings.discoverCharacteristics(peripheralId, serviceUuid, characteristicUuids);
  }

  _onCharacteristicsDiscover (peripheralId, serviceUuid, characteristics, error) {
    const service = this._services[peripheralId][serviceUuid];

    if (service) {
      const characteristics_ = [];

      for (let i = 0; i < characteristics.length; i++) {
        const characteristicUuid = characteristics[i].uuid;

        const characteristic = new Characteristic(
          this,
          peripheralId,
          serviceUuid,
          characteristicUuid,
          characteristics[i].properties
        );

        this._characteristics[peripheralId][serviceUuid][characteristicUuid] = characteristic;
        this._descriptors[peripheralId][serviceUuid][characteristicUuid] = {};

        characteristics_.push(characteristic);
      }

      service.characteristics = characteristics_;

      service.emit('characteristicsDiscover', characteristics_, error);
    } else {
      this.emit('warning', `unknown peripheral ${peripheralId}, ${serviceUuid} characteristics discover!`);
    }
  }

  read (peripheralId, serviceUuid, characteristicUuid) {
    this._bindings.read(peripheralId, serviceUuid, characteristicUuid);
  }

  _onRead (peripheralId, serviceUuid, characteristicUuid, data, isNotification, error) {
    const characteristic = this._characteristics[peripheralId][serviceUuid][characteristicUuid];

    if (characteristic) {
      characteristic.emit('data', data, isNotification, error);
    } else {
      this.emit('warning', `unknown peripheral ${peripheralId}, ${serviceUuid}, ${characteristicUuid} read!`);
    }
  }

  write (peripheralId, serviceUuid, characteristicUuid, data, withoutResponse) {
    this._bindings.write(peripheralId, serviceUuid, characteristicUuid, data, withoutResponse);
  }

  _onWrite (peripheralId, serviceUuid, characteristicUuid, error) {
    const characteristic = this._characteristics[peripheralId][serviceUuid][characteristicUuid];

    if (characteristic) {
      characteristic.emit('write', error);
    } else {
      this.emit('warning', `unknown peripheral ${peripheralId}, ${serviceUuid}, ${characteristicUuid} write!`);
    }
  }

  broadcast (peripheralId, serviceUuid, characteristicUuid, broadcast) {
    this._bindings.broadcast(peripheralId, serviceUuid, characteristicUuid, broadcast);
  }

  _onBroadcast (peripheralId, serviceUuid, characteristicUuid, state) {
    const characteristic = this._characteristics[peripheralId][serviceUuid][characteristicUuid];

    if (characteristic) {
      characteristic.emit('broadcast', state);
    } else {
      this.emit('warning', `unknown peripheral ${peripheralId}, ${serviceUuid}, ${characteristicUuid} broadcast!`);
    }
  }

  notify (peripheralId, serviceUuid, characteristicUuid, notify) {
    this._bindings.notify(peripheralId, serviceUuid, characteristicUuid, notify);
  }

  _onNotify (peripheralId, serviceUuid, characteristicUuid, state, error) {
    const characteristic = this._characteristics[peripheralId][serviceUuid][characteristicUuid];
    if (characteristic) {
      characteristic.emit('notify', state, error);
    } else {
      this.emit('warning', `unknown peripheral ${peripheralId}, ${serviceUuid}, ${characteristicUuid} notify!`);
    }
  }

  discoverDescriptors (peripheralId, serviceUuid, characteristicUuid) {
    this._bindings.discoverDescriptors(peripheralId, serviceUuid, characteristicUuid);
  }

  _onDescriptorsDiscover (peripheralId, serviceUuid, characteristicUuid, descriptors, error) {
    const characteristic = this._characteristics[peripheralId][serviceUuid][characteristicUuid];

    if (characteristic) {
      const descriptors_ = [];

      for (let i = 0; i < descriptors.length; i++) {
        const descriptorUuid = descriptors[i];

        const descriptor = new Descriptor(
          this,
          peripheralId,
          serviceUuid,
          characteristicUuid,
          descriptorUuid
        );

        this._descriptors[peripheralId][serviceUuid][characteristicUuid][descriptorUuid] = descriptor;

        descriptors_.push(descriptor);
      }

      characteristic.descriptors = descriptors_;

      characteristic.emit('descriptorsDiscover', descriptors_, error);
    } else {
      this.emit('warning', `unknown peripheral ${peripheralId}, ${serviceUuid}, ${characteristicUuid} descriptors discover!`);
    }
  }

  readValue (peripheralId, serviceUuid, characteristicUuid, descriptorUuid) {
    this._bindings.readValue(peripheralId, serviceUuid, characteristicUuid, descriptorUuid);
  }

  _onValueRead (peripheralId, serviceUuid, characteristicUuid, descriptorUuid, data, error) {
    const descriptor = this._descriptors[peripheralId][serviceUuid][characteristicUuid][descriptorUuid];

    if (descriptor) {
      descriptor.emit('valueRead', data, error);
    } else {
      this.emit('warning', `unknown peripheral ${peripheralId}, ${serviceUuid}, ${characteristicUuid}, ${descriptorUuid} value read!`);
    }
  }

  writeValue (peripheralId, serviceUuid, characteristicUuid, descriptorUuid, data) {
    this._bindings.writeValue(peripheralId, serviceUuid, characteristicUuid, descriptorUuid, data);
  }

  _onValueWrite (peripheralId, serviceUuid, characteristicUuid, descriptorUuid, error) {
    const descriptor = this._descriptors[peripheralId][serviceUuid][characteristicUuid][descriptorUuid];

    if (descriptor) {
      descriptor.emit('valueWrite', error);
    } else {
      this.emit('warning', `unknown peripheral ${peripheralId}, ${serviceUuid}, ${characteristicUuid}, ${descriptorUuid} value write!`);
    }
  }

  readHandle (peripheralId, handle) {
    this._bindings.readHandle(peripheralId, handle);
  }

  _onHandleRead (peripheralId, handle, data, error) {
    const peripheral = this._peripherals.get(peripheralId);

    if (peripheral) {
      peripheral.emit(`handleRead${handle}`, data, error);
    } else {
      this.emit('warning', `unknown peripheral ${peripheralId} handle read!`);
    }
  }

  writeHandle (peripheralId, handle, data, withoutResponse) {
    this._bindings.writeHandle(peripheralId, handle, data, withoutResponse);
  }

  _onHandleWrite (peripheralId, handle, error) {
    const peripheral = this._peripherals.get(peripheralId);

    if (peripheral) {
      peripheral.emit(`handleWrite${handle}`, error);
    } else {
      this.emit('warning', `unknown peripheral ${peripheralId} handle write!`);
    }
  }

  _onHandleNotify (peripheralId, handle, data, error) {
    const peripheral = this._peripherals.get(peripheralId);

    if (peripheral) {
      peripheral.emit('handleNotify', handle, data, error);
    } else {
      this.emit('warning', `unknown peripheral ${peripheralId} handle notify!`);
    }
  }

  _onMtu (peripheralId, mtu) {
    const peripheral = this._peripherals.get(peripheralId);
    if (peripheral && mtu) {
      peripheral.mtu = mtu;
      peripheral.emit('mtu', mtu);
    }
  }

  async _withDisconnectHandler (peripheralId, operation) {
    return new Promise((resolve, reject) => {
      const disconnectListener = reason => reject(new Error(`Disconnected ${reason}`));
      this.once(`disconnect:${peripheralId}`, disconnectListener);
      
      Promise.resolve(operation())
        .then(result => {
          this.removeListener(`disconnect:${peripheralId}`, disconnectListener);
          resolve(result);
        })
        .catch(error => {
          this.removeListener(`disconnect:${peripheralId}`, disconnectListener);
          reject(error);
        });
    });
  }
}

module.exports = Noble;
