'use strict';

var Promise = require('bluebird');
var crypto = require('crypto');
var debug = require('debug')('august');
var events = require('events');
var noble = require('noble');
var util = require('util');
var _ = require('underscore');

// promisification of noble
Promise.promisifyAll(require('noble/lib/characteristic').prototype);
Promise.promisifyAll(require('noble/lib/peripheral').prototype);
Promise.promisifyAll(require('noble/lib/service').prototype);

// Calculates the security checksum of a command buffer.
function securityChecksum(buffer) {
  return (0 - (buffer.readUInt32LE(0x00) + buffer.readUInt32LE(0x04) + buffer.readUInt32LE(0x08))) >>> 0;
}

///
// LockSession

function LockSession(writeCharacteristic, readCharacteristic, isSecure) {
  if (!writeCharacteristic || !readCharacteristic) {
    throw new Error('write and/or read characteristic not found');
  }
  this._writeCharacteristic = writeCharacteristic;
  this._readCharacteristic = readCharacteristic;
  this._isSecure = isSecure;
  return this;
}

util.inherits(LockSession, events.EventEmitter);

LockSession.prototype.setKey = function(key) {
  var cipherSuite, iv;
  if (this._isSecure) {
    cipherSuite = 'aes-128-ecb';
    iv = '';
  } else {
    cipherSuite = 'aes-128-cbc';
    iv = new Buffer(0x10);
    iv.fill(0);
  }

  this._encryptCipher = crypto.createCipheriv(cipherSuite, key, iv);
  this._encryptCipher.setAutoPadding(false);
  this._decryptCipher = crypto.createDecipheriv(cipherSuite, key, iv);
  this._decryptCipher.setAutoPadding(false);
};

LockSession.prototype.start = function() {
  // decrypt all reads, modifying the buffer in place
  this._readCharacteristic.on('read', function(data, isNotify) {
    if (!data) {
      throw new Error('read returned no data');
    }

    debug('read data: ' + data.toString('hex'));

    if (this._decryptCipher) {
      var cipherText = data.slice(0x00, 0x10);
      var plainText = this._decryptCipher.update(cipherText);
      plainText.copy(cipherText);

      debug('decrypted data: ' + data.toString('hex'));
    }

    // the notification flag is not being set properly on OSX Yosemite, so just
    // forcing it to true.
    if (process.platform === 'darwin') {
      isNotify = true;
    }

    if (isNotify) {
      this.emit('notification', data);
    }
  }.bind(this));

  // enable notifications on the read characterestic
  debug('enabling notifications on ' + this._readCharacteristic);
  return this._readCharacteristic.notifyAsync(true);
};

LockSession.prototype.execute = function(command) {
  // write the security checksum if on the secure channel
  if (this._isSecure) {
    var checksum = securityChecksum(command);
    command.writeUInt32LE(checksum, 0x0c);
  }

  debug((this._isSecure ? 'secure ' : '') + 'execute command: ' + command.toString('hex'));

  // NOTE: the last two bytes are not encrypted
  // general idea seems to be that if the last byte of the command indicates an offline key offset (is non-zero), the command is "secure" and encrypted with the offline key
  if (this._encryptCipher) {
    var plainText = command.slice(0x00, 0x10);
    var cipherText = this._encryptCipher.update(plainText);
    cipherText.copy(plainText);
    debug('execute command (encrypted): ' + command.toString('hex'));
  }

  // register the notification event listener here, before issuing the write, as the
  // response notification arrives before the write response.
  var waitForNotification = new Promise(function(resolve) {
    this.once('notification', resolve);
  }.bind(this));

  return this._writeCharacteristic.writeAsync(command, false).then(function() {
    debug('write successful, waiting for notification...');
    return waitForNotification;
  }).then(function(data) {
    // perform some basic validation before passing it on
    if (this._isSecure) {
      if (securityChecksum(data) !== data.readUInt32LE(0x0c)) {
        throw new Error("security checksum mismatch");
      }
    } else {
      if (data[0] !== 0xbb && data[0] !== 0xaa) {
        throw new Error("unexpected magic in response");
      }
    }

    return data;
  }.bind(this));
};

module.exports = LockSession;