'use strict'

const stream = require('stream')
const NoFilter = require('nofilter')
const utils = require('./utils')
const constants = require('./constants')
const {
  MT, NUMBYTES, SHIFT32, SIMPLE, SYMS, TAG, BI,
} = constants
const {Buffer} = require('buffer')

const HALF = (MT.SIMPLE_FLOAT << 5) | NUMBYTES.TWO
const FLOAT = (MT.SIMPLE_FLOAT << 5) | NUMBYTES.FOUR
const DOUBLE = (MT.SIMPLE_FLOAT << 5) | NUMBYTES.EIGHT
const TRUE = (MT.SIMPLE_FLOAT << 5) | SIMPLE.TRUE
const FALSE = (MT.SIMPLE_FLOAT << 5) | SIMPLE.FALSE
const UNDEFINED = (MT.SIMPLE_FLOAT << 5) | SIMPLE.UNDEFINED
const NULL = (MT.SIMPLE_FLOAT << 5) | SIMPLE.NULL

const BREAK = Buffer.from([0xff])
const BUF_NAN = Buffer.from('f97e00', 'hex')
const BUF_INF_NEG = Buffer.from('f9fc00', 'hex')
const BUF_INF_POS = Buffer.from('f97c00', 'hex')
const BUF_NEG_ZERO = Buffer.from('f98000', 'hex')

/**
 * Generate the CBOR for a value.  If you are using this, you'll either need
 * to call {@link Encoder.write} with a Buffer, or look into the internals of
 * Encoder to reuse existing non-documented behavior.
 *
 * @callback EncodeFunction
 * @param {Encoder} enc The encoder to use.
 * @param {any} val The value to encode.
 * @returns {boolean} True on success.
 */

/* eslint-disable jsdoc/check-types */
/**
 * A mapping from tag number to a tag decoding function.
 *
 * @typedef {Object.<string, EncodeFunction>} SemanticMap
 */
/* eslint-enable jsdoc/check-types */

/**
 * @type {SemanticMap}
 * @private
 */
const SEMANTIC_TYPES = {}

/**
 * @type {SemanticMap}
 * @private
 */
let current_SEMANTIC_TYPES = {}

/**
 * @param {string} str String to normalize.
 * @returns {"number"|"float"|"int"|"string"} Normalized.
 * @throws {TypeError} Invalid input.
 * @private
 */
function parseDateType(str) {
  if (!str) {
    return 'number'
  }
  switch (str.toLowerCase()) {
    case 'number':
      return 'number'
    case 'float':
      return 'float'
    case 'int':
    case 'integer':
      return 'int'
    case 'string':
      return 'string'
  }
  throw new TypeError(`dateType invalid, got "${str}"`)
}

/**
 * @typedef EncodingOptions
 * @property {any[]|object} [genTypes=[]] Array of pairs of
 *   `type`, `function(Encoder)` for semantic types to be encoded.  Not
 *   needed for Array, Date, Buffer, Map, RegExp, Set, or URL.
 *   If an object, the keys are the constructor names for the types.
 * @property {boolean} [canonical=false] Should the output be
 *   canonicalized.
 * @property {boolean|WeakSet} [detectLoops=false] Should object loops
 *   be detected?  This will currently add memory to track every part of the
 *   object being encoded in a WeakSet.  Do not encode
 *   the same object twice on the same encoder, without calling
 *   `removeLoopDetectors` in between, which will clear the WeakSet.
 *   You may pass in your own WeakSet to be used; this is useful in some
 *   recursive scenarios.
 * @property {("number"|"float"|"int"|"string")} [dateType="number"] -
 *   how should dates be encoded?  "number" means float or int, if no
 *   fractional seconds.
 * @property {any} [encodeUndefined=undefined] How should an
 *   "undefined" in the input be encoded.  By default, just encode a CBOR
 *   undefined.  If this is a buffer, use those bytes without re-encoding
 *   them.  If this is a function, the function will be called (which is a
 *   good time to throw an exception, if that's what you want), and the
 *   return value will be used according to these rules.  Anything else will
 *   be encoded as CBOR.
 * @property {boolean} [disallowUndefinedKeys=false] Should
 *   "undefined" be disallowed as a key in a Map that is serialized?  If
 *   this is true, encode(new Map([[undefined, 1]])) will throw an
 *   exception.  Note that it is impossible to get a key of undefined in a
 *   normal JS object.
 * @property {boolean} [collapseBigIntegers=false] Should integers
 *   that come in as ECMAscript bigint's be encoded
 *   as normal CBOR integers if they fit, discarding type information?
 * @property {number} [chunkSize=4096] Number of characters or bytes
 *   for each chunk, if obj is a string or Buffer, when indefinite encoding.
 * @property {boolean} [omitUndefinedProperties=false] When encoding
 *   objects or Maps, do not include a key if its corresponding value is
 *   `undefined`.
 */

