// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0

// The two addressing schemes a schema is written in: the URI references `$id`,
// `$ref` and `$anchor` carry (RFC 3986 §5, the reference resolution a validator
// needs and no more), and the JSON Pointer a fragment beginning with `/` is
// (RFC 6901). Both are here because both are addressing, and neither is JSON.

///|
/// A reference split into the part that names a document and the part that names
/// a place inside it. The fragment comes back without its `#`.
fn split_fragment(reference : String) -> (String, String) {
  let text = reference[:]
  for i = 0; i < text.length(); i = i + 1 {
    if text[i] == '#' {
      return (text[0:i].to_owned(), text[i + 1:].to_owned())
    }
  }
  (reference, "")
}

///|
/// `reference` resolved against `base`, RFC 3986 §5.2 in the shapes a schema
/// uses: an absolute URI stands alone, a network reference takes the base's
/// scheme, an absolute path takes its authority, and a relative path is merged
/// onto the base's directory. A fragment-only reference keeps the base entirely.
fn resolve(base : String, reference : String) -> String {
  let (target, fragment) = split_fragment(reference)
  let (base_document, _) = split_fragment(base)
  let merged = if target == "" {
    base_document
  } else if has_scheme(target) {
    target
  } else if target.has_prefix("//") {
    scheme_of(base_document) + target
  } else if target.has_prefix("/") {
    authority_of(base_document) + target
  } else {
    directory_of(base_document) + target
  }
  let tidy = without_dots(merged)
  if fragment == "" && !reference.has_suffix("#") {
    tidy
  } else {
    tidy + "#" + fragment
  }
}

///|
/// A path with its `.` and `..` segments taken out, RFC 3986 §5.2.4.
///
/// `./bar.json` beside `nested/foo.json` is `nested/bar.json`, and a validator
/// that left the dot in would look for a resource nobody named.
fn without_dots(uri : String) -> String {
  if !uri.contains("./") && !uri.has_suffix("/.") && !uri.has_suffix("/..") {
    return uri
  }
  let authority = authority_of(uri)
  let head = if authority == "" { scheme_of(uri) } else { authority }
  let path = uri[head.length():].to_owned()
  let kept : Array[String] = []
  let rooted = path.has_prefix("/")
  let mut trailing = false
  for segment in split_path(path) {
    match segment {
      "." => trailing = true
      ".." => {
        if kept.length() > 0 {
          kept.pop() |> ignore
        }
        trailing = true
      }
      _ => {
        kept.push(segment)
        trailing = false
      }
    }
  }
  let out = StringBuilder()
  out.write_string(head)
  if rooted {
    out.write_char('/')
  }
  for i = 0; i < kept.length(); i = i + 1 {
    if i > 0 {
      out.write_char('/')
    }
    out.write_string(kept[i])
  }
  if trailing && kept.length() > 0 {
    out.write_char('/')
  }
  out.to_string()
}

///|
/// A path's segments, with the empty ones its slashes leave behind dropped
/// except for the one a trailing slash means.
fn split_path(path : String) -> Array[String] {
  let text = path[:]
  let out : Array[String] = []
  let piece = StringBuilder()
  let mut trailing = false
  for i = 0; i < text.length(); i = i + 1 {
    if text[i] == '/' {
      let segment = piece.to_string()
      if segment != "" {
        out.push(segment)
      }
      piece.reset()
      trailing = true
    } else {
      piece.write_string(text[i:i + 1].to_owned())
      trailing = false
    }
  }
  let last = piece.to_string()
  if last != "" {
    out.push(last)
  } else if trailing && out.length() > 0 {
    out.push("")
  }
  out
}

///|
/// Whether a reference begins with a scheme, which is what makes it absolute.
///
/// A scheme is a letter followed by letters, digits, `+`, `-` or `.`, and then a
/// colon — so a Windows path and a relative name that happens to hold a colon
/// are not mistaken for one.
fn has_scheme(reference : String) -> Bool {
  let text = reference[:]
  if text.length() == 0 || !is_alpha(text[0].to_int()) {
    return false
  }
  for i = 1; i < text.length(); i = i + 1 {
    let c = text[i]
    if c == ':' {
      return i > 0
    }
    if !(is_alpha(c.to_int()) ||
      is_digit(c.to_int()) ||
      c == '+' ||
      c == '-' ||
      c == '.') {
      return false
    }
  }
  false
}

///|
/// `scheme:` of a URI, or the empty string when it has none.
fn scheme_of(uri : String) -> String {
  let text = uri[:]
  for i = 0; i < text.length(); i = i + 1 {
    if text[i] == ':' {
      return text[0:i + 1].to_owned()
    }
  }
  ""
}

