/* global BigInt */
const { inherits } = require('util')

const { DecoderBuffer } = require('../base/buffer')
const Node = require('../base/node')

// Import DER constants
const der = require('../constants/der')

function DERDecoder (entity) {
  this.enc = 'der'
  this.name = entity.name
  this.entity = entity

  // Construct base tree
  this.tree = new DERNode()
  this.tree._init(entity.body)
}

DERDecoder.prototype.decode = function decode (data, options) {
  if (!DecoderBuffer.isDecoderBuffer(data)) {
    data = new DecoderBuffer(data, options)
  }

  return this.tree._decode(data, options)
}

// Tree methods

function DERNode (parent) {
  Node.call(this, 'der', parent)
}
inherits(DERNode, Node)

DERNode.prototype._peekTag = function peekTag (buffer, tag, any) {
  if (buffer.isEmpty()) { return false }

  const state = buffer.save()
  const decodedTag = derDecodeTag(buffer, `Failed to peek tag: "${tag}"`)
  if (buffer.isError(decodedTag)) { return decodedTag }

  buffer.restore(state)

  return decodedTag.tag === tag || decodedTag.tagStr === tag || (decodedTag.tagStr + 'of') === tag || any
}

DERNode.prototype._decodeTag = function decodeTag (buffer, tag, any) {
  const decodedTag = derDecodeTag(buffer,
    `Failed to decode tag of "${tag}"`)
  if (buffer.isError(decodedTag)) { return decodedTag }

  let len = derDecodeLen(buffer,
    decodedTag.primitive,
    `Failed to get length of "${tag}"`)

  // Failure
  if (buffer.isError(len)) { return len }

  if (!any &&
      decodedTag.tag !== tag &&
      decodedTag.tagStr !== tag &&
      decodedTag.tagStr + 'of' !== tag) {
    return buffer.error(`Failed to match tag: "${tag}"`)
  }

  if (decodedTag.primitive || len !== null) { return buffer.skip(len, `Failed to match body of: "${tag}"`) }

  // Indefinite length... find END tag
  const state = buffer.save()
  const res = this._skipUntilEnd(
    buffer,
    `Failed to skip indefinite length body: "${this.tag}"`)
  if (buffer.isError(res)) { return res }

  len = buffer.offset - state.offset
  buffer.restore(state)
  return buffer.skip(len, `Failed to match body of: "${tag}"`)
}

DERNode.prototype._skipUntilEnd = function skipUntilEnd (buffer, fail) {
  for (;;) {
    const tag = derDecodeTag(buffer, fail)
    if (buffer.isError(tag)) { return tag }
    const len = derDecodeLen(buffer, tag.primitive, fail)
    if (buffer.isError(len)) { return len }

    let res
    if (tag.primitive || len !== null) { res = buffer.skip(len) } else { res = this._skipUntilEnd(buffer, fail) }

    // Failure
    if (buffer.isError(res)) { return res }

    if (tag.tagStr === 'end') { break }
  }
}

DERNode.prototype._decodeList = function decodeList (buffer, tag, decoder,
  options) {
  const result = []
  while (!buffer.isEmpty()) {
    const possibleEnd = this._peekTag(buffer, 'end')
    if (buffer.isError(possibleEnd)) { return possibleEnd }

    const res = decoder.decode(buffer, 'der', options)
    if (buffer.isError(res) && possibleEnd) { break }
    result.push(res)
  }
  return result
}

