///|
/// XML parsing error used by the project-local reader.
pub(all) suberror XmlParseError {
  XmlParseError(String)
}

///|
pub struct XmlElement {
  name : String
  attributes : Array[(String, String)]
}

///|
pub fn XmlElement::get(self : XmlElement, name : String) -> String? {
  for pair in self.attributes {
    if pair.0 == name {
      return Some(pair.1)
    }
  }
  None
}

///|
pub(all) enum EventKind {
  Start(XmlElement)
  Empty(XmlElement)
  Text(String)
  End(String)
  Eof
}

///|
pub struct Event {
  kind : EventKind
}

///|
pub struct XmlReader {
  input : Array[Char]
  mut pos : Int
  mut line : Int
  mut column : Int
  tag_stack : Array[String]
  mut saw_root : Bool
  mut root_closed : Bool
}

///|
fn is_xml_whitespace(c : Char) -> Bool {
  c == ' ' || c == '\t' || c == '\n' || c == '\r'
}

///|
fn is_name_char(c : Char) -> Bool {
  (c >= 'A' && c <= 'Z') ||
  (c >= 'a' && c <= 'z') ||
  is_digit(c) ||
  c == '_' ||
  c == ':' ||
  c == '-' ||
  c == '.'
}

///|
fn is_all_whitespace(text : String) -> Bool {
  for c in text {
    if !is_xml_whitespace(c) {
      return false
    }
  }
  true
}

///|
pub fn XmlReader::from_string(text : String) -> XmlReader {
  {
    input: text.to_array(),
    pos: 0,
    line: 1,
    column: 1,
    tag_stack: [],
    saw_root: false,
    root_closed: false,
  }
}

///|
fn XmlReader::peek(self : XmlReader) -> Char? {
  if self.pos < self.input.length() {
    Some(self.input[self.pos])
  } else {
    None
  }
}

///|
fn XmlReader::advance(self : XmlReader) -> Char? {
  if self.pos < self.input.length() {
    let c = self.input[self.pos]
    self.pos += 1
    if c == '\n' {
      self.line += 1
      self.column = 1
    } else {
      self.column += 1
    }
    Some(c)
  } else {
    None
  }
}

///|
fn XmlReader::skip_whitespace(self : XmlReader) -> Unit {
  while self.peek() is Some(c) && is_xml_whitespace(c) {
    let _ = self.advance()
  }
}

///|
fn XmlReader::check_string(self : XmlReader, text : String) -> Bool {
  let chars = text.to_array()
  if self.pos + chars.length() > self.input.length() {
    return false
  }
  for i = 0; i < chars.length(); i = i + 1 {
    if self.input[self.pos + i] != chars[i] {
      return false
    }
  }
  true
}

///|
fn XmlReader::skip_string(self : XmlReader, text : String) -> Bool {
  if !self.check_string(text) {
    return false
  }
  for i = 0; i < text.length(); i = i + 1 {
    let _ = self.advance()
  }
  true
}

///|
fn XmlReader::read_name(self : XmlReader) -> String raise XmlParseError {
  let buf = StringBuilder()
  while self.peek() is Some(c) && is_name_char(c) {
    buf.write_char(c)
    let _ = self.advance()
  }
  let name = buf.to_string()
  if name == "" {
    raise XmlParseError("expected XML name at line " + self.line.to_string())
  }
  name
}

///|
fn parse_entity_number(raw : String, start : Int, base : Int) -> Int? {
  if start >= raw.length() {
    return None
  }
  let mut value = 0
  for i = start; i < raw.length(); i = i + 1 {
    let c = raw[i].unsafe_to_char()
    let digit = if is_digit(c) {
      c.to_int() - '0'.to_int()
    } else if c >= 'a' && c <= 'f' {
      c.to_int() - 'a'.to_int() + 10
    } else if c >= 'A' && c <= 'F' {
      c.to_int() - 'A'.to_int() + 10
    } else {
      return None
    }
    if digit >= base {
      return None
    }
    value = value * base + digit
  }
  Some(value)
}

///|
fn XmlReader::read_entity(self : XmlReader) -> String raise XmlParseError {
  let _ = self.advance()
  let buf = StringBuilder()
  while self.peek() is Some(c) && c != ';' {
    buf.write_char(c)
    let _ = self.advance()
  }
  match self.peek() {
    Some(';') => {
      let _ = self.advance()
    }
    _ => raise XmlParseError("unterminated entity at line " + self.line.to_string())
  }
  let raw = buf.to_string()
  match raw {
    "amp" => "&"
    "lt" => "<"
    "gt" => ">"
    "quot" => "\""
    "apos" => "'"
    _ =>
      if raw.length() > 0 && raw[0].unsafe_to_char() == '#' {
        let (start, base) = if raw.length() > 1 && raw[1].unsafe_to_char() == 'x' {
          (2, 16)
        } else {
          (1, 10)
        }
        match parse_entity_number(raw, start, base) {
          Some(code) => {
            let out = StringBuilder()
            out.write_char(code.unsafe_to_char())
            out.to_string()
          }
          None => raise XmlParseError("invalid numeric entity &" + raw + ";")
        }
      } else {
        raise XmlParseError("unknown entity &" + raw + ";")
      }
  }
}