/**
 * Transform JavaScript values into CBOR bytes.  The `Writable` side of
 * the stream is in object mode.
 *
 * @extends stream.Transform
 */
class Encoder extends stream.Transform {
  /**
   * Creates an instance of Encoder.
   *
   * @param {EncodingOptions} [options={}] Options for the encoder.
   */
  constructor(options = {}) {
    const {
      canonical = false,
      encodeUndefined,
      disallowUndefinedKeys = false,
      dateType = 'number',
      collapseBigIntegers = false,
      detectLoops = false,
      omitUndefinedProperties = false,
      genTypes = [],
      ...superOpts
    } = options

    super({
      ...superOpts,
      readableObjectMode: false,
      writableObjectMode: true,
    })

    this.canonical = canonical
    this.encodeUndefined = encodeUndefined
    this.disallowUndefinedKeys = disallowUndefinedKeys
    this.dateType = parseDateType(dateType)
    this.collapseBigIntegers = this.canonical ? true : collapseBigIntegers

    /** @type {WeakSet?} */
    this.detectLoops = undefined
    if (typeof detectLoops === 'boolean') {
      if (detectLoops) {
        this.detectLoops = new WeakSet()
      }
    } else if (detectLoops instanceof WeakSet) {
      this.detectLoops = detectLoops
    } else {
      throw new TypeError('detectLoops must be boolean or WeakSet')
    }
    this.omitUndefinedProperties = omitUndefinedProperties

    this.semanticTypes = {...Encoder.SEMANTIC_TYPES}

    if (Array.isArray(genTypes)) {
      for (let i = 0, len = genTypes.length; i < len; i += 2) {
        this.addSemanticType(genTypes[i], genTypes[i + 1])
      }
    } else {
      for (const [k, v] of Object.entries(genTypes)) {
        this.addSemanticType(k, v)
      }
    }
  }

  _transform(fresh, encoding, cb) {
    const ret = this.pushAny(fresh)
    // Old transformers might not return bool.  undefined !== false
    return cb((ret === false) ? new Error('Push Error') : undefined)
  }

  // eslint-disable-next-line class-methods-use-this
  _flush(cb) {
    return cb()
  }

  /**
   * @param {number} val Number(0-255) to encode.
   * @returns {boolean} True on success.
   * @ignore
   */
  _pushUInt8(val) {
    const b = Buffer.allocUnsafe(1)
    b.writeUInt8(val, 0)
    return this.push(b)
  }

  /**
   * @param {number} val Number(0-65535) to encode.
   * @returns {boolean} True on success.
   * @ignore
   */
  _pushUInt16BE(val) {
    const b = Buffer.allocUnsafe(2)
    b.writeUInt16BE(val, 0)
    return this.push(b)
  }

  /**
   * @param {number} val Number(0..2**32-1) to encode.
   * @returns {boolean} True on success.
   * @ignore
   */
  _pushUInt32BE(val) {
    const b = Buffer.allocUnsafe(4)
    b.writeUInt32BE(val, 0)
    return this.push(b)
  }

  /**
   * @param {number} val Number to encode as 4-byte float.
   * @returns {boolean} True on success.
   * @ignore
   */
  _pushFloatBE(val) {
    const b = Buffer.allocUnsafe(4)
    b.writeFloatBE(val, 0)
    return this.push(b)
  }

  /**
   * @param {number} val Number to encode as 8-byte double.
   * @returns {boolean} True on success.
   * @ignore
   */
  _pushDoubleBE(val) {
    const b = Buffer.allocUnsafe(8)
    b.writeDoubleBE(val, 0)
    return this.push(b)
  }

  /**
   * @returns {boolean} True on success.
   * @ignore
   */
  _pushNaN() {
    return this.push(BUF_NAN)
  }

  /**
   * @param {number} obj Positive or negative infinity.
   * @returns {boolean} True on success.
   * @ignore
   */
  _pushInfinity(obj) {
    const half = (obj < 0) ? BUF_INF_NEG : BUF_INF_POS
    return this.push(half)
  }

