///|
/// A goal frame on the goal stack: the goal plus the cut mark of the clause
/// body it belongs to. The mark is the length of the choice-point stack when
/// the enclosing clause was entered; `!` truncates the stack to it.
priv struct Frame {
  goal : Term
  mark : Int
}

///|
/// Immutable goal stack. Pushing creates a new cell, so frames pushed before
/// a choice point are automatically snapshotted by reference.
priv enum FStack {
  FNil
  FCons(Frame, FStack)
}

///|
/// How to resume from a choice point during backtracking.
priv enum Alt {
  /// Try the next clause of a predicate call.
  Clauses(goal~ : Term, key~ : (String, Int), next~ : Int)
  /// Solve a pending goal, e.g. the second branch of a disjunction.
  Goal(goal~ : Term, mark~ : Int)
}

///|
/// A suspended search state that backtracking can resume.
priv struct Choice {
  goals : FStack
  subst : Subst
  /// Number of active `dif/2` constraints when the choice point was pushed;
  /// backtracking truncates `Machine.diffs` back to this length.
  diffs_len : Int
  alt : Alt
}

///|
/// The SLD resolution machine. `subst` maps are never mutated in place, so
/// each choice point holds an implicit snapshot of the substitution.
///
/// `diffs` holds the active `dif/2` disequality constraints; like `choices`,
/// it is truncated on backtracking (see [`Choice::diffs_len`]).
priv struct Machine {
  prog : Program
  mut stack : FStack
  mut subst : Subst
  choices : Array[Choice]
  /// Active `dif/2` constraints, in the order they were posted.
  diffs : Array[(Term, Term)]
  /// True right after a solution has been yielded; the next step must
  /// resume by backtracking.
  mut resumed : Bool
}

///|
/// Builds a fresh machine for the given program, with an empty goal stack,
/// substitution, constraint store and no choice points.
///
/// Declared as the custom constructor `Machine(...)`, like
/// [`Clause::Clause`](index.html#clause).
fn Machine::Machine(prog : Program) -> Machine {
  {
    prog,
    stack: FNil,
    subst: @immut_hashmap.HashMap([]),
    choices: [],
    diffs: [],
    resumed: false,
  }
}

///|
/// Records a choice point, snapshotting the current goal stack, substitution
/// and `dif/2` constraint count.
fn Machine::push_choice(self : Machine, alt : Alt) -> Unit {
  self.choices.push({
    goals: self.stack,
    subst: self.subst,
    diffs_len: self.diffs.length(),
    alt,
  })
}

///|
/// Commits `s` as the machine's substitution. Returns `false` when `s`
/// violates an active `dif/2` constraint (the caller must then fail and
/// backtrack).
fn Machine::commit(self : Machine, s : Subst) -> Bool {
  self.subst = s
  self.check_diffs(s)
}

///|
/// Checks every active `dif/2` constraint against substitution `s`: if any
/// two constrained terms have become identical, the constraint is violated.
/// Permanently-satisfied constraints are kept (they never fail again), so
/// this is a pure predicate over `s`.
fn Machine::check_diffs(self : Machine, s : Subst) -> Bool {
  for p in self.diffs {
    let (a, b) = p
    if identical_terms(a.deref(s), b.deref(s)) {
      return false
    }
  }
  true
}

///|
/// Pushes a goal frame with the given cut mark.
fn Machine::push_goal(self : Machine, g : Term, mark : Int) -> Unit {
  self.stack = FCons({ goal: g, mark }, self.stack)
}

///|
/// Runs the machine until it yields a solution or the search is exhausted.
///
/// - `goal_barrier`: the stack reference that counts as "no goals left"
///   (the top-level solver uses `FNil`; `not/1` uses the stack at its entry)
/// - `choice_barrier`: backtracking never pops choices below this index
///   (`-1` means no barrier; `not/1` uses the choice count at its entry)
fn Machine::step(
  self : Machine,
  goal_barrier : FStack,
  choice_barrier : Int,
) -> Subst? {
  for ;; {
    if self.resumed {
      self.resumed = false
      if !self.backtrack(choice_barrier) {
        return None
      }
    }
    // Solve goals until failure, success, or exhaustion.
    for ;; {
      match self.stack {
        _ if physical_equal(self.stack, goal_barrier) => {
          self.resumed = true
          return Some(self.subst)
        }
        FNil => abort("internal error: goal stack underflow")
        FCons(fr, rest) => {
          self.stack = rest
          if !self.execute(fr) {
            break
          }
        }
      }
    }
    // The current branch failed; backtrack and try the next alternative.
    if !self.backtrack(choice_barrier) {
      return None
    }
  }
}

