///|
/// A structurally invalid AST, or an AST exceeding the serialization depth limit.
pub suberror SerializationError {
  SerializationError(String)
}

///|
fn canonical_error(
  expected : String,
  node : Node,
) -> String raise SerializationError {
  raise SerializationError("Expected " + expected + ", got " + node.type_name())
}

///|
fn canonical_depth(depth : Int32) -> Unit raise SerializationError {
  // Arrays in synthetic nodes can refer back to their parents. Do not use
  // structural equality here: it would itself recurse forever on such graphs.
  if depth > 128 {
    raise SerializationError(
      "Serialization depth limit (128) exceeded; possible cyclic AST",
    )
  }
}

///|
/// Python str.splitlines(True) keeps all line separators, including CRLF.
/// Joining those lines with four spaces does not indent after a final separator.
fn canonical_indent(content : String) -> String {
  let chars = content.to_array()
  let out = StringBuilder()
  let mut i = 0
  while i < chars.length() {
    let c = chars[i]
    out.write_char(c)
    i += 1
    if c == '\r' && i < chars.length() && chars[i] == '\n' {
      out.write_char('\n')
      i += 1
    }
    if i < chars.length() &&
      (
        c == '\n' ||
        c == '\r' ||
        c == '\u000b' ||
        c == '\u000c' ||
        c == '\u001c' ||
        c == '\u001d' ||
        c == '\u001e' ||
        c == '\u0085' ||
        c == '\u2028' ||
        c == '\u2029'
      ) {
      out.write_string("    ")
    }
  }
  out.to_string()
}

///|
fn canonical_id(node : Node) -> String raise SerializationError {
  match node.kind {
    Identifier(name) => name
    _ => canonical_error("Identifier", node)
  }
}

///|
fn canonical_comment(
  node : Node,
  prefix : String,
) -> String raise SerializationError {
  let content = match node.kind {
    Comment(content) | GroupComment(content) | ResourceComment(content) =>
      content
    _ => return canonical_error("Comment", node)
  }
  let out = StringBuilder()
  for line in content.split("\n") {
    out.write_string(prefix)
    if !line.is_empty() {
      out.write_char(' ')
      out.write_string(line.to_owned())
    }
    out.write_char('\n')
  }
  out.to_string()
}

///|
fn canonical_pattern(
  node : Node,
  depth : Int32,
) -> String raise SerializationError {
  canonical_depth(depth)
  let elements = match node.kind {
    Pattern(elements) => elements
    _ => return canonical_error("Pattern", node)
  }
  let out = StringBuilder()
  let mut multiline = false
  for element in elements {
    match element.kind {
      TextElement(value) => {
        out.write_string(value)
        if value.contains("\n") {
          multiline = true
        }
      }
      Placeable(expr) => {
        if expr.kind is SelectExpression(_, _) {
          multiline = true
        }
        out.write_string(canonical_expression(element, depth + 1))
      }
      _ => return canonical_error("TextElement or Placeable", element)
    }
  }
  if multiline && !elements.is_empty() {
    match elements[0].kind {
      TextElement(value) => {
        let chars = value.to_array()
        if chars.is_empty() {
          raise SerializationError(
            "Empty first TextElement in multiline Pattern",
          )
        }
        if chars[0] == '[' || chars[0] == '.' || chars[0] == '*' {
          multiline = false
        }
      }
      _ => ()
    }
  }
  (if multiline { "\n    " } else { " " }) + canonical_indent(out.to_string())
}

///|
fn canonical_arguments(
  node : Node,
  depth : Int32,
) -> String raise SerializationError {
  canonical_depth(depth)
  let (positional, named) = match node.kind {
    CallArguments(positional, named) => (positional, named)
    _ => return canonical_error("CallArguments", node)
  }
  let parts : Array[String] = []
  for arg in positional {
    parts.push(canonical_expression(arg, depth + 1))
  }
  for arg in named {
    match arg.kind {
      NamedArgument(name, value) =>
        parts.push(
          canonical_id(name) + ": " + canonical_expression(value, depth + 1),
        )
      _ => return canonical_error("NamedArgument", arg)
    }
  }
  "(" + parts.join(", ") + ")"
}

///|
fn canonical_variant(
  node : Node,
  depth : Int32,
) -> String raise SerializationError {
  canonical_depth(depth)
  match node.kind {
    Variant(key, value, default) => {
      let key = match key.kind {
        Identifier(name) | NumberLiteral(name) => name
        _ =>
          return canonical_error("Identifier or NumberLiteral variant key", key)
      }
      "\n" +
      (if default { "   *" } else { "    " }) +
      "[" +
      key +
      "]" +
      canonical_indent(canonical_pattern(value, depth + 1))
    }
    _ => canonical_error("Variant", node)
  }
}

