///|
fn clamp_offset(source : String, offset : Int) -> Int {
  if offset < 0 {
    0
  } else if offset > source.length() {
    source.length()
  } else {
    offset
  }
}

///|
/// Convert a zero-based character offset into a one-based line and column.
pub fn source_position(source : String, offset : Int) -> SourcePosition {
  let target = clamp_offset(source, offset)
  let mut current = 0
  let mut line = 1
  let mut column = 1
  for ch in source {
    if current >= target {
      break
    }
    if ch == '\n' {
      line += 1
      column = 1
    } else {
      column += 1
    }
    current += 1
  }
  { offset: target, line, column }
}

///|
/// Create a half-open source span from two offsets.
pub fn source_span(source : String, start : Int, end : Int) -> SourceSpan {
  let safe_start = clamp_offset(source, start)
  let safe_end = if end < safe_start {
    safe_start
  } else {
    clamp_offset(source, end)
  }
  {
    start: source_position(source, safe_start),
    end: source_position(source, safe_end),
  }
}

///|
pub fn SourcePosition::offset(self : SourcePosition) -> Int {
  self.offset
}

///|
pub fn SourcePosition::line(self : SourcePosition) -> Int {
  self.line
}

///|
pub fn SourcePosition::column(self : SourcePosition) -> Int {
  self.column
}

///|
pub fn SourcePosition::display(self : SourcePosition) -> String {
  "\{self.line}:\{self.column}"
}

///|
pub fn SourceSpan::start(self : SourceSpan) -> SourcePosition {
  self.start
}

///|
pub fn SourceSpan::end(self : SourceSpan) -> SourcePosition {
  self.end
}

///|
pub fn SourceSpan::length(self : SourceSpan) -> Int {
  self.end.offset - self.start.offset
}

///|
pub fn MessageDiagnostic::code(self : MessageDiagnostic) -> String {
  self.code
}

///|
pub fn MessageDiagnostic::message(self : MessageDiagnostic) -> String {
  self.message
}

///|
pub fn MessageDiagnostic::position(self : MessageDiagnostic) -> SourcePosition {
  self.position
}

///|
pub fn MessageDiagnostic::display(self : MessageDiagnostic) -> String {
  "\{self.code} at \{self.position.display()}: \{self.message}"
}

///|
fn parse_error_code(message : String) -> String {
  if message.has_prefix("Expected argument name") {
    "expected-argument"
  } else if message.has_prefix("Unsupported formatter") {
    "unsupported-formatter"
  } else if message.has_prefix("Did you mean 'selectordinal'") {
    "invalid-selectordinal-name"
  } else if message.has_prefix("Duplicate selector") {
    "duplicate-selector"
  } else if message.has_prefix("Invalid plural selector") {
    "invalid-plural-selector"
  } else if message.has_prefix("Choice needs an other") {
    "missing-other"
  } else if message.has_prefix("Unclosed") {
    "unclosed-branch"
  } else if message.has_prefix("Dangling escape") {
    "dangling-escape"
  } else if message.has_prefix("Expected choice selector") {
    "expected-selector"
  } else {
    "message-syntax"
  }
}

///|
/// Convert a parser error into a stable diagnostic with line and column.
pub fn diagnostic_from_error(
  source : String,
  error : MessageError,
) -> MessageDiagnostic {
  match error {
    ParseError(position~, message~) =>
      {
        code: parse_error_code(message),
        message,
        position: source_position(source, position),
      }
    InvalidCatalog(message) =>
      { code: "invalid-catalog", message, position: source_position(source, 0) }
    InvalidLocale(message) =>
      { code: "invalid-locale", message, position: source_position(source, 0) }
    InvalidCommand(message) =>
      { code: "invalid-command", message, position: source_position(source, 0) }
    EvaluationLimit(limit) =>
      {
        code: "evaluation-limit",
        message: "Message nesting exceeds limit \{limit}.",
        position: source_position(source, 0),
      }
    MissingArgument(name) =>
      {
        code: "missing-argument",
        message: "Missing argument '\{name}'.",
        position: source_position(source, 0),
      }
    WrongArgumentType(name) =>
      {
        code: "wrong-argument-type",
        message: "Argument '\{name}' has the wrong type.",
        position: source_position(source, 0),
      }
    MissingChoice(selector) =>
      {
        code: "missing-choice",
        message: "No branch matches '\{selector}'.",
        position: source_position(source, 0),
      }
  }
}