///|
/// Pops choice points and tries their alternatives until one succeeds or the
/// search space is exhausted (respecting `choice_barrier`).
fn Machine::backtrack(self : Machine, choice_barrier : Int) -> Bool {
  for ;; {
    if self.choices.length() == choice_barrier {
      return false
    }
    match self.choices.pop() {
      None => return false
      Some(ch) => {
        self.stack = ch.goals
        self.subst = ch.subst
        self.diffs.truncate(ch.diffs_len)
        match ch.alt {
          Clauses(goal=g, key=k, next=n) => {
            let mark = self.choices.length()
            match self.clauses_of(k) {
              None => continue
              Some(cls) =>
                if self.try_clauses(g, k, cls, n, mark) {
                  return true
                }
            }
          }
          Goal(goal=g, mark=m) => {
            self.push_goal(g, m)
            return true
          }
        }
      }
    }
  }
}

///|
/// Executes one goal frame. Returns `false` when the goal fails, which makes
/// the caller backtrack.
fn Machine::execute(self : Machine, fr : Frame) -> Bool {
  let g = fr.goal.deref(self.subst)
  match g {
    Atom(name) =>
      match name {
        "true" => true
        "fail" | "false" => false
        "!" => {
          self.choices.truncate(fr.mark)
          true
        }
        _ => self.call_pred(g, (name, 0))
      }
    Compound(name, args) =>
      if name == "," && args.length() == 2 {
        self.push_goal(args[1], fr.mark)
        self.push_goal(args[0], fr.mark)
        true
      } else if name == ";" && args.length() == 2 {
        match args[0] {
          // (A -> B ; C): soft cut — commit to B once A succeeds.
          Compound("->", [cond, then]) => {
            let m0 = self.choices.length()
            self.push_choice(Goal(goal=args[1], mark=fr.mark))
            // Execute order (LIFO): cond, then `!`, then the committed
            // branch.
            self.push_goal(then, fr.mark)
            self.push_goal(atom("!"), m0)
            self.push_goal(cond, fr.mark)
            true
          }
          _ => {
            self.push_choice(Goal(goal=args[1], mark=fr.mark))
            self.push_goal(args[0], fr.mark)
            true
          }
        }
      } else if name == "->" && args.length() == 2 {
        // (A -> B) without an else branch: commit to B once A succeeds,
        // and fail entirely when A fails.
        let m0 = self.choices.length()
        self.push_goal(args[1], fr.mark)
        self.push_goal(atom("!"), m0)
        self.push_goal(args[0], fr.mark)
        true
      } else {
        match self.run_builtin(name, args, fr.mark) {
          Some(result) => result
          None => self.call_pred(g, (name, args.length()))
        }
      }
    _ => false
  }
}

///|
/// Calls a user predicate: tries its clauses in order, standardizing apart
/// (renaming the clause's variables) before each attempt. Dynamic
/// predicates are tried through their live dynamic clauses; static
/// predicates through the program's clause list.
fn Machine::call_pred(self : Machine, g : Term, key : (String, Int)) -> Bool {
  let mark = self.choices.length()
  match self.clauses_of(key) {
    None => false
    Some(cls) => self.try_clauses(g, key, cls, 0, mark)
  }
}

///|
/// Tries clauses `cls[idx..]` against goal `g`. `mark` is the choice length
/// recorded when the call was made (used by cut).
fn Machine::try_clauses(
  self : Machine,
  g : Term,
  key : (String, Int),
  cls : Array[Clause],
  idx : Int,
  mark : Int,
) -> Bool {
  for i in idx.. continue
      Some(s2) =>
        // Reject the clause when the binding violates a `dif/2` constraint.
        if self.check_diffs(s2) {
          if i + 1 < cls.length() {
            self.push_choice(Clauses(goal=g, key~, next=i + 1))
          }
          self.subst = s2
          if !(body is Atom("true")) {
            self.push_goal(body, mark)
          }
          return true
        }
    }
  }
  false
}

///|
/// Standardizes a clause apart: renames every variable of head and body to a
/// fresh id, so repeated invocations of the same clause share no variables.
fn fresh_rename(cl : Clause) -> (Term, Term) {
  let ids = collect_var_ids(cl.head, collect_var_ids(cl.body, Map([])))
  let map : Map[Int, Int] = Map([])
  for id, _ in ids {
    map[id] = fresh_id()
  }
  (rename_vars(cl.head, map), rename_vars(cl.body, map))
}

///|
/// Allocates a fresh variable id from the global counter.
fn fresh_id() -> Int {
  var_counter.val = var_counter.val + 1
  var_counter.val
}

///|
/// `not(g)`: succeeds iff `g` has no solution. `g` runs with a fresh cut
/// mark and fresh barriers, so cuts inside `g` cannot escape its scope.
fn Machine::not(self : Machine, g : Term) -> Bool {
  let saved_stack = self.stack
  let saved_subst = self.subst
  let choice_barrier = self.choices.length()
  let diff_barrier = self.diffs.length()
  self.push_goal(g, choice_barrier)
  let succeeded = self.step(saved_stack, choice_barrier) is Some(_)
  self.stack = saved_stack
  self.subst = saved_subst
  self.choices.truncate(choice_barrier)
  self.diffs.truncate(diff_barrier)
  self.resumed = false
  !succeeded
}

