// TODO: shift some of the bytes logic to bytes-utils so we can use Buffer
// where possible

import { Token, Type } from './token.js'
import { decodeErrPrefix } from './common.js'
import { compare } from './byte-utils.js'

/**
 * @typedef {import('../interface.js').ByteWriter} ByteWriter
 * @typedef {import('../interface.js').DecodeOptions} DecodeOptions
 * @typedef {import('../interface.js').EncodeOptions} EncodeOptions
 */

export const MINOR_FALSE = 20
export const MINOR_TRUE = 21
export const MINOR_NULL = 22
export const MINOR_UNDEFINED = 23

/**
 * @param {Uint8Array} _data
 * @param {number} _pos
 * @param {number} _minor
 * @param {DecodeOptions} options
 * @returns {Token}
 */
export function decodeUndefined (_data, _pos, _minor, options) {
  if (options.allowUndefined === false) {
    throw new Error(`${decodeErrPrefix} undefined values are not supported`)
  } else if (options.coerceUndefinedToNull === true) {
    return new Token(Type.null, null, 1)
  }
  return new Token(Type.undefined, undefined, 1)
}

/**
 * @param {Uint8Array} _data
 * @param {number} _pos
 * @param {number} _minor
 * @param {DecodeOptions} options
 * @returns {Token}
 */
export function decodeBreak (_data, _pos, _minor, options) {
  if (options.allowIndefinite === false) {
    throw new Error(`${decodeErrPrefix} indefinite length items not allowed`)
  }
  return new Token(Type.break, undefined, 1)
}

/**
 * @param {number} value
 * @param {number} bytes
 * @param {DecodeOptions} options
 * @returns {Token}
 */
function createToken (value, bytes, options) {
  if (options) {
    if (options.allowNaN === false && Number.isNaN(value)) {
      throw new Error(`${decodeErrPrefix} NaN values are not supported`)
    }
    if (options.allowInfinity === false && (value === Infinity || value === -Infinity)) {
      throw new Error(`${decodeErrPrefix} Infinity values are not supported`)
    }
  }
  return new Token(Type.float, value, bytes)
}

/**
 * @param {Uint8Array} data
 * @param {number} pos
 * @param {number} _minor
 * @param {DecodeOptions} options
 * @returns {Token}
 */
export function decodeFloat16 (data, pos, _minor, options) {
  return createToken(readFloat16(data, pos + 1), 3, options)
}

/**
 * @param {Uint8Array} data
 * @param {number} pos
 * @param {number} _minor
 * @param {DecodeOptions} options
 * @returns {Token}
 */
export function decodeFloat32 (data, pos, _minor, options) {
  return createToken(readFloat32(data, pos + 1), 5, options)
}

/**
 * @param {Uint8Array} data
 * @param {number} pos
 * @param {number} _minor
 * @param {DecodeOptions} options
 * @returns {Token}
 */
export function decodeFloat64 (data, pos, _minor, options) {
  return createToken(readFloat64(data, pos + 1), 9, options)
}

/**
 * @param {ByteWriter} writer
 * @param {Token} token
 * @param {EncodeOptions} options
 */
export function encodeFloat (writer, token, options) {
  const float = token.value

  if (float === false) {
    writer.pushByte(Type.float.majorEncoded | MINOR_FALSE)
  } else if (float === true) {
    writer.pushByte(Type.float.majorEncoded | MINOR_TRUE)
  } else if (float === null) {
    writer.pushByte(Type.float.majorEncoded | MINOR_NULL)
  } else if (float === undefined) {
    writer.pushByte(Type.float.majorEncoded | MINOR_UNDEFINED)
  } else {
    let decoded
    let success = false
    if (!options || options.float64 !== true) {
      encodeFloat16(float)
      decoded = readFloat16(ui8a, 1)
      if (float === decoded || Number.isNaN(float)) {
        ui8a[0] = 0xf9
        writer.push(ui8a.slice(0, 3))
        success = true
      } else {
        encodeFloat32(float)
        decoded = readFloat32(ui8a, 1)
        if (float === decoded) {
          ui8a[0] = 0xfa
          writer.push(ui8a.slice(0, 5))
          success = true
        }
      }
    }
    if (!success) {
      encodeFloat64(float)
      decoded = readFloat64(ui8a, 1)
      ui8a[0] = 0xfb
      writer.push(ui8a.slice(0, 9))
    }
  }
}

