///|
/// Source positions are zero-based UTF-16 line and column indices.
pub(all) struct Location {
  path : Array[PathSegment]
  is_property_key : Bool
  offset : Int
  line : Int
  column : Int
} derive(Eq, @debug.Debug)

///|
fn path_with(
  path : Array[PathSegment],
  part : PathSegment,
) -> Array[PathSegment] {
  let result = path.copy()
  result.push(part)
  result
}

///|
priv enum LocatePhase {
  LocKey
  LocColon
  LocValue
  LocDone
} derive(Eq)

///|
priv struct LocateFrame {
  object : Bool
  mut phase : LocatePhase
  mut key : String
  mut index : Int
}

///| Track the source prefix, rather than inferring cursor state from recovered

///| tree spans. A comma, colon or bracket affects the position only after its

///| source unit has passed the cursor. Property strings supply their decoded

///| name at their opening quote and remain key positions until after the colon.

///| The stack also retains useful insertion slots at an unfinished document's

///|
/// EOF, where a recovered tree's final span cannot distinguish open and closed.
fn location_prefix(doc : Document, at : Int) -> (Array[PathSegment], Bool) {
  let stack : Array[LocateFrame] = []
  let mut started = false
  for token in doc.tokens {
    if token.offset > at {
      break
    }
    match token.kind {
      OpenBrace | OpenBracket => {
        if token.offset == at {
          break
        }
        if stack.is_empty() {
          if started {
            continue
          }
        } else {
          let parent = stack[stack.length() - 1]
          if parent.object &&
            (parent.phase == LocKey || parent.phase == LocDone) {
            continue
          }
          parent.phase = LocDone
        }
        started = true
        let object = token.kind == OpenBrace
        stack.push({
          object,
          phase: if object {
            LocKey
          } else {
            LocValue
          },
          key: "",
          index: 0,
        })
      }
      CloseBrace | CloseBracket => {
        if token.offset == at {
          break
        }
        if !stack.is_empty() {
          let frame = stack[stack.length() - 1]
          if frame.object == (token.kind == CloseBrace) {
            stack.pop() |> ignore
          }
        }
      }
      Colon => {
        if token.offset == at {
          break
        }
        if !stack.is_empty() {
          let frame = stack[stack.length() - 1]
          if frame.object {
            frame.phase = LocValue
          }
        }
      }
      Comma => {
        if token.offset == at {
          break
        }
        if !stack.is_empty() {
          let frame = stack[stack.length() - 1]
          if frame.object {
            frame.key = ""
            frame.phase = LocKey
          } else {
            frame.index += 1
            frame.phase = LocValue
          }
        }
      }
      StringToken => {
        if !stack.is_empty() {
          let frame = stack[stack.length() - 1]
          if frame.object && (frame.phase == LocKey || frame.phase == LocDone) {
            frame.key = token.value
            frame.phase = LocColon
          } else {
            frame.phase = LocDone
          }
        }
        started = true
        if at <= token.offset + token.length {
          break
        }
      }
      NumberToken | TrueToken | FalseToken | NullToken => {
        if !stack.is_empty() {
          let frame = stack[stack.length() - 1]
          if !frame.object || frame.phase != LocKey {
            frame.phase = LocDone
          }
        }
        started = true
        if at <= token.offset + token.length {
          break
        }
      }
      Eof => break
      _ => ()
    }
  }
  if stack.is_empty() {
    return ([], false)
  }
  let path : Array[PathSegment] = []
  for frame in stack {
    path.push(if frame.object { Key(frame.key) } else { Index(frame.index) })
  }
  let frame = stack[stack.length() - 1]
  (path, frame.object && (frame.phase == LocKey || frame.phase == LocColon))
}

///| Find an editor cursor's structural path, including unfinished property slots.

///|
/// Empty Key("") denotes an object key slot; use is_property_key to distinguish it.
/// At a comma or closing bracket offset, the cursor is still before that token.
/// At a property's closing quote and in the gap before its colon, it remains a
/// key position. After the colon it is a value position. Unclosed containers at
/// EOF retain their current key/index slot. This single-prefix scan is linear
/// in source units plus tokens and stores one frame per open container.
pub fn get_location(
  doc : Document,
  offset : Int,
) -> Result[Location, EditError] {
  if offset < 0 || offset > doc.text.length() {
    return Err(InvalidEdit("cursor offset is outside the source"))
  }
  let mut line = 0
  let mut column = 0
  let mut i = 0
  while i < offset {
    let c = doc.text.code_unit_at(i)
    if c == 13 {
      line += 1
      column = 0
      if i + 1 < offset && doc.text.code_unit_at(i + 1) == 10 {
        i += 1
      }
    } else if c == 10 {
      line += 1
      column = 0
    } else {
      column += 1
    }
    i += 1
  }
  let (path, is_property_key) = location_prefix(doc, offset)
  Ok({ path, is_property_key, offset, line, column, })
}

///|
/// Encode a typed path as RFC 6901 JSON Pointer. Numeric object keys stay keys.
pub fn to_pointer(path : Array[PathSegment]) -> String {
  let mut result = ""
  for part in path {
    let value = match part {
      Key(s) => s
      Index(i) => i.to_string()
    }
    result += "/"
    for c in value {
      result += match c {
        '~' => "~0"
        '/' => "~1"
        _ => c.to_string()
      }
    }
  }
  result
}

///|
pub extend Location with Eq::{equal, not_equal}

///|
pub extend Location with @debug.Debug::{to_repr}