///|
/// Parse a message and return a diagnostic instead of raising on failure.
pub fn diagnose_message(template : String) -> MessageDiagnostic? {
  try parse_message(template) catch {
    error => Some(diagnostic_from_error(template, error))
  } noraise {
    _ => None
  }
}

///|
pub fn ChoiceKind::name(self : ChoiceKind) -> String {
  match self {
    Select => "select"
    Plural => "plural"
    SelectOrdinal => "selectordinal"
  }
}

///|
pub fn ChoiceKind::is_numeric(self : ChoiceKind) -> Bool {
  match self {
    Select => false
    Plural | SelectOrdinal => true
  }
}

///|
pub fn ChoiceKind::from_name(name : String) -> ChoiceKind? {
  match name {
    "select" => Some(Select)
    "plural" => Some(Plural)
    "selectordinal" => Some(SelectOrdinal)
    _ => None
  }
}

///|
pub fn ArgumentRole::name(self : ArgumentRole) -> String {
  match self {
    Interpolation => "interpolation"
    SelectSelector => "select"
    CardinalSelector => "plural"
    OrdinalSelector => "selectordinal"
  }
}

///|
pub fn ArgumentRole::expects_number(self : ArgumentRole) -> Bool {
  match self {
    CardinalSelector | OrdinalSelector => true
    Interpolation | SelectSelector => false
  }
}

///|
priv struct ArgumentAccumulator {
  roles : Map[String, ArgumentRole]
  mut occurrences : Int
}

///|
fn ArgumentAccumulator::new() -> ArgumentAccumulator {
  { roles: Map([]), occurrences: 0 }
}

///|
fn ArgumentAccumulator::record(
  self : ArgumentAccumulator,
  role : ArgumentRole,
) -> Unit {
  self.roles[role.name()] = role
  self.occurrences += 1
}

///|
fn record_argument(
  uses : Map[String, ArgumentAccumulator],
  name : String,
  role : ArgumentRole,
) -> Unit {
  let accumulator = match uses.get(name) {
    Some(value) => value
    None => {
      let value = ArgumentAccumulator::new()
      uses[name] = value
      value
    }
  }
  accumulator.record(role)
}

///|
priv struct AnalysisAccumulator {
  arguments : Map[String, ArgumentAccumulator]
  mut node_count : Int
  mut text_node_count : Int
  mut argument_node_count : Int
  mut choice_node_count : Int
  mut pound_node_count : Int
  mut maximum_depth : Int
}

///|
fn AnalysisAccumulator::new() -> AnalysisAccumulator {
  {
    arguments: Map([]),
    node_count: 0,
    text_node_count: 0,
    argument_node_count: 0,
    choice_node_count: 0,
    pound_node_count: 0,
    maximum_depth: 0,
  }
}

///|
fn choice_role(kind : String) -> ArgumentRole {
  match kind {
    "select" => SelectSelector
    "selectordinal" => OrdinalSelector
    _ => CardinalSelector
  }
}

