///|
/// A lossless path segment distinguishes literal object keys from array indices.
pub(all) enum PathSegment {
  Field(String)
  Index(Int32)
} derive(Debug, Eq)

///|
fn path_key(key : String) -> String {
  let chars = key.to_array()
  if chars.is_empty() {
    return "[\"\"]"
  }
  let simple = chars.all(fn(c) {
    (c >= 'a' && c <= 'z') ||
    (c >= 'A' && c <= 'Z') ||
    (c >= '0' && c <= '9') ||
    c == '_'
  })
  if simple {
    key
  } else {
    "[" + Json::string(key).stringify() + "]"
  }
}

///|
/// Decode the canonical path notation, including quoted keys and indices.
pub fn parse_path(path : String) -> Array[PathSegment] {
  let chars = path.to_array()
  let result : Array[PathSegment] = []
  let mut i = 0
  while i < chars.length() {
    if chars[i] == '.' {
      i = i + 1
      continue
    }
    if chars[i] != '[' {
      let start = i
      while i < chars.length() && chars[i] != '.' && chars[i] != '[' {
        i = i + 1
      }
      result.push(Field(chars_to_string(chars, start, i)))
      continue
    }
    let start = i + 1
    i = start
    let mut quoted = false
    let mut escaped = false
    while i < chars.length() {
      let c = chars[i]
      if escaped {
        escaped = false
      } else if quoted && c == '\\' {
        escaped = true
      } else if c == '"' {
        quoted = !quoted
      } else if !quoted && c == ']' {
        break
      }
      i = i + 1
    }
    let content = chars_to_string(chars, start, i)
    if content.has_prefix("\"") {
      let key = @json.parse(content) catch { _ => Json::string(content) }
      match key {
        String(key) => result.push(Field(key))
        _ => result.push(Field(content))
      }
    } else {
      let mut index = 0
      let mut numeric = !content.is_empty()
      for c in content.to_array() {
        if c < '0' || c > '9' || index > 214748364 {
          numeric = false
          break
        }
        let digit = c.to_int() - 48
        if index == 214748364 && digit > 7 {
          numeric = false
          break
        }
        index = index * 10 + digit
      }
      result.push(if numeric { Index(index) } else { Field(content) })
    }
    if i < chars.length() {
      i = i + 1
    }
  }
  result
}

///|
/// Format structured path segments without losing special object keys.
pub fn format_path_segments(segments : Array[PathSegment]) -> String {
  let parts = segments.map(fn(segment) {
    match segment {
      Field(key) => path_key(key)
      Index(index) => "[\{index}]"
    }
  })
  format_path(parts)
}

///|
fn stack_segments(parts : Array[String]) -> Array[PathSegment] {
  let result : Array[PathSegment] = []
  for part in parts {
    result.append(parse_path(part))
  }
  result
}

///|
fn last_field_name(parts : Array[String]) -> String {
  let segments = stack_segments(parts)
  match segments.last() {
    Some(Field(key)) => key
    Some(Index(index)) => "[\{index}]"
    None => ""
  }
}