///|
/// The pinned PlantUML version whose native behavior this library follows.
pub let plantuml_version : String = "1.2026.9"

///|
/// The source commit of the pinned PlantUML version.
pub let plantuml_commit : String = "18d0aae311a4f1eb3723bb4b0ad939185e22f166"

///|
/// One parsed native block of a supported kind.
pub(all) enum Document {
  Regex(@regex_syntax.Document)
  Ebnf(@ebnf_syntax.Document)
  Json(@json_syntax.Document)
  Yaml(@yaml_syntax.Document)
  Activity(@activity.Document)
} derive(Eq, Debug)

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

///|
pub extend Document with @debug.Debug::{to_repr}

///|
pub fn Document::kind(self : Document) -> @envelope.Kind {
  match self {
    Regex(_) => Regex
    Ebnf(_) => Ebnf
    Json(_) => Json
    Yaml(_) => Yaml
    Activity(_) => Activity
  }
}

///|
/// The result of reading. `tree` reproduces every input code unit. Activity
/// recovery can retain a partial `document`; diagnostics determine validity.
/// The other native readers return no document after an error.
pub struct ReadResult {
  tree : @cst.Node
  document : Document?
  diagnostics : ReadOnlyArray[@diagrams.Diagnostic]
}

///|
fn finish(
  scan : @envelope.Scan,
  nodes : Array[@cst.Node],
  document : Document?,
  diagnostics : ReadOnlyArray[@diagrams.Diagnostic],
) -> ReadResult {
  { tree: scan.document(nodes), document, diagnostics, }
}

///|
fn read_scan(scan : @envelope.Scan, limits : @envelope.Limits) -> ReadResult {
  match scan.kind {
    Some(Regex) => {
      let r = @regex_syntax.read(scan, limits~)
      finish(scan, r.nodes, r.document.map(d => Regex(d)), r.diagnostics)
    }
    Some(Ebnf) => {
      let r = @ebnf_syntax.read(scan, limits~)
      finish(scan, r.nodes, r.document.map(d => Ebnf(d)), r.diagnostics)
    }
    Some(Json) => {
      let r = @json_syntax.read(scan, limits~)
      finish(scan, r.nodes, r.document.map(d => Json(d)), r.diagnostics)
    }
    Some(Yaml) => {
      let r = @yaml_syntax.read(scan, limits~)
      finish(scan, r.nodes, r.document.map(d => Yaml(d)), r.diagnostics)
    }
    Some(Activity) => {
      let r = @activity.read(scan, limits~)
      finish(
        scan,
        r.nodes.iter().to_array(),
        r.document.map(d => Activity(d)),
        r.diagnostics,
      )
    }
    _ => {
      let nodes = scan.line_nodes(line => {
        Some(@cst.Node::new(Error, line.tokens(Error)))
      })
      let diagnostics = scan.diagnostics.iter().collect()
      if !diagnostics.iter().any(d => d.severity == Error) {
        diagnostics.push(
          @diagrams.Diagnostic::error(
            "plantuml/unsupported-diagram", "Expected @startregex, @startebnf, @startjson or @startyaml",
          ),
        )
      }
      finish(scan, nodes, None, ReadOnlyArray::from_array(diagnostics))
    }
  }
}

///|
/// Read one regex, EBNF, JSON, YAML or modern/legacy activity block with its
/// matching end marker. Activities use `@startuml` and `@enduml`. Other UML
/// kinds, extra blocks and text outside the block are errors.
pub fn read(
  source : String,
  limits~ : @envelope.Limits = @envelope.Limits::new(),
) -> ReadResult {
  read_scan(@envelope.scan(source, limits~), limits)
}

///|
/// Read a raw body of an explicit kind, without block markers.
pub fn read_body(
  kind : @envelope.Kind,
  body : String,
  dialect? : @activity.Dialect,
  limits~ : @envelope.Limits = @envelope.Limits::new(),
) -> ReadResult {
  let scan = @envelope.scan_body(kind, body, limits~)
  if kind == Activity {
    if dialect == None {
      return finish(
        scan,
        scan.line_nodes(line => Some(@cst.Node::new(Data, line.tokens(Text)))),
        None,
        [
          @diagrams.Diagnostic::error(
            "activity/missing-dialect", "Activity body reading requires an explicit dialect",
          ),
        ],
      )
    }
    let r = @activity.read(scan, limits~, dialect?)
    finish(
      scan,
      r.nodes.iter().to_array(),
      r.document.map(d => Activity(d)),
      r.diagnostics,
    )
  } else {
    read_scan(scan, limits)
  }
}

