// Copyright 2026 moonpddl contributors
//
// SPDX-License-Identifier: MIT

///|
/// A name collected while reading a typed list.
priv struct PendingName {
  name : String
  mut types : Array[String]
}

///|
fn sexp_pos(node : Sexp) -> Position {
  match node {
    SAtom(_, pos) => pos
    SVar(_, pos) => pos
    SList(_, pos) => pos
  }
}

///|
fn sexp_label(node : Sexp) -> String {
  match node {
    SAtom(text, _) => "'\{text}'"
    SVar(text, _) => "'?\{text}'"
    SList(_, _) => "'('"
  }
}

///|
fn expect_atom(
  node : Sexp,
  what : String,
) -> (String, Position) raise PddlError {
  match node {
    SAtom(text, pos) => (text, pos)
    _ =>
      raise syntax_error(
        sexp_pos(node),
        "expected \{what}, found \{sexp_label(node)}",
      )
  }
}

///|
fn expect_list(
  node : Sexp,
  what : String,
) -> (Array[Sexp], Position) raise PddlError {
  match node {
    SList(items, pos) => (items, pos)
    _ =>
      raise syntax_error(
        sexp_pos(node),
        "expected \{what}, found \{sexp_label(node)}",
      )
  }
}

///|
fn check_duplicates(
  entries : Array[TypedName],
  what : String,
  pos : Position,
) -> Unit raise PddlError {
  let seen : Map[String, Bool] = Map([])
  for entry in entries {
    if seen.contains(entry.name) {
      raise syntax_error(pos, "duplicate \{what} '\{entry.name}'")
    }
    seen.set(entry.name, true)
  }
}