/**
 * @param {Token} token
 * @param {EncodeOptions} options
 * @returns {number}
 */
encodeFloat.encodedSize = function encodedSize (token, options) {
  const float = token.value

  if (float === false || float === true || float === null || float === undefined) {
    return 1
  }

  if (!options || options.float64 !== true) {
    encodeFloat16(float)
    let decoded = readFloat16(ui8a, 1)
    if (float === decoded || Number.isNaN(float)) {
      return 3
    }
    encodeFloat32(float)
    decoded = readFloat32(ui8a, 1)
    if (float === decoded) {
      return 5
    }
  }
  return 9
}

const buffer = new ArrayBuffer(9)
const dataView = new DataView(buffer, 1)
const ui8a = new Uint8Array(buffer, 0)

/**
 * @param {number} inp
 */
function encodeFloat16 (inp) {
  if (inp === Infinity) {
    dataView.setUint16(0, 0x7c00, false)
  } else if (inp === -Infinity) {
    dataView.setUint16(0, 0xfc00, false)
  } else if (Number.isNaN(inp)) {
    dataView.setUint16(0, 0x7e00, false)
  } else {
    dataView.setFloat32(0, inp)
    const valu32 = dataView.getUint32(0)
    const exponent = (valu32 & 0x7f800000) >> 23
    const mantissa = valu32 & 0x7fffff

    if (exponent === 0xff) {
      // too big, Infinity, but this should be hard (impossible?) to trigger
      dataView.setUint16(0, 0x7c00, false)
    } else if (exponent === 0x00) {
      // 0.0, -0.0 and subnormals, shouldn't be possible to get here because 0.0 should be counted as an int
      // Use valu32 for sign bit since bitwise ops on floats lose -0 sign
      dataView.setUint16(0, ((valu32 & 0x80000000) >> 16) | (mantissa >> 13), false)
    } else { // standard numbers
      // chunks of logic here borrowed from https://github.com/PJK/libcbor/blob/c78f437182533e3efa8d963ff4b945bb635c2284/src/cbor/encoding.c#L127
      const logicalExponent = exponent - 127
      // Now we know that 2^exponent <= 0 logically
      if (logicalExponent < -24) {
        /* No unambiguous representation exists, this float is not a half float
          and is too small to be represented using a half, round off to zero.
          Consistent with the reference implementation. */
        // should be difficult (impossible?) to get here in JS
        dataView.setUint16(0, 0)
      } else if (logicalExponent < -14) {
        /* Offset the remaining decimal places by shifting the significand, the
          value is lost. This is an implementation decision that works around the
          absence of standard half-float in the language. */
        dataView.setUint16(0, ((valu32 & 0x80000000) >> 16) | /* sign bit */ (1 << (24 + logicalExponent)), false)
      } else {
        dataView.setUint16(0, ((valu32 & 0x80000000) >> 16) | ((logicalExponent + 15) << 10) | (mantissa >> 13), false)
      }
    }
  }
}

/**
 * @param {Uint8Array} ui8a
 * @param {number} pos
 * @returns {number}
 */
function readFloat16 (ui8a, pos) {
  if (ui8a.length - pos < 2) {
    throw new Error(`${decodeErrPrefix} not enough data for float16`)
  }

  const half = (ui8a[pos] << 8) + ui8a[pos + 1]
  if (half === 0x7c00) {
    return Infinity
  }
  if (half === 0xfc00) {
    return -Infinity
  }
  if (half === 0x7e00) {
    return NaN
  }
  const exp = (half >> 10) & 0x1f
  const mant = half & 0x3ff
  let val
  if (exp === 0) {
    val = mant * (2 ** -24)
  } else if (exp !== 31) {
    val = (mant + 1024) * (2 ** (exp - 25))
  } else {
    // may not be possible to get here
    val = mant === 0 ? Infinity : NaN
  }
  return (half & 0x8000) ? -val : val
}

/**
 * @param {number} inp
 */
function encodeFloat32 (inp) {
  dataView.setFloat32(0, inp, false)
}