  /**
   * Choose the best float representation for a number and encode it.
   *
   * @param {number} obj A number that is known to be not-integer, but not
   *    how many bytes of precision it needs.
   * @returns {boolean} True on success.
   * @ignore
   */
  _pushFloat(obj) {
    if (this.canonical) {
      // TODO: is this enough slower to hide behind canonical?
      // It's certainly enough of a hack (see utils.parseHalf)

      // From section 3.9:
      // If a protocol allows for IEEE floats, then additional canonicalization
      // rules might need to be added.  One example rule might be to have all
      // floats start as a 64-bit float, then do a test conversion to a 32-bit
      // float; if the result is the same numeric value, use the shorter value
      // and repeat the process with a test conversion to a 16-bit float.  (This
      // rule selects 16-bit float for positive and negative Infinity as well.)

      // which seems pretty much backwards to me.
      const b2 = Buffer.allocUnsafe(2)
      if (utils.writeHalf(b2, obj)) {
        // I have convinced myself that there are no cases where writeHalf
        // will return true but `utils.parseHalf(b2) !== obj)`
        return this._pushUInt8(HALF) && this.push(b2)
      }
    }
    if (Math.fround(obj) === obj) {
      return this._pushUInt8(FLOAT) && this._pushFloatBE(obj)
    }

    return this._pushUInt8(DOUBLE) && this._pushDoubleBE(obj)
  }

  /**
   * Choose the best integer representation for a postive number and encode
   * it.  If the number is over MAX_SAFE_INTEGER, fall back on float (but I
   * don't remember why).
   *
   * @param {number} obj A positive number that is known to be an integer,
   *    but not how many bytes of precision it needs.
   * @param {number} mt The Major Type number to combine with the integer.
   *    Not yet shifted.
   * @param {number} [orig] The number before it was transformed to positive.
   *    If the mt is NEG_INT, and the positive number is over MAX_SAFE_INT,
   *    then we'll encode this as a float rather than making the number
   *    negative again and losing precision.
   * @returns {boolean} True on success.
   * @ignore
   */
  _pushInt(obj, mt, orig) {
    const m = mt << 5

    if (obj < 24) {
      return this._pushUInt8(m | obj)
    }
    if (obj <= 0xff) {
      return this._pushUInt8(m | NUMBYTES.ONE) && this._pushUInt8(obj)
    }
    if (obj <= 0xffff) {
      return this._pushUInt8(m | NUMBYTES.TWO) && this._pushUInt16BE(obj)
    }
    if (obj <= 0xffffffff) {
      return this._pushUInt8(m | NUMBYTES.FOUR) && this._pushUInt32BE(obj)
    }
    let max = Number.MAX_SAFE_INTEGER
    if (mt === MT.NEG_INT) {
      // Special case for Number.MIN_SAFE_INTEGER - 1
      max--
    }
    if (obj <= max) {
      return this._pushUInt8(m | NUMBYTES.EIGHT) &&
        this._pushUInt32BE(Math.floor(obj / SHIFT32)) &&
        this._pushUInt32BE(obj % SHIFT32)
    }
    if (mt === MT.NEG_INT) {
      return this._pushFloat(orig)
    }
    return this._pushFloat(obj)
  }

  /**
   * Choose the best integer representation for a number and encode it.
   *
   * @param {number} obj A number that is known to be an integer,
   *    but not how many bytes of precision it needs.
   * @returns {boolean} True on success.
   * @ignore
   */
  _pushIntNum(obj) {
    if (Object.is(obj, -0)) {
      return this.push(BUF_NEG_ZERO)
    }

    if (obj < 0) {
      return this._pushInt(-obj - 1, MT.NEG_INT, obj)
    }
    return this._pushInt(obj, MT.POS_INT)
  }

  /**
   * @param {number} obj Plain JS number to encode.
   * @returns {boolean} True on success.
   * @ignore
   */
  _pushNumber(obj) {
    if (isNaN(obj)) {
      return this._pushNaN()
    }
    if (!isFinite(obj)) {
      return this._pushInfinity(obj)
    }
    if (Math.round(obj) === obj) {
      return this._pushIntNum(obj)
    }
    return this._pushFloat(obj)
  }

