///|
/// Maximum canonical selector length in Unicode scalar values.
pub const SELECTOR_MAX_LENGTH : Int = 2048

///|
/// Maximum number of segments after the format root.
pub const SELECTOR_MAX_DEPTH : Int = 32

///|
/// Maximum number of bracketed selectors, including an XLSX coordinate leaf.
pub const SELECTOR_MAX_SELECTIONS : Int = 32

///|
/// Maximum segment or key name length.
pub const SELECTOR_MAX_NAME_LENGTH : Int = 32

///|
/// Maximum decoded named-selector value length in Unicode scalar values.
pub const SELECTOR_MAX_VALUE_LENGTH : Int = 256

///|
let selector_max_echo_length = 160

///|
let selector_max_message_length = 320

///|
let selector_max_index_digits = 9

///|
let selector_max_column = 16384

///|
let selector_max_row = 1048576

///|
priv enum ParsedSelectorSegment {
  Regular(SelectorSegment)
  Coordinate(SelectorCoordinate)
}

///|
priv enum SelectorUnicodeScan {
  Valid
  TooLong
  Invalid(Int)
}

///|
priv struct SelectorParser {
  source : String
  chars : Array[Char]
  mut position : Int
  mut selection_count : Int
}

///|
fn scan_selector_unicode(text : String, limit : Int) -> SelectorUnicodeScan {
  let unit_length = text.length()
  let mut unit_offset = 0
  let mut scalar_offset = 0
  while unit_offset < unit_length {
    let unit = text[unit_offset]
    if unit.is_leading_surrogate() {
      if unit_offset + 1 >= unit_length ||
        !text[unit_offset + 1].is_trailing_surrogate() {
        return Invalid(scalar_offset)
      }
      unit_offset = unit_offset + 2
    } else if unit.is_trailing_surrogate() {
      return Invalid(scalar_offset)
    } else {
      unit_offset = unit_offset + 1
    }
    scalar_offset = scalar_offset + 1
    if scalar_offset > limit {
      return TooLong
    }
  }
  Valid
}

///|
fn bounded_selector_text(text : String, limit : Int) -> String {
  let output = StringBuilder::new()
  let mut length = 0
  let mut truncated = false
  for character in text {
    if length == limit {
      truncated = true
      break
    }
    output.write_char(character) |> ignore
    length = length + 1
  }
  if truncated {
    output.write_char('…') |> ignore
  }
  output.to_string()
}

///|
fn SelectorParser::new(source : String) -> SelectorParser {
  { source, chars: source.to_array(), position: 0, selection_count: 0 }
}

///|
fn SelectorParser::at_end(self : SelectorParser) -> Bool {
  self.position >= self.chars.length()
}

///|
fn SelectorParser::peek(self : SelectorParser) -> Char? {
  if self.at_end() {
    None
  } else {
    Some(self.chars[self.position])
  }
}

///|
fn SelectorParser::advance(self : SelectorParser) -> Char? {
  let value = self.peek()
  if value is Some(_) {
    self.position = self.position + 1
  }
  value
}

///|
fn SelectorParser::fail_at(
  self : SelectorParser,
  code : String,
  offset : Int,
  message : String,
) -> SelectorError {
  SelectorError(
    code~,
    offset~,
    input=bounded_selector_text(self.source, selector_max_echo_length),
    message=bounded_selector_text(message, selector_max_message_length),
  )
}

///|
fn SelectorParser::fail(
  self : SelectorParser,
  code : String,
  message : String,
) -> SelectorError {
  self.fail_at(code, self.position, message)
}

///|
fn SelectorParser::expect(
  self : SelectorParser,
  expected : Char,
  code : String,
  message : String,
) -> Unit raise SelectorError {
  match self.peek() {
    Some(actual) if actual == expected => self.advance() |> ignore
    _ => raise self.fail(code, message)
  }
}

///|
fn selector_name_start(character : Char) -> Bool {
  character is ('a'..='z')
}

///|
fn selector_name_continue(character : Char) -> Bool {
  selector_name_start(character) ||
  character.is_ascii_digit() ||
  character == '-' ||
  character == '_'
}

///|
fn SelectorParser::parse_name(
  self : SelectorParser,
  role : String,
) -> String raise SelectorError {
  let start = self.position
  guard self.peek() is Some(first) && selector_name_start(first) else {
    raise self.fail(
      "office.selector.invalid_name",
      "expected a lowercase ASCII \{role} name",
    )
  }
  self.advance() |> ignore
  while self.peek() is Some(character) && selector_name_continue(character) {
    self.advance() |> ignore
  }
  let length = self.position - start
  if length > SELECTOR_MAX_NAME_LENGTH {
    raise self.fail_at(
      "office.selector.name_too_long",
      start,
      "\{role} name exceeds \{SELECTOR_MAX_NAME_LENGTH} characters",
    )
  }
  let output = StringBuilder::new()
  for index in start.. ignore
  }
  output.to_string()
}