DERNode.prototype._decodeStr = function decodeStr (buffer, tag) {
  if (tag === 'bitstr') {
    const unused = buffer.readUInt8()
    if (buffer.isError(unused)) { return unused }
    return { unused: unused, data: buffer.raw() }
  } else if (tag === 'bmpstr') {
    const raw = buffer.raw()
    if (raw.length % 2 === 1) { return buffer.error('Decoding of string type: bmpstr length mismatch') }

    let str = ''
    for (let i = 0; i < raw.length / 2; i++) {
      str += String.fromCharCode(raw.readUInt16BE(i * 2))
    }
    return str
  } else if (tag === 'numstr') {
    const numstr = buffer.raw().toString('ascii')
    if (!this._isNumstr(numstr)) {
      return buffer.error('Decoding of string type: numstr unsupported characters')
    }
    return numstr
  } else if (tag === 'octstr') {
    return buffer.raw()
  } else if (tag === 'objDesc') {
    return buffer.raw()
  } else if (tag === 'printstr') {
    const printstr = buffer.raw().toString('ascii')
    if (!this._isPrintstr(printstr)) {
      return buffer.error('Decoding of string type: printstr unsupported characters')
    }
    return printstr
  } else if (/str$/.test(tag)) {
    return buffer.raw().toString()
  } else {
    return buffer.error(`Decoding of string type: ${tag} unsupported`)
  }
}

DERNode.prototype._decodeObjid = function decodeObjid (buffer, values, relative) {
  let result
  const identifiers = []
  let ident = 0
  let subident = 0
  while (!buffer.isEmpty()) {
    subident = buffer.readUInt8()
    ident <<= 7
    ident |= subident & 0x7f
    if ((subident & 0x80) === 0) {
      identifiers.push(ident)
      ident = 0
    }
  }
  if (subident & 0x80) { identifiers.push(ident) }

  const first = (identifiers[0] / 40) | 0
  const second = identifiers[0] % 40

  if (relative) { result = identifiers } else { result = [first, second].concat(identifiers.slice(1)) }

  if (values) {
    let tmp = values[result.join(' ')]
    if (tmp === undefined) { tmp = values[result.join('.')] }
    if (tmp !== undefined) { result = tmp }
  }

  return result
}

DERNode.prototype._decodeTime = function decodeTime (buffer, tag) {
  const str = buffer.raw().toString()

  let year
  let mon
  let day
  let hour
  let min
  let sec
  if (tag === '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
  } else if (tag === '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 }
  } else {
    return buffer.error(`Decoding ${tag} time is not supported yet`)
  }

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

DERNode.prototype._decodeNull = function decodeNull () {
  return null
}

DERNode.prototype._decodeBool = function decodeBool (buffer) {
  const res = buffer.readUInt8()
  if (buffer.isError(res)) { return res } else { return res !== 0 }
}

DERNode.prototype._decodeInt = function decodeInt (buffer, values) {
  // Bigint, return as it is (assume big endian)
  const raw = buffer.raw()
  let res = BigInt(`0x${raw.toString('hex')}`)

  if (values) {
    res = values[res.toString(10)] || res
  }

  return res
}

DERNode.prototype._use = function use (entity, obj) {
  if (typeof entity === 'function') { entity = entity(obj) }
  return entity._getDecoder('der').tree
}

// Utility methods

function derDecodeTag (buf, fail) {
  let tag = buf.readUInt8(fail)
  if (buf.isError(tag)) { return tag }

  const cls = der.tagClass[tag >> 6]
  const primitive = (tag & 0x20) === 0

  // Multi-octet tag - load
  if ((tag & 0x1f) === 0x1f) {
    let oct = tag
    tag = 0
    while ((oct & 0x80) === 0x80) {
      oct = buf.readUInt8(fail)
      if (buf.isError(oct)) { return oct }

      tag <<= 7
      tag |= oct & 0x7f
    }
  } else {
    tag &= 0x1f
  }
  const tagStr = der.tag[tag]

  return {
    cls: cls,
    primitive: primitive,
    tag: tag,
    tagStr: tagStr
  }
}

function derDecodeLen (buf, primitive, fail) {
  let len = buf.readUInt8(fail)
  if (buf.isError(len)) { return len }

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

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

  // Long form
  const num = len & 0x7f
  if (num > 4) { return buf.error('length octect is too long') }

  len = 0
  for (let i = 0; i < num; i++) {
    len <<= 8
    const j = buf.readUInt8(fail)
    if (buf.isError(j)) { return j }
    len |= j
  }

  return len
}

module.exports = DERDecoder
