/**
 *  Copyright (c) 2019 GraphQL Contributors
 *  All rights reserved.
 *
 *  This source code is licensed under the BSD-style license found in the
 *  LICENSE file in the root directory of this source tree. An additional grant
 *  of patent rights can be found in the PATENTS file in the same directory.
 */

/**
 * This JSON parser simply walks the input, generating an AST. Use this in lieu
 * of JSON.parse if you need character offset parse errors and an AST parse tree
 * with location information.
 *
 * If an error is encountered, a SyntaxError will be thrown, with properties:
 *
 *   - message: string
 *   - start: int - the start inclusive offset of the syntax error
 *   - end: int - the end exclusive offset of the syntax error
 *
 */
type JSONEOFValue = {
  kind: "EOF"
  start: number
  end: number
}

type JSONNullValue = {
  kind: "Null"
  start: number
  end: number
}

type JSONNumberValue = {
  kind: "Number"
  start: number
  end: number
  value: number
}

type JSONStringValue = {
  kind: "String"
  start: number
  end: number
  value: string
}

type JSONBooleanValue = {
  kind: "Boolean"
  start: number
  end: number
  value: boolean
}

type JSONPrimitiveValue =
  | JSONNullValue
  | JSONEOFValue
  | JSONStringValue
  | JSONNumberValue
  | JSONBooleanValue

export type JSONObjectValue = {
  kind: "Object"
  start: number
  end: number
  // eslint-disable-next-line no-use-before-define
  members: JSONObjectMember[]
}

export type JSONArrayValue = {
  kind: "Array"
  start: number
  end: number
  // eslint-disable-next-line no-use-before-define
  values: JSONValue[]
}

export type JSONValue = JSONObjectValue | JSONArrayValue | JSONPrimitiveValue

export type JSONObjectMember = {
  kind: "Member"
  start: number
  end: number
  key: JSONStringValue
  value: JSONValue
}

export default function jsonParse(
  str: string
): JSONObjectValue | JSONArrayValue {
  string = str
  strLen = str.length
  start = end = lastEnd = -1
  ch()
  lex()
  try {
    const ast = parseObj()
    expect("EOF")
    return ast
  } catch (e) {
    // Try parsing expecting a root array
    const ast = parseArr()
    expect("EOF")
    return ast
  }
}

let string: string
let strLen: number
let start: number
let end: number
let lastEnd: number
let code: number
let kind: string

function parseObj(): JSONObjectValue {
  const nodeStart = start
  const members = []
  expect("{")
  if (!skip("}")) {
    do {
      members.push(parseMember())
    } while (skip(","))
    expect("}")
  }
  return {
    kind: "Object",
    start: nodeStart,
    end: lastEnd,
    members,
  }
}

function parseMember(): JSONObjectMember {
  const nodeStart = start
  const key = kind === "String" ? (curToken() as JSONStringValue) : null
  expect("String")
  expect(":")
  const value = parseVal()
  return {
    kind: "Member",
    start: nodeStart,
    end: lastEnd,
    key: key!,
    value,
  }
}

function parseArr(): JSONArrayValue {
  const nodeStart = start
  const values: JSONValue[] = []
  expect("[")
  if (!skip("]")) {
    do {
      values.push(parseVal())
    } while (skip(","))
    expect("]")
  }
  return {
    kind: "Array",
    start: nodeStart,
    end: lastEnd,
    values,
  }
}

function parseVal(): JSONValue {
  switch (kind) {
    case "[":
      return parseArr()
    case "{":
      return parseObj()
    case "String":
    case "Number":
    case "Boolean":
    case "Null":
      // eslint-disable-next-line no-case-declarations
      const token = curToken()
      lex()
      return token
  }
  return expect("Value") as never
}

function curToken(): JSONPrimitiveValue {
  return {
    kind: kind as any,
    start,
    end,
    value: JSON.parse(string.slice(start, end)),
  }
}

function expect(str: string) {
  if (kind === str) {
    lex()
    return
  }

  let found
  if (kind === "EOF") {
    found = "[end of file]"
  } else if (end - start > 1) {
    found = `\`${string.slice(start, end)}\``
  } else {
    const match = string.slice(start).match(/^.+?\b/)
    found = `\`${match ? match[0] : string[start]}\``
  }

  throw syntaxError(`Expected ${str} but found ${found}.`)
}

type SyntaxError = {
  message: string
  start: number
  end: number
}

function syntaxError(message: string): SyntaxError {
  return { message, start, end }
}

function skip(k: string) {
  if (kind === k) {
    lex()
    return true
  }
}

function ch() {
  if (end < strLen) {
    end++
    code = end === strLen ? 0 : string.charCodeAt(end)
  }
}

function lex() {
  lastEnd = end

  while (code === 9 || code === 10 || code === 13 || code === 32) {
    ch()
  }

  if (code === 0) {
    kind = "EOF"
    return
  }

  start = end

  switch (code) {
    // "
    case 34:
      kind = "String"
      return readString()
    // -, 0-9
    case 45:
    case 48:
    case 49:
    case 50:
    case 51:
    case 52:
    case 53:
    case 54:
    case 55:
    case 56:
    case 57:
      kind = "Number"
      return readNumber()
    // f
    case 102:
      if (string.slice(start, start + 5) !== "false") {
        break
      }
      end += 4
      ch()

      kind = "Boolean"
      return
    // n
    case 110:
      if (string.slice(start, start + 4) !== "null") {
        break
      }
      end += 3
      ch()

      kind = "Null"
      return
    // t
    case 116:
      if (string.slice(start, start + 4) !== "true") {
        break
      }
      end += 3
      ch()

      kind = "Boolean"
      return
  }

  kind = string[start]
  ch()
}

function readString() {
  ch()
  while (code !== 34 && code > 31) {
    if (code === (92 as any)) {
      // \
      ch()
      switch (code) {
        case 34: // "
        case 47: // /
        case 92: // \
        case 98: // b
        case 102: // f
        case 110: // n
        case 114: // r
        case 116: // t
          ch()
          break
        case 117: // u
          ch()
          readHex()
          readHex()
          readHex()
          readHex()
          break
        default:
          throw syntaxError("Bad character escape sequence.")
      }
    } else if (end === strLen) {
      throw syntaxError("Unterminated string.")
    } else {
      ch()
    }
  }

  if (code === 34) {
    ch()
    return
  }

  throw syntaxError("Unterminated string.")
}

function readHex() {
  if (
    (code >= 48 && code <= 57) || // 0-9
    (code >= 65 && code <= 70) || // A-F
    (code >= 97 && code <= 102) // a-f
  ) {
    return ch()
  }
  throw syntaxError("Expected hexadecimal digit.")
}

function readNumber() {
  if (code === 45) {
    // -
    ch()
  }

  if (code === 48) {
    // 0
    ch()
  } else {
    readDigits()
  }

  if (code === 46) {
    // .
    ch()
    readDigits()
  }

  if (code === 69 || code === 101) {
    // E e
    ch()
    if (code === (43 as any) || code === (45 as any)) {
      // + -
      ch()
    }
    readDigits()
  }
}

function readDigits() {
  if (code < 48 || code > 57) {
    // 0 - 9
    throw syntaxError("Expected decimal digit.")
  }
  do {
    ch()
  } while (code >= 48 && code <= 57) // 0 - 9
}