  /**
   * @param {string} obj String to encode.
   * @returns {boolean} True on success.
   * @ignore
   */
  _pushString(obj) {
    const len = Buffer.byteLength(obj, 'utf8')
    return this._pushInt(len, MT.UTF8_STRING) && this.push(obj, 'utf8')
  }

  /**
   * @param {boolean} obj Bool to encode.
   * @returns {boolean} True on success.
   * @ignore
   */
  _pushBoolean(obj) {
    return this._pushUInt8(obj ? TRUE : FALSE)
  }

  /**
   * @param {undefined} obj Ignored.
   * @returns {boolean} True on success.
   * @ignore
   */
  _pushUndefined(obj) {
    switch (typeof this.encodeUndefined) {
      case 'undefined':
        return this._pushUInt8(UNDEFINED)
      case 'function':
        return this.pushAny(this.encodeUndefined(obj))
      case 'object': {
        const buf = utils.bufferishToBuffer(this.encodeUndefined)
        if (buf) {
          return this.push(buf)
        }
      }
    }
    return this.pushAny(this.encodeUndefined)
  }

  /**
   * @param {null} obj Ignored.
   * @returns {boolean} True on success.
   * @ignore
   */
  _pushNull(obj) {
    return this._pushUInt8(NULL)
  }

  /**
   * @param {number} tag Tag number to encode.
   * @returns {boolean} True on success.
   * @ignore
   */
  _pushTag(tag) {
    return this._pushInt(tag, MT.TAG)
  }

  /**
   * @param {bigint} obj BigInt to encode.
   * @returns {boolean} True on success.
   * @ignore
   */
  _pushJSBigint(obj) {
    let m = MT.POS_INT
    let tag = TAG.POS_BIGINT
    // BigInt doesn't have -0
    if (obj < 0) {
      obj = -obj + BI.MINUS_ONE
      m = MT.NEG_INT
      tag = TAG.NEG_BIGINT
    }

    if (this.collapseBigIntegers &&
        (obj <= BI.MAXINT64)) {
      // Special handiling for 64bits
      if (obj <= 0xffffffff) {
        return this._pushInt(Number(obj), m)
      }
      return this._pushUInt8((m << 5) | NUMBYTES.EIGHT) &&
        this._pushUInt32BE(Number(obj / BI.SHIFT32)) &&
        this._pushUInt32BE(Number(obj % BI.SHIFT32))
    }

    let str = obj.toString(16)
    if (str.length % 2) {
      str = `0${str}`
    }
    const buf = Buffer.from(str, 'hex')
    return this._pushTag(tag) && Encoder._pushBuffer(this, buf)
  }

  /**
   * @param {object} obj Object to encode.
   * @returns {boolean} True on success.
   * @throws {Error} Loop detected.
   * @ignore
   */
  _pushObject(obj, opts) {
    if (!obj) {
      return this._pushNull(obj)
    }
    opts = {
      indefinite: false,
      skipTypes: false,
      ...opts,
    }
    if (!opts.indefinite) {
      // This will only happen the first time through for indefinite encoding
      if (this.detectLoops) {
        if (this.detectLoops.has(obj)) {
          throw new Error(`\
Loop detected while CBOR encoding.
Call removeLoopDetectors before resuming.`)
        } else {
          this.detectLoops.add(obj)
        }
      }
    }
    if (!opts.skipTypes) {
      const f = obj.encodeCBOR
      if (typeof f === 'function') {
        return f.call(obj, this)
      }
      const converter = this.semanticTypes[obj.constructor.name]
      if (converter) {
        return converter.call(obj, this, obj)
      }
    }
    const keys = Object.keys(obj).filter(k => {
      const tv = typeof obj[k]
      return (tv !== 'function') &&
        (!this.omitUndefinedProperties || (tv !== 'undefined'))
    })
    const cbor_keys = {}
    if (this.canonical) {
      // Note: this can't be a normal sort, because 'b' needs to sort before
      // 'aa'
      keys.sort((a, b) => {
        // Always strings, so don't bother to pass options.
        // hold on to the cbor versions, since there's no need
        // to encode more than once
        const a_cbor = cbor_keys[a] || (cbor_keys[a] = Encoder.encode(a))
        const b_cbor = cbor_keys[b] || (cbor_keys[b] = Encoder.encode(b))

        return a_cbor.compare(b_cbor)
      })
    }
    if (opts.indefinite) {
      if (!this._pushUInt8((MT.MAP << 5) | NUMBYTES.INDEFINITE)) {
        return false
      }
    } else if (!this._pushInt(keys.length, MT.MAP)) {
      return false
    }
    let ck = null
    for (let j = 0, len2 = keys.length; j < len2; j++) {
      const k = keys[j]
      if (this.canonical && ((ck = cbor_keys[k]))) {
        if (!this.push(ck)) { // Already a Buffer
          return false
        }
      } else if (!this._pushString(k)) {
        return false
      }
      if (!this.pushAny(obj[k])) {
        return false
      }
    }
    if (opts.indefinite) {
      if (!this.push(BREAK)) {
        return false
      }
    } else if (this.detectLoops) {
      this.detectLoops.delete(obj)
    }
    return true
  }

