'use strict'

const {Buffer} = require('buffer')
const NoFilter = require('nofilter')
const stream = require('stream')
const constants = require('./constants')
const {NUMBYTES, SHIFT32, BI, SYMS} = constants
const MAX_SAFE_HIGH = 0x1fffff

/**
 * Convert a UTF8-encoded Buffer to a JS string.  If possible, throw an error
 * on invalid UTF8.  Byte Order Marks are not looked at or stripped.
 *
 * @private
 */
const td = new TextDecoder('utf8', {fatal: true, ignoreBOM: true})
exports.utf8 = buf => td.decode(buf)
exports.utf8.checksUTF8 = true

function isReadable(s) {
  // Is this a readable stream?  In the webpack version, instanceof isn't
  // working correctly.
  if (s instanceof stream.Readable) {
    return true
  }
  return ['read', 'on', 'pipe'].every(f => typeof s[f] === 'function')
}

exports.isBufferish = function isBufferish(b) {
  return b &&
    (typeof b === 'object') &&
    ((Buffer.isBuffer(b)) ||
      (b instanceof Uint8Array) ||
      (b instanceof Uint8ClampedArray) ||
      (b instanceof ArrayBuffer) ||
      (b instanceof DataView))
}

exports.bufferishToBuffer = function bufferishToBuffer(b) {
  if (Buffer.isBuffer(b)) {
    return b
  } else if (ArrayBuffer.isView(b)) {
    return Buffer.from(b.buffer, b.byteOffset, b.byteLength)
  } else if (b instanceof ArrayBuffer) {
    return Buffer.from(b)
  }
  return null
}

exports.parseCBORint = function parseCBORint(ai, buf) {
  switch (ai) {
    case NUMBYTES.ONE:
      return buf.readUInt8(0)
    case NUMBYTES.TWO:
      return buf.readUInt16BE(0)
    case NUMBYTES.FOUR:
      return buf.readUInt32BE(0)
    case NUMBYTES.EIGHT: {
      const f = buf.readUInt32BE(0)
      const g = buf.readUInt32BE(4)
      if (f > MAX_SAFE_HIGH) {
        return (BigInt(f) * BI.SHIFT32) + BigInt(g)
      }
      return (f * SHIFT32) + g
    }
    default:
      throw new Error(`Invalid additional info for int: ${ai}`)
  }
}

exports.writeHalf = function writeHalf(buf, half) {
  // Assume 0, -0, NaN, Infinity, and -Infinity have already been caught

  // HACK: everyone settle in.  This isn't going to be pretty.
  // Translate cn-cbor's C code (from Carsten Borman):

  // uint32_t be32;
  // uint16_t be16, u16;
  // union {
  //   float f;
  //   uint32_t u;
  // } u32;
  // u32.f = float_val;

  const u32 = Buffer.allocUnsafe(4)
  u32.writeFloatBE(half, 0)
  const u = u32.readUInt32BE(0)

  // If ((u32.u & 0x1FFF) == 0) { /* worth trying half */

  // hildjj: If the lower 13 bits aren't 0,
  // we will lose precision in the conversion.
  // mant32 = 24bits, mant16 = 11bits, 24-11 = 13
  if ((u & 0x1FFF) !== 0) {
    return false
  }

  // Sign, exponent, mantissa
  //   int s16 = (u32.u >> 16) & 0x8000;
  //   int exp = (u32.u >> 23) & 0xff;
  //   int mant = u32.u & 0x7fffff;

  let s16 = (u >> 16) & 0x8000 // Top bit is sign
  const exp = (u >> 23) & 0xff // Then 5 bits of exponent
  const mant = u & 0x7fffff

  // Hildjj: zeros already handled.  Assert if you don't believe me.
  //   if (exp == 0 && mant == 0)
  //     ;              /* 0.0, -0.0 */

  //   else if (exp >= 113 && exp <= 142) /* normalized */
  //     s16 += ((exp - 112) << 10) + (mant >> 13);

  if ((exp >= 113) && (exp <= 142)) {
    s16 += ((exp - 112) << 10) + (mant >> 13)
  } else if ((exp >= 103) && (exp < 113)) {
    // Denormalized numbers
    //   else if (exp >= 103 && exp < 113) { /* denorm, exp16 = 0 */
    //     if (mant & ((1 << (126 - exp)) - 1))
    //       goto float32;         /* loss of precision */
    //     s16 += ((mant + 0x800000) >> (126 - exp));

    if (mant & ((1 << (126 - exp)) - 1)) {
      return false
    }
    s16 += ((mant + 0x800000) >> (126 - exp))
  } else {
  //   } else if (exp == 255 && mant == 0) { /* Inf */
  //     s16 += 0x7c00;

    // hildjj: Infinity already handled

    //   } else
    //     goto float32;           /* loss of range */

    return false
  }

  // Done
  //   ensure_writable(3);
  //   u16 = s16;
  //   be16 = hton16p((const uint8_t*)&u16);
  buf.writeUInt16BE(s16)
  return true
}

