var protocol = require('./protocol');

// Connect
module.exports.connect = function(opts) {
  var opts = opts || {}
    , protocolId = opts.protocolId
    , protocolVersion = opts.protocolVersion
    , will = opts.will
    , clean = opts.clean
    , keepalive = opts.keepalive
    , clientId = opts.clientId
    , username = opts.username
    , password = opts.password;

  var length = 0;

  // Must be a string and non-falsy
  if(!protocolId || typeof protocolId !== "string") {
    return new Error('Invalid protocol id');
  } else {
    length += protocolId.length + 2;
  }

  // Must be a 1 byte number
  if (!protocolVersion ||
      'number' !== typeof protocolVersion ||
      protocolVersion > 255 ||
      protocolVersion < 0) {

    return new Error('Invalid protocol version');
  } else {
    length += 1;
  }

  // Must be a non-falsy string
  if(!clientId ||
      'string' !== typeof clientId) {
    return new Error('Invalid client id');
  } else {
    length += clientId.length + 2;
  }

  // Must be a two byte number
  if ('number' !== typeof keepalive ||
      keepalive < 0 ||
      keepalive > 65535) {
    return new Error('Invalid keepalive');
  } else {
    length += 2;
  }

  // Connect flags
  length += 1;

  // If will exists...
  if (will) {
    // It must be an object
    if ('object' !== typeof will) {
      return new Error('Invalid will');
    }
    // It must have topic typeof string
    if (!will.topic || 'string' !== typeof will.topic) {
      return new Error('Invalid will - invalid topic');
    } else {
      length += will.topic.length + 2;
    }
    // TODO: should be a buffer
    // It must have payload typeof string
    if (!will.payload || 'string' !== typeof will.payload) {
      return new Error('Invalid will - invalid payload');
    } else {
      length += will.payload.length + 2;
    }
  }

  // Username
  if (username) {
    if (username.length) {
      length += username.length + 2;
    } else {
      return new Error('Invalid username')
    }
  }

  // Password
  if (password) {
    if (password.length) {
      length += password.length + 2;
    } else {
      return new Error('Invalid password')
    }
  }

  var buffer = new Buffer(1 + calc_length_length(length) + length)
    , pos = 0;

  // Generate header
  buffer.writeUInt8(protocol.codes['connect'] << protocol.CMD_SHIFT, pos++);

  // Generate length
  pos += write_length(buffer, pos, length);

  // Generate protocol ID
  pos += write_string(buffer, pos, protocolId);
  buffer.writeUInt8(protocolVersion, pos++);

  // Connect flags
  var flags = 0;
  flags |= username ? protocol.USERNAME_MASK : 0;
  flags |= password ? protocol.PASSWORD_MASK : 0;
  flags |= (will && will.retain) ? protocol.WILL_RETAIN_MASK : 0;
  flags |= (will && will.qos) ?
    will.qos << protocol.WILL_QOS_SHIFT : 0;
  flags |= will ? protocol.WILL_FLAG_MASK : 0;
  flags |= clean ? protocol.CLEAN_SESSION_MASK : 0;

  buffer.writeUInt8(flags, pos++);

  // Keepalive
  pos += write_number(buffer, pos, keepalive);

  // Client ID
  pos += write_string(buffer, pos, clientId);

  // Will
  if (will) {
    pos += write_string(buffer, pos, will.topic);
    pos += write_string(buffer, pos, will.payload);
  }

  // Username and password
  if (username && typeof username === 'string')
    pos += write_string(buffer, pos, username);
  else if (username) {
    pos += write_number(buffer, pos, username.length);
    pos += write_buffer(buffer, pos, username);
  }

  if (password && typeof password === 'string')
    pos += write_string(buffer, pos, password);
  else if (password) {
    pos += write_number(buffer, pos, password.length);
    pos += write_buffer(buffer, pos, password);
  }

  return buffer;
};

// Connack
module.exports.connack = function(opts) {
  var opts = opts || {}
    , rc = opts.returnCode;


  // Check return code
  if ('number' !== typeof rc) {
    return new Error('Invalid return code');
  }

  var buffer = new Buffer(4)
    , pos = 0;

  buffer.writeUInt8(protocol.codes['connack'] << protocol.CMD_SHIFT, pos++);
  pos += write_length(buffer, pos, 2);
  pos += write_number(buffer, pos, rc);

  return buffer;
};

