/*!
 * asn1.js - asn1 parsing for bcoin
 * Copyright (c) 2016, Christopher Jeffrey (MIT License).
 * https://github.com/bcoin-org/bcoin
 *
 * Parts of this software are based on asn1.js.
 * https://github.com/indutny/asn1.js
 *
 * Copyright Fedor Indutny, 2013.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to
 * deal in the Software without restriction, including without limitation the
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
 * sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 * IN THE SOFTWARE.
 */

'use strict';

var assert = require('assert');
var BufferReader = require('../utils/reader');
var asn1 = exports;

asn1.parseTag = function parseTag(p) {
  var tag = p.readU8();
  var primitive = (tag & 0x20) === 0;
  var oct;

  if ((tag & 0x1f) === 0x1f) {
    oct = tag;
    tag = 0;
    while ((oct & 0x80) === 0x80) {
      oct = p.readU8();
      tag <<= 7;
      tag |= oct & 0x7f;
    }
  } else {
    tag &= 0x1f;
  }

  return {
    primitive: primitive,
    tag: tag,
    len: asn1.parseLen(p, primitive)
  };
};

asn1.parseLen = function parseLen(p, primitive) {
  var len = p.readU8();
  var num, i, j;

  // Indefinite form
  if (!primitive && len === 0x80)
    return null;

  // Definite form
  if ((len & 0x80) === 0) {
    // Short form
    return len;
  }

  // Long form
  num = len & 0x7f;
  assert(num < 4, 'length octect is too long');

  len = 0;
  for (i = 0; i < num; i++) {
    len <<= 8;
    j = p.readU8();
    len |= j;
  }

  return len;
};

asn1.parseCert = function parseCert(data) {
  var d = BufferReader(data);
  var p;

  d.start();

  p = BufferReader(asn1.parseSeq(d));

  return {
    tbs: asn1.parseTBS(p),
    sigAlg: asn1.parseAlgIdent(p),
    sig: asn1.parseBitstr(p),
    raw: d.endData(true)
  };
};

asn1.parseTBS = function parseTBS(data) {
  var d = BufferReader(data);
  var p;

  d.start();

  p = BufferReader(asn1.parseSeq(d));

  return {
    version: asn1.parseExplicitInt(p, 0, true),
    serial: asn1.parseInt(p),
    sig: asn1.parseAlgIdent(p),
    issuer: asn1.parseName(p),
    validity: asn1.parseValidity(p),
    subject: asn1.parseName(p),
    pubkey: asn1.parsePubkey(p),
    raw: d.endData(true)
  };
};

asn1.parseSeq = function parseSeq(data) {
  var p = BufferReader(data);
  var tag = asn1.parseTag(p);
  assert.equal(tag.tag, 0x10); // seq
  return p.readBytes(tag.len, true);
};

asn1.parseInt = function parseInt(data, readNum) {
  var p = BufferReader(data);
  var tag = asn1.parseTag(p);
  var num;

  assert.equal(tag.tag, 0x02); // int

  num = p.readBytes(tag.len, true);

  if (readNum)
    return num.readUIntBE(0, num.length);

  return num;
};

asn1.parseExplicitInt = function parseExplicitInt(data, i, readNum) {
  var p = BufferReader(data);
  var off = p.offset;
  var tag = asn1.parseTag(p);
  if (tag.tag !== i) {
    p.seek(-(p.offset - off));
    return -1;
  }
  return asn1.parseInt(p, readNum);
};

asn1.parseBitstr = function parseBitstr(data) {
  var p = BufferReader(data);
  var tag = asn1.parseTag(p);
  assert.equal(tag.tag, 0x03); // bitstr
  return asn1.alignBitstr(p.readBytes(tag.len, true));
};

asn1.parseString = function parseString(data) {
  var p = BufferReader(data);
  var tag = asn1.parseTag(p);
  switch (tag.tag) {
    case 0x03: // bitstr
      return asn1.alignBitstr(p.readBytes(tag.len, true));
    case 0x04: // octstr
    case 0x12: // numstr
    case 0x13: // prinstr
    case 0x14: // t61str
    case 0x15: // videostr
    case 0x16: // ia5str
    case 0x19: // graphstr
    case 0x0c: // utf8str
    case 0x1a: // iso646str
    case 0x1b: // genstr
    case 0x1c: // unistr
    case 0x1d: // charstr
    case 0x1e: // bmpstr
      return p.readString('utf8', tag.len);
    default:
      assert(false, 'Bad string.');
  }
};

asn1.alignBitstr = function(data) {
  var padding = data[0];
  var bits = (data.length - 1) * 8 - padding;
  var buf = data.slice(1);
  var shift = 8 - (bits % 8);
  var i, out;

  if (shift === 8 || buf.length === 0)
    return buf;

  out = new Buffer(buf.length);
  out[0] = buf[0] >>> shift;

  for (i = 1; i < buf.length; i++) {
    out[i] = buf[i - 1] << (8 - shift);
    out[i] |= buf[i] >>> shift;
  }

  return out;
};

asn1.parsePubkey = function parsePubkey(data) {
  var p = BufferReader(data);
  p = BufferReader(asn1.parseSeq(p));
  return {
    alg: asn1.parseAlgIdent(p),
    pubkey: asn1.parseBitstr(p)
  };
};

asn1.parseName = function parseName(data) {
  var p = BufferReader(data);
  var values = [];
  var tag;

  p = BufferReader(asn1.parseSeq(p));

  while (p.left()) {
    tag = asn1.parseTag(p);
    assert.equal(tag.tag, 0x11); // set
    tag = asn1.parseTag(p);
    assert.equal(tag.tag, 0x10); // seq
    values.push({
      type: asn1.parseOID(p),
      value: asn1.parseString(p)
    });
  }

  return values;
};

