///|
/// A position counts Unicode scalar values, not UTF-16 units or graphemes.
pub(all) enum Position {
  Start(Int32)
  End(Int32)
} derive(Eq, Debug)

///|
/// ASCII run classes are deliberate; non-ASCII letters are not folded.
pub(all) enum RunKind {
  Digits
  Letters
} derive(Eq, Debug)

///|
/// Pure, finite-data expressions. No eval, I/O, regex execution or host calls.
pub(all) enum Expr {
  Field(Int32)
  Literal(String)
  Slice(Expr, Position, Position)
  Token(Expr, String, Int32)
  Run(Expr, RunKind, Int32)
  Trim(Expr)
  Lower(Expr)
  Upper(Expr)
  Concat(Array[Expr])
  DateText(Expr, String, String)
} derive(Eq, Debug)

///|
pub(all) suberror LoomError {
  LoomError(String)
} derive(Eq, Debug)

///|
pub(all) struct Example {
  input : Array[String]
  output : String
} derive(Eq, Debug)

///|
pub(all) struct Table {
  columns : Array[String]
  rows : Array[Array[String]]
} derive(Eq, Debug)

///|
fn char_slice(chars : Array[Char], start : Int32, end : Int32) -> String {
  String::from_array(chars[start:end])
}

///|
fn resolve(pos : Position, len : Int32) -> Int32 raise LoomError {
  let n = match pos {
    Start(n) | End(n) => n
  }
  if n < 0 || n > len {
    raise LoomError("slice position outside input")
  }
  match pos {
    Start(n) => n
    End(n) => len - n
  }
}

///|
fn select_part(parts : Array[String], index : Int32) -> String raise LoomError {
  let i = if index < 0 { parts.length() + index } else { index }
  if i < 0 || i >= parts.length() {
    raise LoomError("token or run index outside input")
  }
  parts[i]
}

///|
fn runs(text : String, kind : RunKind) -> Array[String] {
  let chars = text.to_array()
  let result : Array[String] = []
  let mut start = -1
  for i = 0; i <= chars.length(); i = i + 1 {
    let yes = i < chars.length() &&
      (match kind {
        Digits => chars[i].is_ascii_digit()
        Letters => chars[i].is_ascii_alphabetic()
      })
    if yes && start < 0 {
      start = i
    }
    if !yes && start >= 0 {
      result.push(char_slice(chars, start, i))
      start = -1
    }
  }
  result
}

///|
/// Evaluate one row. A malformed selector fails instead of silently padding.
pub fn evaluate(expr : Expr, row : Array[String]) -> String raise LoomError {
  check_tree(expr, 0, { used: 0, })
  eval_at(expr, row, 0)
}

///|
fn eval_at(
  expr : Expr,
  row : Array[String],
  depth : Int32,
) -> String raise LoomError {
  if depth > 16 {
    raise LoomError("expression depth exceeds 16")
  }
  let value = match expr {
    Field(i) => {
      if i < 0 || i >= row.length() {
        raise LoomError("missing input column")
      }
      row[i]
    }
    Literal(s) => s
    DateText(e, source, target) =>
      convert_date(eval_at(e, row, depth + 1), source, target)
    Slice(e, from, to) => {
      let chars = eval_at(e, row, depth + 1).to_array()
      let start = resolve(from, chars.length())
      let end = resolve(to, chars.length())
      if start > end {
        raise LoomError("slice start exceeds end")
      }
      char_slice(chars, start, end)
    }
    Token(e, separator, index) => {
      if separator.is_empty() {
        raise LoomError("empty separator")
      }
      select_part(
        eval_at(e, row, depth + 1)
        .split(separator)
        .map(v => v.to_owned())
        .collect(),
        index,
      )
    }
    Run(e, kind, index) =>
      select_part(runs(eval_at(e, row, depth + 1), kind), index)
    Trim(e) => eval_at(e, row, depth + 1).trim().to_owned()
    Lower(e) =>
      String::from_iter(
        eval_at(e, row, depth + 1).iter().map(c => c.to_ascii_lowercase()),
      )
    Upper(e) =>
      String::from_iter(
        eval_at(e, row, depth + 1).iter().map(c => c.to_ascii_uppercase()),
      )
    Concat(parts) => {
      if parts.length() > 32 {
        raise LoomError("concatenation exceeds 32 parts")
      }
      let out = StringBuilder()
      for part in parts {
        out.write_string(eval_at(part, row, depth + 1))
        if out.to_string().length() > 65536 {
          raise LoomError("output exceeds 65536 UTF-16 units")
        }
      }
      out.to_string()
    }
  }
  if value.length() > 65536 {
    raise LoomError("output exceeds 65536 UTF-16 units")
  }
  value
}

///|
/// A stable readable description; it is not executable host code.
pub fn explain(expr : Expr) -> String {
  match expr {
    Field(i) => "column[\{i}]"
    Literal(s) => Json(s).stringify()
    DateText(e, source, target) => "date(\{explain(e)}, \{source}, \{target})"
    Slice(e, a, b) =>
      "slice(\{explain(e)}, \{position_text(a)}, \{position_text(b)})"
    Token(e, s, i) => "token(\{explain(e)}, \{Json(s).stringify()}, \{i})"
    Run(e, k, i) => "run(\{explain(e)}, \{kind_text(k)}, \{i})"
    Trim(e) => "trim(\{explain(e)})"
    Lower(e) => "ascii_lower(\{explain(e)})"
    Upper(e) => "ascii_upper(\{explain(e)})"
    Concat(parts) => "concat(" + parts.map(explain).join(", ") + ")"
  }
}

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

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

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

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

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

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

///|
fn position_text(p : Position) -> String {
  match p {
    Start(n) => "start+\{n}"
    End(n) => "end-\{n}"
  }
}

///|
fn kind_text(k : RunKind) -> String {
  match k {
    Digits => "digits"
    Letters => "letters"
  }
}

///|
/// Convert a typed evaluation failure to a value for batch processing.
pub fn attempt(expr : Expr, row : Array[String]) -> Result[String, String] {
  Ok(evaluate(expr, row)) catch {
    LoomError(message) => Err(message)
  }
}

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

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

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

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

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

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