///|
let xlsx_cli_max_query_selector_chars : Int = 4096

///|
let xlsx_cli_max_query_predicates : Int = 16

///|
let xlsx_cli_max_query_value_chars : Int = 1024

///|
priv enum XlsxCellKind {
  Formula
  Number
  StringValue
  Boolean
  ErrorValue
}

///|
priv enum XlsxNumberOperator {
  Greater
  GreaterOrEqual
  Less
  LessOrEqual
  Equal
  NotEqual
}

///|
priv enum XlsxCellPredicate {
  HasType(XlsxCellKind)
  ValueCompare(XlsxNumberOperator, Double)
  HasFormula
  FormulaContains(String)
  TextEquals(String)
  TextContains(String)
}

///|
priv enum PreparedXlsxCellPredicate {
  PreparedHasType(XlsxCellKind)
  PreparedValueCompare(XlsxNumberOperator, Double)
  PreparedHasFormula
  PreparedFormulaContains(OfficeLinearPattern)
  PreparedTextEquals(String)
  PreparedTextContains(OfficeLinearPattern)
}

///|
priv struct XlsxQuerySpec {
  selector : String
  predicates : Array[XlsxCellPredicate]
  offset : Int
  limit : Int
}

///|
fn xlsx_query_failure(message : String, selector : String) -> CliFailure {
  xlsx_cli_failure(
    "office.xlsx.invalid_query",
    message,
    details=Json::object({
      "selector": Json::string(bounded_text(selector, 240)),
    }),
  )
}

///|
fn require_xlsx_query_number_value(
  label : String,
  value : String,
  selector : String,
) -> String raise CliFailure {
  let trimmed = value.trim().to_owned()
  if trimmed == "" {
    raise xlsx_query_failure("\{label} needs a non-empty value", selector)
  }
  if !has_at_most_chars(trimmed, xlsx_cli_max_query_value_chars) {
    raise xlsx_query_failure(
      "\{label} value exceeds \{xlsx_cli_max_query_value_chars} characters",
      selector,
    )
  }
  trimmed
}

///|
fn xlsx_query_quoted_literal_end(value : String) -> Int? {
  let mut escaped = false
  for index in 1.. escaped = true
        '"' => return Some(index)
        _ => ()
      }
    }
  }
  None
}

///|
fn xlsx_query_literal_has_valid_unicode(value : String) -> Bool {
  let mut offset = 0
  while offset < value.length() {
    let unit = value[offset]
    if unit.is_leading_surrogate() {
      if offset + 1 >= value.length() ||
        !value[offset + 1].is_trailing_surrogate() {
        return false
      }
      offset += 2
    } else if unit.is_trailing_surrogate() {
      return false
    } else {
      offset += 1
    }
  }
  true
}

///|
fn require_xlsx_query_literal(
  label : String,
  value : String,
  selector : String,
) -> String raise CliFailure {
  let decoded = if value.has_prefix("\"") {
    guard xlsx_query_quoted_literal_end(value) is Some(close) &&
      close == value.length() - 1 else {
      raise xlsx_query_failure(
        "\{label} quoted value must be one complete JSON string",
        selector,
      )
    }
    let parsed = @json.parse(value) catch {
      _ =>
        raise xlsx_query_failure(
          "\{label} quoted value contains an invalid JSON string escape",
          selector,
        )
    }
    guard parsed is String(text) else {
      raise xlsx_query_failure(
        "\{label} quoted value must be a JSON string",
        selector,
      )
    }
    text
  } else {
    value
  }
  if decoded == "" {
    raise xlsx_query_failure("\{label} needs a non-empty value", selector)
  }
  if !xlsx_query_literal_has_valid_unicode(decoded) {
    raise xlsx_query_failure(
      "\{label} value contains an isolated UTF-16 surrogate",
      selector,
    )
  }
  if !has_at_most_chars(decoded, xlsx_cli_max_query_value_chars) {
    raise xlsx_query_failure(
      "\{label} value exceeds \{xlsx_cli_max_query_value_chars} characters",
      selector,
    )
  }
  decoded
}