  /**
   * @param {any[]} objs Array of supported things.
   * @returns {Buffer} Concatenation of encodings for the supported things.
   * @ignore
   */
  _encodeAll(objs) {
    const bs = new NoFilter({highWaterMark: this.readableHighWaterMark})
    this.pipe(bs)
    for (const o of objs) {
      this.pushAny(o)
    }
    this.end()
    return bs.read()
  }

  /**
   * Add an encoding function to the list of supported semantic types.  This
   * is useful for objects for which you can't add an encodeCBOR method.
   *
   * @param {string|Function} type The type to encode.
   * @param {EncodeFunction} fun The encoder to use.
   * @returns {EncodeFunction?} The previous encoder or undefined if there
   *   wasn't one.
   * @throws {TypeError} Invalid function.
   */
  addSemanticType(type, fun) {
    const typeName = (typeof type === 'string') ? type : type.name
    const old = this.semanticTypes[typeName]

    if (fun) {
      if (typeof fun !== 'function') {
        throw new TypeError('fun must be of type function')
      }
      this.semanticTypes[typeName] = fun
    } else if (old) {
      delete this.semanticTypes[typeName]
    }
    return old
  }

  /**
   * Push any supported type onto the encoded stream.
   *
   * @param {any} obj The thing to encode.
   * @returns {boolean} True on success.
   * @throws {TypeError} Unknown type for obj.
   */
  pushAny(obj) {
    switch (typeof obj) {
      case 'number':
        return this._pushNumber(obj)
      case 'bigint':
        return this._pushJSBigint(obj)
      case 'string':
        return this._pushString(obj)
      case 'boolean':
        return this._pushBoolean(obj)
      case 'undefined':
        return this._pushUndefined(obj)
      case 'object':
        return this._pushObject(obj)
      case 'symbol':
        switch (obj) {
          case SYMS.NULL:
            return this._pushNull(null)
          case SYMS.UNDEFINED:
            return this._pushUndefined(undefined)
          // TODO: Add pluggable support for other symbols
          default:
            throw new TypeError(`Unknown symbol: ${obj.toString()}`)
        }
      default:
        throw new TypeError(
          `Unknown type: ${typeof obj}, ${(typeof obj.toString === 'function') ? obj.toString() : ''}`
        )
    }
  }

  /**
   * Encode an array and all of its elements.
   *
   * @param {Encoder} gen Encoder to use.
   * @param {any[]} obj Array to encode.
   * @param {object} [opts] Options.
   * @param {boolean} [opts.indefinite=false] Use indefinite encoding?
   * @returns {boolean} True on success.
   */
  static pushArray(gen, obj, opts) {
    opts = {
      indefinite: false,
      ...opts,
    }
    const len = obj.length
    if (opts.indefinite) {
      if (!gen._pushUInt8((MT.ARRAY << 5) | NUMBYTES.INDEFINITE)) {
        return false
      }
    } else if (!gen._pushInt(len, MT.ARRAY)) {
      return false
    }
    for (let j = 0; j < len; j++) {
      if (!gen.pushAny(obj[j])) {
        return false
      }
    }
    if (opts.indefinite) {
      if (!gen.push(BREAK)) {
        return false
      }
    }
    return true
  }

  /**
   * Remove the loop detector WeakSet for this Encoder.
   *
   * @returns {boolean} True when the Encoder was reset, else false.
   */
  removeLoopDetectors() {
    if (!this.detectLoops) {
      return false
    }
    this.detectLoops = new WeakSet()
    return true
  }