///|
/// All solutions of the query (a conjunction of goals) as a lazy iterator of
/// raw substitutions.
pub fn Program::solve_subst(self : Program, goals : Array[Term]) -> Iter[Subst] {
  let m = Machine(self)
  // Push in reverse: the goal stack pops the last pushed frame first, so
  // goals are executed in their given order.
  for i = goals.length() - 1; i >= 0; i = i - 1 {
    m.push_goal(goals[i], 0)
  }
  Iter::new(() => m.step(FNil, -1))
}

///|
/// One answer of a query: the bindings of the query's variables, keyed by
/// name (anonymous variables starting with `_` are skipped).
///
/// `order` records the names in order of first appearance in the query, so
/// answers render deterministically as `X = john, Y = mary` regardless of
/// hash-map iteration order.
pub struct Answer {
  bindings : Map[String, Term]
  order : Array[String]
} derive(Debug)

///|
/// Collects the named variables of the query, in order of first appearance.
fn collect_query_vars(goals : Array[Term]) -> Array[VarRef] {
  let seen : Map[Int, Unit] = Map([])
  let out : Array[VarRef] = []
  for g in goals {
    collect_vars_into(g, seen, out)
  }
  out
}

///|
fn collect_vars_into(
  t : Term,
  seen : Map[Int, Unit],
  out : Array[VarRef],
) -> Unit {
  match t {
    Var(v) =>
      if !seen.contains(v.id) {
        seen[v.id] = ()
        out.push(v)
      }
    List(xs) =>
      for x in xs {
        collect_vars_into(x, seen, out)
      }
    Compound(_, args) =>
      for x in args {
        collect_vars_into(x, seen, out)
      }
    _ => ()
  }
}

///|
/// Builds an [`Answer`] from a substitution and the query it answers.
/// Bindings that just say `X = X` (an unbound query variable) are omitted.
pub fn Answer::from_subst(subst : Subst, goals : Array[Term]) -> Answer {
  let bindings : Map[String, Term] = Map([])
  let order : Array[String] = []
  for v in collect_query_vars(goals) {
    if v.name != "" && !v.name.has_prefix("_") && !bindings.contains(v.name) {
      let value = Var(v).resolve(subst)
      match value {
        Var(v2) =>
          if v2.id != v.id {
            bindings[v.name] = value
            order.push(v.name)
          }
        _ => {
          bindings[v.name] = value
          order.push(v.name)
        }
      }
    }
  }
  { bindings, order }
}

///|
/// The binding of the query variable named `name`, if any.
///
/// ```mbt check
/// test {
///   let x = variable("X")
///   let p = Program([Clause::fact(compound("p", [atom("a")]))])
///   let a = p.solve_first([compound("p", [x])]).unwrap()
///   assert_eq(a.get("X"), Some(atom("a")))
///   assert_eq(a.get("Y"), None)
/// }
/// ```
pub fn Answer::get(self : Answer, name : String) -> Term? {
  self.bindings.get(name)
}

///|
/// Renders an answer as `X = john, Y = mary` (or `true` for no bindings).
/// Bindings are listed in order of first appearance in the query.
pub impl Show for Answer with fn to_string(self) {
  let parts = self.order.map(name => "\{name} = \{self.bindings[name]}")
  if parts.length() == 0 {
    "true"
  } else {
    parts.join(", ")
  }
}

///|
/// Runs a query against the program, yielding each solution as an
/// [`Answer`]. The iterator is lazy: solutions are produced on demand, and
/// infinite solution spaces can be explored with `take`/`next`.
///
/// ```mbt check
/// test {
///   let p = Program([
///     Clause::fact(compound("parent", [atom("john"), atom("mary")])),
///     Clause::fact(compound("parent", [atom("john"), atom("jane")])),
///   ])
///   let x = variable("X")
///   let answers = p.solve([compound("parent", [x, variable("_")])]).to_array()
///   assert_eq(answers.length(), 2)
///   assert_eq(answers[0].to_string(), "X = john")
/// }
/// ```
pub fn Program::solve(self : Program, goals : Array[Term]) -> Iter[Answer] {
  self.solve_subst(goals).map(s => Answer::from_subst(s, goals))
}

///|
/// The first solution of a query, if any.
pub fn Program::solve_first(self : Program, goals : Array[Term]) -> Answer? {
  self.solve(goals).next()
}

///|
/// All solutions of a query. Use with care on programs with infinite
/// solution spaces (use [`solve`] with `take`/`next` instead).
pub fn Program::solve_all(self : Program, goals : Array[Term]) -> Array[Answer] {
  self.solve(goals).to_array()
}