///|
/// Everything up to the end of the authority — `https://example.test` — or the
/// empty string when the URI has no authority.
fn authority_of(uri : String) -> String {
  let text = uri[:]
  let scheme = scheme_of(uri)
  if scheme == "" || !text[scheme.length():].has_prefix("//") {
    return ""
  }
  for i = scheme.length() + 2; i < text.length(); i = i + 1 {
    if text[i] == '/' {
      return text[0:i].to_owned()
    }
  }
  uri
}

///|
/// The URI with everything after its last `/` removed, which is the directory a
/// relative reference is merged onto.
fn directory_of(uri : String) -> String {
  let text = uri[:]
  for i = text.length() - 1; i >= 0; i = i - 1 {
    if text[i] == '/' {
      return text[0:i + 1].to_owned()
    }
  }
  ""
}

///|
fn is_alpha(code : Int) -> Bool {
  (code >= 0x41 && code <= 0x5A) || (code >= 0x61 && code <= 0x7A)
}

///|
fn is_digit(code : Int) -> Bool {
  code >= 0x30 && code <= 0x39
}

///|
/// The value a JSON Pointer points at, or `None` when the document has nothing
/// there (RFC 6901 §4).
fn pointer(document : Json, path : String) -> Json? {
  if path == "" {
    return Some(document)
  }
  if !path.has_prefix("/") {
    return None
  }
  let mut here = document
  for token in split_pointer(path) {
    match here {
      Object(members) =>
        match members.get(token) {
          Some(next) => here = next
          None => return None
        }
      Array(items) => {
        let index = index_of(token)
        if index < 0 || index >= items.length() {
          return None
        }
        here = items[index]
      }
      _ => return None
    }
  }
  Some(here)
}

///|
/// A pointer's reference tokens, each unescaped: `~1` is a `/` that belongs to a
/// name and `~0` a `~`, undone in that order so a name holding `~1` survives.
fn split_pointer(path : String) -> Array[String] {
  let text = path[:]
  let out : Array[String] = []
  let token = StringBuilder()
  let mut i = 1
  while i <= text.length() {
    if i == text.length() || text[i] == '/' {
      out.push(unescape(token.to_string()))
      token.reset()
    } else if text[i] == '%' && i + 2 < text.length() {
      // A pointer that arrived inside a URI is percent-encoded.
      match (nibble(text[i + 1].to_int()), nibble(text[i + 2].to_int())) {
        (Some(hi), Some(lo)) => {
          token.write_char(((hi << 4) | lo).unsafe_to_char())
          i = i + 2
        }
        _ => token.write_string(text[i:i + 1].to_owned())
      }
    } else {
      token.write_string(text[i:i + 1].to_owned())
    }
    i = i + 1
  }
  out
}

///|
/// `~1` and `~0` read back as the characters they stand for.
fn unescape(token : String) -> String {
  let text = token[:]
  let out = StringBuilder()
  let mut i = 0
  while i < text.length() {
    if text[i] == '~' && i + 1 < text.length() {
      match text[i + 1] {
        '1' => {
          out.write_char('/')
          i = i + 2
          continue
        }
        '0' => {
          out.write_char('~')
          i = i + 2
          continue
        }
        _ => ()
      }
    }
    out.write_string(text[i:i + 1].to_owned())
    i = i + 1
  }
  out.to_string()
}

///|
/// A reference token as the array index it spells, or `-1` when it spells none.
/// A leading zero is not an index, which is what keeps `01` out of an array.
fn index_of(token : String) -> Int {
  if token.length() == 0 {
    return -1
  }
  if token.length() > 1 && token[:][0] == '0' {
    return -1
  }
  let mut value = 0
  for c in token {
    if !is_digit(c.to_int()) {
      return -1
    }
    value = value * 10 + (c.to_int() - 0x30)
    if value < 0 {
      return -1
    }
  }
  value
}

///|
fn nibble(code : Int) -> Int? {
  if code >= 0x30 && code <= 0x39 {
    Some(code - 0x30)
  } else if code >= 0x41 && code <= 0x46 {
    Some(code - 0x41 + 10)
  } else if code >= 0x61 && code <= 0x66 {
    Some(code - 0x61 + 10)
  } else {
    None
  }
}

///|
/// One step deeper in a pointer, with the characters a reference token escapes.
fn step(path : String, token : String) -> String {
  let out = StringBuilder()
  out.write_string(path)
  out.write_char('/')
  for c in token {
    match c {
      '~' => out.write_string("~0")
      '/' => out.write_string("~1")
      _ => out.write_char(c)
    }
  }
  out.to_string()
}