  /**
   * @param {Encoder} gen Encoder.
   * @param {Date} obj Date to encode.
   * @returns {boolean} True on success.
   * @ignore
   */
  static _pushDate(gen, obj) {
    switch (gen.dateType) {
      case 'string':
        return gen._pushTag(TAG.DATE_STRING) &&
          gen._pushString(obj.toISOString())
      case 'int':
        return gen._pushTag(TAG.DATE_EPOCH) &&
          gen._pushIntNum(Math.round(obj.getTime() / 1000))
      case 'float':
        // Force float
        return gen._pushTag(TAG.DATE_EPOCH) &&
          gen._pushFloat(obj.getTime() / 1000)
      case 'number':
      default:
        // If we happen to have an integral number of seconds,
        // use integer.  Otherwise, use float.
        return gen._pushTag(TAG.DATE_EPOCH) &&
          gen.pushAny(obj.getTime() / 1000)
    }
  }

  /**
   * @param {Encoder} gen Encoder.
   * @param {Buffer} obj Buffer to encode.
   * @returns {boolean} True on success.
   * @ignore
   */
  static _pushBuffer(gen, obj) {
    return gen._pushInt(obj.length, MT.BYTE_STRING) && gen.push(obj)
  }

  /**
   * @param {Encoder} gen Encoder.
   * @param {NoFilter} obj Buffer to encode.
   * @returns {boolean} True on success.
   * @ignore
   */
  static _pushNoFilter(gen, obj) {
    return Encoder._pushBuffer(gen, /** @type {Buffer} */ (obj.slice()))
  }

  /**
   * @param {Encoder} gen Encoder.
   * @param {RegExp} obj RegExp to encode.
   * @returns {boolean} True on success.
   * @ignore
   */
  static _pushRegexp(gen, obj) {
    return gen._pushTag(TAG.REGEXP) && gen.pushAny(obj.source)
  }

  /**
   * @param {Encoder} gen Encoder.
   * @param {Set} obj Set to encode.
   * @returns {boolean} True on success.
   * @ignore
   */
  static _pushSet(gen, obj) {
    if (!gen._pushTag(TAG.SET)) {
      return false
    }
    if (!gen._pushInt(obj.size, MT.ARRAY)) {
      return false
    }
    for (const x of obj) {
      if (!gen.pushAny(x)) {
        return false
      }
    }
    return true
  }

  /**
   * @param {Encoder} gen Encoder.
   * @param {URL} obj URL to encode.
   * @returns {boolean} True on success.
   * @ignore
   */
  static _pushURL(gen, obj) {
    return gen._pushTag(TAG.URI) && gen.pushAny(obj.toString())
  }

  /**
   * @param {Encoder} gen Encoder.
   * @param {object} obj Boxed String, Number, or Boolean object to encode.
   * @returns {boolean} True on success.
   * @ignore
   */
  static _pushBoxed(gen, obj) {
    return gen.pushAny(obj.valueOf())
  }

  /**
   * @param {Encoder} gen Encoder.
   * @param {Map} obj Map to encode.
   * @returns {boolean} True on success.
   * @throws {Error} Map key that is undefined.
   * @ignore
   */
  static _pushMap(gen, obj, opts) {
    opts = {
      indefinite: false,
      ...opts,
    }
    let entries = [...obj.entries()]
    if (gen.omitUndefinedProperties) {
      entries = entries.filter(([k, v]) => v !== undefined)
    }
    if (opts.indefinite) {
      if (!gen._pushUInt8((MT.MAP << 5) | NUMBYTES.INDEFINITE)) {
        return false
      }
    } else if (!gen._pushInt(entries.length, MT.MAP)) {
      return false
    }
    // Memoizing the cbor only helps in certain cases, and hurts in most
    // others.  Just avoid it.
    if (gen.canonical) {
      // Keep the key/value pairs together, so we don't have to do odd
      // gets with object keys later
      const enc = new Encoder({
        genTypes: gen.semanticTypes,
        canonical: gen.canonical,
        detectLoops: Boolean(gen.detectLoops), // Give enc its own loop detector
        dateType: gen.dateType,
        disallowUndefinedKeys: gen.disallowUndefinedKeys,
        collapseBigIntegers: gen.collapseBigIntegers,
      })
      const bs = new NoFilter({highWaterMark: gen.readableHighWaterMark})
      enc.pipe(bs)
      entries.sort(([a], [b]) => {
        // Both a and b are the keys
        enc.pushAny(a)
        const a_cbor = bs.read()
        enc.pushAny(b)
        const b_cbor = bs.read()
        return a_cbor.compare(b_cbor)
      })
      for (const [k, v] of entries) {
        if (gen.disallowUndefinedKeys && (typeof k === 'undefined')) {
          throw new Error('Invalid Map key: undefined')
        }
        if (!(gen.pushAny(k) && gen.pushAny(v))) {
          return false
        }
      }
    } else {
      for (const [k, v] of entries) {
        if (gen.disallowUndefinedKeys && (typeof k === 'undefined')) {
          throw new Error('Invalid Map key: undefined')
        }
        if (!(gen.pushAny(k) && gen.pushAny(v))) {
          return false
        }
      }
    }
    if (opts.indefinite) {
      if (!gen.push(BREAK)) {
        return false
      }
    }
    return true
  }