///|
pub struct PrintResult {
  output : String?
  diagnostics : ReadOnlyArray[@diagrams.Diagnostic]
} derive(Eq, Debug)

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

///|
pub extend PrintResult with @debug.Debug::{to_repr}

///|
fn statements_of(document : Document) -> ReadOnlyArray[@envelope.Statement] {
  match document {
    Regex(d) => d.statements
    Ebnf(d) => d.statements
    Json(d) => d.statements
    Yaml(d) => d.statements
    Activity(d) => d.statements
  }
}

///|
fn name_of(document : Document) -> String? {
  match document {
    Regex(d) => d.name
    Ebnf(d) => d.name
    Json(d) => d.name
    Yaml(d) => d.name
    Activity(d) => d.name
  }
}

///|
/// Write only the body of a document, from its typed values.
pub fn print_body(
  document : Document,
  limits~ : @envelope.Limits = @envelope.Limits::new(),
) -> PrintResult {
  let result = match document {
    Regex(d) => @regex_syntax.print_body(d, limits~)
    Ebnf(d) => @ebnf_syntax.print_body(d, limits~)
    Json(d) => @json_syntax.print_body(d, limits~)
    Yaml(d) => @yaml_syntax.print_body(d, limits~)
    Activity(d) => @activity.print_body(d, limits~)
  }
  match result {
    Ok(text) => { output: Some(text), diagnostics: [], }
    Err(diagnostics) => { output: None, diagnostics, }
  }
}

///|
/// Write a complete native block: start marker, retained statements, the
/// body and the end marker. The output limit applies to the whole text.
pub fn print(
  document : Document,
  limits~ : @envelope.Limits = @envelope.Limits::new(),
) -> PrintResult {
  let body = print_body(document, limits~)
  guard body.output is Some(text) else { return body }
  let word = document.kind().word()
  let out = StringBuilder()
  out.write_string("@start" + word)
  if name_of(document) is Some(name) {
    out.write_string(" " + name)
  }
  out.write_char('\n')
  for statement in statements_of(document) {
    for line in statement.lines() {
      out.write_string(line)
      out.write_char('\n')
    }
  }
  out.write_string(text)
  out.write_string("@end" + word + "\n")
  let output = out.to_string()
  if output.length() > limits.output {
    return {
      output: None,
      diagnostics: [
        @diagrams.Diagnostic::error(
          "plantuml/print-limit", "Output limit exceeded",
        ),
      ],
    }
  }
  { output: Some(output), diagnostics: [], }
}

///|
pub struct Conversion {
  diagram : @render.Diagram?
  diagnostics : ReadOnlyArray[@diagrams.Diagnostic]
}

///|
/// Convert a document to its format-independent model.
pub fn to_diagram(
  document : Document,
  id? : String,
  limits~ : @envelope.Limits = @envelope.Limits::new(),
) -> Conversion {
  match document {
    Regex(d) => {
      let c = @regex_syntax.to_regex(d, id=id.unwrap_or("regex"), limits~)
      { diagram: c.regex.map(m => Regex(m)), diagnostics: c.diagnostics, }
    }
    Ebnf(d) => {
      let c = @ebnf_syntax.to_grammar(d, id=id.unwrap_or("ebnf"), limits~)
      { diagram: c.grammar.map(g => Ebnf(g)), diagnostics: c.diagnostics, }
    }
    Json(d) => {
      let c = @json_syntax.to_data_tree(d, id=id.unwrap_or("json"), limits~)
      { diagram: c.tree.map(t => DataTree(t)), diagnostics: c.diagnostics, }
    }
    Activity(d) => {
      let c = @activity.to_flowchart(d, id=id.unwrap_or("activity"), limits~)
      {
        diagram: c.flowchart.map(m => Flowchart(m)),
        diagnostics: c.diagnostics,
      }
    }
    Yaml(d) => {
      let c = @yaml_syntax.to_data_tree(d, id=id.unwrap_or("yaml"), limits~)
      { diagram: c.tree.map(t => DataTree(t)), diagnostics: c.diagnostics, }
    }
  }
}