asn1.parseValidity = function parseValidity(data) {
  var p = BufferReader(data);
  p = BufferReader(asn1.parseSeq(p));
  return {
    notBefore: asn1.parseTime(p),
    notAfter: asn1.parseTime(p)
  };
};

asn1.parseTime = function parseTime(data) {
  var p = BufferReader(data);
  var tag = asn1.parseTag(p);
  var str = p.readString('ascii', tag.len);
  var year, mon, day, hour, min, sec;

  switch (tag.tag) {
    case 0x17: // utctime
      year = str.slice(0, 2) | 0;
      mon = str.slice(2, 4) | 0;
      day = str.slice(4, 6) | 0;
      hour = str.slice(6, 8) | 0;
      min = str.slice(8, 10) | 0;
      sec = str.slice(10, 12) | 0;
      if (year < 70)
        year = 2000 + year;
      else
        year = 1900 + year;
      break;
    case 0x18: // gentime
      year = str.slice(0, 4) | 0;
      mon = str.slice(4, 6) | 0;
      day = str.slice(6, 8) | 0;
      hour = str.slice(8, 10) | 0;
      min = str.slice(10, 12) | 0;
      sec = str.slice(12, 14) | 0;
      break;
    default:
      assert(false);
      break;
  }

  return Date.UTC(year, mon - 1, day, hour, min, sec, 0) / 1000;
};

asn1.parseOID = function parseOID(data) {
  var p = BufferReader(data);
  var tag = asn1.parseTag(p);
  var ids = [];
  var ident = 0;
  var subident = 0;
  var objid, result, first, second;

  assert.equal(tag.tag, 0x06); // objid

  objid = p.readBytes(tag.len, true);
  p = BufferReader(objid);

  while (p.left()) {
    subident = p.readU8();
    ident <<= 7;
    ident |= subident & 0x7f;
    if ((subident & 0x80) === 0) {
      ids.push(ident);
      ident = 0;
    }
  }

  if (subident & 0x80)
    ids.push(ident);

  first = (ids[0] / 40) | 0;
  second = ids[0] % 40;
  result = [first, second].concat(ids.slice(1));

  return result.join('.');
};

asn1.parseAlgIdent = function parseAlgIdent(data) {
  var p = BufferReader(data);
  var params = null;
  var alg;

  p = BufferReader(asn1.parseSeq(p));

  alg = asn1.parseOID(p);

  if (p.left() > 0) {
    params = p.readBytes(asn1.parseTag(p).len, true);
    if (params.length === 0)
      params = null;
  }

  return {
    alg: alg,
    params: params
  };
};

asn1.parseRSAPublic = function parseRSAPublic(data) {
  var p = BufferReader(data);
  p = BufferReader(asn1.parseSeq(p));
  return {
    modulus: asn1.parseInt(p),
    publicExponent: asn1.parseInt(p)
  };
};

asn1.parseRSAPrivate = function parseRSAPrivate(data) {
  var p = BufferReader(data);
  p = BufferReader(asn1.parseSeq(p));
  return {
    version: asn1.parseInt(p, true),
    modulus: asn1.parseInt(p),
    publicExponent: asn1.parseInt(p),
    privateExponent: asn1.parseInt(p),
    prime1: asn1.parseInt(p),
    prime2: asn1.parseInt(p),
    exponent1: asn1.parseInt(p),
    exponent2: asn1.parseInt(p),
    coefficient: asn1.parseInt(p)
  };
};

asn1.parsePEM = function parsePEM(pem) {
  var buf = '';
  var chunks = [];
  var s, tag, type;

  while (pem.length) {
    if (s = /^-----BEGIN ([^\-]+)-----/.exec(pem)) {
      pem = pem.substring(s[0].length);
      tag = s[1];
      continue;
    }
    if (s = /^-----END ([^\-]+)-----/.exec(pem)) {
      pem = pem.substring(s[0].length);
      assert(tag === s[1], 'Tag mismatch.');
      buf = new Buffer(buf, 'base64');
      type = tag.split(' ')[0].toLowerCase();
      chunks.push({ tag: tag, type: type, data: buf });
      buf = '';
      tag = null;
      continue;
    }
    if (s = /^[a-zA-Z0-9\+=\/]+/.exec(pem)) {
      pem = pem.substring(s[0].length);
      buf += s[0];
      continue;
    }
    if (s = /^\s+/.exec(pem)) {
      pem = pem.substring(s[0].length);
      continue;
    }
    throw new Error('PEM parse error.');
  }

  assert(chunks.length !== 0, 'PEM parse error.');
  assert(!tag, 'Un-ended tag.');
  assert(buf.length === 0, 'Trailing data.');

  return chunks;
};

asn1.fromPEM = function fromPEM(pem) {
  var chunks = asn1.parsePEM(pem);
  var body = chunks[0];
  var extra = chunks[1];
  var params, alg;

  if (extra) {
    if (extra.tag.indexOf('PARAMETERS') !== -1)
      params = extra.data;
  }

  switch (body.type) {
    case 'dsa':
      alg = 'dsa';
      break;
    case 'rsa':
      alg = 'rsa';
      break;
    case 'ec':
      alg = 'ecdsa';
      break;
  }

  return {
    type: body.type,
    alg: alg,
    data: body.data,
    params: params
  };
};

asn1.toPEM = function toPEM(der, type, suffix) {
  var pem = '';
  var i;

  if (suffix)
    type += ' ' + suffix;

  type = type.toUpperCase();
  der = der.toString('base64');

  for (i = 0; i < der.length; i += 64)
    pem += der.slice(i, i + 64) + '\r\n';

  return ''
    + '-----BEGIN ' + type + '-----\r\n'
    + pem
    + '-----END ' + type + '-----\r\n';
};