///|
fn SelectorParser::count_selection(
  self : SelectorParser,
) -> Unit raise SelectorError {
  self.selection_count = self.selection_count + 1
  if self.selection_count > SELECTOR_MAX_SELECTIONS {
    raise self.fail(
      "office.selector.selection_limit",
      "selector has more than \{SELECTOR_MAX_SELECTIONS} bracketed selections",
    )
  }
}

///|
fn SelectorParser::parse_positive_index(
  self : SelectorParser,
  start : Int,
) -> Int raise SelectorError {
  let mut value = 0
  let mut digits = 0
  while self.peek() is Some(character) && character.is_ascii_digit() {
    digits = digits + 1
    if digits > selector_max_index_digits {
      raise self.fail_at(
        "office.selector.index_overflow",
        start,
        "positional index exceeds \{selector_max_index_digits} digits",
      )
    }
    value = value * 10 + (character.to_int() - '0'.to_int())
    self.advance() |> ignore
  }
  if digits == 0 {
    raise self.fail_at(
      "office.selector.invalid_index", start, "expected a positive positional index",
    )
  }
  if value == 0 {
    raise self.fail_at(
      "office.selector.zero_index", start, "positional selectors are 1-based",
    )
  }
  value
}

///|
fn SelectorParser::parse_json_string(
  self : SelectorParser,
) -> String raise SelectorError {
  let start = self.position
  guard self.peek() == Some('"') else {
    raise self.fail(
      "office.selector.unsupported_predicate", "named selectors require a JSON string value",
    )
  }
  let raw = StringBuilder::new()
  raw.write_char('"') |> ignore
  self.advance() |> ignore
  let mut closed = false
  while !self.at_end() {
    guard self.advance() is Some(character) else { break }
    raw.write_char(character) |> ignore
    if character == '\\' {
      guard self.advance() is Some(escaped) else {
        raise self.fail_at(
          "office.selector.invalid_string", start, "unterminated JSON escape in named selector",
        )
      }
      raw.write_char(escaped) |> ignore
    } else if character == '"' {
      closed = true
      break
    }
  }
  if !closed {
    raise self.fail_at(
      "office.selector.unclosed_string", start, "unterminated JSON string in named selector",
    )
  }
  let decoded = @json.parse(raw.to_string()) catch {
    _ =>
      raise self.fail_at(
        "office.selector.invalid_string", start, "invalid JSON string escape in named selector",
      )
  }
  guard decoded is String(value) else {
    raise self.fail_at(
      "office.selector.invalid_string", start, "named selector value must be a JSON string",
    )
  }
  match scan_selector_unicode(value, SELECTOR_MAX_VALUE_LENGTH) {
    Invalid(_) =>
      raise self.fail_at(
        "office.selector.invalid_string", start, "named selector value contains an isolated UTF-16 surrogate",
      )
    TooLong =>
      raise self.fail_at(
        "office.selector.value_too_long",
        start,
        "named selector value exceeds \{SELECTOR_MAX_VALUE_LENGTH} characters",
      )
    Valid => ()
  }
  if value == "" {
    raise self.fail_at(
      "office.selector.empty_value", start, "named selector values cannot be empty",
    )
  }
  value
}

///|
fn SelectorParser::parse_regular_selection(
  self : SelectorParser,
) -> SegmentSelection raise SelectorError {
  self.expect(
    '[', "office.selector.invalid_selection", "expected '[' to start a segment selection",
  )
  self.count_selection()
  let selection_start = self.position
  let selection = if self.peek() is Some(character) &&
    character.is_ascii_digit() {
    Position(self.parse_positive_index(selection_start))
  } else if self.peek() is Some(character) && selector_name_start(character) {
    let key = self.parse_name("selector key")
    self.expect(
      '=', "office.selector.unsupported_predicate", "only key=\"value\" named selectors are supported",
    )
    Key(key, self.parse_json_string())
  } else {
    raise self.fail(
      "office.selector.unsupported_predicate", "expected a positive index or key=\"value\" selection",
    )
  }
  self.expect(
    ']', "office.selector.unclosed_selection", "expected ']' after segment selection",
  )
  selection
}

///|
fn SelectorParser::parse_coordinate_token(
  self : SelectorParser,
) -> (String, Int) raise SelectorError {
  self.expect(
    '[', "office.selector.invalid_coordinate", "expected '[' before XLSX coordinate",
  )
  self.count_selection()
  let start = self.position
  let value = StringBuilder::new()
  while self.peek() is Some(character) && character != ']' {
    if character == '[' || character == '/' || character == '"' {
      raise self.fail(
        "office.selector.invalid_coordinate", "XLSX coordinates contain only A1 letters, digits, and ':'",
      )
    }
    value.write_char(character) |> ignore
    self.advance() |> ignore
  }
  if self.at_end() {
    raise self.fail_at(
      "office.selector.unclosed_selection", start, "expected ']' after XLSX coordinate",
    )
  }
  self.advance() |> ignore
  (value.to_string(), start)
}

