///|
/// Configuration for formatting Gherkin output.
///
/// Use `WriteConfig::default()` for standard formatting, then
/// override specific fields via struct update syntax:
///
/// ```
/// let config = { ..WriteConfig::default(), indent: "    " }
/// ```
pub(all) struct WriteConfig {
  indent : String
  table_cell_padding : Int
  trailing_newline : Bool
  include_comments : Bool
} derive(Debug, Eq)

///|
/// Create a `WriteConfig` with standard Gherkin formatting defaults.
pub fn WriteConfig::default() -> WriteConfig {
  {
    indent: "  ",
    table_cell_padding: 1,
    trailing_newline: true,
    include_comments: true,
  }
}

///|
/// A push-based Gherkin writer for building Gherkin text from arbitrary sources.
///
/// Manages indentation and formatting automatically. Call structural
/// methods (`feature`, `scenario`, etc.) with matching `end_*` methods,
/// and leaf methods (`step`, `tags`, etc.) for content.
///
/// Implements `GherkinHandler`, enabling reader-to-writer piping:
///
/// ```
/// let w = GherkinWriter::new()
/// parse_with_handler(source, w)
/// let output = w.to_string()
/// ```
pub struct GherkinWriter {
  priv buf : Array[String]
  priv config : WriteConfig
  priv mut depth : Int
  priv mut needs_blank : Bool
}

///|
/// Create a new `GherkinWriter` with the given configuration.
pub fn GherkinWriter::new(
  config? : WriteConfig = WriteConfig::default(),
) -> GherkinWriter {
  { buf: [], config, depth: 0, needs_blank: false, }
}

///|
/// Return the accumulated Gherkin text.
pub fn GherkinWriter::to_string(self : GherkinWriter) -> String {
  let result = self.buf.join("\n")
  if self.config.trailing_newline && result.length() > 0 {
    result + "\n"
  } else {
    result
  }
}

// ---------------------------------------------------------------------------
// Private helpers
// ---------------------------------------------------------------------------

///|
fn GherkinWriter::current_indent(self : GherkinWriter) -> String {
  let mut s = ""
  for i = 0; i < self.depth; i = i + 1 {
    s = s + self.config.indent
  }
  s
}

///|
/// Emit blank line if needed, then emit an indented line.
fn GherkinWriter::emit_line(self : GherkinWriter, text : String) -> Unit {
  if self.needs_blank {
    self.buf.push("")
    self.needs_blank = false
  }
  self.buf.push(self.current_indent() + text)
}

///|
/// Split a string by newline characters.
fn split_lines(s : String) -> Array[String] {
  let lines : Array[String] = []
  let mut current : Array[Char] = []
  for c in s.iter() {
    if c == '\n' {
      lines.push(String::from_array(current))
      current = []
    } else if c != '\r' {
      current.push(c)
    }
  }
  lines.push(String::from_array(current))
  lines
}

///|
/// Format table rows as column-aligned pipe-delimited lines.
/// Returns an array of formatted row strings (without leading indent).
fn format_table(rows : Array[Array[String]], padding : Int) -> Array[String] {
  if rows.is_empty() {
    return []
  }
  let num_cols = rows[0].length()
  // Calculate max width per column
  let widths : Array[Int] = Array::make(num_cols, 0)
  for row in rows {
    for i, cell in row {
      if cell.length() > widths[i] {
        widths[i] = cell.length()
      }
    }
  }
  // Build padding string
  let mut pad = ""
  for i = 0; i < padding; i = i + 1 {
    pad = pad + " "
  }
  // Format each row
  let result : Array[String] = []
  for row in rows {
    let mut line = "|"
    for i, cell in row {
      // Right-pad cell value to column width
      let mut padded = cell
      let needed = widths[i] - cell.length()
      for j = 0; j < needed; j = j + 1 {
        padded = padded + " "
      }
      line = line + pad + padded + pad + "|"
    }
    result.push(line)
  }
  result
}

// ---------------------------------------------------------------------------
// Structural methods (start/end pairs manage indent depth)
// ---------------------------------------------------------------------------