///|
/// Read, convert and render one native block as standalone SVG with the
/// options of `renderer`, including any supplied trace highlights.
/// Diagnostics accumulate; any error gives no output.
pub fn render_svg(
  source : String,
  renderer~ : @svg.SvgRenderer = @svg.SvgRenderer::new(),
  limits~ : @envelope.Limits = @envelope.Limits::new(),
  id? : String,
) -> @render.Rendered {
  let read = read(source, limits~)
  let diagnostics = read.diagnostics.iter().collect()
  if diagnostics.iter().any(d => d.severity == Error) {
    return @render.Rendered::failed("image/svg+xml", diagnostics)
  }
  guard read.document is Some(document) else {
    return @render.Rendered::failed("image/svg+xml", diagnostics)
  }
  let converted = to_diagram(document, id?, limits~)
  diagnostics.append(converted.diagnostics.iter().collect())
  guard converted.diagram is Some(diagram) else {
    return @render.Rendered::failed("image/svg+xml", diagnostics)
  }
  let rendered = @render.Renderer::render(renderer, diagram)
  diagnostics.append(rendered.diagnostics.iter().collect())
  match rendered.output {
    Some(output) => @render.Rendered::ok(output, "image/svg+xml", diagnostics~)
    None => @render.Rendered::failed("image/svg+xml", diagnostics)
  }
}

///|
/// Read and convert one regex or EBNF block, invoke `provider` once for
/// `input` and render the validated trace. `start_rule` names an EBNF start
/// rule by its rule name. This is the only PlantUML entry point that calls a
/// provider.
pub fn[P : @railroad_trace.RailroadTraceProvider] render_traced_svg(
  source : String,
  provider : P,
  input : String,
  start_rule? : String,
  renderer~ : @svg.SvgRenderer = @svg.SvgRenderer::new(),
  limits~ : @envelope.Limits = @envelope.Limits::new(),
  trace_limits~ : @railroad_trace.TraceLimits = @railroad_trace.TraceLimits::new(),
  matched~ : @svg.ChannelStyle = @svg.ChannelStyle::new(),
  not_matched~ : @svg.ChannelStyle = @svg.ChannelStyle::new(dashed=true),
  taken~ : @svg.ChannelStyle = @svg.ChannelStyle::hidden(),
  failure~ : @svg.ChannelStyle = @svg.ChannelStyle::new(),
) -> @svg.TracedRender {
  fn refused(
    diagnostics : ArrayView[@diagrams.Diagnostic],
  ) -> @svg.TracedRender {
    @svg.TracedRender::refused(diagnostics)
  }
  let read = read(source, limits~)
  guard read.document is Some(document) else {
    return refused(read.diagnostics)
  }
  let converted = to_diagram(document, limits~)
  guard converted.diagram is Some(diagram) else {
    return refused(converted.diagnostics)
  }
  let rule_id = match (diagram, start_rule) {
    (Ebnf(grammar), Some(name)) => {
      let ids = grammar.rules().filter(r => r.name == name).map(r => r.id)
      match ids {
        [id] => Some(id)
        [] =>
          return refused([
            @diagrams.Diagnostic::error(
              "trace/invalid-request",
              "No rule is named '\{name}'",
            ),
          ])
        _ =>
          return refused([
            @diagrams.Diagnostic::error(
              "trace/invalid-request",
              "More than one rule is named '\{name}'",
            ),
          ])
      }
    }
    _ => None
  }
  renderer.render_traced(
    diagram,
    provider,
    input,
    start_rule?=rule_id,
    limits=trace_limits,
    matched~,
    not_matched~,
    taken~,
    failure~,
  )
}

///|
/// The stable diagnostic codes of the PlantUML module.
pub let diagnostic_codes : ReadOnlyArray[String] = ReadOnlyArray::from_iter(
  @envelope.diagnostic_codes
  .iter()
  .concat(@regex_syntax.diagnostic_codes.iter())
  .concat(@ebnf_syntax.diagnostic_codes.iter())
  .concat(@json_syntax.diagnostic_codes.iter())
  .concat(@yaml_syntax.diagnostic_codes.iter())
  .concat(["plantuml/print-limit"].iter()),
)