///|
/// Half-open Unicode scalar offsets, as used by Python fluent.syntax.
/// Offsets count both code points of CRLF even though parsing treats it as EOL.
pub(all) struct Span {
  start : Int32
  end : Int32
} derive(Eq)

///|
/// Typed Fluent syntax, deliberately independent of runtime evaluation.
pub(all) enum Syntax {
  Resource(Array[Node])
  Message(Node, Node?, Array[Node], Node?)
  Term(Node, Node, Array[Node], Node?)
  Pattern(Array[Node])
  TextElement(String)
  Placeable(Node)
  StringLiteral(String)
  NumberLiteral(String)
  MessageReference(Node, Node?)
  TermReference(Node, Node?, Node?)
  VariableReference(Node)
  FunctionReference(Node, Node)
  SelectExpression(Node, Array[Node])
  CallArguments(Array[Node], Array[Node])
  Attribute(Node, Node)
  Variant(Node, Node, Bool)
  NamedArgument(Node, Node)
  Identifier(String)
  Comment(String)
  GroupComment(String)
  ResourceComment(String)
  Junk(String, Array[Node])
  Annotation(String, Array[String?], String)
} derive(Eq)

///|
pub(all) struct Node {
  kind : Syntax
  span : Span
} derive(Eq)

///|
/// Construct a synthetic node. Negative positions indicate no source origin.
pub fn Node::new(kind : Syntax) -> Node {
  { kind, span: { start: -1, end: -1, }, }
}

///|
pub fn Node::with_span(kind : Syntax, start : Int32, end : Int32) -> Node {
  { kind, span: { start, end, }, }
}

///|
pub fn Node::type_name(self : Node) -> String {
  match self.kind {
    Resource(_) => "Resource"
    Message(_, _, _, _) => "Message"
    Term(_, _, _, _) => "Term"
    Pattern(_) => "Pattern"
    TextElement(_) => "TextElement"
    Placeable(_) => "Placeable"
    StringLiteral(_) => "StringLiteral"
    NumberLiteral(_) => "NumberLiteral"
    MessageReference(_, _) => "MessageReference"
    TermReference(_, _, _) => "TermReference"
    VariableReference(_) => "VariableReference"
    FunctionReference(_, _) => "FunctionReference"
    SelectExpression(_, _) => "SelectExpression"
    CallArguments(_, _) => "CallArguments"
    Attribute(_, _) => "Attribute"
    Variant(_, _, _) => "Variant"
    NamedArgument(_, _) => "NamedArgument"
    Identifier(_) => "Identifier"
    Comment(_) => "Comment"
    GroupComment(_) => "GroupComment"
    ResourceComment(_) => "ResourceComment"
    Junk(_, _) => "Junk"
    Annotation(_, _, _) => "Annotation"
  }
}

///|
fn json_nodes(nodes : Array[Node], spans : Bool) -> Json {
  Json::array(nodes.map(n => n.to_json_ast(with_spans=spans)))
}

///|
fn json_optional(node : Node?, spans : Bool) -> Json {
  match node {
    Some(n) => n.to_json_ast(with_spans=spans)
    None => Json::null()
  }
}

///|
/// Mozilla-compatible AST JSON, suitable for differential tests and tooling.
/// Source offsets are omitted by default, not normalized to UTF-16 offsets.
pub fn Node::to_json_ast(self : Node, with_spans~ : Bool = false) -> Json {
  let f : Map[String, Json] = { "type": Json::string(self.type_name()) }
  match self.kind {
    Resource(body) => f["body"] = json_nodes(body, with_spans)
    Message(id, value, attrs, comment) => {
      f["id"] = id.to_json_ast(with_spans~)
      f["value"] = json_optional(value, with_spans)
      f["attributes"] = json_nodes(attrs, with_spans)
      f["comment"] = json_optional(comment, with_spans)
    }
    Term(id, value, attrs, comment) => {
      f["id"] = id.to_json_ast(with_spans~)
      f["value"] = value.to_json_ast(with_spans~)
      f["attributes"] = json_nodes(attrs, with_spans)
      f["comment"] = json_optional(comment, with_spans)
    }
    Pattern(elements) => f["elements"] = json_nodes(elements, with_spans)
    TextElement(value) | StringLiteral(value) | NumberLiteral(value) =>
      f["value"] = Json::string(value)
    Placeable(expression) =>
      f["expression"] = expression.to_json_ast(with_spans~)
    MessageReference(id, attribute) => {
      f["id"] = id.to_json_ast(with_spans~)
      f["attribute"] = json_optional(attribute, with_spans)
    }
    TermReference(id, attribute, arguments) => {
      f["id"] = id.to_json_ast(with_spans~)
      f["attribute"] = json_optional(attribute, with_spans)
      f["arguments"] = json_optional(arguments, with_spans)
    }
    VariableReference(id) => f["id"] = id.to_json_ast(with_spans~)
    FunctionReference(id, arguments) => {
      f["id"] = id.to_json_ast(with_spans~)
      f["arguments"] = arguments.to_json_ast(with_spans~)
    }
    SelectExpression(selector, variants) => {
      f["selector"] = selector.to_json_ast(with_spans~)
      f["variants"] = json_nodes(variants, with_spans)
    }
    CallArguments(positional, named) => {
      f["positional"] = json_nodes(positional, with_spans)
      f["named"] = json_nodes(named, with_spans)
    }
    Attribute(id, value) => {
      f["id"] = id.to_json_ast(with_spans~)
      f["value"] = value.to_json_ast(with_spans~)
    }
    Variant(key, value, default) => {
      f["key"] = key.to_json_ast(with_spans~)
      f["value"] = value.to_json_ast(with_spans~)
      f["default"] = Json::boolean(default)
    }
    NamedArgument(name, value) => {
      f["name"] = name.to_json_ast(with_spans~)
      f["value"] = value.to_json_ast(with_spans~)
    }
    Identifier(name) => f["name"] = Json::string(name)
    Comment(content) | GroupComment(content) | ResourceComment(content) =>
      f["content"] = Json::string(content)
    Junk(content, annotations) => {
      f["content"] = Json::string(content)
      f["annotations"] = json_nodes(annotations, with_spans)
    }
    Annotation(code, args, message) => {
      f["code"] = Json::string(code)
      f["arguments"] = Json::array(
        args.map(a => {
          match a {
            Some(s) => Json::string(s)
            None => Json::null()
          }
        }),
      )
      f["message"] = Json::string(message)
    }
  }
  if with_spans && self.span.start >= 0 {
    f["span"] = Json::object({
      "type": Json::string("Span"),
      "start": Json(self.span.start),
      "end": Json(self.span.end),
    })
  }
  Json::object(f)
}

///|
/// Structural equality independent of source locations.
pub fn Node::same_syntax(self : Node, other : Node) -> Bool {
  self.to_json_ast() == other.to_json_ast()
}

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

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

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