///|
/// Emit a feature header. Call `end_feature()` when done.
///
/// When `language` is not `"en"`, a `# language:` directive is emitted first.
pub fn GherkinWriter::feature(
  self : GherkinWriter,
  keyword : String,
  name : String,
  language? : String = "en",
  description? : String = "",
) -> Unit {
  if language != "en" {
    self.buf.push("# language: " + language)
  }
  self.emit_line(keyword + ": " + name)
  self.depth = 1
  if description.length() > 0 {
    for line in split_lines(description) {
      self.buf.push(self.current_indent() + line)
    }
  }
  self.needs_blank = true
}

///|
/// Close the current feature scope.
pub fn GherkinWriter::end_feature(self : GherkinWriter) -> Unit {
  self.depth = 0
}

///|
/// Emit a scenario header. Call `end_scenario()` when done.
pub fn GherkinWriter::scenario(
  self : GherkinWriter,
  keyword : String,
  name : String,
  description? : String = "",
) -> Unit {
  self.emit_line(keyword + ": " + name)
  self.depth += 1
  if description.length() > 0 {
    for line in split_lines(description) {
      self.buf.push(self.current_indent() + line)
    }
  }
}

///|
/// Close the current scenario scope.
pub fn GherkinWriter::end_scenario(self : GherkinWriter) -> Unit {
  self.depth -= 1
  self.needs_blank = true
}

///|
/// Emit a background header. Call `end_background()` when done.
pub fn GherkinWriter::background(
  self : GherkinWriter,
  keyword : String,
  name? : String = "",
  description? : String = "",
) -> Unit {
  let header = if name.length() > 0 {
    keyword + ": " + name
  } else {
    keyword + ":"
  }
  self.emit_line(header)
  self.depth += 1
  if description.length() > 0 {
    for line in split_lines(description) {
      self.buf.push(self.current_indent() + line)
    }
  }
}

///|
/// Close the current background scope.
pub fn GherkinWriter::end_background(self : GherkinWriter) -> Unit {
  self.depth -= 1
  self.needs_blank = true
}

///|
/// Emit a rule header. Call `end_rule()` when done.
pub fn GherkinWriter::rule(
  self : GherkinWriter,
  keyword : String,
  name : String,
  description? : String = "",
) -> Unit {
  self.emit_line(keyword + ": " + name)
  self.depth += 1
  if description.length() > 0 {
    for line in split_lines(description) {
      self.buf.push(self.current_indent() + line)
    }
  }
  self.needs_blank = true
}

///|
/// Close the current rule scope.
pub fn GherkinWriter::end_rule(self : GherkinWriter) -> Unit {
  self.depth -= 1
  self.needs_blank = true
}

// ---------------------------------------------------------------------------
// Leaf methods
// ---------------------------------------------------------------------------

///|
/// Emit a step line.
pub fn GherkinWriter::step(
  self : GherkinWriter,
  keyword : String,
  text : String,
) -> Unit {
  self.emit_line(keyword + text)
}

///|
/// Emit a doc string block after a step.
pub fn GherkinWriter::doc_string(
  self : GherkinWriter,
  content : String,
  delimiter? : String = "\"\"\"",
  media_type? : String? = None,
) -> Unit {
  let saved_depth = self.depth
  self.depth += 1
  let indent = self.current_indent()
  let opener = match media_type {
    Some(mt) => delimiter + mt
    None => delimiter
  }
  self.buf.push(indent + opener)
  for line in split_lines(content) {
    self.buf.push(indent + line)
  }
  self.buf.push(indent + delimiter)
  self.depth = saved_depth
}

///|
/// Emit a data table with column-aligned cells after a step.
///
/// Each inner array is one row of cell values. All rows must have
/// the same number of elements.
pub fn GherkinWriter::data_table(
  self : GherkinWriter,
  rows : Array[Array[String]],
) -> Unit {
  let saved_depth = self.depth
  self.depth += 1
  let indent = self.current_indent()
  let formatted = format_table(rows, self.config.table_cell_padding)
  for line in formatted {
    self.buf.push(indent + line)
  }
  self.depth = saved_depth
}

