// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

///|
/// Create a new writer
pub fn Writer::new() -> Writer {
  {
    buffer: StringBuilder(),
    element_stack: [],
    seen_root: false,
    wrote_anything: false,
    doctype_root: None,
  }
}

///|
/// Write an XML event
pub fn Writer::write_event(
  self : Writer,
  event : Event,
) -> Unit raise WriterError {
  match event.kind {
    Start(elem) =>
      self.start_element(
        elem.name,
        elem.attributes.map(fn(attribute) { (attribute.name, attribute.value) }),
      )
    End(name) => self.end_element(name)
    Empty(elem) =>
      self.empty_element(
        elem.name,
        elem.attributes.map(fn(attribute) { (attribute.name, attribute.value) }),
      )
    Text(content) => self.text(content)
    CData(content) => self.cdata(content)
    Comment(content) => self.comment(content)
    PI(target~, data~) => self.write_processing_instruction(target, data)
    Decl(version~, encoding~, standalone~) =>
      self.write_declaration(version, encoding, standalone)
    DocType(name) => self.write_doctype(name)
    Eof => self.validate_complete()
  }
}

///|
/// Write a start element
pub fn Writer::start_element(
  self : Writer,
  name : String,
  attributes : Array[(String, String)],
) -> Unit raise WriterError {
  validate_writer_name(name, "element")
  validate_writer_attributes(attributes)
  self.record_element_start(name)
  self.write_opening_tag(name, attributes)
  self.buffer.write_string(">")
  self.element_stack.push(name)
  self.wrote_anything = true
}

///|
/// Write an end element
pub fn Writer::end_element(
  self : Writer,
  name : String,
) -> Unit raise WriterError {
  validate_writer_name(name, "element")
  if self.element_stack.length() == 0 {
    raise InvalidStructure("end element without a matching start: " + name)
  }
  let expected = self.element_stack[self.element_stack.length() - 1]
  if expected != name {
    raise InvalidStructure(
      "end element mismatch: expected " + expected + ", found " + name,
    )
  }
  self.buffer.write_string("")
  let _ = self.element_stack.pop()
  self.wrote_anything = true
}

///|
/// Write a self-closing element
pub fn Writer::empty_element(
  self : Writer,
  name : String,
  attributes : Array[(String, String)],
) -> Unit raise WriterError {
  validate_writer_name(name, "element")
  validate_writer_attributes(attributes)
  self.record_element_start(name)
  self.write_opening_tag(name, attributes)
  self.buffer.write_string("/>")
  self.wrote_anything = true
}

///|
/// Write text content (escaped)
pub fn Writer::text(self : Writer, content : String) -> Unit raise WriterError {
  validate_writer_content(content, "text")
  if self.element_stack.length() == 0 {
    for c in content {
      if !is_whitespace(c) {
        raise InvalidStructure(
          "non-whitespace text is not allowed outside the root",
        )
      }
    }
  }
  self.buffer.write_string(escape(content))
  if content.length() > 0 {
    self.wrote_anything = true
  }
}

///|
/// Write CDATA section
pub fn Writer::cdata(self : Writer, content : String) -> Unit raise WriterError {
  validate_writer_content(content, "CDATA")
  if content.find("]]>") is Some(_) {
    raise InvalidContent("CDATA cannot contain ']]>'")
  }
  if self.element_stack.length() == 0 {
    raise InvalidStructure("CDATA is only allowed inside an element")
  }
  self.buffer.write_string("")
  self.wrote_anything = true
}

///|
/// Write a comment
pub fn Writer::comment(
  self : Writer,
  content : String,
) -> Unit raise WriterError {
  validate_writer_content(content, "comment")
  if content.find("--") is Some(_) || content.has_suffix("-") {
    raise InvalidContent("comment cannot contain '--' or end with '-'")
  }
  self.buffer.write_string("")
  self.wrote_anything = true
}

///|
/// Get the generated XML string
pub fn Writer::to_string(self : Writer) -> String raise WriterError {
  self.validate_complete()
  self.buffer.to_string()
}

///|
/// Escape attribute value (escapes < > & " ')
fn escape_attr(text : String) -> String {
  escape(text)
}

///|
fn validate_writer_name(name : String, kind : String) -> Unit raise WriterError {
  let chars = name.to_array()
  if chars.length() == 0 || !is_name_start_char(chars[0]) {
    raise InvalidName("invalid " + kind + " name: " + name)
  }
  for i = 1; i < chars.length(); i = i + 1 {
    if !is_name_char(chars[i]) {
      raise InvalidName("invalid " + kind + " name: " + name)
    }
  }
}

