///|
fn span(loc : @location.Location) -> @ir.SourceLocation {
  {
    start_line: loc.start.lnum,
    start_column: loc.start.cnum - loc.start.bol,
    end_line: loc.end.lnum,
    end_column: loc.end.cnum - loc.end.bol,
  }
}

///|
/// Parser attachment owns comment/declaration association. Remove only the doc
/// marker's conventional space and empty separator edges, retaining Markdown.
fn source_documentation(doc : @ast.DocString) -> String? {
  let lines = doc.content
    .to_array()
    .map(line => line.strip_prefix(" ").unwrap_or(line[:]).to_owned())
  let mut first = 0
  let mut last = lines.length()
  while first < last && lines[first] == "" {
    first += 1
  }
  while last > first && lines[last - 1] == "" {
    last -= 1
  }
  if first == last {
    None
  } else {
    Some(lines[first:last].to_owned().join("\n"))
  }
}

///|
fn reject(
  code : String,
  detail : String,
  loc : @location.Location,
) -> Unit raise FrontendError {
  raise Invalid({
    code: "moonbit_frontend." + code,
    detail,
    source: Some(span(loc)),
  })
}

///|
fn boolean_type() -> @ir.Type raise {
  @ir.Type::Reference(
    @ir.TypeAttributes::default(),
    @ir.FQName::parse("morphir/SDK:basics#bool"),
    [],
  )
}

///|
fn require_bool(
  ty : @ast.Type?,
  loc : @location.Location,
) -> Unit raise FrontendError {
  match ty {
    Some(@ast.Name(constr_id~, tys~, ..)) =>
      if !(constr_id.id is @ast.Ident(name="Bool")) || !tys.is_empty() {
        reject(
          "unsupported_type", "Only explicitly declared Bool is supported", loc,
        )
      }
    _ =>
      reject("unsupported_type", "An explicit Bool annotation is required", loc)
  }
}

///|
fn identifier(var_ : @ast.Var) -> String raise FrontendError {
  match var_.name {
    @ast.Ident(name~) => name
    _ => {
      reject(
        "qualified_reference",
        "Imports and qualified references are unsupported",
        var_.loc,
      )
      abort("unreachable")
    }
  }
}

///|
priv struct Lowerer {
  scope : Map[String, @ir.Name]
  boolean : @ir.Type
  limits : Limits
  cancelled : () -> Bool
  mut expressions : Int32
  names : BinderNames
  origins : Array[@ion.IonValue]
}

///|
fn Lowerer::attrs(
  self : Lowerer,
  loc : @location.Location,
) -> @ir.ValueAttributes {
  {
    source: Some(span(loc)),
    inferred_type: Some(self.boolean),
    extensions: Map([]),
  }
}

///|
fn Lowerer::literal(
  self : Lowerer,
  value : Bool,
  loc : @location.Location,
) -> @ir.Value {
  @ir.Value::Literal(self.attrs(loc), @ir.Literal::BoolLiteral(value))
}

///|
fn Lowerer::negate(
  self : Lowerer,
  value : @ir.Value,
  loc : @location.Location,
) -> @ir.Value {
  @ir.Value::IfThenElse(
    self.attrs(loc),
    value,
    self.literal(false, loc),
    self.literal(true, loc),
  )
}

///|
fn Lowerer::fresh(
  self : Lowerer,
  value : @ir.Value,
  role : String,
  loc : @location.Location,
) -> @ir.Name raise {
  let base = match value {
    Variable(_, name) => name.to_canonical()
    _ => "boolean"
  }
  let name = self.names.fresh(base, role)
  self.origins.push(
    ion_record([
      ("temporary", origin(role, loc)),
      ("identity", @ion.IonValue::string(name.to_canonical())),
      ("origin", @ion.IonValue::string(base)),
    ]),
  )
  name
}