///|
fn analyze_nodes(
  nodes : Array[MessageNode],
  depth : Int,
  accumulator : AnalysisAccumulator,
) -> Unit {
  if depth > accumulator.maximum_depth {
    accumulator.maximum_depth = depth
  }
  for node in nodes {
    accumulator.node_count += 1
    match node {
      TextNode(_) => accumulator.text_node_count += 1
      ArgumentNode(name) => {
        accumulator.argument_node_count += 1
        record_argument(accumulator.arguments, name, Interpolation)
      }
      PoundNode => accumulator.pound_node_count += 1
      ChoiceNode(name, kind, cases) => {
        accumulator.choice_node_count += 1
        record_argument(accumulator.arguments, name, choice_role(kind))
        for _, branch in cases {
          analyze_nodes(branch, depth + 1, accumulator)
        }
      }
    }
  }
}

///|
fn sorted_argument_names(
  arguments : Map[String, ArgumentAccumulator],
) -> Array[String] {
  let names : Array[String] = []
  for name, _ in arguments {
    names.push(name)
  }
  names.sort()
  names
}

///|
fn accumulator_roles(accumulator : ArgumentAccumulator) -> Array[ArgumentRole] {
  let roles : Array[ArgumentRole] = []
  for _, role in accumulator.roles {
    roles.push(role)
  }
  roles.sort_by((left, right) => left.name().compare(right.name()))
  roles
}

///|
/// Analyze the argument signature and structural complexity of a message.
pub fn Message::analyze(self : Message) -> MessageAnalysis {
  let accumulator = AnalysisAccumulator::new()
  analyze_nodes(self.nodes, 1, accumulator)
  let arguments : Array[ArgumentUse] = []
  for name in sorted_argument_names(accumulator.arguments) {
    let argument_use = accumulator.arguments[name]
    arguments.push({
      name,
      roles: accumulator_roles(argument_use),
      occurrences: argument_use.occurrences,
    })
  }
  {
    arguments,
    node_count: accumulator.node_count,
    text_node_count: accumulator.text_node_count,
    argument_node_count: accumulator.argument_node_count,
    choice_node_count: accumulator.choice_node_count,
    pound_node_count: accumulator.pound_node_count,
    maximum_depth: accumulator.maximum_depth,
  }
}

///|
pub fn Message::argument_uses(self : Message) -> Array[ArgumentUse] {
  self.analyze().arguments
}

///|
pub fn Message::node_count(self : Message) -> Int {
  self.analyze().node_count
}

///|
pub fn Message::maximum_depth(self : Message) -> Int {
  self.analyze().maximum_depth
}

///|
pub fn ArgumentUse::name(self : ArgumentUse) -> String {
  self.name
}

///|
pub fn ArgumentUse::roles(self : ArgumentUse) -> Array[ArgumentRole] {
  self.roles
}

///|
pub fn ArgumentUse::occurrences(self : ArgumentUse) -> Int {
  self.occurrences
}

///|
pub fn ArgumentUse::uses_role(self : ArgumentUse, role : ArgumentRole) -> Bool {
  for existing in self.roles {
    if existing == role {
      return true
    }
  }
  false
}

///|
pub fn ArgumentUse::expects_number(self : ArgumentUse) -> Bool {
  for role in self.roles {
    if role.expects_number() {
      return true
    }
  }
  false
}

///|
pub fn MessageAnalysis::arguments(self : MessageAnalysis) -> Array[ArgumentUse] {
  self.arguments
}

///|
pub fn MessageAnalysis::node_count(self : MessageAnalysis) -> Int {
  self.node_count
}

///|
pub fn MessageAnalysis::text_node_count(self : MessageAnalysis) -> Int {
  self.text_node_count
}

///|
pub fn MessageAnalysis::argument_node_count(self : MessageAnalysis) -> Int {
  self.argument_node_count
}

///|
pub fn MessageAnalysis::choice_node_count(self : MessageAnalysis) -> Int {
  self.choice_node_count
}

///|
pub fn MessageAnalysis::pound_node_count(self : MessageAnalysis) -> Int {
  self.pound_node_count
}

///|
pub fn MessageAnalysis::maximum_depth(self : MessageAnalysis) -> Int {
  self.maximum_depth
}