///|
/// Parses a PDDL type annotation after `-`. Returns the type names and the
/// index of the first item after the annotation. A single atom is one type;
/// `(either a b ...)` is a union of types.
fn parse_type_annotation(
  items : ArrayView[Sexp],
  dash_index : Int32,
  context : String,
) -> (Array[String], Int32) raise PddlError {
  let pos = sexp_pos(items[dash_index])
  if dash_index + 1 >= items.length() {
    raise syntax_error(pos, "expected a type name after '-'")
  }
  match items[dash_index + 1] {
    SList(inner, inner_pos) => {
      if inner.length() == 0 {
        raise syntax_error(inner_pos, "empty '(either ...)' in \{context}")
      }
      let (head, _) = expect_atom(inner[0], "'either'")
      if head != "either" {
        raise syntax_error(
          inner_pos,
          "expected a type name or '(either ...)' after '-', found a list starting with '\{head}'",
        )
      }
      if inner.length() < 2 {
        raise syntax_error(
          inner_pos,
          "'(either ...)' needs at least one type in \{context}",
        )
      }
      let types : Array[String] = []
      for i in 1..
      raise syntax_error(
        var_pos,
        "expected a type name after '-', found variable '?\{name}'",
      )
    SAtom(text, atom_pos) => {
      if text.get_char(0) == Some(':') {
        raise syntax_error(atom_pos, "invalid type name '\{text}'")
      }
      ([text], dash_index + 2)
    }
  }
}

///|
/// Parses a PDDL typed list. When `vars` is true the entries must be
/// variables (`?x`); otherwise they must be plain names.
fn parse_typed_list(
  items : ArrayView[Sexp],
  vars : Bool,
  default_type : String,
  context : String,
) -> Array[TypedName] raise PddlError {
  let result : Array[TypedName] = []
  let pending : Array[PendingName] = []
  let mut i = 0
  while i < items.length() {
    match items[i] {
      SAtom(name, pos) =>
        if name == "-" {
          if pending.length() == 0 {
            raise syntax_error(pos, "unexpected '-' in \{context}")
          }
          let (types, next) = parse_type_annotation(items, i, context)
          for entry in pending {
            result.push({ name: entry.name, types: types.copy(), })
          }
          pending.clear()
          i = next
        } else {
          if vars {
            raise syntax_error(
              pos,
              "expected a variable in \{context}, found '\{name}'",
            )
          }
          pending.push({ name, types: [], })
          i += 1
        }
      SVar(name, pos) => {
        if !vars {
          raise syntax_error(
            pos,
            "expected a name in \{context}, found variable '?\{name}'",
          )
        }
        pending.push({ name, types: [], })
        i += 1
      }
      SList(_, pos) =>
        raise syntax_error(pos, "expected a name in \{context}, found '('")
    }
  }
  for entry in pending {
    result.push({ name: entry.name, types: [default_type], })
  }
  result
}

///|
fn supported_requirement(req : String) -> Bool {
  let supported = [
    ":strips", ":typing", ":negative-preconditions", ":disjunctive-preconditions",
    ":equality", ":existential-preconditions", ":universal-preconditions", ":quantified-preconditions",
    ":conditional-effects", ":fluents", ":action-costs", ":adl",
  ]
  supported.contains(req)
}

///|
fn parse_requirements(items : ArrayView[Sexp]) -> Array[String] raise PddlError {
  let result : Array[String] = []
  for item in items {
    let (req, pos) = expect_atom(item, "a requirement keyword")
    if req.get_char(0) != Some(':') {
      raise syntax_error(
        pos,
        "expected a requirement keyword starting with ':', found '\{req}'",
      )
    }
    if !supported_requirement(req) {
      raise unsupported_error(pos, "requirement '\{req}'")
    }
    result.push(req)
  }
  result
}

///|
fn is_reserved_name(name : String) -> Bool {
  let reserved = [
    "and", "or", "not", "imply", "exists", "forall", "either", "=", "<", "<=", ">",
    ">=", "when", "increase", "decrease", "assign", "scale-up", "scale-down",
  ]
  reserved.contains(name)
}

///|
fn push_type_decl(
  result : Array[TypeDecl],
  seen : Map[String, Bool],
  pos : Position,
  name : String,
  parents : Array[String],
) -> Unit raise PddlError {
  if name == "object" {
    raise syntax_error(
      pos, "type 'object' is predefined and cannot be redeclared",
    )
  }
  if name.get_char(0) == Some(':') {
    raise syntax_error(pos, "invalid type name '\{name}'")
  }
  if seen.contains(name) {
    raise syntax_error(pos, "duplicate type '\{name}'")
  }
  seen.set(name, true)
  result.push({
    name,
    parents: if parents.length() > 0 {
      parents.copy()
    } else {
      ["object"]
    },
  })
}

///|
fn parse_type_section(
  items : ArrayView[Sexp],
  pos : Position,
) -> Array[TypeDecl] raise PddlError {
  let result : Array[TypeDecl] = []
  let seen : Map[String, Bool] = Map([])
  let pending : Array[PendingName] = []
  let mut i = 0
  while i < items.length() {
    match items[i] {
      SAtom(name, type_pos) =>
        if name == "-" {
          if pending.length() == 0 {
            raise syntax_error(type_pos, "unexpected '-' in :types")
          }
          let (parents, next) = parse_type_annotation(items, i, ":types")
          for entry in pending {
            entry.types = parents.copy()
          }
          i = next
          for entry in pending {
            push_type_decl(result, seen, pos, entry.name, entry.types)
          }
          pending.clear()
        } else {
          if name.get_char(0) == Some(':') {
            raise syntax_error(type_pos, "invalid type name '\{name}'")
          }
          pending.push({ name, types: [], })
          i += 1
        }
      SVar(name, var_pos) =>
        raise syntax_error(
          var_pos,
          "expected a type name in :types, found variable '?\{name}'",
        )
      SList(_, list_pos) =>
        raise syntax_error(
          list_pos, "expected a type name in :types, found '('",
        )
    }
  }
  for entry in pending {
    push_type_decl(result, seen, pos, entry.name, entry.types)
  }
  let known : Map[String, Bool] = Map([])
  known.set("object", true)
  for decl in result {
    known.set(decl.name, true)
  }
  for decl in result {
    for parent in decl.parents {
      if !known.contains(parent) {
        raise syntax_error(
          pos,
          "unknown supertype '\{parent}' for type '\{decl.name}'",
        )
      }
    }
  }
  let parents : Map[String, Array[String]] = Map([])
  for decl in result {
    parents.set(decl.name, decl.parents)
  }
  for decl in result {
    let queue = decl.parents.copy()
    let visited : Map[String, Bool] = Map([])
    let mut steps = 0
    while queue.length() > 0 && steps <= result.length() + 1 {
      let current = queue.remove(0)
      steps += 1
      if current == decl.name {
        raise syntax_error(
          pos,
          "cyclic type hierarchy involving '\{decl.name}'",
        )
      }
      if visited.contains(current) {
        continue
      }
      visited.set(current, true)
      match parents.get(current) {
        Some(next) =>
          for p in next {
            queue.push(p)
          }
        None => ()
      }
    }
    if steps > result.length() + 1 {
      raise syntax_error(pos, "cyclic type hierarchy involving '\{decl.name}'")
    }
  }
  result
}

///|
fn parse_predicates(
  items : ArrayView[Sexp],
) -> Array[Predicate] raise PddlError {
  let result : Array[Predicate] = []
  let seen : Map[String, Bool] = Map([])
  for item in items {
    let (decl, decl_pos) = expect_list(item, "a predicate declaration")
    if decl.length() == 0 {
      raise syntax_error(decl_pos, "empty predicate declaration")
    }
    let (name, name_pos) = expect_atom(decl[0], "a predicate name")
    if is_reserved_name(name) {
      raise syntax_error(
        name_pos,
        "'\{name}' is reserved and cannot be used as a predicate name",
      )
    }
    if name.get_char(0) == Some(':') {
      raise syntax_error(name_pos, "invalid predicate name '\{name}'")
    }
    if seen.contains(name) {
      raise syntax_error(name_pos, "duplicate predicate '\{name}'")
    }
    seen.set(name, true)
    let params = parse_typed_list(
      decl[1:],
      true,
      "object",
      "predicate '\{name}'",
    )
    check_duplicates(params, "parameter", decl_pos)
    result.push({ name, params, })
  }
  result
}

///|
/// Parses the `:functions` section. Every function returns `number`;
/// the return type annotation is optional and, when present, must be
/// `number` (PDDL `object` fluents are outside the endpoint).
fn parse_functions(
  items : ArrayView[Sexp],
  section_pos : Position,
) -> Array[FunctionDecl] raise PddlError {
  let result : Array[FunctionDecl] = []
  let seen : Map[String, Bool] = Map([])
  let mut i = 0
  while i < items.length() {
    match items[i] {
      SList(decl, decl_pos) => {
        if decl.length() == 0 {
          raise syntax_error(decl_pos, "empty function declaration")
        }
        let (name, name_pos) = expect_atom(decl[0], "a function name")
        if is_reserved_name(name) {
          raise syntax_error(
            name_pos,
            "'\{name}' is reserved and cannot be used as a function name",
          )
        }
        if name.get_char(0) == Some(':') {
          raise syntax_error(name_pos, "invalid function name '\{name}'")
        }
        if seen.contains(name) {
          raise syntax_error(name_pos, "duplicate function '\{name}'")
        }
        seen.set(name, true)
        let params = parse_typed_list(
          decl[1:],
          true,
          "object",
          "function '\{name}'",
        )
        check_duplicates(params, "parameter", decl_pos)
        if name == "total-cost" && params.length() > 0 {
          raise syntax_error(decl_pos, "total-cost must be declared 0-ary")
        }
        result.push({ name, params, })
        i += 1
      }
      SAtom(text, atom_pos) =>
        if text == "-" {
          if result.length() == 0 {
            raise syntax_error(section_pos, "empty ':functions' section")
          }
          if i + 1 >= items.length() {
            raise syntax_error(atom_pos, "expected 'number' after '-'")
          }
          let (ty, ty_pos) = expect_atom(items[i + 1], "'number'")
          if ty != "number" {
            raise unsupported_error(
              ty_pos,
              "function type '\{ty}'; functions must return number (object fluents are not supported)",
            )
          }
          i += 2
        } else {
          raise syntax_error(
            atom_pos,
            "expected a function declaration, found '\{text}'",
          )
        }
      SVar(name, var_pos) =>
        raise syntax_error(
          var_pos,
          "expected a function declaration, found variable '?\{name}'",
        )
    }
  }
  if result.length() == 0 {
    raise syntax_error(section_pos, "empty ':functions' section")
  }
  result
}

///|
fn parse_action(
  section : ArrayView[Sexp],
  pos : Position,
) -> Action raise PddlError {
  if section.length() < 2 {
    raise syntax_error(pos, "expected an action name after ':action'")
  }
  let (name, name_pos) = expect_atom(section[1], "an action name")
  if name.get_char(0) == Some(':') {
    raise syntax_error(
      name_pos,
      "expected an action name, found keyword '\{name}'",
    )
  }
  let mut params : Array[TypedName] = []
  let mut precondition : Formula = And([])
  let mut effect : Effect = AndEffect([])
  let seen : Map[String, Bool] = Map([])
  let mut i = 2
  while i < section.length() {
    let (key, key_pos) = expect_atom(section[i], "an action keyword")
    if i + 1 >= section.length() {
      raise syntax_error(
        key_pos,
        "missing value for '\{key}' in action '\{name}'",
      )
    }
    if seen.contains(key) {
      raise syntax_error(key_pos, "duplicate '\{key}' in action '\{name}'")
    }
    seen.set(key, true)
    if key == ":parameters" {
      let (value, value_pos) = expect_list(section[i + 1], "a parameter list")
      let parsed = parse_typed_list(
        value,
        true,
        "object",
        "action '\{name}' parameters",
      )
      check_duplicates(parsed, "parameter", value_pos)
      params = parsed
    } else if key == ":precondition" {
      precondition = parse_formula(section[i + 1])
    } else if key == ":effect" {
      effect = parse_effect(section[i + 1])
    } else {
      raise unsupported_error(key_pos, "action keyword '\{key}'")
    }
    i += 2
  }
  { name, params, precondition, effect, }
}

///|
/// Parses a PDDL domain.
pub fn parse_domain(source : String) -> Domain raise PddlError {
  let forms = parse_sexps(lex(source))
  if forms.length() == 0 {
    raise syntax_error(
      { line: 1, column: 1, },
      "empty input; expected a (define (domain ...)) form",
    )
  }
  if forms.length() > 1 {
    raise syntax_error(
      sexp_pos(forms[1]),
      "expected exactly one (define ...) form",
    )
  }
  let (top, top_pos) = expect_list(forms[0], "a (define ...) form")
  if top.length() < 2 {
    raise syntax_error(top_pos, "expected (define (domain ) ...)")
  }
  let (head, head_pos) = expect_atom(top[0], "'define'")
  if head != "define" {
    raise syntax_error(head_pos, "expected 'define', found '\{head}'")
  }
  let (title, title_pos) = expect_list(top[1], "a (domain ) form")
  if title.length() != 2 {
    raise syntax_error(title_pos, "expected (domain )")
  }
  let (kind, kind_pos) = expect_atom(title[0], "'domain'")
  if kind != "domain" {
    raise syntax_error(kind_pos, "expected 'domain', found '\{kind}'")
  }
  let (name, _) = expect_atom(title[1], "a domain name")
  let mut requirements : Array[String] = []
  let mut types : Array[TypeDecl] = []
  let mut constants : Array[TypedName] = []
  let mut predicates : Array[Predicate] = []
  let mut functions : Array[FunctionDecl] = []
  let actions : Array[Action] = []
  let seen_sections : Map[String, Bool] = Map([])
  for idx in 2.. PAtom raise PddlError {
  let atom = parse_atom(items, pos, "init")
  for arg in atom.args {
    match arg {
      Var(name) =>
        raise syntax_error(pos, "variable '?\{name}' is not allowed in :init")
      Const(_) => ()
    }
  }
  atom
}

///|
fn parse_term(node : Sexp, context : String) -> Term raise PddlError {
  match node {
    SAtom(name, _) => Const(name)
    SVar(name, _) => Var(name)
    SList(_, pos) =>
      raise syntax_error(
        pos,
        "expected an object or variable in \{context}, found '('",
      )
  }
}

///|
fn parse_atom(
  items : ArrayView[Sexp],
  pos : Position,
  context : String,
) -> PAtom raise PddlError {
  if items.length() == 0 {
    raise syntax_error(pos, "empty predicate application")
  }
  let (name, name_pos) = expect_atom(items[0], "a predicate name")
  if name.get_char(0) == Some(':') {
    raise syntax_error(
      name_pos,
      "expected a predicate name, found keyword '\{name}'",
    )
  }
  let args : Array[Term] = []
  for i in 1.. Double? {
  let chars = text.to_array()
  if chars.length() == 0 {
    return None
  }
  let mut index = 0
  let mut negative = false
  if chars[0] == '-' {
    negative = true
    index = 1
    if chars.length() == 1 {
      return None
    }
  }
  let int_digits : Array[Char] = []
  while index < chars.length() && chars[index] >= '0' && chars[index] <= '9' {
    int_digits.push(chars[index])
    index += 1
  }
  if int_digits.length() == 0 {
    return None
  }
  if int_digits.length() > 15 {
    return None
  }
  let frac_digits : Array[Char] = []
  if index < chars.length() && chars[index] == '.' {
    index += 1
    while index < chars.length() && chars[index] >= '0' && chars[index] <= '9' {
      frac_digits.push(chars[index])
      index += 1
    }
    if frac_digits.length() == 0 {
      return None
    }
    if frac_digits.length() > 12 {
      return None
    }
  }
  if index != chars.length() {
    return None
  }
  let mut int_part = 0L
  for c in int_digits {
    int_part = int_part * 10L + Int64::from_int(c.to_int() - 48)
  }
  let mut frac_part = 0L
  for c in frac_digits {
    frac_part = frac_part * 10L + Int64::from_int(c.to_int() - 48)
  }
  let mut scale = 1.0
  let mut digit_index = 0
  while digit_index < frac_digits.length() {
    scale = scale * 10.0
    digit_index += 1
  }
  let mut value = int_part.to_double() + frac_part.to_double() / scale
  if negative {
    value = -value
  }
  Some(value)
}

///|
fn is_numeric_literal(node : Sexp) -> Bool {
  match node {
    SAtom(text, _) => parse_number(text) is Some(_)
    _ => false
  }
}

///|
/// Upper bound on formula, effect and numeric-expression nesting depth.
/// The S-expression reader already bounds nesting at 1024, but semantic
/// recursion frames are larger, so a legal S-expression deep enough could
/// overflow the native stack with no diagnostic. 256 is far beyond any
/// real-world model and safely below the observed crash threshold.
const MAX_FORMULA_DEPTH : Int32 = 256

///|
fn formula_depth_error(pos : Position) -> PddlError {
  syntax_error(pos, "formula nesting exceeds the limit of \{MAX_FORMULA_DEPTH}")
}

///|
/// True when the operand must be read as a numeric expression: any list
/// (function application or arithmetic) or a numeric literal.
fn is_numeric_operand(node : Sexp) -> Bool {
  match node {
    SList(_, _) => true
    other => is_numeric_literal(other)
  }
}

///|
/// Parses a PDDL 2.1 `` numeric expression.
fn parse_num_expr(node : Sexp) -> NumExpr raise PddlError {
  parse_num_expr_at(node, 0)
}

///|
fn parse_num_expr_at(node : Sexp, depth : Int32) -> NumExpr raise PddlError {
  if depth >= MAX_FORMULA_DEPTH {
    raise formula_depth_error(sexp_pos(node))
  }
  match node {
    SAtom(text, _) =>
      match parse_number(text) {
        Some(value) => NumLit(value)
        None => NumFunc({ name: text, args: [], })
      }
    SVar(name, pos) =>
      raise syntax_error(
        pos,
        "variable '?\{name}' cannot appear in a numeric expression",
      )
    SList(items, pos) => {
      if items.length() == 0 {
        raise syntax_error(pos, "empty numeric expression")
      }
      let (head, head_pos) = expect_atom(
        items[0],
        "a numeric operator or function name",
      )
      if head == "+" || head == "*" {
        if items.length() < 3 {
          raise syntax_error(
            head_pos,
            "'\{head}' needs at least two operands in a numeric expression",
          )
        }
        let mut acc = parse_num_expr_at(items[1], depth + 1)
        for i in 2..= 3 {
          let mut acc = parse_num_expr_at(items[1], depth + 1)
          for i in 2.." ||
        head == ">=" {
        raise syntax_error(
          head_pos,
          "comparison '\{head}' cannot appear inside a numeric expression",
        )
      } else {
        NumFunc(parse_atom(items, pos, "numeric expression"))
      }
    }
  }
}

///|
fn compare_op_of(head : String) -> CompareOp {
  match head {
    "<" => CmpLT
    "<=" => CmpLE
    ">" => CmpGT
    ">=" => CmpGE
    _ => CmpEQ
  }
}

///|
fn parse_formula(node : Sexp) -> Formula raise PddlError {
  parse_formula_at(node, 0)
}

///|
fn parse_formula_at(node : Sexp, depth : Int32) -> Formula raise PddlError {
  if depth >= MAX_FORMULA_DEPTH {
    raise formula_depth_error(sexp_pos(node))
  }
  match node {
    SAtom(text, pos) =>
      raise syntax_error(pos, "expected a formula list, found '\{text}'")
    SVar(text, pos) =>
      raise syntax_error(
        pos,
        "expected a formula list, found variable '?\{text}'",
      )
    SList(items, pos) => {
      if items.length() == 0 {
        return And([])
      }
      let (head, head_pos) = expect_atom(items[0], "a formula operator")
      if head == "and" || head == "or" {
        let children : Array[Formula] = []
        for i in 1.." ||
        head == ">=" {
        if items.length() != 3 {
          raise syntax_error(
            head_pos,
            "'\{head}' expects exactly two expressions",
          )
        }
        Compare(
          compare_op_of(head),
          parse_num_expr(items[1]),
          parse_num_expr(items[2]),
        )
      } else if head == "when" ||
        head == "increase" ||
        head == "decrease" ||
        head == "assign" ||
        head == "scale-up" ||
        head == "scale-down" {
        raise syntax_error(head_pos, "'\{head}' is only allowed in effects")
      } else if head.get_char(0) == Some(':') {
        raise unsupported_error(head_pos, "formula operator '\{head}'")
      } else {
        Atom(parse_atom(items, pos, "formula"))
      }
    }
  }
}

///|
fn parse_numeric_effect(
  items : Array[Sexp],
  head : String,
  pos : Position,
) -> Effect raise PddlError {
  if items.length() != 3 {
    raise syntax_error(
      pos,
      "'\{head}' expects a function term and an expression",
    )
  }
  let fluent : PAtom = match items[1] {
    SAtom(name, _) => { name, args: [], }
    SList(inner, inner_pos) => parse_atom(inner, inner_pos, "numeric effect")
    SVar(name, var_pos) =>
      raise syntax_error(
        var_pos,
        "expected a function term, found variable '?\{name}'",
      )
  }
  let op = match head {
    "increase" => OpIncrease
    "decrease" => OpDecrease
    "assign" => OpAssign
    "scale-up" => OpScaleUp
    _ => OpScaleDown
  }
  NumEffect(op, fluent, parse_num_expr(items[2]))
}

///|
fn parse_effect(node : Sexp) -> Effect raise PddlError {
  parse_effect_at(node, 0)
}

///|
fn parse_effect_at(node : Sexp, depth : Int32) -> Effect raise PddlError {
  if depth >= MAX_FORMULA_DEPTH {
    raise formula_depth_error(sexp_pos(node))
  }
  match node {
    SAtom(text, pos) =>
      raise syntax_error(pos, "expected an effect list, found '\{text}'")
    SVar(text, pos) =>
      raise syntax_error(
        pos,
        "expected an effect list, found variable '?\{text}'",
      )
    SList(items, pos) => {
      if items.length() == 0 {
        return AndEffect([])
      }
      let (head, head_pos) = expect_atom(items[0], "an effect operator")
      if head == "and" {
        let children : Array[Effect] = []
        for i in 1.." || head == ">=" {
        raise syntax_error(
          head_pos,
          "comparison '\{head}' is not allowed in effects",
        )
      } else if head.get_char(0) == Some(':') {
        raise unsupported_error(head_pos, "effect operator '\{head}'")
      } else {
        AddEffect(parse_atom(items, pos, "effect"))
      }
    }
  }
}

///|
fn parse_init_func_app(node : Sexp, context : String) -> PAtom raise PddlError {
  match node {
    SAtom(name, pos) => {
      if name.get_char(0) == Some(':') {
        raise syntax_error(
          pos,
          "expected a function name, found keyword '\{name}'",
        )
      }
      { name, args: [], }
    }
    SList(inner, pos) => {
      let atom = parse_atom(inner, pos, context)
      for arg in atom.args {
        match arg {
          Var(name) =>
            raise syntax_error(
              pos,
              "variable '?\{name}' is not allowed in \{context}",
            )
          Const(_) => ()
        }
      }
      atom
    }
    SVar(name, var_pos) =>
      raise syntax_error(
        var_pos,
        "expected a function term in \{context}, found variable '?\{name}'",
      )
  }
}

///|
fn parse_init_elem(node : Sexp) -> InitElem raise PddlError {
  let (items, pos) = expect_list(node, "an init element")
  if items.length() == 0 {
    raise syntax_error(pos, "empty init element")
  }
  let (head, head_pos) = expect_atom(items[0], "a predicate name")
  if head == "=" {
    if items.length() != 3 {
      raise syntax_error(pos, "expected (= ( ...) )")
    }
    let fluent = parse_init_func_app(items[1], ":init")
    let (value_text, value_pos) = expect_atom(items[2], "a number")
    match parse_number(value_text) {
      Some(value) => InitFunc(fluent, value)
      None =>
        raise syntax_error(
          value_pos,
          "expected a number, found '\{value_text}'",
        )
    }
  } else if head == "not" {
    raise unsupported_error(
      head_pos, "negated init literals; PDDL :init is a positive closed-world state",
    )
  } else if head == "increase" ||
    head == "decrease" ||
    head == "assign" ||
    head == "scale-up" ||
    head == "scale-down" {
    raise syntax_error(
      head_pos, "numeric effects are not allowed in :init; assign a value with (= ( ...) )",
    )
  } else if head.get_char(0) == Some(':') {
    raise syntax_error(
      head_pos,
      "expected a predicate name, found keyword '\{head}'",
    )
  } else {
    InitFact(parse_ground_atom(items, pos))
  }
}

///|
fn parse_metric(
  items : ArrayView[Sexp],
  pos : Position,
) -> Metric raise PddlError {
  if items.length() != 2 {
    raise syntax_error(pos, "':metric' expects a direction and an expression")
  }
  let (direction, direction_pos) = expect_atom(
    items[0],
    "'minimize' or 'maximize'",
  )
  let dir = match direction {
    "minimize" => Minimize
    "maximize" => Maximize
    _ =>
      raise unsupported_error(
        direction_pos,
        "metric direction '\{direction}'; only minimize and maximize are supported",
      )
  }
  Metric(dir, parse_num_expr(items[1]))
}

///|
/// Parses a PDDL problem instance.
pub fn parse_problem(source : String) -> Problem raise PddlError {
  let forms = parse_sexps(lex(source))
  if forms.length() == 0 {
    raise syntax_error(
      { line: 1, column: 1, },
      "empty input; expected a (define (problem ...)) form",
    )
  }
  if forms.length() > 1 {
    raise syntax_error(
      sexp_pos(forms[1]),
      "expected exactly one (define ...) form",
    )
  }
  let (top, top_pos) = expect_list(forms[0], "a (define ...) form")
  if top.length() < 2 {
    raise syntax_error(top_pos, "expected (define (problem ) ...)")
  }
  let (head, head_pos) = expect_atom(top[0], "'define'")
  if head != "define" {
    raise syntax_error(head_pos, "expected 'define', found '\{head}'")
  }
  let (title, title_pos) = expect_list(top[1], "a (problem ) form")
  if title.length() != 2 {
    raise syntax_error(title_pos, "expected (problem )")
  }
  let (kind, kind_pos) = expect_atom(title[0], "'problem'")
  if kind != "problem" {
    raise syntax_error(kind_pos, "expected 'problem', found '\{kind}'")
  }
  let (name, _) = expect_atom(title[1], "a problem name")
  let mut domain_name = ""
  let mut has_domain = false
  let mut requirements : Array[String] = []
  let mut objects : Array[TypedName] = []
  let init : Array[InitElem] = []
  let mut goal : Formula? = None
  let mut metric : Metric? = None
  let seen_sections : Map[String, Bool] = Map([])
  for idx in 2..) section")
  }
  let final_goal = match goal {
    Some(parsed) => parsed
    None =>
      raise syntax_error(top_pos, "problem is missing a (:goal ...) section")
  }
  { name, domain_name, requirements, objects, init, goal: final_goal, metric, }
}