///|
/// Emit an examples section with a pretty-printed table.
///
/// `header` is the column names, `body` is the data rows.
/// Both are arrays of cell value strings.
pub fn GherkinWriter::examples(
  self : GherkinWriter,
  keyword : String,
  name? : String = "",
  description? : String = "",
  header? : Array[String] = [],
  body? : Array[Array[String]] = [],
) -> Unit {
  self.needs_blank = true
  let kw_line = if name.length() > 0 {
    keyword + ": " + name
  } else {
    keyword + ":"
  }
  self.emit_line(kw_line)
  if description.length() > 0 {
    let saved = self.depth
    self.depth += 1
    for line in split_lines(description) {
      self.buf.push(self.current_indent() + line)
    }
    self.depth = saved
  }
  // Build table rows from header + body
  let all_rows : Array[Array[String]] = []
  if header.length() > 0 {
    all_rows.push(header)
  }
  for row in body {
    all_rows.push(row)
  }
  if all_rows.length() > 0 {
    let saved_depth = self.depth
    self.depth += 1
    let indent = self.current_indent()
    let formatted = format_table(all_rows, self.config.table_cell_padding)
    for line in formatted {
      self.buf.push(indent + line)
    }
    self.depth = saved_depth
  }
}

///|
/// Emit tags on one line, space-separated.
///
/// Call before the structural element the tags decorate.
/// Tags are emitted at the current indent level.
pub fn GherkinWriter::tags(self : GherkinWriter, names : Array[String]) -> Unit {
  if names.is_empty() {
    return
  }
  self.emit_line(names.join(" "))
}

///|
/// Emit a comment line.
pub fn GherkinWriter::comment(self : GherkinWriter, text : String) -> Unit {
  self.emit_line(text)
}

// ---------------------------------------------------------------------------
// write() DOM function
// ---------------------------------------------------------------------------

///|
/// Write a `GherkinDocument` AST as formatted Gherkin text.
///
/// Produces well-formatted output with column-aligned tables,
/// properly indented steps, and preserved i18n keywords.
///
/// This is the inverse of `parse`: calling `write(parse(source))`
/// produces valid Gherkin that, when re-parsed, yields an equivalent AST.
pub fn write(
  doc : GherkinDocument,
  config? : WriteConfig = WriteConfig::default(),
) -> String {
  let w = GherkinWriter::new(config~)
  if config.include_comments {
    for c in doc.comments {
      w.comment(c.text)
    }
  }
  match doc.feature {
    Some(f) => write_feature(w, f)
    None => ()
  }
  w.to_string()
}

///|
fn write_feature(w : GherkinWriter, f : Feature) -> Unit {
  if f.tags.length() > 0 {
    w.tags(f.tags.map(fn(t) { t.name }))
  }
  w.feature(f.keyword, f.name, language=f.language, description=f.description)
  for child in f.children {
    match child {
      FeatureChild::Background(bg) => write_background(w, bg)
      FeatureChild::Scenario(s) => write_scenario(w, s)
      FeatureChild::Rule(r) => write_rule(w, r)
    }
  }
  w.end_feature()
}

///|
fn write_scenario(w : GherkinWriter, s : Scenario) -> Unit {
  if s.tags.length() > 0 {
    w.tags(s.tags.map(fn(t) { t.name }))
  }
  w.scenario(s.keyword, s.name, description=s.description)
  for step in s.steps {
    write_step(w, step)
  }
  for e in s.examples {
    write_examples(w, e)
  }
  w.end_scenario()
}

///|
fn write_background(w : GherkinWriter, bg : Background) -> Unit {
  w.background(bg.keyword, description=bg.description)
  for step in bg.steps {
    write_step(w, step)
  }
  w.end_background()
}

///|
fn write_rule(w : GherkinWriter, r : Rule) -> Unit {
  if r.tags.length() > 0 {
    w.tags(r.tags.map(fn(t) { t.name }))
  }
  w.rule(r.keyword, r.name, description=r.description)
  for child in r.children {
    match child {
      RuleChild::Background(bg) => write_background(w, bg)
      RuleChild::Scenario(s) => write_scenario(w, s)
    }
  }
  w.end_rule()
}