///|
fn validate_writer_attributes(
  attributes : Array[(String, String)],
) -> Unit raise WriterError {
  let seen : Map[String, Bool] = Map([])
  for attribute in attributes {
    validate_writer_name(attribute.0, "attribute")
    validate_writer_content(attribute.1, "attribute value")
    if seen.contains(attribute.0) {
      raise InvalidStructure("duplicate attribute: " + attribute.0)
    }
    seen.set(attribute.0, true)
  }
}

///|
fn validate_writer_content(
  content : String,
  kind : String,
) -> Unit raise WriterError {
  for c in content {
    if !is_valid_xml_content_char(c.to_int()) {
      raise InvalidContent("invalid XML character in " + kind)
    }
  }
}

///|
fn Writer::record_element_start(
  self : Writer,
  name : String,
) -> Unit raise WriterError {
  if self.element_stack.length() == 0 {
    if self.seen_root {
      raise InvalidStructure("document cannot contain multiple root elements")
    }
    match self.doctype_root {
      Some(expected) if expected != name =>
        raise InvalidStructure(
          "root element mismatch: DOCTYPE declares " +
          expected +
          ", found " +
          name,
        )
      _ => ()
    }
    self.seen_root = true
  }
}

///|
fn Writer::write_opening_tag(
  self : Writer,
  name : String,
  attributes : Array[(String, String)],
) -> Unit {
  self.buffer.write_string("<")
  self.buffer.write_string(name)
  for attribute in attributes {
    self.buffer.write_string(" ")
    self.buffer.write_string(attribute.0)
    self.buffer.write_string("=\"")
    self.buffer.write_string(escape_attr(attribute.1))
    self.buffer.write_string("\"")
  }
}

///|
fn Writer::validate_complete(self : Writer) -> Unit raise WriterError {
  if self.element_stack.length() > 0 {
    let name = self.element_stack[self.element_stack.length() - 1]
    raise InvalidStructure("unclosed element: " + name)
  }
  if !self.seen_root {
    raise InvalidStructure("document has no root element")
  }
}

///|
fn Writer::write_declaration(
  self : Writer,
  version : String,
  encoding : String?,
  standalone : String?,
) -> Unit raise WriterError {
  if self.wrote_anything {
    raise InvalidDeclaration("XML declaration must be the first output")
  }
  if !is_valid_version(version) {
    raise InvalidDeclaration("invalid XML version: " + version)
  }
  match encoding {
    Some(value) if !is_valid_encoding_name(value) =>
      raise InvalidDeclaration("invalid encoding name: " + value)
    _ => ()
  }
  match standalone {
    Some(value) if value != "yes" && value != "no" =>
      raise InvalidDeclaration("standalone must be 'yes' or 'no'")
    _ => ()
  }
  self.buffer.write_string(" {
      self.buffer.write_string(" encoding=\"")
      self.buffer.write_string(value)
      self.buffer.write_string("\"")
    }
    None => ()
  }
  match standalone {
    Some(value) => {
      self.buffer.write_string(" standalone=\"")
      self.buffer.write_string(value)
      self.buffer.write_string("\"")
    }
    None => ()
  }
  self.buffer.write_string("?>")
  self.wrote_anything = true
}

///|
fn Writer::write_processing_instruction(
  self : Writer,
  target : String,
  data : String,
) -> Unit raise WriterError {
  validate_writer_name(target, "processing instruction target")
  if target.to_lower() == "xml" {
    raise InvalidName("processing instruction target cannot be XML")
  }
  validate_writer_content(data, "processing instruction data")
  if data.find("?>") is Some(_) {
    raise InvalidContent("processing instruction data cannot contain '?>'")
  }
  self.buffer.write_string(" 0 {
    self.buffer.write_string(" ")
    self.buffer.write_string(data)
  }
  self.buffer.write_string("?>")
  self.wrote_anything = true
}

///|
fn Writer::write_doctype(
  self : Writer,
  content : String,
) -> Unit raise WriterError {
  if self.seen_root || self.element_stack.length() > 0 {
    raise InvalidStructure("DOCTYPE must appear before the root element")
  }
  if self.doctype_root is Some(_) {
    raise InvalidStructure(
      "document cannot contain multiple DOCTYPE declarations",
    )
  }
  validate_writer_name(content, "DOCTYPE")
  self.buffer.write_string("")
  self.wrote_anything = true
  self.doctype_root = Some(content)
}