///|
pub fn parse_json(text : String) -> JsonParseResult {
  parse_json_with_limit(text, 64)
}

///|
pub fn parse_json_with_limit(text : String, max_depth : Int) -> JsonParseResult {
  let state = ParserState::new(text, if max_depth < 1 { 1 } else { max_depth })
  state.skip_space()
  let value = parse_json_value(state, 0)
  state.skip_space()
  if value is Some(_) && state.cursor[0] != state.text.length() {
    state.error("$", "trailing content after JSON value")
  }
  let final_value = if state.diagnostics.length() == 0 { value } else { None }
  { value: final_value, diagnostics: { items: state.diagnostics } }
}

///|
fn parse_json_value(state : ParserState, depth : Int) -> JsonValue? {
  if depth > state.max_depth {
    state.error("$", "maximum JSON nesting depth exceeded")
    return None
  }
  match state.peek() {
    None => {
      state.error("$", "expected a JSON value")
      None
    }
    Some(ch) if ch == '"' => parse_json_string(state)
    Some(ch) if ch == '{' => parse_json_object(state, depth)
    Some(ch) if ch == '[' => parse_json_array(state, depth)
    Some(ch) if ch == 't' => parse_literal(state, "true", JBool(true))
    Some(ch) if ch == 'f' => parse_literal(state, "false", JBool(false))
    Some(ch) if ch == 'n' => parse_literal(state, "null", JNull)
    Some(ch) if ch == '-' || (ch >= '0' && ch <= '9') =>
      parse_json_number(state)
    Some(_) => {
      state.error("$", "unexpected character in JSON value")
      None
    }
  }
}

///|
fn parse_json_string(state : ParserState) -> JsonValue? {
  ignore(state.advance())
  let builder = StringBuilder::new()
  while state.cursor[0] < state.text.length() {
    let ch = state.advance().unwrap()
    if ch == '"' {
      return Some(JString(builder.to_string()))
    }
    if ch == '\\' {
      if !parse_json_escape(state, builder) {
        return None
      }
    } else if ch < 32 {
      state.error("$", "control character is not allowed in a JSON string")
      return None
    } else if !append_code_unit(builder, ch) {
      state.error("$", "invalid character in JSON string")
      return None
    }
  }
  state.error("$", "unterminated JSON string")
  None
}

///|
fn parse_json_escape(state : ParserState, builder : StringBuilder) -> Bool {
  match state.advance() {
    None => {
      state.error("$", "unterminated JSON escape")
      false
    }
    Some(ch) => {
      match ch {
        '"' => builder.write_char('"')
        '\\' => builder.write_char('\\')
        '/' => builder.write_char('/')
        'b' => builder.write_char('\u{0008}')
        'f' => builder.write_char('\u{000C}')
        'n' => builder.write_char('\n')
        'r' => builder.write_char('\r')
        't' => builder.write_char('\t')
        _ => {
          state.error("$", "unsupported or invalid JSON escape")
          return false
        }
      }
      true
    }
  }
}

///|
fn parse_json_number(state : ParserState) -> JsonValue? {
  let builder = StringBuilder::new()
  if state.peek() is Some('-') {
    ignore(state.advance())
    builder.write_char('-')
  }
  let mut digits = 0
  while state.cursor[0] < state.text.length() {
    let ch = state.text[state.cursor[0]]
    if ch >= '0' && ch <= '9' {
      digits += 1
      ignore(state.advance())
      match ch.to_char() {
        Some(c) => builder.write_char(c)
        None => ()
      }
    } else {
      break
    }
  }
  if digits == 0 {
    state.error("$", "JSON number requires digits")
    return None
  }
  if state.peek() is Some('.') {
    ignore(state.advance())
    builder.write_char('.')
    let mut fraction_digits = 0
    while state.cursor[0] < state.text.length() {
      let ch = state.text[state.cursor[0]]
      if ch >= '0' && ch <= '9' {
        fraction_digits += 1
        ignore(state.advance())
        match ch.to_char() {
          Some(c) => builder.write_char(c)
          None => ()
        }
      } else {
        break
      }
    }
    if fraction_digits == 0 {
      state.error("$", "JSON fraction requires digits")
      return None
    }
  }
  match state.peek() {
    Some('e') | Some('E') => {
      ignore(state.advance())
      builder.write_char('e')
      match state.peek() {
        Some('+') | Some('-') => {
          let sign = state.advance().unwrap()
          match sign.to_char() {
            Some(c) => builder.write_char(c)
            None => ()
          }
        }
        _ => ()
      }
      let mut exponent_digits = 0
      while state.cursor[0] < state.text.length() {
        let ch = state.text[state.cursor[0]]
        if ch >= '0' && ch <= '9' {
          exponent_digits += 1
          ignore(state.advance())
          match ch.to_char() {
            Some(c) => builder.write_char(c)
            None => ()
          }
        } else {
          break
        }
      }
      if exponent_digits == 0 {
        state.error("$", "JSON exponent requires digits")
        return None
      }
    }
    _ => ()
  }
  Some(JNumber(builder.to_string()))
}

///|
fn parse_literal(
  state : ParserState,
  expected : String,
  value : JsonValue,
) -> JsonValue? {
  for i in 0.. ignore(state.advance())
      _ => {
        state.error("$", "invalid JSON literal")
        return None
      }
    }
  }
  Some(value)
}

///|
fn parse_json_array(state : ParserState, depth : Int) -> JsonValue? {
  ignore(state.advance())
  state.skip_space()
  let values : Array[JsonValue] = []
  if state.peek() is Some(']') {
    ignore(state.advance())
    return Some(JArray(values))
  }
  while true {
    state.skip_space()
    match parse_json_value(state, depth + 1) {
      Some(value) => values.push(value)
      None => return None
    }
    state.skip_space()
    match state.peek() {
      Some(',') => {
        ignore(state.advance())
        state.skip_space()
      }
      Some(']') => {
        ignore(state.advance())
        return Some(JArray(values))
      }
      _ => {
        state.error("$", "expected comma or closing array bracket")
        return None
      }
    }
  }
  None
}

///|
fn parse_json_object(state : ParserState, depth : Int) -> JsonValue? {
  ignore(state.advance())
  state.skip_space()
  let fields : Array[JsonPair] = []
  if state.peek() is Some('}') {
    ignore(state.advance())
    return Some(JObject(fields))
  }
  while true {
    state.skip_space()
    let key = match parse_json_string(state) {
      Some(JString(value)) => value
      _ => {
        state.error("$", "object key must be a JSON string")
        return None
      }
    }
    if fields.any(fn(item) { item.key == key }) {
      state.error(key, "duplicate object key")
      return None
    }
    state.skip_space()
    if state.peek() is Some(':') {
      ignore(state.advance())
    } else {
      state.error(key, "expected colon after object key")
      return None
    }
    state.skip_space()
    let value = match parse_json_value(state, depth + 1) {
      Some(value) => value
      None => return None
    }
    fields.push(JsonPair::new(key, value))
    state.skip_space()
    match state.peek() {
      Some(',') => {
        ignore(state.advance())
        state.skip_space()
      }
      Some('}') => {
        ignore(state.advance())
        return Some(JObject(fields))
      }
      _ => {
        state.error("$", "expected comma or closing object brace")
        return None
      }
    }
  }
  None
}