exports.parseHalf = function parseHalf(buf) {
  const sign = buf[0] & 0x80 ? -1 : 1
  const exp = (buf[0] & 0x7C) >> 2
  const mant = ((buf[0] & 0x03) << 8) | buf[1]
  if (!exp) {
    return sign * 5.9604644775390625e-8 * mant
  } else if (exp === 0x1f) {
    return sign * (mant ? NaN : Infinity)
  }
  return sign * (2 ** (exp - 25)) * (1024 + mant)
}

exports.parseCBORfloat = function parseCBORfloat(buf) {
  switch (buf.length) {
    case 2:
      return exports.parseHalf(buf)
    case 4:
      return buf.readFloatBE(0)
    case 8:
      return buf.readDoubleBE(0)
    default:
      throw new Error(`Invalid float size: ${buf.length}`)
  }
}

exports.hex = function hex(s) {
  return Buffer.from(s.replace(/^0x/, ''), 'hex')
}

exports.bin = function bin(s) {
  s = s.replace(/\s/g, '')
  let start = 0
  let end = (s.length % 8) || 8
  const chunks = []
  while (end <= s.length) {
    chunks.push(parseInt(s.slice(start, end), 2))
    start = end
    end += 8
  }
  return Buffer.from(chunks)
}

exports.arrayEqual = function arrayEqual(a, b) {
  if ((a == null) && (b == null)) {
    return true
  }
  if ((a == null) || (b == null)) {
    return false
  }
  return (a.length === b.length) && a.every((elem, i) => elem === b[i])
}

exports.bufferToBigInt = function bufferToBigInt(buf) {
  return BigInt(`0x${buf.toString('hex')}`)
}

exports.cborValueToString = function cborValueToString(val, float_bytes = -1) {
  switch (typeof val) {
    case 'symbol': {
      switch (val) {
        case SYMS.NULL:
          return 'null'
        case SYMS.UNDEFINED:
          return 'undefined'
        case SYMS.BREAK:
          return 'BREAK'
      }
      // Impossible in node 10
      /* istanbul ignore if */
      if (val.description) {
        return val.description
      }
      // On node10, Symbol doesn't have description.  Parse it out of the
      // toString value, which looks like `Symbol(foo)`.
      const s = val.toString()
      const m = s.match(/^Symbol\((?<name>.*)\)/)
      /* istanbul ignore if */
      if (m && m.groups.name) {
        // Impossible in node 12+
        /* istanbul ignore next */
        return m.groups.name
      }
      return 'Symbol'
    }
    case 'string':
      return JSON.stringify(val)
    case 'bigint':
      return val.toString()
    case 'number': {
      const s = Object.is(val, -0) ? '-0' : String(val)
      return (float_bytes > 0) ? `${s}_${float_bytes}` : s
    }
    case 'object': {
      // A null should be caught above
      const buf = exports.bufferishToBuffer(val)
      if (buf) {
        const hex = buf.toString('hex')
        return (float_bytes === -Infinity) ? hex : `h'${hex}'`
      }
      if (typeof val[Symbol.for('nodejs.util.inspect.custom')] === 'function') {
        return val[Symbol.for('nodejs.util.inspect.custom')]()
      }
      // Shouldn't get non-empty arrays here
      if (Array.isArray(val)) {
        return '[]'
      }
      // This should be all that is left
      return '{}'
    }
  }
  return String(val)
}

exports.guessEncoding = function guessEncoding(input, encoding) {
  if (typeof input === 'string') {
    return new NoFilter(input, (encoding == null) ? 'hex' : encoding)
  }
  const buf = exports.bufferishToBuffer(input)
  if (buf) {
    return new NoFilter(buf)
  }
  if (isReadable(input)) {
    return input
  }
  throw new Error('Unknown input type')
}

const B64URL_SWAPS = {
  '=': '',
  '+': '-',
  '/': '_',
}

/**
 * @param {Buffer|Uint8Array|Uint8ClampedArray|ArrayBuffer|DataView} buf
 *   Buffer to convert.
 * @returns {string} Base64url string.
 * @private
 */
exports.base64url = function base64url(buf) {
  return exports.bufferishToBuffer(buf)
    .toString('base64')
    .replace(/[=+/]/g, c => B64URL_SWAPS[c])
}

/**
 * @param {Buffer|Uint8Array|Uint8ClampedArray|ArrayBuffer|DataView} buf
 *   Buffer to convert.
 * @returns {string} Base64 string.
 * @private
 */
exports.base64 = function base64(buf) {
  return exports.bufferishToBuffer(buf).toString('base64')
}

exports.isBigEndian = function isBigEndian() {
  const array = new Uint8Array(4)
  const view = new Uint32Array(array.buffer)
  return !((view[0] = 1) & array[0])
}