///|
fn XmlReader::read_quoted_value(self : XmlReader) -> String raise XmlParseError {
  let quote = match self.advance() {
    Some(c) => c
    None => raise XmlParseError("expected quoted value")
  }
  if quote != '"' && quote != '\'' {
    raise XmlParseError("expected quoted value at line " + self.line.to_string())
  }
  let buf = StringBuilder()
  while self.peek() is Some(c) {
    if c == quote {
      let _ = self.advance()
      return buf.to_string()
    } else if c == '&' {
      buf.write_string(self.read_entity())
    } else {
      buf.write_char(c)
      let _ = self.advance()
    }
  }
  raise XmlParseError("unterminated attribute value at line " + self.line.to_string())
}

///|
fn XmlReader::read_text(self : XmlReader) -> String raise XmlParseError {
  let buf = StringBuilder()
  while self.peek() is Some(c) && c != '<' {
    if c == '&' {
      buf.write_string(self.read_entity())
    } else {
      buf.write_char(c)
      let _ = self.advance()
    }
  }
  buf.to_string()
}

///|
fn XmlReader::skip_until(self : XmlReader, marker : String, label : String) -> Unit raise XmlParseError {
  while !self.check_string(marker) {
    match self.advance() {
      Some(_) => ()
      None => raise XmlParseError("unterminated " + label + " at line " + self.line.to_string())
    }
  }
  let _ = self.skip_string(marker)
}

///|
fn XmlReader::read_markup(self : XmlReader) -> EventKind? raise XmlParseError {
  let _ = self.advance()
  match self.peek() {
    Some('?') => {
      self.skip_until("?>", "processing instruction")
      None
    }
    Some('!') => {
      if self.check_string("!--") {
        let _ = self.skip_string("!--")
        self.skip_until("-->", "comment")
        None
      } else if self.check_string("![CDATA[") {
        let _ = self.skip_string("![CDATA[")
        let buf = StringBuilder()
        while !self.check_string("]]>") {
          match self.advance() {
            Some(c) => buf.write_char(c)
            None => raise XmlParseError("unterminated CDATA at line " + self.line.to_string())
          }
        }
        let _ = self.skip_string("]]>")
        Some(Text(buf.to_string()))
      } else {
        self.skip_until(">", "declaration")
        None
      }
    }
    Some('/') => {
      let _ = self.advance()
      let name = self.read_name()
      self.skip_whitespace()
      match self.peek() {
        Some('>') => {
          let _ = self.advance()
        }
        _ => raise XmlParseError("expected > after closing tag at line " + self.line.to_string())
      }
      if self.tag_stack.length() == 0 {
        raise XmlParseError("unexpected closing tag ")
      }
      let expected = self.tag_stack[self.tag_stack.length() - 1]
      if expected != name {
        raise XmlParseError("closing tag  does not match <" + expected + ">")
      }
      let _ = self.tag_stack.remove(self.tag_stack.length() - 1)
      if self.tag_stack.length() == 0 {
        self.root_closed = true
      }
      Some(End(name))
    }
    Some(_) => {
      let name = self.read_name()
      let attributes : Array[(String, String)] = []
      let mut empty = false
      for ;; {
        self.skip_whitespace()
        match self.peek() {
          Some('>') => {
            let _ = self.advance()
            break
          }
          Some('/') => {
            let _ = self.advance()
            match self.peek() {
              Some('>') => {
                let _ = self.advance()
                empty = true
                break
              }
              _ => raise XmlParseError("expected /> at line " + self.line.to_string())
            }
          }
          Some(_) => {
            let attr_name = self.read_name()
            self.skip_whitespace()
            match self.advance() {
              Some('=') => ()
              _ => raise XmlParseError("expected = after attribute name")
            }
            self.skip_whitespace()
            let attr_value = self.read_quoted_value()
            attributes.push((attr_name, attr_value))
          }
          None => raise XmlParseError("unterminated start tag")
        }
      }
      let element = { name, attributes, }
      if empty {
        if self.tag_stack.length() == 0 {
          self.saw_root = true
          self.root_closed = true
        }
        Some(Empty(element))
      } else {
        if self.tag_stack.length() == 0 {
          self.saw_root = true
        }
        self.tag_stack.push(name)
        Some(Start(element))
      }
    }
    None => raise XmlParseError("unexpected end of input")
  }
}

///|
pub fn XmlReader::read_event(self : XmlReader) -> Event raise XmlParseError {
  for ;; {
    if self.pos >= self.input.length() {
      if self.tag_stack.length() > 0 {
        raise XmlParseError(
          "unclosed element <" + self.tag_stack[self.tag_stack.length() - 1] + ">",
        )
      }
      return { kind: Eof, }
    }
    if self.peek() is Some('<') {
      match self.read_markup() {
        Some(kind) => return { kind, }
        None => continue
      }
    } else {
      let text = self.read_text()
      if text == "" {
        continue
      }
      if !self.saw_root && is_all_whitespace(text) {
        continue
      }
      if self.root_closed && !is_all_whitespace(text) {
        raise XmlParseError("content after root element")
      }
      return { kind: Text(text), }
    }
  }
}