  /**
   * @param {Encoder} gen Encoder.
   * @param {NodeJS.TypedArray} obj Array to encode.
   * @returns {boolean} True on success.
   * @ignore
   */
  static _pushTypedArray(gen, obj) {
    // See https://tools.ietf.org/html/rfc8746

    let typ = 0b01000000
    let sz = obj.BYTES_PER_ELEMENT
    const {name} = obj.constructor

    if (name.startsWith('Float')) {
      typ |= 0b00010000
      sz /= 2
    } else if (!name.includes('U')) {
      typ |= 0b00001000
    }
    if (name.includes('Clamped') || ((sz !== 1) && !utils.isBigEndian())) {
      typ |= 0b00000100
    }
    typ |= {
      1: 0b00,
      2: 0b01,
      4: 0b10,
      8: 0b11,
    }[sz]
    if (!gen._pushTag(typ)) {
      return false
    }
    return Encoder._pushBuffer(
      gen,
      Buffer.from(obj.buffer, obj.byteOffset, obj.byteLength)
    )
  }

  /**
   * @param {Encoder} gen Encoder.
   * @param { ArrayBuffer } obj Array to encode.
   * @returns {boolean} True on success.
   * @ignore
   */
  static _pushArrayBuffer(gen, obj) {
    return Encoder._pushBuffer(gen, Buffer.from(obj))
  }

  /**
   * Encode the given object with indefinite length.  There are apparently
   * some (IMO) broken implementations of poorly-specified protocols that
   * REQUIRE indefinite-encoding.  See the example for how to add this as an
   * `encodeCBOR` function to an object or class to get indefinite encoding.
   *
   * @param {Encoder} gen The encoder to use.
   * @param {string|Buffer|Array|Map|object} [obj] The object to encode.  If
   *   null, use "this" instead.
   * @param {EncodingOptions} [options={}] Options for encoding.
   * @returns {boolean} True on success.
   * @throws {Error} No object to encode or invalid indefinite encoding.
   * @example <caption>Force indefinite encoding:</caption>
   * const o = {
   *   a: true,
   *   encodeCBOR: cbor.Encoder.encodeIndefinite,
   * }
   * const m = []
   * m.encodeCBOR = cbor.Encoder.encodeIndefinite
   * cbor.encodeOne([o, m])
   */
  static encodeIndefinite(gen, obj, options = {}) {
    if (obj == null) {
      if (this == null) {
        throw new Error('No object to encode')
      }
      obj = this
    }

    // TODO: consider other options
    const {chunkSize = 4096} = options

    let ret = true
    const objType = typeof obj
    let buf = null
    if (objType === 'string') {
      // TODO: make sure not to split surrogate pairs at the edges of chunks,
      // since such half-surrogates cannot be legally encoded as UTF-8.
      ret = ret && gen._pushUInt8((MT.UTF8_STRING << 5) | NUMBYTES.INDEFINITE)
      let offset = 0
      while (offset < obj.length) {
        const endIndex = offset + chunkSize
        ret = ret && gen._pushString(obj.slice(offset, endIndex))
        offset = endIndex
      }
      ret = ret && gen.push(BREAK)
    } else if ((buf = utils.bufferishToBuffer(obj))) {
      ret = ret && gen._pushUInt8((MT.BYTE_STRING << 5) | NUMBYTES.INDEFINITE)
      let offset = 0
      while (offset < buf.length) {
        const endIndex = offset + chunkSize
        ret = ret && Encoder._pushBuffer(gen, buf.slice(offset, endIndex))
        offset = endIndex
      }
      ret = ret && gen.push(BREAK)
    } else if (Array.isArray(obj)) {
      ret = ret && Encoder.pushArray(gen, obj, {
        indefinite: true,
      })
    } else if (obj instanceof Map) {
      ret = ret && Encoder._pushMap(gen, obj, {
        indefinite: true,
      })
    } else {
      if (objType !== 'object') {
        throw new Error('Invalid indefinite encoding')
      }
      ret = ret && gen._pushObject(obj, {
        indefinite: true,
        skipTypes: true,
      })
    }
    return ret
  }