///|
fn canonical_expression(
  node : Node,
  depth : Int32,
) -> String raise SerializationError {
  canonical_depth(depth)
  match node.kind {
    StringLiteral(value) => "\"" + value + "\""
    NumberLiteral(value) => value
    VariableReference(id) => "$" + canonical_id(id)
    MessageReference(id, attribute) => {
      let suffix = match attribute {
        Some(attr) => "." + canonical_id(attr)
        None => ""
      }
      canonical_id(id) + suffix
    }
    TermReference(id, attribute, arguments) => {
      let attr = match attribute {
        Some(attr) => "." + canonical_id(attr)
        None => ""
      }
      let args = match arguments {
        Some(args) => canonical_arguments(args, depth + 1)
        None => ""
      }
      "-" + canonical_id(id) + attr + args
    }
    FunctionReference(id, arguments) =>
      canonical_id(id) + canonical_arguments(arguments, depth + 1)
    SelectExpression(selector, variants) => {
      let out = StringBuilder()
      out.write_string(canonical_expression(selector, depth + 1))
      out.write_string(" ->")
      for variant in variants {
        out.write_string(canonical_variant(variant, depth + 1))
      }
      out.write_char('\n')
      out.to_string()
    }
    Placeable(expr) => {
      let value = canonical_expression(expr, depth + 1)
      match expr.kind {
        Placeable(_) => "{" + value + "}"
        SelectExpression(_, _) => "{ " + value + "}"
        _ => "{ " + value + " }"
      }
    }
    _ => canonical_error("Expression or Placeable", node)
  }
}

///|
fn canonical_attribute(
  node : Node,
  depth : Int32,
) -> String raise SerializationError {
  canonical_depth(depth)
  match node.kind {
    Attribute(id, value) =>
      "\n    ." +
      canonical_id(id) +
      " =" +
      canonical_indent(canonical_pattern(value, depth + 1))
    _ => canonical_error("Attribute", node)
  }
}

///|
fn canonical_entry(
  node : Node,
  has_entries : Bool,
  depth : Int32,
) -> String raise SerializationError {
  canonical_depth(depth)
  match node.kind {
    Message(id, value, attrs, comment) => {
      let out = StringBuilder()
      if comment is Some(comment) {
        out.write_string(canonical_comment(comment, "#"))
      }
      out.write_string(canonical_id(id) + " =")
      if value is Some(value) {
        out.write_string(canonical_pattern(value, depth + 1))
      }
      for attr in attrs {
        out.write_string(canonical_attribute(attr, depth + 1))
      }
      out.write_char('\n')
      out.to_string()
    }
    Term(id, value, attrs, comment) => {
      let out = StringBuilder()
      if comment is Some(comment) {
        out.write_string(canonical_comment(comment, "#"))
      }
      out.write_string("-" + canonical_id(id) + " =")
      out.write_string(canonical_pattern(value, depth + 1))
      for attr in attrs {
        out.write_string(canonical_attribute(attr, depth + 1))
      }
      out.write_char('\n')
      out.to_string()
    }
    Comment(_) | GroupComment(_) | ResourceComment(_) => {
      let prefix = match node.kind {
        GroupComment(_) => "##"
        ResourceComment(_) => "###"
        _ => "#"
      }
      (if has_entries { "\n" } else { "" }) +
      canonical_comment(node, prefix) +
      "\n"
    }
    Junk(content, _) => content
    _ => canonical_error("Message, Term, Comment or Junk entry", node)
  }
}

///|
/// Canonical python-fluent serialization. Spans and Junk annotations are ignored;
/// literal spellings are preserved rather than escaped or normalized again.
/// The root must be a Resource. Invalid child types and excessive nesting raise
/// SerializationError instead of silently dropping nodes or overflowing the stack.
pub fn serialize(
  root : Node,
  with_junk~ : Bool = false,
) -> String raise SerializationError {
  let body = match root.kind {
    Resource(body) => body
    _ => return canonical_error("Resource", root)
  }
  let out = StringBuilder()
  let mut has_entries = false
  for entry in body {
    if entry.kind is Junk(_, _) && !with_junk {
      continue
    }
    out.write_string(canonical_entry(entry, has_entries, 1))
    has_entries = true
  }
  out.to_string()
}