var TeleportJS = (function (Primitive, primitive) { // eslint-disable-line no-unused-vars
  var REF_KEY_PREFIX = '_'
  var SINGLE_REF = REF_KEY_PREFIX + '0'
  var REF_PREFIX = {
    undefined: 'u',
    number: 'n',
    bigint: 'b',
    symbol: 's',
    Map: 'M',
    Set: 'S',
    Date: 'D',
    RegExp: 'R',
    Buffer: 'B',
    Int8Array: 'H',
    Uint8Array: 'I',
    Uint8ClampedArray: 'J',
    Int16Array: 'P',
    Uint16Array: 'Q',
    Int32Array: 'F',
    Uint32Array: 'G',
    Float32Array: 'K',
    Float64Array: 'L'
  }

  /*!
   * ISC License
   *
   * Copyright (c) 2018, Andrea Giammarchi, @WebReflection
   */

  var TeleportJS = {

    parse: function parse (text, reviver) {
      var input = JSON.parse(text, Primitives).map(primitives)
      var len = input.length
      var refs = len > 1 ? input[len - 1] : []
      var value = input[0]
      var $ = reviver || noop
      var tmp = typeof value === 'object' && value
        ? revive(input, refs, new Set(), value, $)
        : (value === SINGLE_REF && refs.length ? reviveRefs(refs, 0) : value)
      return $.call({ '': tmp }, '', tmp)
    },

    stringify: function stringify (value, replacer, space) {
      for (var
        firstRun,
        known = new Map(),
        knownRefs = new Map(),
        refs = [],
        input = [],
        output = [],
        $ = replacer && typeof replacer === typeof input
          ? function (k, v) {
            if (k === '' || replacer.indexOf(k) > -1) return v
          }
          : (replacer || noop),
        i = +set(known, input, $.call({ '': value }, '', value)),
        replace = function (key, value) {
          var after = $.call(this, key, value)
          var refIndex = setRefs(knownRefs, refs, key, after, this)

          // bail out if value set via ref
          if (refIndex) {
            return refIndex
          }

          if (firstRun) {
            firstRun = !firstRun
            return value
            // this was invoking twice each root object
            // return i < 1 ? value : $.call(this, key, value);
          }

          switch (typeof after) {
            case 'object':
              if (after === null) return after
              // eslint-disable-next-line no-fallthrough
            case primitive:
              return known.get(after) || set(known, input, after)
          }
          return after
        };
        i < input.length; i++
      ) {
        firstRun = true
        output[i] = JSON.stringify(input[i], replace, space)
      }
      refs.length && output.push(JSON.stringify(refs))
      return '[' + output.join(',') + ']'
    }

  }

  return TeleportJS

  function noop (key, value) {
    return value
  }

  function reviveRefs (refs, index) {
    var value = refs[index].substring(1)

    switch (refs[index].charAt(0)) {
      case REF_PREFIX.undefined:
        refs[index] = undefined
        break
      case REF_PREFIX.number:
        refs[index] = Number(value)
        break
      case REF_PREFIX.bigint:
        refs[index] = BigInt(value)
        break
      case REF_PREFIX.symbol:
        refs[index] = Symbol.for(value)
        break
      case REF_PREFIX.RegExp:
        var parts = /\/(.*)\/(.*)/.exec(value)
        refs[index] = new RegExp(parts[1], parts[2])
        break
      case REF_PREFIX.Buffer:
        refs[index] = Buffer.from(JSON.parse(value))
        break
      case REF_PREFIX.Date:
        refs[index] = new Date(value)
        break
      case REF_PREFIX.Map:
        refs[index] = new Map(TeleportJS.parse(value))
        break
      case REF_PREFIX.Set:
        refs[index] = new Set(TeleportJS.parse(value))
        break
      case REF_PREFIX.Int8Array:
        refs[index] = new Int8Array(JSON.parse(value))
        break
      case REF_PREFIX.Uint8Array:
        refs[index] = new Uint8Array(JSON.parse(value))
        break
      case REF_PREFIX.Uint8ClampedArray:
        refs[index] = new Uint8ClampedArray(JSON.parse(value))
        break
      case REF_PREFIX.Int16Array:
        refs[index] = new Int16Array(JSON.parse(value))
        break
      case REF_PREFIX.Uint16Array:
        refs[index] = new Uint16Array(JSON.parse(value))
        break
      case REF_PREFIX.Int32Array:
        refs[index] = new Int32Array(JSON.parse(value))
        break
      case REF_PREFIX.Uint32Array:
        refs[index] = new Uint32Array(JSON.parse(value))
        break
      case REF_PREFIX.Float32Array:
        refs[index] = new Float32Array(JSON.parse(value))
        break
      case REF_PREFIX.Float64Array:
        refs[index] = new Float64Array(JSON.parse(value))
        break
    }

    return refs[index]
  }

  function revive (input, refs, parsed, output, $) {
    return Object.keys(output).reduce(
      function (output, key) {
        var value = output[key]
        if (value instanceof Primitive) {
          if (value.startsWith(REF_KEY_PREFIX)) {
            var index = value.substring(1)

            if (refs[index] instanceof Primitive) {
              reviveRefs(refs, index)
            }

            output[key] = refs[index]
            return output
          }

          var tmp = input[value]
          if (typeof tmp === 'object' && !parsed.has(tmp)) {
            parsed.add(tmp)
            output[key] = $.call(output, key, revive(input, refs, parsed, tmp, $))
          } else {
            output[key] = $.call(output, key, tmp)
          }
        } else { output[key] = $.call(output, key, value) }
        return output
      },
      output
    )
  }

  function set (known, input, value) {
    var index = Primitive(input.push(value) - 1)
    known.set(value, index)
    return index
  }

  function setRefs (known, refs, key, value, obj) {
    var after
    var i

    switch (typeof value) {
      // case 'boolean': break
      // case 'function': break
      case 'string':
        if (obj[key] instanceof Date) {
          after = REF_PREFIX.Date + value
        }
        break
      case 'undefined':
        after = REF_PREFIX.undefined
        break
      case 'number':
        if (!Number.isFinite(value)) {
          after = REF_PREFIX.number + Primitive(value)
        }
        break
      case 'bigint':
        after = REF_PREFIX.bigint + Primitive(value)
        break
      case 'symbol':
        var description = Primitive(value)
        after = REF_PREFIX.symbol + description.substring(7, description.length - 1)
        break
      case 'object':
        if (value === null) {
          break
        } else if (value.type === 'Buffer' && value.data && Buffer.isBuffer(obj[key])) {
          after = REF_PREFIX.Buffer + JSON.stringify(value.data)
        } else if (value instanceof RegExp) {
          after = REF_PREFIX.RegExp + Primitive(value)
        } else if (value instanceof Map) {
          var m = []
          for (i of value.entries()) m.push(i)
          after = REF_PREFIX.Map + TeleportJS.stringify(m)
        } else if (value instanceof Set) {
          var s = []
          for (i of value.values()) s.push(i)
          after = REF_PREFIX.Set + TeleportJS.stringify(s)
        } else if (value instanceof Int8Array) {
          after = REF_PREFIX.Int8Array + JSON.stringify(Array.apply([], value))
        } else if (value instanceof Uint8Array) {
          after = REF_PREFIX.Uint8Array + JSON.stringify(Array.apply([], value))
        } else if (value instanceof Uint8ClampedArray) {
          after = REF_PREFIX.Uint8ClampedArray + JSON.stringify(Array.apply([], value))
        } else if (value instanceof Int16Array) {
          after = REF_PREFIX.Int16Array + JSON.stringify(Array.apply([], value))
        } else if (value instanceof Uint16Array) {
          after = REF_PREFIX.Uint16Array + JSON.stringify(Array.apply([], value))
        } else if (value instanceof Int32Array) {
          after = REF_PREFIX.Int32Array + JSON.stringify(Array.apply([], value))
        } else if (value instanceof Uint32Array) {
          after = REF_PREFIX.Uint32Array + JSON.stringify(Array.apply([], value))
        } else if (value instanceof Float32Array) {
          after = REF_PREFIX.Float32Array + JSON.stringify(Array.apply([], value))
        } else if (value instanceof Float64Array) {
          after = REF_PREFIX.Float64Array + JSON.stringify(Array.apply([], value))
        }
        break
    }

    if (!after) {
      return
    }

    var index = known.get(after)

    if (index) {
      return index
    }

    index = REF_KEY_PREFIX + Primitive(refs.push(after) - 1)
    known.set(after, index)
    return index
  }

  // the two kinds of primitives
  //  1. the real one
  //  2. the wrapped one

  function primitives (value) {
    return value instanceof Primitive ? Primitive(value) : value
  }

  function Primitives (key, value) {
    // eslint-disable-next-line valid-typeof
    return typeof value === primitive ? new Primitive(value) : value
  }
}(String, 'string'))
module.exports = TeleportJS
