/* eslint-disable */

/**
 * Original code taken from https://github.com/kvz/locutus
 */

function initCache () {
  const store: any[] = []
  // cache only first element, second is length to jump ahead for the parser
  const cache = function cache (value) {
    store.push(value[0])
    return value
  }

  cache.get = (index) => {
    if (index >= store.length) {
      throw RangeError(`Can't resolve reference ${index + 1}`)
    }

    return store[index]
  }

  return cache
}

function expectType (str, cache) {
  const types = /^(?:N(?=;)|[bidsSaOCrR](?=:)|[^:]+(?=:))/g
  const type = (types.exec(str) || [])[0]

  if (!type) {
    throw SyntaxError('Invalid input: ' + str)
  }

  switch (type) {
    case 'N':
      return cache([ null, 2 ])
    case 'b':
      return cache(expectBool(str))
    case 'i':
      return cache(expectInt(str))
    case 'd':
      return cache(expectFloat(str))
    case 's':
      return cache(expectString(str))
    case 'S':
      return cache(expectEscapedString(str))
    case 'a':
      return expectArray(str, cache)
    case 'O':
      return expectObject(str, cache)
    case 'C':
      return expectClass(str, cache)
    case 'r':
    case 'R':
      return expectReference(str, cache)
    default:
      throw SyntaxError(`Invalid or unsupported data type: ${type}`)
  }
}

function expectBool (str) {
  const reBool = /^b:([01]);/
  const [ match, boolMatch ] = reBool.exec(str) || []

  if (!boolMatch) {
    throw SyntaxError('Invalid bool value, expected 0 or 1')
  }

  return [ boolMatch === '1', match?.length ]
}

function expectInt (str) {
  const reInt = /^i:([+-]?\d+);/
  const [ match, intMatch ] = reInt.exec(str) || []

  if (!intMatch) {
    throw SyntaxError('Expected an integer value')
  }

  return [ parseInt(intMatch, 10), match?.length ]
}

function expectFloat (str) {
  const reFloat = /^d:(NAN|-?INF|(?:\d+\.\d*|\d*\.\d+|\d+)(?:[eE][+-]\d+)?);/
  const [ match, floatMatch ] = reFloat.exec(str) || []

  if (!floatMatch) {
    throw SyntaxError('Expected a float value')
  }

  let floatValue

  switch (floatMatch) {
    case 'NAN':
      floatValue = Number.NaN
      break
    case '-INF':
      floatValue = Number.NEGATIVE_INFINITY
      break
    case 'INF':
      floatValue = Number.POSITIVE_INFINITY
      break
    default:
      floatValue = parseFloat(floatMatch)
      break
  }

  return [ floatValue, match?.length ]
}

function readBytes (str, len, escapedString = false) {
  let bytes = 0
  let out = ''
  let c = 0
  const strLen = str.length
  let wasHighSurrogate = false
  let escapedChars = 0

  while (bytes < len && c < strLen) {
    let chr = str.charAt(c)
    const code = chr.charCodeAt(0)
    const isHighSurrogate = code >= 0xd800 && code <= 0xdbff
    const isLowSurrogate = code >= 0xdc00 && code <= 0xdfff

    if (escapedString && chr === '\\') {
      chr = String.fromCharCode(parseInt(str.substring(c + 1, c + 3), 16))
      escapedChars++

      // each escaped sequence is 3 characters. Go 2 chars ahead.
      // third character will be jumped over a few lines later
      c += 2
    }

    c++

    bytes += isHighSurrogate || (isLowSurrogate && wasHighSurrogate)
      // if high surrogate, count 2 bytes, as expectation is to be followed by low surrogate
      // if low surrogate preceded by high surrogate, add 2 bytes
      ? 2
      : code > 0x7ff
        // otherwise low surrogate falls into this part
        ? 3
        : code > 0x7f
          ? 2
          : 1

    // if high surrogate is not followed by low surrogate, add 1 more byte
    bytes += wasHighSurrogate && !isLowSurrogate ? 1 : 0

    out += chr
    wasHighSurrogate = isHighSurrogate
  }

  return [ out, bytes, escapedChars ]
}

function expectString (str) {
  // PHP strings consist of one-byte characters.
  // JS uses 2 bytes with possible surrogate pairs.
  // Serialized length of 2 is still 1 JS string character
  const reStrLength = /^s:(\d+):"/g // also match the opening " char
  const [ match, byteLenMatch ] = reStrLength.exec(str) || []

  if (!match) {
    throw SyntaxError('Expected a string value')
  }

  const len = parseInt(byteLenMatch, 10)

  str = str.substring(match.length)

  let [ strMatch, bytes ] = readBytes(str, len)

  if (bytes !== len) {
    throw SyntaxError(`Expected string of ${len} bytes, but got ${bytes}`)
  }

  str = str.substring((strMatch as string).length)

  // strict parsing, match closing "; chars
  if (!str.startsWith('";')) {
    throw SyntaxError('Expected ";')
  }

  return [ strMatch, match.length + (strMatch as string).length + 2 ] // skip last ";
}