// Publish
var empty = new Buffer(0);
module.exports.publish = function(opts) {
  var opts = opts || {}
    , dup = opts.dup ? protocol.DUP_MASK : 0
    , qos = opts.qos
    , retain = opts.retain ? protocol.RETAIN_MASK : 0
    , topic = opts.topic
    , payload = opts.payload || empty
    , id = opts.messageId;

  var length = 0;

  // Topic must be a non-empty string
  if (!topic || 'string' !== typeof topic) {
    return new Error('Invalid topic');
  } else {
    length += Buffer.byteLength(topic) + 2;
  }

  // get the payload length
  if (!Buffer.isBuffer(payload)) {
    length += Buffer.byteLength(payload);
  } else {
    length += payload.length;
  }

  // Message id must a number if qos > 0
  if (qos && 'number' !== typeof id) {
    return new Error('Invalid message id')
  } else if (qos) {
    length += 2;
  }

  var buffer = new Buffer(1 + calc_length_length(length) + length)
    , pos = 0;

  // Header
  buffer[pos++] =
    protocol.codes['publish'] << protocol.CMD_SHIFT |
    dup |
    qos << protocol.QOS_SHIFT |
    retain;

  // Remaining length
  pos += write_length(buffer, pos, length);

  // Topic
  pos += write_string(buffer, pos, topic);

  // Message ID
  if (qos > 0) {
    pos += write_number(buffer, pos, id);
  }

  // Payload
  if (!Buffer.isBuffer(payload)) {
    write_string_no_pos(buffer, pos, payload);
  } else {
    write_buffer(buffer, pos, payload);
  }

  return buffer;
};

/* Puback, pubrec, pubrel and pubcomp */
var gen_pubs = function(opts, type) {
  var opts = opts || {}
    , id = opts.messageId
    , dup = (opts.dup && type === 'pubrel') ? protocol.DUP_MASK : 0
    , qos = type === 'pubrel' ? 1 : 0;

  // Check message ID
  if ('number' !== typeof id) {
    return new Error('Invalid message id');
  }

  var buffer = new Buffer(4)
    , pos = 0;

  // Header
  buffer[pos++] =
    protocol.codes[type] << protocol.CMD_SHIFT |
    dup |
    qos << protocol.QOS_SHIFT;

  // Length
  pos += write_length(buffer, pos, 2);

  // Message ID
  pos += write_number(buffer, pos, id);

  return buffer;
}

var pubs = ['puback', 'pubrec', 'pubrel', 'pubcomp'];

for (var i = 0; i < pubs.length; i++) {
  module.exports[pubs[i]] = function(pubType) {
    return function(opts) {
      return gen_pubs(opts, pubType);
    }
  }(pubs[i]);
}

/* Subscribe */
module.exports.subscribe = function(opts) {
  var opts = opts || {}
    , dup = opts.dup ? protocol.DUP_MASK : 0
    , qos = opts.qos || 0
    , id = opts.messageId
    , subs = opts.subscriptions;

  var length = 0;

  // Check mid
  if ('number' !== typeof id) {
    return new Error('Invalid message id');
  } else {
    length += 2;
  }
  // Check subscriptions
  if ('object' === typeof subs && subs.length) {
    for (var i = 0; i < subs.length; i += 1) {
      var topic = subs[i].topic
        , qos = subs[i].qos;

      if ('string' !== typeof topic) {
        return new Error('Invalid subscriptions - invalid topic');
      }
      if ('number' !== typeof qos) {
        return new Error('Invalid subscriptions - invalid qos');
      }

      length += Buffer.byteLength(topic) + 2 + 1;
    }
  } else {
    return new Error('Invalid subscriptions');
  }

  var buffer = new Buffer(1 + calc_length_length(length) + length)
    , pos = 0;

  // Generate header
  buffer.writeUInt8(
    protocol.codes['subscribe'] << protocol.CMD_SHIFT |
    dup |
    1 << protocol.QOS_SHIFT, pos++);

  // Generate length
  pos += write_length(buffer, pos, length);

  // Generate message ID
  pos += write_number(buffer, pos, id);

  // Generate subs
  for (var i = 0; i < subs.length; i++) {
    var sub = subs[i]
      , topic = sub.topic
      , qos = sub.qos;

    // Write topic string
    pos += write_string(buffer, pos, topic);
    // Write qos
    buffer.writeUInt8(qos, pos++);
  }

  return buffer;
};

/* Suback */
module.exports.suback = function(opts) {
  var opts = opts || {}
    , id = opts.messageId
    , granted = opts.granted;

  var length = 0;

  // Check message id
  if ('number' !== typeof id) {
    return new Error('Invalid message id');
  } else {
    length += 2;
  }
  // Check granted qos vector
  if ('object' === typeof granted && granted.length) {
    for (var i = 0; i < granted.length; i += 1) {
      if ('number' !== typeof granted[i]) {
        return new Error('Invalid qos vector');
      }
      length += 1;
    }
  } else {
    return new Error('Invalid qos vector');
  }

  var buffer = new Buffer(1 + calc_length_length(length) + length)
    , pos = 0;

  // Header
  buffer.writeUInt8(protocol.codes['suback'] << protocol.CMD_SHIFT, pos++);

  // Length
  pos += write_length(buffer, pos, length);

  // Message ID
  pos += write_number(buffer, pos, id);

  // Subscriptions
  for (var i = 0; i < granted.length; i++) {
    buffer.writeUInt8(granted[i], pos++);
  }

  return buffer;
};