///|
fn parse_a1_address(
  parser : SelectorParser,
  value : String,
  offset : Int,
) -> CellAddress raise SelectorError {
  let chars = value.to_array()
  let mut split = 0
  while split < chars.length() &&
        (chars[split] is ('A'..='Z') || chars[split] is ('a'..='z')) {
    split = split + 1
  }
  if split == 0 || split == chars.length() || split > 3 {
    raise parser.fail_at(
      "office.selector.invalid_coordinate",
      offset,
      "invalid A1 coordinate '\{bounded_selector_text(value, 32)}'",
    )
  }
  let mut column = 0
  for index in 0.. selector_max_column {
    raise parser.fail_at(
      "office.selector.invalid_coordinate", offset, "A1 column is outside A:XFD",
    )
  }
  if chars[split] == '0' {
    raise parser.fail_at(
      "office.selector.invalid_coordinate",
      offset + split,
      "A1 rows are 1-based and cannot have leading zeroes",
    )
  }
  let mut row = 0
  for index in split.. selector_max_row {
      raise parser.fail_at(
        "office.selector.invalid_coordinate",
        offset + split,
        "A1 row is outside 1:\{selector_max_row}",
      )
    }
  }
  { column, row }
}

///|
fn parse_xlsx_coordinate(
  parser : SelectorParser,
  name : String,
) -> SelectorCoordinate raise SelectorError {
  let (token, offset) = parser.parse_coordinate_token()
  if name == "cell" {
    if token.contains(":") {
      raise parser.fail_at(
        "office.selector.invalid_coordinate", offset, "cell selectors accept exactly one A1 coordinate",
      )
    }
    return Cell(parse_a1_address(parser, token, offset))
  }
  let parts = token.split(":").map(part => part.to_owned()).collect()
  if parts.length() != 2 || parts[0] == "" || parts[1] == "" {
    raise parser.fail_at(
      "office.selector.invalid_coordinate", offset, "range selectors require two A1 coordinates separated by ':'",
    )
  }
  let second_offset = offset + parts[0].to_array().length() + 1
  let first = parse_a1_address(parser, parts[0], offset)
  let second = parse_a1_address(parser, parts[1], second_offset)
  let start : CellAddress = {
    column: first.column.min(second.column),
    row: first.row.min(second.row),
  }
  let finish : CellAddress = {
    column: first.column.max(second.column),
    row: first.row.max(second.row),
  }
  Range(start, finish)
}

///|
fn SelectorParser::parse_segment(
  self : SelectorParser,
  format : DocumentFormat,
) -> ParsedSelectorSegment raise SelectorError {
  let name = self.parse_name("segment")
  if format == Xlsx && (name == "cell" || name == "range") {
    if self.peek() != Some('[') {
      raise self.fail(
        "office.selector.invalid_coordinate",
        "XLSX \{name} leaves require a bracketed A1 coordinate",
      )
    }
    return Coordinate(parse_xlsx_coordinate(self, name))
  }
  let selection = if self.peek() == Some('[') {
    Some(self.parse_regular_selection())
  } else {
    None
  }
  Regular({ name, selection })
}

///|
fn validate_docx_selector(
  parser : SelectorParser,
  segments : Array[SelectorSegment],
  coordinate : SelectorCoordinate?,
) -> SelectorStability raise SelectorError {
  if coordinate is Some(_) {
    raise parser.fail(
      "office.selector.format_mismatch", "DOCX selectors cannot contain XLSX coordinate leaves",
    )
  }
  guard segments.get(0) is Some(story) else {
    raise parser.fail(
      "office.selector.invalid_shape", "DOCX selectors require a story root such as body or header[1]",
    )
  }
  match story.name {
    "body" | "footnotes" | "endnotes" | "comments" =>
      if story.selection is Some(_) {
        raise parser.fail_at(
          "office.selector.invalid_shape",
          6,
          "DOCX story '\{story.name}' does not take a selector",
        )
      }
    "header" | "footer" =>
      if !(story.selection is Some(Position(_))) {
        raise parser.fail_at(
          "office.selector.invalid_shape",
          6,
          "DOCX story '\{story.name}' requires a 1-based positional selector",
        )
      }
    _ =>
      raise parser.fail_at(
        "office.selector.invalid_shape",
        6,
        "unknown DOCX story '\{story.name}'",
      )
  }
  let mut stability = Stable
  for index, segment in segments {
    if index > 0 && segment.selection is None {
      raise parser.fail(
        "office.selector.invalid_shape",
        "DOCX descendant '\{segment.name}' requires [index] or [id=\"value\"]",
      )
    }
    match segment.selection {
      Some(Position(_)) => stability = SnapshotRelative
      Some(Key(key, _)) if key != "id" =>
        raise parser.fail(
          "office.selector.unsupported_predicate",
          "DOCX named selectors use the 'id' key, not '\{key}'",
        )
      _ => ()
    }
  }
  stability
}

