// Copyright 2011 Mark Cavage <mcavage@gmail.com> All rights reserved.

var assert = require('assert'); var ASN1 = require('./types'); var errors = require('./errors');

///— Globals

var newInvalidAsn1Error = errors.newInvalidAsn1Error;

var DEFAULT_OPTS = {

size: 1024,
growthFactor: 8

};

///— Helpers

function merge(from, to) {

assert.ok(from);
assert.equal(typeof(from), 'object');
assert.ok(to);
assert.equal(typeof(to), 'object');

var keys = Object.getOwnPropertyNames(from);
keys.forEach(function(key) {
  if (to[key])
    return;

  var value = Object.getOwnPropertyDescriptor(from, key);
  Object.defineProperty(to, key, value);
});

return to;

}

///— API

function Writer(options) {

options = merge(DEFAULT_OPTS, options || {});

this._buf = new Buffer(options.size || 1024);
this._size = this._buf.length;
this._offset = 0;
this._options = options;

// A list of offsets in the buffer where we need to insert
// sequence tag/len pairs.
this._seq = [];

}

Object.defineProperty(Writer.prototype, 'buffer', {

get: function () {
  if (this._seq.length)
    throw new InvalidAsn1Error(this._seq.length + ' unended sequence(s)');

  return (this._buf.slice(0, this._offset));
}

});

Writer.prototype.writeByte = function(b) {

if (typeof(b) !== 'number')
  throw new TypeError('argument must be a Number');

this._ensure(1);
this._buf[this._offset++] = b;

};

Writer.prototype.writeInt = function(i, tag) {

if (typeof(i) !== 'number')
  throw new TypeError('argument must be a Number');
if (typeof(tag) !== 'number')
  tag = ASN1.Integer;

var sz = 4;

while ((((i & 0xff800000) === 0) || ((i & 0xff800000) === 0xff800000 >> 0)) &&
       (sz > 1)) {
  sz--;
  i <<= 8;
}

if (sz > 4)
  throw new InvalidAsn1Error('BER ints cannot be > 0xffffffff');

this._ensure(2 + sz);
this._buf[this._offset++] = tag;
this._buf[this._offset++] = sz;

while (sz-- > 0) {
  this._buf[this._offset++] = ((i & 0xff000000) >>> 24);
  i <<= 8;
}

};

Writer.prototype.writeNull = function() {

this.writeByte(ASN1.Null);
this.writeByte(0x00);

};

Writer.prototype.writeEnumeration = function(i, tag) {

if (typeof(i) !== 'number')
  throw new TypeError('argument must be a Number');
if (typeof(tag) !== 'number')
  tag = ASN1.Enumeration;

return this.writeInt(i, tag);

};

Writer.prototype.writeBoolean = function(b, tag) {

if (typeof(b) !== 'boolean')
  throw new TypeError('argument must be a Boolean');
if (typeof(tag) !== 'number')
  tag = ASN1.Boolean;

this._ensure(3);
this._buf[this._offset++] = tag;
this._buf[this._offset++] = 0x01;
this._buf[this._offset++] = b ? 0xff : 0x00;

};

Writer.prototype.writeString = function(s, tag) {

if (typeof(s) !== 'string')
  throw new TypeError('argument must be a string (was: ' + typeof(s) + ')');
if (typeof(tag) !== 'number')
  tag = ASN1.OctetString;

var len = Buffer.byteLength(s);
this.writeByte(tag);
this.writeLength(len);
if (len) {
  this._ensure(len);
  this._buf.write(s, this._offset);
  this._offset += len;
}

};

Writer.prototype.writeBuffer = function(buf, tag) {

if (typeof(tag) !== 'number')
  throw new TypeError('tag must be a number');
if (!Buffer.isBuffer(buf))
  throw new TypeError('argument must be a buffer');

this.writeByte(tag);
this.writeLength(buf.length);
this._ensure(buf.length);
buf.copy(this._buf, this._offset, 0, buf.length);
this._offset += buf.length;

};

Writer.prototype.writeStringArray = function(strings) {

if ((!strings instanceof Array))
  throw new TypeError('argument must be an Array[String]');

var self = this;
strings.forEach(function(s) {
  self.writeString(s);
});

};

// This is really to solve DER cases, but whatever for now Writer.prototype.writeOID = function(s, tag) {

if (typeof(s) !== 'string')
  throw new TypeError('argument must be a string');
