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

const BlenoEventEmitter = require('../bleno-event-emitter');
const os = require('os');

const AclStream = require('./acl-stream');
const Hci = require('./hci');
const Gap = require('./gap');
const Gatt = require('./gatt');

class BlenoBindings extends BlenoEventEmitter {
  constructor (options) {
    super();

    this._state = null;

    this._advertising = false;

    this._hci = new Hci(options);
    this._gap = new Gap(this._hci);
    this._gatt = new Gatt(this._hci);

    this._connections = new Map();
  }

  setAddress (address) {
    this._hci.setAddress(address);
  }

  startAdvertising (name, serviceUuids) {
    this._advertising = true;

    this._gatt.updateName(name);
    this._gap.startAdvertising(name, serviceUuids);
  }

  startAdvertisingIBeacon (data) {
    this._advertising = true;

    this._gap.startAdvertisingIBeacon(data);
  }

  startAdvertisingWithEIRData (advertisementData, scanData) {
    this._advertising = true;

    this._gap.startAdvertisingWithEIRData(advertisementData, scanData);
  }

  stopAdvertising () {
    this._advertising = false;

    this._gap.stopAdvertising();
  }

  setServices (services) {
    this._gatt.setServices(services);

    this.emit('servicesSet');
  }

  disconnect (handle = null) {
    if (handle) {
      debug('disconnect by server for handle', handle);
      this._hci.disconnect(handle);
    } else {
      for (const [handle, connection] of this._connections) {
        debug('disconnect by server for handle', handle, connection.address);
        this._hci.disconnect(handle);
      }
    }
  }

  updateRssi () {
    if (this._handle) {
      this._hci.readRssi(this._handle);
    }
  }

  init () {
    
    this._sigIntHandler = this.onSigInt.bind(this);
    this._exitHandler = this.onExit.bind(this);

    process.on('SIGINT', this._sigIntHandler);
    process.on('exit', this._exitHandler);

    this._gap.on('advertisingStart', this.onAdvertisingStart.bind(this));
    this._gap.on('advertisingStop', this.onAdvertisingStop.bind(this));

    this._gatt.on('mtuChange', this.onMtuChange.bind(this));

    this._hci.on('stateChange', this.onStateChange.bind(this));
    this._hci.on('addressChange', this.onAddressChange.bind(this));
    this._hci.on('readLocalVersion', this.onReadLocalVersion.bind(this));

    this._hci.on('leConnComplete', this.onLeConnComplete.bind(this));
    this._hci.on('leConnUpdateComplete', this.onLeConnUpdateComplete.bind(this));
    this._hci.on('rssiRead', this.onRssiRead.bind(this));
    this._hci.on('disconnComplete', this.onDisconnComplete.bind(this));
    this._hci.on('encryptChange', this.onEncryptChange.bind(this));
    this._hci.on('leLtkNegReply', this.onLeLtkNegReply.bind(this));
    this._hci.on('aclDataPkt', this.onAclDataPkt.bind(this));

    this.emit('platform', os.platform());

    this._hci.init();
  }

  stop () {
    process.removeListener('SIGINT', this._sigIntHandler);
    process.removeListener('exit', this._exitHandler);
    this.stopAdvertising();
    this.disconnect();
    this._hci.stop();
  }

  onStateChange (state) {
    if (this._state === state) {
      return;
    }
    this._state = state;

    if (state === 'unauthorized') {
      console.log('Bleno warning: adapter state unauthorized, please run as root or with sudo');
      console.log('               or see README for information on running without root/sudo:');
      console.log('               https://github.com/abandonware/bleno#running-on-linux');
    } else if (state === 'unsupported') {
      console.log('Bleno warning: adapter does not support Bluetooth Low Energy (BLE, Bluetooth Smart).');
      console.log('               Try to run with environment variable:');
      console.log('               [sudo] BLENO_HCI_DEVICE_ID=x node ...');
    }

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

  onAddressChange (address) {
    this.emit('addressChange', address);
  }

  onReadLocalVersion (hciVer, hciRev, lmpVer, manufacturer, lmpSubVer) {
  }

  onAdvertisingStart (error) {
    this.emit('advertisingStart', error);
  }

  onAdvertisingStop () {
    this.emit('advertisingStop');
  }

  onLeConnComplete (status, handle, role, addressType, address, interval, latency, supervisionTimeout, masterClockAccuracy) {
    if (role !== 1) {
      return;
    }

    const aclStream = new AclStream(this._hci, handle, this._hci.addressType, this._hci.address, addressType, address);
    this._connections.set(handle, { address, aclStream });
    this._gatt.registerAclStream(aclStream, handle);

    this.emit('accept', address, handle);
    
    if (this._advertising) {
      this._gap.restartAdvertising();
    }
  }

  onLeConnUpdateComplete (handle, interval, latency, supervisionTimeout) {
    // no-op
  }

  onDisconnComplete (handle, reason) {
    if (this._connections.has(handle) === false) {
      return;
    }

    const { address, aclStream } = this._connections.get(handle);
  
    if (aclStream) {
      aclStream.close();
    }
    
    this._connections.delete(handle);

    this.emit('disconnect', address, handle);

    if (this._advertising) {
      this._gap.restartAdvertising();
    }
  }

  onEncryptChange (handle, encrypt) {
    const connection = this._connections.get(handle);
    if (connection && connection.aclStream) {
      connection.aclStream.pushEncrypt(encrypt);
    }
  }

  onLeLtkNegReply (handle) {
    const connection = this._connections.get(handle);
    if (connection && connection.aclStream) {
      connection.aclStream.pushLtkNegReply();
    }
  }

  onMtuChange (mtu) {
    this.emit('mtuChange', mtu);
  }

  onRssiRead (handle, rssi) {
    this.emit('rssiUpdate', rssi);
  }

  onAclDataPkt (handle, cid, data) {
    const connection = this._connections.get(handle);
    if (connection && connection.aclStream) {
      connection.aclStream.push(cid, data);
    }
  }

  onSigInt () {
    const sigIntListeners = process.listeners('SIGINT');

    if (sigIntListeners[sigIntListeners.length - 1] === this._sigIntHandler) {
      // we are the last listener, so exit
      // this will trigger onExit, and clean up
      process.exit(1);
    }
  }

  onExit () {
    this.stop();
  }
}

module.exports = BlenoBindings;