///|
fn validate_xlsx_selector(
  parser : SelectorParser,
  segments : Array[SelectorSegment],
  coordinate : SelectorCoordinate?,
) -> SelectorStability raise SelectorError {
  if segments.length() == 1 && segments[0].name == "workbook" {
    if segments[0].selection is Some(_) || coordinate is Some(_) {
      raise parser.fail(
        "office.selector.invalid_shape", "XLSX workbook is a singleton and cannot take a selection or coordinate leaf",
      )
    }
    return Stable
  }
  if segments.length() != 1 || segments[0].name != "sheet" {
    raise parser.fail(
      "office.selector.invalid_shape", "XLSX selectors require workbook or exactly one sheet segment before an optional cell/range leaf",
    )
  }
  match segments[0].selection {
    Some(Position(_)) => SnapshotRelative
    Some(Key("name", _)) if coordinate is None => Stable
    Some(Key("name", _)) => SnapshotRelative
    Some(Key(key, _)) =>
      raise parser.fail(
        "office.selector.unsupported_predicate",
        "XLSX sheet selectors use the 'name' key, not '\{key}'",
      )
    None =>
      raise parser.fail(
        "office.selector.invalid_shape", "XLSX sheet requires [index] or [name=\"value\"]",
      )
  }
}

///|
/// Parses and validates one bounded, format-explicit `office.selector/1`
/// address. The returned AST is the adapter boundary for later resolvers.
pub fn parse_selector(source : String) -> OfficeSelector raise SelectorError {
  match scan_selector_unicode(source, SELECTOR_MAX_LENGTH) {
    Invalid(offset) =>
      raise SelectorError(
        code="office.selector.invalid_unicode",
        offset~,
        input="",
        message="selector contains an isolated UTF-16 surrogate",
      )
    TooLong =>
      raise SelectorError(
        code="office.selector.too_long",
        offset=SELECTOR_MAX_LENGTH,
        input=bounded_selector_text(source, selector_max_echo_length),
        message="selector exceeds \{SELECTOR_MAX_LENGTH} characters",
      )
    Valid => ()
  }
  let parser = SelectorParser::new(source)
  parser.expect(
    '/', "office.selector.invalid_root", "selector must start with '/docx/' or '/xlsx/'",
  )
  let format_offset = parser.position
  let format_name = parser.parse_name("format")
  let format = match format_name {
    "docx" => Docx
    "xlsx" => Xlsx
    _ =>
      raise parser.fail_at(
        "office.selector.invalid_format",
        format_offset,
        "unsupported selector format '\{format_name}' (expected docx or xlsx)",
      )
  }
  parser.expect(
    '/', "office.selector.invalid_root", "format root must be followed by '/' and a segment",
  )
  if parser.at_end() || parser.peek() == Some('/') {
    raise parser.fail(
      "office.selector.empty_segment", "selector contains an empty segment",
    )
  }
  let segments : Array[SelectorSegment] = []
  let mut coordinate : SelectorCoordinate? = None
  let mut depth = 0
  while !parser.at_end() {
    let start = parser.position
    depth = depth + 1
    if depth > SELECTOR_MAX_DEPTH {
      raise parser.fail_at(
        "office.selector.depth_limit",
        start,
        "selector exceeds \{SELECTOR_MAX_DEPTH} segments",
      )
    }
    match parser.parse_segment(format) {
      Regular(segment) => {
        if coordinate is Some(_) {
          raise parser.fail_at(
            "office.selector.invalid_shape", start, "XLSX coordinate leaves must be final",
          )
        }
        segments.push(segment)
      }
      Coordinate(value) => {
        if coordinate is Some(_) {
          raise parser.fail_at(
            "office.selector.invalid_shape", start, "selector contains more than one coordinate leaf",
          )
        }
        coordinate = Some(value)
      }
    }
    if parser.at_end() {
      break
    }
    parser.expect(
      '/', "office.selector.unexpected_character", "expected '/' between selector segments",
    )
    if parser.at_end() || parser.peek() == Some('/') {
      raise parser.fail(
        "office.selector.empty_segment", "selector contains an empty segment",
      )
    }
  }
  let stability = match format {
    Docx => validate_docx_selector(parser, segments, coordinate)
    Xlsx => validate_xlsx_selector(parser, segments, coordinate)
  }
  {
    format,
    segments: ReadOnlyArray::from_array(segments),
    coordinate,
    stability,
  }
}