/**
 * @param {Uint8Array} ui8a
 * @param {number} pos
 * @returns {number}
 */
function readFloat32 (ui8a, pos) {
  if (ui8a.length - pos < 4) {
    throw new Error(`${decodeErrPrefix} not enough data for float32`)
  }
  const offset = (ui8a.byteOffset || 0) + pos
  return new DataView(ui8a.buffer, offset, 4).getFloat32(0, false)
}

/**
 * @param {number} inp
 */
function encodeFloat64 (inp) {
  dataView.setFloat64(0, inp, false)
}

/**
 * @param {Uint8Array} ui8a
 * @param {number} pos
 * @returns {number}
 */
function readFloat64 (ui8a, pos) {
  if (ui8a.length - pos < 8) {
    throw new Error(`${decodeErrPrefix} not enough data for float64`)
  }
  const offset = (ui8a.byteOffset || 0) + pos
  return new DataView(ui8a.buffer, offset, 8).getFloat64(0, false)
}

/**
 * @typedef {Token & {
 *   _keyBytes?: Uint8Array,
 *   _keyBytesFloat64?: Uint8Array
 * }} TokenEx
 */

/**
 * The canonical CBOR bytes for a major 7 map key. The four simple values encode
 * to a single byte and every float to its 3, 5 or 9 byte form, exactly as
 * {@link encodeFloat} writes them. This mirrors the width selection in
 * `encodeFloat` because the sorter needs the bytes in hand to compare.
 * @param {Token} token
 * @param {boolean} float64
 * @returns {Uint8Array}
 */
function encodeMajorSevenBytes (token, float64) {
  const float = token.value
  if (float === false) {
    return Uint8Array.of(Type.float.majorEncoded | MINOR_FALSE)
  }
  if (float === true) {
    return Uint8Array.of(Type.float.majorEncoded | MINOR_TRUE)
  }
  if (float === null) {
    return Uint8Array.of(Type.float.majorEncoded | MINOR_NULL)
  }
  if (float === undefined) {
    return Uint8Array.of(Type.float.majorEncoded | MINOR_UNDEFINED)
  }
  if (!float64) {
    encodeFloat16(float)
    if (float === readFloat16(ui8a, 1) || Number.isNaN(float)) {
      ui8a[0] = 0xf9
      return ui8a.slice(0, 3)
    }
    encodeFloat32(float)
    if (float === readFloat32(ui8a, 1)) {
      ui8a[0] = 0xfa
      return ui8a.slice(0, 5)
    }
  }
  encodeFloat64(float)
  ui8a[0] = 0xfb
  return ui8a.slice(0, 9)
}

/**
 * Get the canonical CBOR bytes for a major 7 map key, honouring
 * `options.float64`. Bytes are cached because Array.sort compares each key
 * multiple times.
 * @param {Token} token
 * @param {EncodeOptions} [options]
 * @returns {Uint8Array}
 */
function majorSevenBytes (token, options) {
  const tokenEx = /** @type {TokenEx} */ (token)
  const float64 = options?.float64 === true
  const cached = float64 ? tokenEx._keyBytesFloat64 : tokenEx._keyBytes
  if (cached !== undefined) {
    return cached
  }

  const bytes = encodeMajorSevenBytes(token, float64)
  if (float64) {
    tokenEx._keyBytesFloat64 = bytes
  } else {
    tokenEx._keyBytes = bytes
  }
  return bytes
}

/**
 * Order two major 7 map keys by RFC 7049 §3.9 canonical order: shorter encoding
 * first, then bytewise. Because the four simple values encode to one byte they
 * sort ahead of every float, and comparing the `float64`-honouring encoded bytes
 * (rather than the numeric value) makes negative floats, mixed encoding widths
 * and `NaN` deterministic regardless of insertion order.
 * @param {Token} tok1
 * @param {Token} tok2
 * @param {EncodeOptions} [options]
 * @returns {number}
 */
encodeFloat.compareTokens = function compareTokens (tok1, tok2, options) {
  const b1 = majorSevenBytes(tok1, options)
  const b2 = majorSevenBytes(tok2, options)
  if (b1.length !== b2.length) {
    return b1.length < b2.length ? -1 : 1
  }
  return compare(b1, b2)
}