function expectEscapedString (str) {
  const reStrLength = /^S:(\d+):"/g // also match the opening " char
  const [ match, strLenMatch ] = reStrLength.exec(str) || []

  if (!match) {
    throw SyntaxError('Expected an escaped string value')
  }

  const len = parseInt(strLenMatch, 10)

  str = str.substring(match.length)

  let [ strMatch, bytes, escapedChars ] = readBytes(str, len, true)

  if (bytes !== len) {
    throw SyntaxError(`Expected escaped string of ${len} bytes, but got ${bytes}`)
  }

  str = str.substring((strMatch as string).length + (escapedChars as number) * 2)

  // strict parsing, match closing "; chars
  if (!str.startsWith('";')) {
    throw SyntaxError('Expected ";')
  }

  return [ strMatch, match.length + (strMatch as string).length + 2 ] // skip last ";
}

function expectKeyOrIndex (str) {
  try {
    return expectString(str)
  } catch (err) {}

  try {
    return expectEscapedString(str)
  } catch (err) {}

  try {
    return expectInt(str)
  } catch (err) {
    throw SyntaxError('Expected key or index')
  }
}

function expectObject (str, cache) {
  // O:<class name length>:"class name":<prop count>:{<props and values>}
  // O:8:"stdClass":2:{s:3:"foo";s:3:"bar";s:3:"bar";s:3:"baz";}
  const reObjectLiteral = /^O:(\d+):"([^"]+)":(\d+):\{/
  const [ objectLiteralBeginMatch, /* classNameLengthMatch */, className, propCountMatch ] = reObjectLiteral.exec(str) || []

  if (!objectLiteralBeginMatch) {
    throw SyntaxError('Invalid input')
  }

  if (className !== 'stdClass') {
    throw SyntaxError(`Unsupported object type: ${className}`)
  }

  let totalOffset = objectLiteralBeginMatch.length

  const propCount = parseInt(propCountMatch, 10)
  const obj = {}
  cache([obj])

  str = str.substring(totalOffset)

  for (let i = 0; i < propCount; i++) {
    const prop = expectKeyOrIndex(str)
    str = str.substring(prop[1])
    totalOffset += prop[1] as number

    const value = expectType(str, cache)
    str = str.substring(value[1])
    totalOffset += value[1]

    if (prop[0]) {
        obj[prop[0]] = value[0]
    }
  }

  // strict parsing, expect } after object literal
  if (str.charAt(0) !== '}') {
    throw SyntaxError('Expected }')
  }

  return [ obj, totalOffset + 1 ] // skip final }
}

function expectClass (str, cache) {
  // can't be well supported, because requires calling eval (or similar)
  // in order to call serialized constructor name
  // which is unsafe
  // or assume that constructor is defined in global scope
  // but this is too much limiting
  throw Error('Not yet implemented')
}

function expectReference (str, cache) {
  const reRef = /^[rR]:([1-9]\d*);/
  const [ match, refIndex ] = reRef.exec(str) || []

  if (!match) {
    throw SyntaxError('Expected reference value')
  }

  return [ cache.get(parseInt(refIndex, 10) - 1), match.length ]
}