///|
fn parse_xlsx_number_predicate(
  rest : String,
  selector : String,
) -> XlsxCellPredicate raise CliFailure {
  let (operator, number_text) : (XlsxNumberOperator, String) = match rest {
    [.. ">=", .. tail] => (GreaterOrEqual, tail.to_owned())
    [.. "<=", .. tail] => (LessOrEqual, tail.to_owned())
    [.. "!=", .. tail] => (NotEqual, tail.to_owned())
    [.. ">", .. tail] => (Greater, tail.to_owned())
    [.. "<", .. tail] => (Less, tail.to_owned())
    [.. "=", .. tail] => (Equal, tail.to_owned())
    _ =>
      raise xlsx_query_failure(
        "a value predicate requires one of >, >=, <, <=, =, or !=", selector,
      )
  }
  let bounded = require_xlsx_query_number_value(
    "value comparison", number_text, selector,
  )
  let number = @string.parse_double(bounded) catch {
    _ =>
      raise xlsx_query_failure(
        "value predicate expects a finite number", selector,
      )
  }
  if number != number || number.is_inf() {
    raise xlsx_query_failure(
      "value predicate expects a finite number", selector,
    )
  }
  ValueCompare(operator, number)
}

///|
fn parse_xlsx_predicate(
  body : String,
  selector : String,
) -> XlsxCellPredicate raise CliFailure {
  match body {
    [.. "type=", .. kind] =>
      match kind {
        "formula" => HasType(Formula)
        "number" => HasType(Number)
        "string" => HasType(StringValue)
        "bool" => HasType(Boolean)
        "error" => HasType(ErrorValue)
        _ =>
          raise xlsx_query_failure(
            "type predicate expects formula, number, string, bool, or error", selector,
          )
      }
    [.. "value", .. tail] =>
      parse_xlsx_number_predicate(tail.to_owned(), selector)
    [.. "formula~=", .. tail] =>
      FormulaContains(
        require_xlsx_query_literal("formula~=", tail.to_owned(), selector),
      )
    "formula" => HasFormula
    [.. "text~=", .. tail] =>
      TextContains(
        require_xlsx_query_literal("text~=", tail.to_owned(), selector),
      )
    [.. "text=", .. tail] =>
      TextEquals(require_xlsx_query_literal("text=", tail.to_owned(), selector))
    _ => raise xlsx_query_failure("unknown XLSX cell predicate", selector)
  }
}

///|
/// Finds the terminator for one outer predicate group while permitting
/// balanced brackets in unquoted values and arbitrary brackets in JSON-string
/// values. The former supports normal Excel structured references such as
/// `Table1[[#Headers],[Amount]]`; the latter makes every exact string
/// representable without confusing it with selector structure.
fn xlsx_query_predicate_close(
  rest : String,
  selector : String,
) -> Int raise CliFailure {
  let mut depth = 0
  let mut in_string = false
  let mut escaped = false
  for index in 0.. escaped = true
          '"' => in_string = false
          _ => ()
        }
      }
    } else {
      match character {
        '"' => in_string = true
        '[' => depth += 1
        ']' => {
          depth -= 1
          if depth == 0 {
            return index
          }
          if depth < 0 {
            break
          }
        }
        _ => ()
      }
    }
  }
  raise xlsx_query_failure("query selector is missing a closing ']'", selector)
}