///|
fn write_step(w : GherkinWriter, s : Step) -> Unit {
  w.step(s.keyword, s.text)
  match s.argument {
    Some(StepArgument::DocString(ds)) =>
      w.doc_string(ds.content, delimiter=ds.delimiter, media_type=ds.media_type)
    Some(StepArgument::DataTable(dt)) => {
      let rows : Array[Array[String]] = []
      for row in dt.rows {
        rows.push(row.cells.map(fn(c) { c.value }))
      }
      w.data_table(rows)
    }
    None => ()
  }
}

///|
fn write_examples(w : GherkinWriter, e : Examples) -> Unit {
  let header : Array[String] = match e.table_header {
    Some(row) => row.cells.map(fn(c) { c.value })
    None => []
  }
  let body : Array[Array[String]] = []
  for row in e.table_body {
    body.push(row.cells.map(fn(c) { c.value }))
  }
  w.examples(e.keyword, name=e.name, description=e.description, header~, body~)
}

// ---------------------------------------------------------------------------
// GherkinHandler implementation (enables parse_with_handler → writer piping)
// ---------------------------------------------------------------------------

///|
pub impl GherkinHandler for GherkinWriter with fn on_feature(self, e) {
  if e.tags.length() > 0 {
    self.tags(e.tags.map(fn(t) { t.name }))
  }
  self.feature(
    e.keyword,
    e.name,
    language=e.language,
    description=e.description,
  )
}

///|
pub impl GherkinHandler for GherkinWriter with fn on_end_feature(self) {
  self.end_feature()
}

///|
pub impl GherkinHandler for GherkinWriter with fn on_scenario(self, e) {
  if e.tags.length() > 0 {
    self.tags(e.tags.map(fn(t) { t.name }))
  }
  self.scenario(e.keyword, e.name, description=e.description)
}

///|
pub impl GherkinHandler for GherkinWriter with fn on_end_scenario(self) {
  self.end_scenario()
}

///|
pub impl GherkinHandler for GherkinWriter with fn on_background(self, e) {
  self.background(e.keyword, description=e.description)
}

///|
pub impl GherkinHandler for GherkinWriter with fn on_end_background(self) {
  self.end_background()
}

///|
pub impl GherkinHandler for GherkinWriter with fn on_rule(self, e) {
  if e.tags.length() > 0 {
    self.tags(e.tags.map(fn(t) { t.name }))
  }
  self.rule(e.keyword, e.name, description=e.description)
}

///|
pub impl GherkinHandler for GherkinWriter with fn on_end_rule(self) {
  self.end_rule()
}

///|
pub impl GherkinHandler for GherkinWriter with fn on_step(self, e) {
  self.step(e.keyword, e.text)
}

///|
pub impl GherkinHandler for GherkinWriter with fn on_doc_string(self, e) {
  self.doc_string(e.content, delimiter=e.delimiter, media_type=e.media_type)
}

///|
pub impl GherkinHandler for GherkinWriter with fn on_data_table(self, e) {
  let rows : Array[Array[String]] = []
  for row in e.rows {
    rows.push(row.cells.map(fn(c) { c.value }))
  }
  self.data_table(rows)
}

///|
pub impl GherkinHandler for GherkinWriter with fn on_examples(self, e) {
  let header : Array[String] = match e.table_header {
    Some(row) => row.cells.map(fn(c) { c.value })
    None => []
  }
  let body : Array[Array[String]] = []
  for row in e.table_body {
    body.push(row.cells.map(fn(c) { c.value }))
  }
  self.examples(
    e.keyword,
    name=e.name,
    description=e.description,
    header~,
    body~,
  )
}

///|
pub impl GherkinHandler for GherkinWriter with fn on_comment(self, e) {
  if self.config.include_comments {
    self.comment(e.text)
  }
}

///|
pub extend WriteConfig with @moonbitlang/core/debug.Debug::{to_repr}

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

///|
pub extend GherkinWriter with GherkinHandler::{
  on_step,
  on_end_document,
  on_tag,
  on_doc_string,
  on_examples,
  on_end_feature,
  on_end_rule,
  on_document,
  on_data_table,
  on_background,
  on_comment,
  on_feature,
  on_scenario,
  on_rule,
  on_end_background,
  on_end_scenario,
}