/* Unsubscribe */
module.exports.unsubscribe = function(opts) {
  var opts = opts || {}
    , id = opts.messageId
    , dup = opts.dup ? protocol.DUP_MASK : 0
    , unsubs = opts.unsubscriptions;

  var length = 0;

  // Check message id
  if ('number' !== typeof id) {
    return new Error('Invalid message id');
  } else {
    length += 2;
  }
  // Check unsubs
  if ('object' === typeof unsubs && unsubs.length) {
    for (var i = 0; i < unsubs.length; i += 1) {
      if ('string' !== typeof unsubs[i]) {
        return new Error('Invalid unsubscriptions');
      }
      length += Buffer.byteLength(unsubs[i]) + 2;
    }
  } else {
    return new Error('Invalid unsubscriptions');
  }

  var buffer = new Buffer(1 + calc_length_length(length) + length)
    , pos = 0;

  // Header
  buffer[pos++] =
    protocol.codes['unsubscribe'] << protocol.CMD_SHIFT |
    dup |
    1 << protocol.QOS_SHIFT;

  // Length
  pos += write_length(buffer, pos, length);

  // Message ID
  pos += write_number(buffer, pos, id);

  // Unsubs
  for (var i = 0; i < unsubs.length; i++) {
    pos += write_string(buffer, pos, unsubs[i]);
  }

  return buffer;
};

/* Unsuback */
/* Note: uses gen_pubs since unsuback is the same as suback */
module.exports.unsuback = function(type) {
  return function(opts) {
    return gen_pubs(opts, type);
  }
}('unsuback');

/* Pingreq, pingresp, disconnect */
var empties = ['pingreq', 'pingresp', 'disconnect'];

for (var i = 0; i < empties.length; i++) {
  module.exports[empties[i]] = function(type) {
    return function(opts) {
      var buf = new Buffer(2);
      buf[0] = protocol.codes[type] << 4;
      buf[1] = 0;
      return buf;
    }
  }(empties[i]);
}

/**
 * calc_length_length - calculate the length of the remaining
 * length field
 *
 * @api private
 */

function calc_length_length(length) {
  if (length >= 0 && length < 128) {
    return 1;
  } else if (length >= 128 && length < 16384) {
    return 2;
  } else if (length >= 16384 && length < 2097152) {
    return 3;
  } else if (length >= 2097152 && length < 268435456) {
    return 4;
  } else {
    return 0;
  }
};

/**
 * write_length - write an MQTT style length field to the buffer
 *
 * @param <Buffer> buffer - destination
 * @param <Number> pos - offset
 * @param <Number> length - length (>0)
 * @returns <Number> number of bytes written
 *
 * @api private
 */

function write_length(buffer, pos, length) {
  var digit = 0
    , origPos = pos;

  do {
    digit = length % 128 | 0
    length = length / 128 | 0;
    if (length > 0) {
        digit = digit | 0x80;
    }
    buffer.writeUInt8(digit, pos++);
  } while (length > 0);

  return pos - origPos;
};

/**
 * write_string - write a utf8 string to the buffer
 *
 * @param <Buffer> buffer - destination
 * @param <Number> pos - offset
 * @param <String> string - string to write
 * @return <Number> number of bytes written
 *
 * @api private
 */

function write_string(buffer, pos, string) {
  var strlen = Buffer.byteLength(string);
  write_number(buffer, pos, strlen);

  write_string_no_pos(buffer, pos + 2, string);

  return strlen + 2;
};

function write_string_no_pos(buffer, pos, string) {
  buffer.write(string, pos);
}

/**
 * write_buffer - write buffer to buffer
 *
 * @param <Buffer> buffer - dest buffer
 * @param <Number> pos - offset
 * @param <Buffer> src - source buffer
 * @return <Number> number of bytes written
 *
 * @api private
 */

function write_buffer(buffer, pos, src) {
  src.copy(buffer, pos);
  return src.length;
}

/**
 * write_number - write a two byte number to the buffer
 *
 * @param <Buffer> buffer - destination
 * @param <Number> pos - offset
 * @param <String> number - number to write
 * @return <Number> number of bytes written
 *
 * @api private
 */
function write_number(buffer, pos, number) {
  buffer.writeUInt8(number >> 8, pos);
  buffer.writeUInt8(number & 0x00FF, pos + 1);

  return 2;
};