///|
fn parse_xlsx_query_selector(
  source : String,
) -> Array[XlsxCellPredicate] raise CliFailure {
  if !has_at_most_chars(source, xlsx_cli_max_query_selector_chars) {
    raise xlsx_query_failure(
      "query selector exceeds \{xlsx_cli_max_query_selector_chars} characters",
      source,
    )
  }
  let selector = source.trim().to_owned()
  if selector != "cell" && !selector.has_prefix("cell[") {
    raise xlsx_query_failure(
      "selector must be 'cell' followed by optional [predicate] groups", selector,
    )
  }
  let predicates : Array[XlsxCellPredicate] = []
  let mut rest = selector[4:].to_owned()
  while rest != "" {
    if predicates.length() >= xlsx_cli_max_query_predicates {
      raise xlsx_cli_failure(
        "office.xlsx.query_predicate_limit",
        "at most \{xlsx_cli_max_query_predicates} XLSX query predicates are allowed",
        details=Json::object({
          "limit": Json::number(xlsx_cli_max_query_predicates.to_double()),
        }),
      )
    }
    if !rest.has_prefix("[") {
      raise xlsx_query_failure("expected '[' in query selector", selector)
    }
    let close = xlsx_query_predicate_close(rest, selector)
    predicates.push(parse_xlsx_predicate(rest[1:close].to_owned(), selector))
    rest = rest[close + 1:].to_owned()
  }
  predicates
}

///|
fn XlsxNumberOperator::matches(
  self : XlsxNumberOperator,
  value : Double,
  bound : Double,
) -> Bool {
  match self {
    Greater => value > bound
    GreaterOrEqual => value >= bound
    Less => value < bound
    LessOrEqual => value <= bound
    Equal => value == bound
    NotEqual => value != bound
  }
}

///|
fn XlsxCellPredicate::prepare(
  self : XlsxCellPredicate,
  work : OfficeQueryWorkBudget,
) -> PreparedXlsxCellPredicate raise CliFailure {
  match self {
    HasType(kind) => PreparedHasType(kind)
    ValueCompare(operator, bound) => PreparedValueCompare(operator, bound)
    HasFormula => PreparedHasFormula
    FormulaContains(needle) =>
      PreparedFormulaContains(OfficeLinearPattern::compile(needle, false, work))
    TextEquals(expected) => PreparedTextEquals(expected)
    TextContains(needle) =>
      PreparedTextContains(OfficeLinearPattern::compile(needle, false, work))
  }
}

///|
fn prepare_xlsx_query_predicates(
  predicates : Array[XlsxCellPredicate],
  work : OfficeQueryWorkBudget,
) -> Array[PreparedXlsxCellPredicate] raise CliFailure {
  predicates.map(predicate => predicate.prepare(work))
}

///|
async fn PreparedXlsxCellPredicate::matches(
  self : PreparedXlsxCellPredicate,
  cell : XlsxCellSnapshot,
  work : OfficeQueryWorkBudget,
) -> Bool {
  match self {
    PreparedHasType(kind) => {
      work.charge(1)
      match kind {
        Formula => cell.formula is Some(_)
        Number => cell.raw is Some(Numeric(_))
        StringValue => cell.raw is Some(String(_))
        Boolean => cell.raw is Some(Bool(_))
        ErrorValue => cell.raw is Some(Error(_))
      }
    }
    PreparedValueCompare(operator, bound) => {
      work.charge(1)
      match cell.raw {
        Some(Numeric(value)) => operator.matches(value, bound)
        _ => false
      }
    }
    PreparedHasFormula => {
      work.charge(1)
      cell.formula is Some(_)
    }
    PreparedFormulaContains(pattern) =>
      match cell.formula {
        Some(formula) => pattern.is_in_cooperative(formula, work)
        None => {
          work.charge(1)
          false
        }
      }
    PreparedTextEquals(expected) =>
      match cell.raw {
        Some(String(value)) =>
          query_strings_equal_cooperative(value, expected, work)
        _ => {
          work.charge(1)
          false
        }
      }
    PreparedTextContains(pattern) =>
      match cell.raw {
        Some(String(value)) => pattern.is_in_cooperative(value, work)
        _ => {
          work.charge(1)
          false
        }
      }
  }
}

///|
async fn xlsx_cell_matches(
  cell : XlsxCellSnapshot,
  predicates : Array[PreparedXlsxCellPredicate],
  work : OfficeQueryWorkBudget,
) -> Bool {
  work.charge(1)
  if !cell.is_present() {
    return false
  }
  for predicate in predicates {
    if !predicate.matches(cell, work) {
      return false
    }
  }
  true
}