  /**
   * Encode one or more JavaScript objects, and return a Buffer containing the
   * CBOR bytes.
   *
   * @param {...any} objs The objects to encode.
   * @returns {Buffer} The encoded objects.
   */
  static encode(...objs) {
    return new Encoder()._encodeAll(objs)
  }

  /**
   * Encode one or more JavaScript objects canonically (slower!), and return
   * a Buffer containing the CBOR bytes.
   *
   * @param {...any} objs The objects to encode.
   * @returns {Buffer} The encoded objects.
   */
  static encodeCanonical(...objs) {
    return new Encoder({
      canonical: true,
    })._encodeAll(objs)
  }

  /**
   * Encode one JavaScript object using the given options.
   *
   * @static
   * @param {any} obj The object to encode.
   * @param {EncodingOptions} [options={}] Passed to the Encoder constructor.
   * @returns {Buffer} The encoded objects.
   */
  static encodeOne(obj, options) {
    return new Encoder(options)._encodeAll([obj])
  }

  /**
   * Encode one JavaScript object using the given options in a way that
   * is more resilient to objects being larger than the highWaterMark
   * number of bytes.  As with the other static encode functions, this
   * will still use a large amount of memory.  Use a stream-based approach
   * directly if you need to process large and complicated inputs.
   *
   * @param {any} obj The object to encode.
   * @param {EncodingOptions} [options={}] Passed to the Encoder constructor.
   * @returns {Promise<Buffer>} A promise for the encoded buffer.
   */
  static encodeAsync(obj, options) {
    return new Promise((resolve, reject) => {
      const bufs = []
      const enc = new Encoder(options)
      enc.on('data', buf => bufs.push(buf))
      enc.on('error', reject)
      enc.on('finish', () => resolve(Buffer.concat(bufs)))
      enc.pushAny(obj)
      enc.end()
    })
  }

  /**
   * The currently supported set of semantic types.  May be modified by plugins.
   *
   * @type {SemanticMap}
   */
  static get SEMANTIC_TYPES() {
    return current_SEMANTIC_TYPES
  }

  static set SEMANTIC_TYPES(val) {
    current_SEMANTIC_TYPES = val
  }

  /**
   * Reset the supported semantic types to the original set, before any
   * plugins modified the list.
   */
  static reset() {
    Encoder.SEMANTIC_TYPES = {...SEMANTIC_TYPES}
  }
}

Object.assign(SEMANTIC_TYPES, {
  Array: Encoder.pushArray,
  Date: Encoder._pushDate,
  Buffer: Encoder._pushBuffer,
  [Buffer.name]: Encoder._pushBuffer, // Might be mangled
  Map: Encoder._pushMap,
  NoFilter: Encoder._pushNoFilter,
  [NoFilter.name]: Encoder._pushNoFilter, // Mßight be mangled
  RegExp: Encoder._pushRegexp,
  Set: Encoder._pushSet,
  ArrayBuffer: Encoder._pushArrayBuffer,
  Uint8ClampedArray: Encoder._pushTypedArray,
  Uint8Array: Encoder._pushTypedArray,
  Uint16Array: Encoder._pushTypedArray,
  Uint32Array: Encoder._pushTypedArray,
  Int8Array: Encoder._pushTypedArray,
  Int16Array: Encoder._pushTypedArray,
  Int32Array: Encoder._pushTypedArray,
  Float32Array: Encoder._pushTypedArray,
  Float64Array: Encoder._pushTypedArray,
  URL: Encoder._pushURL,
  Boolean: Encoder._pushBoxed,
  Number: Encoder._pushBoxed,
  String: Encoder._pushBoxed,
})

// Safari needs to get better.
if (typeof BigUint64Array !== 'undefined') {
  SEMANTIC_TYPES[BigUint64Array.name] = Encoder._pushTypedArray
}
if (typeof BigInt64Array !== 'undefined') {
  SEMANTIC_TYPES[BigInt64Array.name] = Encoder._pushTypedArray
}

Encoder.reset()
module.exports = Encoder