function expectArray (str, cache) {
  const reArrayLength = /^a:(\d+):{/
  const [ arrayLiteralBeginMatch, arrayLengthMatch ] = reArrayLength.exec(str) || []

  if (!arrayLengthMatch) {
    throw SyntaxError('Expected array length annotation')
  }

  str = str.substring(arrayLiteralBeginMatch?.length)

  const array = expectArrayItems(str, parseInt(arrayLengthMatch, 10), cache)

  // strict parsing, expect closing } brace after array literal
  if (str.charAt(array[1]) !== '}') {
    throw SyntaxError('Expected }')
  }

  return [ array[0], (arrayLiteralBeginMatch?.length ?? 0) + (array[1] as number) + 1 ] // jump over }
}

function expectArrayItems (str, expectedItems = 0, cache) {
  let key
  let hasStringKeys = false
  let item
  let totalOffset = 0
  let items: any[] = []
  cache([items])

  for (let i = 0; i < expectedItems; i++) {
    key = expectKeyOrIndex(str)

    // this is for backward compatibility with previous implementation
    if (!hasStringKeys) {
      hasStringKeys = (typeof key[0] === 'string')
    }

    str = str.substring(key[1])
    totalOffset += key[1]

    // references are resolved immediately, so if duplicate key overwrites previous array index
    // the old value is anyway resolved
    // fixme: but next time the same reference should point to the new value
    item = expectType(str, cache)
    str = str.substring(item[1])
    totalOffset += item[1]

    items[key[0]] = item[0]
  }

  // this is for backward compatibility with previous implementation
  if (hasStringKeys) {
    items = Object.assign({}, items)
  }

  return [ items, totalOffset ]
}

function unserialize (str) {
  //       discuss at: https://locutus.io/php/unserialize/
  //      original by: Arpad Ray (mailto:arpad@php.net)
  //      improved by: Pedro Tainha (https://www.pedrotainha.com)
  //      improved by: Kevin van Zonneveld (https://kvz.io)
  //      improved by: Kevin van Zonneveld (https://kvz.io)
  //      improved by: Chris
  //      improved by: James
  //      improved by: Le Torbi
  //      improved by: Eli Skeggs
  //      bugfixed by: dptr1988
  //      bugfixed by: Kevin van Zonneveld (https://kvz.io)
  //      bugfixed by: Brett Zamir (https://brett-zamir.me)
  //      bugfixed by: philippsimon (https://github.com/philippsimon/)
  //       revised by: d3x
  //         input by: Brett Zamir (https://brett-zamir.me)
  //         input by: Martin (https://www.erlenwiese.de/)
  //         input by: kilops
  //         input by: Jaroslaw Czarniak
  //         input by: lovasoa (https://github.com/lovasoa/)
  //      improved by: Rafał Kukawski
  // reimplemented by: Rafał Kukawski
  //           note 1: We feel the main purpose of this function should be
  //           note 1: to ease the transport of data between php & js
  //           note 1: Aiming for PHP-compatibility, we have to translate objects to arrays
  //        example 1: unserialize('a:3:{i:0;s:5:"Kevin";i:1;s:3:"van";i:2;s:9:"Zonneveld";}')
  //        returns 1: ['Kevin', 'van', 'Zonneveld']
  //        example 2: unserialize('a:2:{s:9:"firstName";s:5:"Kevin";s:7:"midName";s:3:"van";}')
  //        returns 2: {firstName: 'Kevin', midName: 'van'}
  //        example 3: unserialize('a:3:{s:2:"ü";s:2:"ü";s:3:"四";s:3:"四";s:4:"𠜎";s:4:"𠜎";}')
  //        returns 3: {'ü': 'ü', '四': '四', '𠜎': '𠜎'}
  //        example 4: unserialize(undefined)
  //        returns 4: false
  //        example 5: unserialize('O:8:"stdClass":1:{s:3:"foo";b:1;}')
  //        returns 5: { foo: true }
  //        example 6: unserialize('a:2:{i:0;N;i:1;s:0:"";}')
  //        returns 6: [null, ""]
  //        example 7: unserialize('S:7:"\\65\\73\\63\\61\\70\\65\\64";')
  //        returns 7: 'escaped'

  try {
    if (typeof str !== 'string') {
      return false
    }

    return expectType(str, initCache())[0]
  } catch (err) {
    console.error(err)
    return false
  }
}

function substr_replace (str, replace, start, length) {
  //  discuss at: https://locutus.io/php/substr_replace/
  // original by: Brett Zamir (https://brett-zamir.me)
  //   example 1: substr_replace('ABCDEFGH:/MNRPQR/', 'bob', 0)
  //   returns 1: 'bob'
  //   example 2: var $var = 'ABCDEFGH:/MNRPQR/'
  //   example 2: substr_replace($var, 'bob', 0, $var.length)
  //   returns 2: 'bob'
  //   example 3: substr_replace('ABCDEFGH:/MNRPQR/', 'bob', 0, 0)
  //   returns 3: 'bobABCDEFGH:/MNRPQR/'
  //   example 4: substr_replace('ABCDEFGH:/MNRPQR/', 'bob', 10, -1)
  //   returns 4: 'ABCDEFGH:/bob/'
  //   example 5: substr_replace('ABCDEFGH:/MNRPQR/', 'bob', -7, -1)
  //   returns 5: 'ABCDEFGH:/bob/'
  //   example 6: substr_replace('ABCDEFGH:/MNRPQR/', '', 10, -1)
  //   returns 6: 'ABCDEFGH://'

  if (start < 0) {
    // start position in str
    start = start + str.length
  }
  length = length !== undefined ? length : str.length
  if (length < 0) {
    length = length + str.length - start
  }

  return [
    str.slice(0, start),
    replace.substring(0, length),
    replace.slice(length),
    str.slice(start + length)
  ].join('')
}

export class Locutus {

    static unserialize<T = unknown>(data: string): T {
        return unserialize(data);
    }

    static substrReplace(str: string, replace: string, start: number, length?: number): string {
        return substr_replace(str, replace, start, length);
    }

}