if (typeof(tag) !== 'number')
  tag = ASN1.OID;

if (!/^([0-9]+\.){3,}[0-9]+$/.test(s))
  throw new Error('argument is not a valid OID string');

function encodeOctet(bytes, octet) {
  if (octet < 128) {
      bytes.push(octet);
  } else if (octet < 16384) {
      bytes.push((octet >>> 7) | 0x80);
      bytes.push(octet & 0x7F);
  } else if (octet < 2097152) {
    bytes.push((octet >>> 14) | 0x80);
    bytes.push(((octet >>> 7) | 0x80) & 0xFF);
    bytes.push(octet & 0x7F);
  } else if (octet < 268435456) {
    bytes.push((octet >>> 21) | 0x80);
    bytes.push(((octet >>> 14) | 0x80) & 0xFF);
    bytes.push(((octet >>> 7) | 0x80) & 0xFF);
    bytes.push(octet & 0x7F);
  } else {
    bytes.push(((octet >>> 28) | 0x80) & 0xFF);
    bytes.push(((octet >>> 21) | 0x80) & 0xFF);
    bytes.push(((octet >>> 14) | 0x80) & 0xFF);
    bytes.push(((octet >>> 7) | 0x80) & 0xFF);
    bytes.push(octet & 0x7F);
  }
}

var tmp = s.split('.');
var bytes = [];
bytes.push(parseInt(tmp[0], 10) * 40 + parseInt(tmp[1], 10));
tmp.slice(2).forEach(function(b) {
  encodeOctet(bytes, parseInt(b, 10));
});

var self = this;
this._ensure(2 + bytes.length);
this.writeByte(tag);
this.writeLength(bytes.length);
bytes.forEach(function(b) {
  self.writeByte(b);
});

};

Writer.prototype.writeLength = function(len) {

if (typeof(len) !== 'number')
  throw new TypeError('argument must be a Number');

this._ensure(4);

if (len <= 0x7f) {
  this._buf[this._offset++] = len;
} else if (len <= 0xff) {
  this._buf[this._offset++] = 0x81;
  this._buf[this._offset++] = len;
} else if (len <= 0xffff) {
  this._buf[this._offset++] = 0x82;
  this._buf[this._offset++] = len >> 8;
  this._buf[this._offset++] = len;
} else if (len <= 0xffffff) {
  this._buf[this._offset++] = 0x83;
  this._buf[this._offset++] = len >> 16;
  this._buf[this._offset++] = len >> 8;
  this._buf[this._offset++] = len;
} else {
  throw new InvalidAsn1ERror('Length too long (> 4 bytes)');
}

};

Writer.prototype.startSequence = function(tag) {

if (typeof(tag) !== 'number')
  tag = ASN1.Sequence | ASN1.Constructor;

this.writeByte(tag);
this._seq.push(this._offset);
this._ensure(3);
this._offset += 3;

};

Writer.prototype.endSequence = function() {

var seq = this._seq.pop();
var start = seq + 3;
var len = this._offset - start;

if (len <= 0x7f) {
  this._shift(start, len, -2);
  this._buf[seq] = len;
} else if (len <= 0xff) {
  this._shift(start, len, -1);
  this._buf[seq] = 0x81;
  this._buf[seq + 1] = len;
} else if (len <= 0xffff) {
  this._buf[seq] = 0x82;
  this._buf[seq + 1] = len >> 8;
  this._buf[seq + 2] = len;
} else if (len <= 0xffffff) {
  this._shift(start, len, 1);
  this._buf[seq] = 0x83;
  this._buf[seq + 1] = len >> 16;
  this._buf[seq + 2] = len >> 8;
  this._buf[seq + 3] = len;
} else {
  throw new InvalidAsn1Error('Sequence too long');
}

};

Writer.prototype._shift = function(start, len, shift) {

assert.ok(start !== undefined);
assert.ok(len !== undefined);
assert.ok(shift);

this._buf.copy(this._buf, start + shift, start, start + len);
this._offset += shift;

};

Writer.prototype._ensure = function(len) {

assert.ok(len);

if (this._size - this._offset < len) {
  var sz = this._size * this._options.growthFactor;
  if (sz - this._offset < len)
    sz += len;

  var buf = new Buffer(sz);

  this._buf.copy(buf, 0, 0, this._offset);
  this._buf = buf;
  this._size = sz;
}

};

///— Exported API

module.exports = Writer;