///|
/// Equality binds both operands once, in source order, without duplicating trees.
fn Lowerer::equality(
  self : Lowerer,
  lhs : @ir.Value,
  rhs : @ir.Value,
  different : Bool,
  loc : @location.Location,
) -> @ir.Value raise {
  let left = self.fresh(lhs, "equality-left", loc)
  let right = self.fresh(rhs, "equality-right", loc)
  let l = @ir.Value::Variable(self.attrs(loc), left)
  let r = @ir.Value::Variable(self.attrs(loc), right)
  let comparison = if different {
    @ir.Value::IfThenElse(self.attrs(loc), l, self.negate(r, loc), r)
  } else {
    @ir.Value::IfThenElse(self.attrs(loc), l, r, self.negate(r, loc))
  }
  @ir.Value::LetDefinition(
    self.attrs(loc),
    left,
    @ir.ValueDefinition::ExpressionBody([], self.boolean, lhs),
    @ir.Value::LetDefinition(
      self.attrs(loc),
      right,
      @ir.ValueDefinition::ExpressionBody([], self.boolean, rhs),
      comparison,
    ),
  )
}

///|
fn Lowerer::lower(
  self : Lowerer,
  expr : @ast.Expr,
  depth : Int32,
) -> @ir.Value raise {
  let loc = expr.loc()
  if (self.cancelled)() {
    reject("cancelled", "Frontend cancelled", loc)
  }
  if depth > self.limits.max_depth ||
    self.expressions >= self.limits.max_expressions {
    reject("expression_limit", "Expression traversal budget exceeded", loc)
  }
  self.expressions += 1
  match expr {
    @ast.Constant(c=@ast.Bool(value), ..) => self.literal(value, loc)
    @ast.Ident(id~, ..) => {
      let text = identifier(id)
      match self.scope.get(text) {
        Some(name) => @ir.Value::Variable(self.attrs(loc), name)
        None => {
          reject(
            "unknown_reference",
            "Expected a Boolean parameter: " + text,
            loc,
          )
          abort("unreachable")
        }
      }
    }
    @ast.Group(expr~, ..) => self.lower(expr, depth + 1)
    @ast.Constraint(expr~, ty~, ..) => {
      require_bool(Some(ty), loc)
      self.lower(expr, depth + 1)
    }
    @ast.If(cond~, ifso~, ifnot=Some(other), ..) =>
      @ir.Value::IfThenElse(
        self.attrs(loc),
        self.lower(cond, depth + 1),
        self.lower(ifso, depth + 1),
        self.lower(other, depth + 1),
      )
    @ast.Unary(op~, expr~, ..) => {
      if identifier(op) != "!" {
        reject(
          "unsupported_operator", "Only Boolean negation is supported", loc,
        )
      }
      self.negate(self.lower(expr, depth + 1), loc)
    }
    @ast.Infix(op~, lhs~, rhs~, ..) => {
      let operator = identifier(op)
      if !["&&", "||", "==", "!="].contains(operator) {
        reject(
          "unsupported_operator", "Only Boolean operators are supported", loc,
        )
      }
      let left = self.lower(lhs, depth + 1)
      let right = self.lower(rhs, depth + 1)
      match operator {
        "&&" =>
          @ir.Value::IfThenElse(
            self.attrs(loc),
            left,
            right,
            self.literal(false, loc),
          )
        "||" =>
          @ir.Value::IfThenElse(
            self.attrs(loc),
            left,
            self.literal(true, loc),
            right,
          )
        _ => self.equality(left, right, operator == "!=", loc)
      }
    }
    _ => {
      reject(
        "unsupported_expression",
        "Expected a pure Boolean expression in " + LEGACY_BOOL_PROFILE,
        loc,
      )
      abort("unreachable")
    }
  }
}

///|
/// parse_string discards lexer errors. Reject them before admitting its recovered AST.
fn check_lexing(input : Input) -> Unit raise FrontendError {
  let lexed = @lexer.tokens_from_string(
    input.source[:],
    name=input.unit_id,
    comment=true,
  )
  if !lexed.errors.is_empty() {
    let (start, end, error) = lexed.errors[0]
    reject(
      "syntax",
      "Lexing error: " +
      Show::to_string(error) +
      ". Correct the token or use a supported identifier spelling.",
      { start, end, },
    )
  }
}