// 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 reader from a string
pub fn Reader::from_string(input : String) -> Reader {
  let chars = input.to_array()
  // Skip BOM (Byte Order Mark, U+FEFF) if present at start
  let start_pos = if chars.length() > 0 && chars[0] == '\u{FEFF}' {
    1
  } else {
    0
  }
  {
    input: chars,
    pos: start_pos,
    offset: start_pos,
    line: 1,
    column: 1,
    entities: Map([]),
    entity_expansion_remaining: Ref(MAX_ENTITY_EXPANSION_CHARS),
    entity_stack: [],
    namespace_aware: false,
    attr_types: Map([]),
    default_attributes: Map([]),
    tag_stack: [],
    seen_root: false,
    seen_doctype: false,
    root_closed: false,
    seen_content: false,
    just_saw_decl: false,
    pending_events: [],
    pending_index: 0,
    internal_subset_events: [],
    just_saw_doctype: false,
    in_entity_content: false,
    in_reference: false,
  }
}

///|
/// Check if the reader has reached the end
pub fn Reader::is_eof(self : Reader) -> Bool {
  self.pos >= self.input.length()
}

///|
/// Get current line number (1-indexed)
pub fn Reader::line(self : Reader) -> Int32 {
  self.line
}

///|
/// Get current column number (1-indexed)
pub fn Reader::column(self : Reader) -> Int32 {
  self.column
}

///|
fn Reader::source_position(self : Reader) -> SourcePosition {
  { offset: self.offset, line: self.line, column: self.column, }
}

///|
fn source_span(start : SourcePosition, end : SourcePosition) -> SourceSpan {
  { start, end, }
}

///|
fn Reader::consume_entity_output(
  self : Reader,
  amount : Int32,
) -> Unit raise XmlErrorKind {
  if amount > self.entity_expansion_remaining.val {
    raise InvalidEntity("entity expansion limit exceeded")
  }
  self.entity_expansion_remaining.val -= amount
}

///|
fn Reader::expand_attribute_entity_value(
  self : Reader,
  value : String,
) -> String raise XmlErrorKind {
  let budget : EntityExpansionBudget = {
    remaining: self.entity_expansion_remaining,
  }
  let seen = Map::from_array(self.entity_stack.map(fn(name) { (name, true) }))
  expand_entity_value_with_budget(
    value,
    self.entities,
    seen,
    budget,
    self.entity_stack.length(),
  )
}

///|
fn Reader::normalize_input_content(self : Reader, value : String) -> String {
  if self.in_entity_content {
    value
  } else {
    normalize_line_endings(value)
  }
}

///|
fn Reader::push_pending_event(self : Reader, event : Event) -> Unit {
  self.pending_events.push(event)
}

///|
fn Reader::pop_pending_event(self : Reader) -> Event {
  let event = self.pending_events[self.pending_index]
  self.pending_index += 1
  if self.pending_index == self.pending_events.length() {
    self.pending_events.clear()
    self.pending_index = 0
  }
  event
}

///|
/// Read all events until EOF (inclusive) as an Array.
/// Includes the final Eof event in the result.
/// Raises XmlError if parsing fails.
pub fn Reader::read_events_until_eof(
  self : Reader,
) -> Array[Event] raise XmlError {
  let events : Array[Event] = []
  for ;; {
    let event = self.read_event()
    events.push(event)
    if event.kind is Eof {
      return events
    }
  }
}

///|
/// Peek at current character without advancing
fn Reader::peek(self : Reader) -> Char? {
  if self.pos < self.input.length() {
    Some(self.input[self.pos])
  } else {
    None
  }
}

///|
/// Peek at character at offset from current position
fn Reader::peek_at(self : Reader, offset : Int32) -> Char? {
  let idx = self.pos + offset
  if idx < self.input.length() {
    Some(self.input[idx])
  } else {
    None
  }
}

///|
/// Advance by one character
fn Reader::advance(self : Reader) -> Char? raise XmlErrorKind {
  if self.pos < self.input.length() {
    let c = self.input[self.pos]
    if self.in_entity_content && !self.in_reference {
      self.consume_entity_output(1)
    }
    self.pos += 1
    self.offset += if c.to_int() > 0xFFFF { 2 } else { 1 }
    if c == '\r' {
      self.line += 1
      self.column = 1
    } else if c == '\n' {
      if self.pos < 2 || self.input[self.pos - 2] != '\r' {
        self.line += 1
      }
      self.column = 1
    } else {
      self.column += 1
    }
    Some(c)
  } else {
    None
  }
}

///|
/// Skip whitespace characters
fn Reader::skip_whitespace(self : Reader) -> Unit raise XmlErrorKind {
  while self.peek() is Some(c) && is_whitespace(c) {
    let _ = self.advance()
  }
}

///|
/// Skip whitespace characters and return count of characters skipped
fn Reader::skip_whitespace_count(self : Reader) -> Int32 raise XmlErrorKind {
  let mut count = 0
  while self.peek() is Some(c) && is_whitespace(c) {
    let _ = self.advance()
    count += 1
  }
  count
}

///|
/// Read characters while predicate is true
fn Reader::read_while(
  self : Reader,
  pred : (Char) -> Bool,
) -> String raise XmlErrorKind {
  let buf = StringBuilder()
  while self.peek() is Some(c) && pred(c) {
    buf.write_char(c)
    let _ = self.advance()
  }
  buf.to_string()
}

///|
/// Read until a specific string is found (exclusive)
fn Reader::read_until(self : Reader, end : String) -> String raise XmlErrorKind {
  let buf = StringBuilder()
  let end_chars = end.to_array()
  let end_len = end_chars.length()
  while !self.is_eof() {
    // Check if we're at the end marker
    let mut found = true
    for i = 0; i < end_len; i = i + 1 {
      match self.peek_at(i) {
        Some(c) if c == end_chars[i] => continue
        _ => {
          found = false
          break
        }
      }
    }
    if found {
      return self.normalize_input_content(buf.to_string())
    }
    match self.advance() {
      Some(c) => {
        // Validate character is allowed in XML content
        let code = c.to_int()
        if !is_valid_xml_content_char(code) {
          raise InvalidSyntax("invalid character in content")
        }
        buf.write_char(c)
      }
      None => raise UnexpectedEof
    }
  }
  raise UnexpectedEof
}

///|
/// Skip a specific string
fn Reader::skip_string(self : Reader, s : String) -> Bool raise XmlErrorKind {
  let chars = s.to_array()
  for i = 0; i < chars.length(); i = i + 1 {
    match self.peek_at(i) {
      Some(c) if c == chars[i] => continue
      _ => return false
    }
  }
  // All matched, advance
  for i = 0; i < chars.length(); i = i + 1 {
    let _ = self.advance()
  }
  true
}

///|
/// Check if next chars match a string (without consuming)
fn Reader::check_string(self : Reader, s : String) -> Bool {
  let chars = s.to_array()
  for i = 0; i < chars.length(); i = i + 1 {
    match self.peek_at(i) {
      Some(c) if c == chars[i] => continue
      _ => return false
    }
  }
  true
}

///|
/// Reject non-markup content after the root element with a span covering the
/// complete offending token. Whitespace in the epilog is legal and is not
/// included in the error span.
fn Reader::reject_trailing_content(self : Reader) -> Unit raise XmlErrorKind {
  match self.peek() {
    Some('&') => {
      let _ = self.advance()
      while self.peek() is Some(c) &&
            c != ';' &&
            c != '<' &&
            c != '&' &&
            !is_whitespace(c) {
        let _ = self.advance()
      }
      if self.peek() is Some(';') {
        let _ = self.advance()
      }
      raise InvalidSyntax("character reference after root element")
    }
    Some(c) if c != '<' => {
      while self.peek() is Some(c) && c != '<' && c != '&' {
        let _ = self.advance()
      }
      raise InvalidSyntax("content after root element")
    }
    _ => ()
  }
}

///|
/// Read the next XML event
pub fn Reader::read_event(self : Reader) -> Event raise XmlError {
  if self.is_eof() &&
    self.pending_events.length() == 0 &&
    self.tag_stack.length() > 0 {
    let unclosed = self.tag_stack[self.tag_stack.length() - 1]
    raise At(
      error=InvalidSyntax("unclosed element: <" + unclosed.0 + ">"),
      span=unclosed.1,
    )
  }
  if self.root_closed && self.pending_events.length() == 0 {
    self.skip_whitespace() catch {
      error =>
        raise At(
          error~,
          span=source_span(self.source_position(), self.source_position()),
        )
    }
    let start = self.source_position()
    self.reject_trailing_content() catch {
      error => raise At(error~, span=source_span(start, self.source_position()))
    }
  }
  let start = self.source_position()
  self.read_event_internal() catch {
    error => raise At(error~, span=source_span(start, self.source_position()))
  }
}

///|
fn Reader::read_event_internal(self : Reader) -> Event raise XmlErrorKind {
  if self.just_saw_doctype && !(self.peek() is Some(c) && is_whitespace(c)) {
    self.just_saw_doctype = false
    for event in self.internal_subset_events {
      self.push_pending_event(event)
    }
    self.internal_subset_events.clear()
  }
  // Return pending events first (from split text)
  if self.pending_events.length() > 0 {
    let event = self.pop_pending_event()
    // Skip empty Text events
    match event.kind {
      Text(s) if s == "" => return self.read_event_internal()
      _ => {
        self.seen_content = true
        return event
      }
    }
  }
  if self.is_eof() {
    // Check for unclosed tags
    if self.tag_stack.length() > 0 {
      let unclosed = self.tag_stack[self.tag_stack.length() - 1]
      raise InvalidSyntax("unclosed element: <" + unclosed.0 + ">")
    }
    // Empty document is not valid
    if !self.seen_root {
      raise InvalidSyntax("empty document - no root element")
    }
    let position = self.source_position()
    return { kind: Eof, span: source_span(position, position), }
  }
  let event_start = self.source_position()
  match self.peek() {
    Some('<') => {
      let parsed = self.read_tag(event_start)
      // Track root element state
      match parsed {
        Start(_) => {
          self.seen_content = true
          if self.root_closed {
            raise InvalidSyntax("content after root element")
          } else if self.tag_stack.length() == 1 {
            // Just pushed the root element
            self.seen_root = true
          }
        }
        Empty(_) => {
          self.seen_content = true
          // Empty element: if no root seen yet, this is the root and it's now closed
          if self.root_closed {
            raise InvalidSyntax("content after root element")
          } else if !self.seen_root {
            self.seen_root = true
            self.root_closed = true
          }
        }
        End(_) => {
          self.seen_content = true
          if self.tag_stack.length() == 0 && self.seen_root {
            // Root element just closed
            self.root_closed = true
          }
        }
        CData(_) => {
          self.seen_content = true
          // CDATA only allowed inside elements
          if self.root_closed {
            raise InvalidSyntax("CDATA section after root element")
          }
          if !self.seen_root || self.tag_stack.length() == 0 {
            raise InvalidSyntax("CDATA section not allowed in prolog")
          }
        }
        Text(_) => {
          self.seen_content = true
          self.just_saw_decl = false
        }
        Comment(_) => {
          self.seen_content = true
          self.just_saw_decl = false
        }
        DocType(_) => {
          self.seen_content = true
          self.just_saw_decl = false
          self.just_saw_doctype = true // Track for internal subset event ordering
        }
        PI(_) => {
          self.seen_content = true
          self.just_saw_decl = false
        }
        Decl(_) => {
          self.seen_content = true
          self.just_saw_decl = true // Skip next whitespace
        }
        Eof => ()
      }
      { kind: parsed, span: source_span(event_start, self.source_position()), }
    }
    Some(_) =>
      // Check for non-whitespace content in prolog
      if !self.seen_root {
        // Before root element: only literal whitespace is allowed
        if self.just_saw_decl {
          // Skip whitespace after XML declaration (lxml behavior)
          while self.peek() is Some(c) {
            if c == ' ' || c == '\t' || c == '\n' || c == '\r' {
              let _ = self.advance()
            } else if c == '<' {
              break
            } else if c == '&' {
              raise InvalidSyntax("character reference before root element")
            } else {
              raise InvalidSyntax("content before root element")
            }
          }
          self.just_saw_decl = false
          return self.read_event_internal()
        }
        // Read prolog whitespace
        let text_event = self.read_whitespace_only()
        // Mark that we've seen content
        self.seen_content = true
        // Move internal subset events to pending queue
        if self.just_saw_doctype {
          self.just_saw_doctype = false
          for event in self.internal_subset_events {
            self.push_pending_event(event)
          }
          self.internal_subset_events.clear()
        }
        {
          kind: text_event,
          span: source_span(event_start, self.source_position()),
        }
      } else {
        let text_event = self.read_text()
        // Skip empty Text events (lxml doesn't emit them)
        match text_event.kind {
          Text(s) if s == "" => return self.read_event_internal()
          _ => {
            // Mark that we've seen content (affects XML declaration placement check)
            self.seen_content = true
            text_event
          }
        }
      }
    None => {
      // Check for unclosed tags at EOF
      if self.tag_stack.length() > 0 {
        let unclosed = self.tag_stack[self.tag_stack.length() - 1]
        raise InvalidSyntax("unclosed element: <" + unclosed.0 + ">")
      }
      // Empty document is not valid
      if !self.seen_root {
        raise InvalidSyntax("empty document - no root element")
      }
      let position = self.source_position()
      { kind: Eof, span: source_span(position, position), }
    }
  }
}

///|
/// Read a tag (element, comment, CDATA, PI, DOCTYPE, etc.)
fn Reader::read_tag(
  self : Reader,
  start : SourcePosition,
) -> EventKind raise XmlErrorKind {
  let _ = self.advance() // consume '<'
  match self.peek() {
    Some('/') => self.read_end_tag()
    Some('!') => self.read_special()
    Some('?') => self.read_pi()
    Some(_) => self.read_start_tag(start)
    None => raise UnexpectedEof
  }
}

///|
/// Read an end tag: 
fn Reader::read_end_tag(self : Reader) -> EventKind raise XmlErrorKind {
  let _ = self.advance() // consume '/'
  let name = self.read_name()
  // Empty end tag name is invalid
  if name == "" {
    raise InvalidSyntax("empty end tag name")
  }
  // Validate tag matching
  if self.tag_stack.length() == 0 {
    raise InvalidSyntax("unexpected end tag, no matching start tag")
  }
  let expected = self.tag_stack.pop().unwrap().0
  if expected != name {
    raise InvalidSyntax(
      "end tag mismatch: expected , found ",
    )
  }
  self.skip_whitespace()
  match self.peek() {
    Some('>') => {
      let _ = self.advance()
      End(name)
    }
    _ => raise InvalidSyntax("expected '>' after end tag name")
  }
}

///|
/// Read a start tag or empty element:  or 
fn Reader::read_start_tag(
  self : Reader,
  start : SourcePosition,
) -> EventKind raise XmlErrorKind {
  let name = self.read_name()
  if name == "" {
    raise InvalidSyntax("empty start tag name")
  }
  let attributes = self.read_attributes(name)
  self.skip_whitespace()
  match self.peek() {
    Some('/') => {
      let _ = self.advance() // consume '/'
      // No whitespace allowed between / and > per XML spec
      match self.peek() {
        Some('>') => {
          let _ = self.advance()
          Empty({ name, attributes, })
        }
        _ => raise InvalidSyntax("expected '>' after '/'")
      }
    }
    Some('>') => {
      let _ = self.advance()
      self.tag_stack.push((name, source_span(start, self.source_position())))
      Start({ name, attributes, })
    }
    _ => raise InvalidSyntax("expected '>' or '/>' at end of start tag")
  }
}

///|
/// Read special tags: , , 
fn Reader::read_special(self : Reader) -> EventKind raise XmlErrorKind {
  let _ = self.advance() // consume '!'
  if self.check_string("--") {
    // Comment
    let _ = self.skip_string("--")
    let content = self.read_comment_content()
    Comment(content)
  } else if self.check_string("[CDATA[") {
    // CDATA - validate position before parsing
    if self.root_closed {
      raise InvalidSyntax("CDATA section after root element")
    }
    if !self.seen_root || self.tag_stack.length() == 0 {
      raise InvalidSyntax("CDATA section not allowed in prolog")
    }
    let _ = self.skip_string("[CDATA[")
    let content = self.read_until("]]>")
    let _ = self.skip_string("]]>")
    CData(content)
  } else if self.check_string("DOCTYPE") {
    // DOCTYPE
    if self.seen_root || self.seen_doctype {
      raise InvalidSyntax("DOCTYPE must appear once before the root element")
    }
    let _ = self.skip_string("DOCTYPE")
    if self.skip_whitespace_count() == 0 {
      raise InvalidSyntax("whitespace required after DOCTYPE")
    }
    let name = self.read_name()
    if name == "" {
      raise InvalidSyntax("DOCTYPE requires a name")
    }
    // Parse external ID if present (SYSTEM or PUBLIC)
    let had_whitespace = self.skip_whitespace_count() > 0
    if self.check_string("SYSTEM") {
      if !had_whitespace {
        raise InvalidSyntax("whitespace required before SYSTEM")
      }
      let _ = self.skip_string("SYSTEM")
      if self.skip_whitespace_count() == 0 {
        raise InvalidSyntax("whitespace required before system literal")
      }
      self.skip_quoted_string()
    } else if self.check_string("PUBLIC") {
      if !had_whitespace {
        raise InvalidSyntax("whitespace required before PUBLIC")
      }
      let _ = self.skip_string("PUBLIC")
      if self.skip_whitespace_count() == 0 {
        raise InvalidSyntax("whitespace required before public ID")
      }
      self.read_and_validate_public_id()
      if self.skip_whitespace_count() == 0 {
        raise InvalidSyntax("whitespace required before system literal")
      }
      self.skip_quoted_string()
    }
    self.skip_whitespace()
    // Check for internal subset
    if self.peek() is Some('[') {
      // Parse internal subset [...] and extract entities
      let _ = self.advance() // consume '['
      self.parse_internal_subset()
      self.skip_whitespace()
    }
    if self.peek() != Some('>') {
      raise InvalidSyntax("expected '>' at end of DOCTYPE")
    }
    let _ = self.advance()
    self.seen_doctype = true
    DocType(name)
  } else {
    raise InvalidSyntax("invalid special tag after '"
fn Reader::read_comment_content(self : Reader) -> String raise XmlErrorKind {
  let buf = StringBuilder()
  while !self.is_eof() {
    // Check for "--"
    if self.check_string("--") {
      let _ = self.skip_string("--")
      // Must be followed by ">" to be valid comment end
      match self.peek() {
        Some('>') => {
          let _ = self.advance()
          return self.normalize_input_content(buf.to_string())
        }
        _ => raise InvalidSyntax("'--' is not allowed in comments")
      }
    }
    match self.advance() {
      Some(c) => {
        let code = c.to_int()
        if !is_valid_xml_content_char(code) {
          raise InvalidSyntax("invalid character in comment")
        }
        buf.write_char(c)
      }
      None => raise UnexpectedEof
    }
  }
  raise UnexpectedEof
}

///|
/// Read processing instruction: 
fn Reader::read_pi(self : Reader) -> EventKind raise XmlErrorKind {
  let _ = self.advance() // consume '?'
  let target = self.read_non_element_name()
  // PI target is required
  if target == "" {
    raise InvalidSyntax("processing instruction target is required")
  }
  // Check for "xml" in any case - case-insensitive match for reserved target
  let target_lower = target.to_lower()
  if target_lower == "xml" {
    if target != "xml" {
      // "XML", "Xml", etc. are not valid - only lowercase "xml" is allowed
      raise InvalidSyntax("PI target 'xml' is reserved (case-insensitive)")
    }
    // XML declaration
    self.read_xml_decl()
  } else {
    // After target, must have whitespace before data, or ?> to end
    match self.peek() {
      Some(c) if is_whitespace(c) => {
        // Skip whitespace before data
        let _ = self.advance()
        let data = self.read_pi_data()
        let _ = self.skip_string("?>")
        PI(target~, data~)
      }
      Some('?') =>
        // Check for ?>
        if self.check_string("?>") {
          let _ = self.skip_string("?>")
          PI(target~, data="")
        } else {
          raise InvalidSyntax("expected whitespace or '?>' after PI target")
        }
      _ => raise InvalidSyntax("expected whitespace or '?>' after PI target")
    }
  }
}

///|
/// Read PI data, validating characters
fn Reader::read_pi_data(self : Reader) -> String raise XmlErrorKind {
  let buf = StringBuilder()
  while !self.is_eof() {
    // Check if we're at the end marker ?>
    if self.check_string("?>") {
      return self.normalize_input_content(buf.to_string())
    }
    match self.advance() {
      Some(c) => {
        let code = c.to_int()
        if !is_valid_xml_content_char(code) {
          raise InvalidSyntax("invalid character in processing instruction")
        }
        buf.write_char(c)
      }
      None => raise UnexpectedEof
    }
  }
  raise UnexpectedEof
}

///|
/// Read XML declaration: 
/// Strict validation per XML 1.0 spec:
/// - Pseudo-attributes must be lowercase (version, encoding, standalone)
/// - Order: version (required), encoding (optional), standalone (optional)
/// - No duplicates allowed
/// - standalone must be "yes" or "no"
/// - Whitespace required between pseudo-attributes
fn Reader::read_xml_decl(self : Reader) -> EventKind raise XmlErrorKind {
  // XML declaration must appear at the very beginning of the document
  // Check if any content has been seen (elements, text, DOCTYPE, etc.)
  if self.seen_content {
    raise InvalidSyntax("XML declaration must be at the start of the document")
  }
  // Check if inside element (tag_stack not empty)
  if self.tag_stack.length() > 0 {
    raise InvalidSyntax("XML declaration cannot appear inside an element")
  }
  let mut version = ""
  let mut encoding : String? = None
  let mut standalone : String? = None
  let mut seen_version = false
  let mut seen_encoding = false
  let mut seen_standalone = false
  let mut need_whitespace = false
  if self.skip_whitespace_count() == 0 {
    raise InvalidSyntax("whitespace required before XML version")
  }
  while !self.check_string("?>") {
    // Check for required whitespace between pseudo-attributes
    let had_whitespace = self.skip_whitespace_count() > 0
    if self.check_string("?>") {
      break
    }
    if need_whitespace && !had_whitespace {
      raise InvalidSyntax(
        "whitespace required between XML declaration pseudo-attributes",
      )
    }
    let attr_name = self.read_name()
    if attr_name == "" {
      raise InvalidSyntax("expected XML declaration pseudo-attribute or '?>'")
    }
    self.skip_whitespace()
    match self.peek() {
      Some('=') => {
        let _ = self.advance()
      }
      _ => raise InvalidSyntax("expected '=' in XML declaration")
    }
    self.skip_whitespace()
    // Use raw attribute value (no entity decoding) for XML declaration
    let value = self.read_raw_attr_value()
    match attr_name {
      "version" => {
        if seen_version {
          raise InvalidSyntax("duplicate 'version' in XML declaration")
        }
        if seen_encoding || seen_standalone {
          raise InvalidSyntax("'version' must come first in XML declaration")
        }
        // Validate version format: VersionNum ::= '1.' [0-9]+
        if !is_valid_version(value) {
          raise InvalidSyntax("invalid version: " + value)
        }
        seen_version = true
        version = value
      }
      "encoding" => {
        if seen_encoding {
          raise InvalidSyntax("duplicate 'encoding' in XML declaration")
        }
        if !seen_version {
          raise InvalidSyntax("'version' must come before 'encoding'")
        }
        if seen_standalone {
          raise InvalidSyntax("'encoding' must come before 'standalone'")
        }
        // Validate encoding name: [A-Za-z]([A-Za-z0-9._-])*
        if !is_valid_encoding_name(value) {
          raise InvalidSyntax("invalid encoding name: " + value)
        }
        seen_encoding = true
        encoding = Some(value)
      }
      "standalone" => {
        if seen_standalone {
          raise InvalidSyntax("duplicate 'standalone' in XML declaration")
        }
        if !seen_version {
          raise InvalidSyntax("'version' must come before 'standalone'")
        }
        // standalone must be exactly "yes" or "no" (case-sensitive)
        if value != "yes" && value != "no" {
          raise InvalidSyntax(
            "'standalone' must be 'yes' or 'no', got: " + value,
          )
        }
        seen_standalone = true
        standalone = Some(value)
      }
      _ => {
        // Reject uppercase variants and unknown attributes
        if attr_name == "VERSION" ||
          attr_name == "Version" ||
          (attr_name.to_lower() == "version" && attr_name != "version") {
          raise InvalidSyntax("'version' must be lowercase in XML declaration")
        }
        if attr_name == "ENCODING" ||
          attr_name == "Encoding" ||
          (attr_name.to_lower() == "encoding" && attr_name != "encoding") {
          raise InvalidSyntax("'encoding' must be lowercase in XML declaration")
        }
        if attr_name == "STANDALONE" ||
          attr_name == "Standalone" ||
          (attr_name.to_lower() == "standalone" && attr_name != "standalone") {
          raise InvalidSyntax(
            "'standalone' must be lowercase in XML declaration",
          )
        }
        raise InvalidSyntax(
          "unknown pseudo-attribute in XML declaration: " + attr_name,
        )
      }
    }
    need_whitespace = true
  }
  if !seen_version {
    raise InvalidSyntax("XML declaration requires a version")
  }
  if !self.skip_string("?>") {
    raise InvalidSyntax("expected '?>' at end of XML declaration")
  }

  // Validate UTF-16 encoding - must have BOM if declared as UTF-16
  match encoding {
    Some(enc) => {
      let enc_upper = enc.to_upper()
      if enc_upper == "UTF-16" ||
        enc_upper == "UTF-16LE" ||
        enc_upper == "UTF-16BE" {
        // UTF-16 encoded content should have BOM (U+FEFF) at start
        // If input is ASCII/UTF-8 claiming to be UTF-16, it's invalid
        if self.input.length() > 0 && self.input[0] != '\u{FEFF}' {
          // Check if content looks like ASCII (no high bytes)
          // Real UTF-16 would have null bytes in ASCII text
          let all_ascii = self.input.length() < 100 &&
            self.input.iter().all(fn(c) { c.to_int() < 128 })
          if all_ascii {
            raise InvalidSyntax(
              "encoding declared as UTF-16 but content is ASCII/UTF-8",
            )
          }
        }
      }
    }
    None => ()
  }
  Decl(version~, encoding~, standalone~)
}

///|
/// Read element name (validates XML NameStartChar + NameChar)
fn Reader::read_name(self : Reader) -> String raise XmlErrorKind {
  // First character must be a NameStartChar
  match self.peek() {
    Some(c) if is_name_start_char(c) => {
      let _ = self.advance()
      let buf = StringBuilder()
      buf.write_char(c)
      // Rest can be any NameChar
      while self.peek() is Some(nc) && is_name_char(nc) {
        buf.write_char(nc)
        let _ = self.advance()
      }
      buf.to_string()
    }
    Some(c) if is_name_char(c) =>
      // Character is valid NameChar but not NameStartChar (e.g., '-', '.', digit)
      raise InvalidSyntax("name cannot start with '" + c.to_string() + "'")
    _ => ""
  }
}

///|
/// Read attributes
fn Reader::read_attributes(
  self : Reader,
  elem_name : String,
) -> Array[XmlAttribute] raise XmlErrorKind {
  let attributes : Array[XmlAttribute] = []
  let mut need_whitespace = false
  while true {
    let had_whitespace = self.skip_whitespace_count() > 0
    match self.peek() {
      Some(c) if is_name_start_char(c) => {
        // Whitespace required between attributes (but not before the first)
        if need_whitespace && !had_whitespace {
          raise InvalidSyntax("whitespace required between attributes")
        }
        let attribute_start = self.source_position()
        let name_start = attribute_start
        let attr_name = self.read_name()
        let name_end = self.source_position()
        self.skip_whitespace()
        match self.peek() {
          Some('=') => {
            let _ = self.advance()
            self.skip_whitespace()
            let (raw_value, value_span) = self.read_attr_value()
            // Normalize attribute value based on declared type
            let normalized_value = self.normalize_attr_by_type(
              elem_name, attr_name, raw_value,
            )
            // Reject duplicate attributes
            let exists = attributes
              .iter()
              .any(fn(attribute) { attribute.name == attr_name })
            if exists {
              raise InvalidSyntax("duplicate attribute: " + attr_name)
            }
            attributes.push({
              name: attr_name,
              value: normalized_value,
              span: source_span(attribute_start, self.source_position()),
              name_span: source_span(name_start, name_end),
              value_span,
            })
            need_whitespace = true
          }
          _ => raise InvalidSyntax("expected '=' after attribute name")
        }
      }
      Some('/') | Some('>') => break
      Some(c) if is_name_char(c) =>
        // Invalid attribute name start (e.g., starts with digit or '-')
        raise InvalidSyntax(
          "attribute name cannot start with '" + c.to_string() + "'",
        )
      Some(c) =>
        raise InvalidSyntax(
          "unexpected character in tag: '" + c.to_string() + "'",
        )
      None => break
    }
  }
  match self.default_attributes.get(elem_name) {
    Some(defaults) =>
      for attribute in defaults {
        if !attributes
          .iter()
          .any(fn(existing) { existing.name == attribute.name }) {
          attributes.push({
            ..attribute,
            value: self.normalize_attr_by_type(
              elem_name,
              attribute.name,
              attribute.value,
            ),
          })
        }
      }
    None => ()
  }
  attributes
}

///|
/// Normalize attribute value based on declared type in DTD.
/// CDATA: only replace newlines/tabs with spaces
/// Tokenized types (ID, IDREF, NMTOKEN, etc.): collapse whitespace, trim
fn Reader::normalize_attr_by_type(
  self : Reader,
  elem_name : String,
  attr_name : String,
  value : String,
) -> String {
  let key = elem_name + ":" + attr_name
  let attr_type = self.attr_types.get(key).unwrap_or("CDATA")
  if attr_type == "CDATA" {
    // CDATA: preserve character references, return as-is (lxml behavior)
    value
  } else {
    // Tokenized types: collapse whitespace and trim
    let buf = StringBuilder()
    let mut in_whitespace = true // Start true to skip leading
    let mut need_space = false
    for c in value {
      if c == ' ' {
        if !in_whitespace {
          need_space = true
          in_whitespace = true
        }
      } else {
        if need_space {
          buf.write_char(' ')
          need_space = false
        }
        buf.write_char(c)
        in_whitespace = false
      }
    }
    buf.to_string()
  }
}

///|
/// Read raw attribute value (quoted) without entity decoding
/// Used for XML declaration attributes where entities should be kept as-is
fn Reader::read_raw_attr_value(self : Reader) -> String raise XmlErrorKind {
  let quote = match self.peek() {
    Some('"') => '"'
    Some('\'') => '\''
    _ => raise InvalidSyntax("expected quoted attribute value")
  }
  let _ = self.advance() // consume opening quote
  let buf = StringBuilder()
  while !self.is_eof() {
    match self.peek() {
      Some(c) if c == quote => {
        let _ = self.advance()
        break
      }
      Some(c) => {
        buf.write_char(c)
        let _ = self.advance()
      }
      None => break
    }
  }
  buf.to_string()
}

///|
/// Read attribute value (quoted) and decode entities
fn Reader::read_attr_value(
  self : Reader,
) -> (String, SourceSpan) raise XmlErrorKind {
  let quote = match self.peek() {
    Some('"') => '"'
    Some('\'') => '\''
    _ => raise InvalidSyntax("expected quoted attribute value")
  }
  let _ = self.advance() // consume opening quote
  let value_start = self.source_position()
  let buf = StringBuilder()
  while !self.is_eof() {
    match self.peek() {
      Some(c) if c == quote => {
        let value_end = self.source_position()
        let _ = self.advance()
        return (buf.to_string(), source_span(value_start, value_end))
      }
      Some('<') =>
        // < is not allowed in attribute values
        raise InvalidSyntax("'<' not allowed in attribute value")
      Some('&') => {
        self.in_reference = true
        let _ = self.advance()
        // Read entity name
        let entity_name = self.read_while(fn(c) {
          c != ';' && c != quote && c != '<'
        })
        match self.peek() {
          Some(';') => {
            let _ = self.advance()
            self.in_reference = false
            // First check if it's a character reference
            if entity_name.has_prefix("#") {
              // Character reference - decode directly
              let entity = "&" + entity_name + ";"
              let decoded = unescape(entity)
              if self.in_entity_content {
                self.consume_entity_output(decoded.length())
              }
              buf.write_string(decoded)
            } else {
              // Entity reference - expand with validation
              let entity_ref = "&" + entity_name + ";"
              let expanded = self.expand_attribute_entity_value(entity_ref)
              buf.write_string(expanded)
            }
          }
          _ =>
            // Entity reference must end with ';'
            raise InvalidSyntax("entity reference must end with ';'")
        }
      }
      Some(c) => {
        // Check for invalid XML characters
        let code = c.to_int()
        if !is_valid_xml_content_char(code) {
          raise InvalidSyntax("invalid character in attribute value")
        }
        // Normalize whitespace: tab, newline, CR -> space
        if c == '\t' || c == '\n' || c == '\r' {
          buf.write_char(' ')
        } else {
          buf.write_char(c)
        }
        let _ = self.advance()
        if c == '\r' && self.peek() == Some('\n') && !self.in_entity_content {
          let _ = self.advance()
        }
      }
      None => break
    }
  }
  (buf.to_string(), source_span(value_start, self.source_position()))
}

///|
/// Normalize line endings in a string: \r\n -> \n, \r -> \n
fn normalize_line_endings(s : String) -> String {
  let chars = s.to_array()
  let buf = StringBuilder()
  let len = chars.length()
  let mut i = 0
  while i < len {
    if chars[i] == '\r' {
      buf.write_char('\n')
      // Skip following \n if present
      if i + 1 < len && chars[i + 1] == '\n' {
        i += 1
      }
    } else {
      buf.write_char(chars[i])
    }
    i += 1
  }
  buf.to_string()
}

///|
/// Parse authored entity content and report malformed replacement text.
fn Reader::parse_entity_content(
  self : Reader,
  name : String,
  content : String,
  entity_span : SourceSpan,
) -> Array[Event] raise XmlErrorKind {
  if self.entity_stack.contains(name) {
    raise InvalidEntity("recursive entity reference: &" + name + ";")
  }
  if self.entity_stack.length() >= MAX_ENTITY_EXPANSION_DEPTH {
    raise InvalidEntity("entity expansion depth exceeded")
  }
  let sub_reader = {
    ..Reader::from_string(content),
    pos: 0,
    offset: 0,
    in_entity_content: true,
    entities: self.entities,
    attr_types: self.attr_types,
    default_attributes: self.default_attributes,
    entity_expansion_remaining: self.entity_expansion_remaining,
    entity_stack: self.entity_stack,
    namespace_aware: self.namespace_aware,
  }
  sub_reader.seen_root = true
  let position = sub_reader.source_position()
  sub_reader.tag_stack.push(
    ("_entity_content_", source_span(position, position)),
  )
  let events : Array[Event] = []
  self.entity_stack.push(name)
  errdefer {
    let _ = self.entity_stack.pop()
  }
  while !sub_reader.is_eof() || sub_reader.pending_events.length() > 0 {
    let event = sub_reader.read_event_internal()
    match event.kind {
      Eof => raise InvalidSyntax("malformed entity replacement text")
      Start(element) => {
        let attributes = element.attributes.map(fn(attribute) -> XmlAttribute {
          {
            name: attribute.name,
            value: attribute.value,
            span: entity_span,
            name_span: entity_span,
            value_span: entity_span,
          }
        })
        events.push({
          kind: Start({ name: element.name, attributes, }),
          span: entity_span,
        })
      }
      Empty(element) => {
        let attributes = element.attributes.map(fn(attribute) -> XmlAttribute {
          {
            name: attribute.name,
            value: attribute.value,
            span: entity_span,
            name_span: entity_span,
            value_span: entity_span,
          }
        })
        events.push({
          kind: Empty({ name: element.name, attributes, }),
          span: entity_span,
        })
      }
      parsed_kind => events.push({ kind: parsed_kind, span: entity_span, })
    }
  }
  if sub_reader.tag_stack.length() != 1 {
    raise InvalidSyntax("malformed entity replacement text")
  }
  let _ = self.entity_stack.pop()
  events
}

///|
/// Read text content - splits on entities to match lxml behavior
fn Reader::read_text(self : Reader) -> Event raise XmlErrorKind {
  let buf = StringBuilder()
  let mut text_start = self.source_position()
  while !self.is_eof() {
    match self.peek() {
      Some('<') => break
      Some('&') => {
        // Push current buffer as text event before processing entity
        let current_text = buf.to_string()
        if current_text.length() > 0 {
          self.push_pending_event({
            kind: Text(current_text),
            span: source_span(text_start, self.source_position()),
          })
          buf.reset()
        }
        // Read entity
        let entity_start = self.source_position()
        self.in_reference = true
        let _ = self.advance() // consume '&'
        // Check for valid entity name start (# for char ref, or NameStartChar)
        match self.peek() {
          Some('#') => {
            // Character reference
            let entity_name = self.read_while(fn(c) { c != ';' && c != '<' })
            match self.peek() {
              Some(';') => {
                let _ = self.advance()
                self.in_reference = false
                let entity = "&" + entity_name + ";"
                let decoded = unescape(entity)
                if self.in_entity_content {
                  self.consume_entity_output(decoded.length())
                }
                // Character references preserve literal character (no line ending normalization)
                self.push_pending_event({
                  kind: Text(decoded),
                  span: source_span(entity_start, self.source_position()),
                })
              }
              _ => raise InvalidSyntax("entity reference must end with ';'")
            }
          }
          Some(c) if is_name_start_char(c) => {
            // Entity reference - read name
            let entity_name = self.read_name()
            match self.peek() {
              Some(';') => {
                let _ = self.advance()
                self.in_reference = false
                match self.entities.get(entity_name) {
                  Some(stored_value) => {
                    let entity_span = source_span(
                      entity_start,
                      self.source_position(),
                    )
                    let parsed = self.parse_entity_content(
                      entity_name, stored_value, entity_span,
                    )
                    if parsed.iter().all(fn(event) { event.kind is Text(_) }) {
                      let text = StringBuilder()
                      for event in parsed {
                        if event.kind is Text(value) {
                          text.write_string(value)
                        }
                      }
                      self.push_pending_event({
                        kind: Text(text.to_string()),
                        span: entity_span,
                      })
                    } else {
                      for event in parsed {
                        self.push_pending_event(event)
                      }
                    }
                  }
                  None => {
                    if !is_predefined_entity(entity_name) {
                      raise InvalidEntity(
                        "undefined entity reference: &" + entity_name + ";",
                      )
                    }
                    let decoded = unescape("&" + entity_name + ";")
                    self.consume_entity_output(decoded.length())
                    self.push_pending_event({
                      kind: Text(decoded),
                      span: source_span(entity_start, self.source_position()),
                    })
                  }
                }
              }
              _ => raise InvalidSyntax("entity reference must end with ';'")
            }
          }
          _ => raise InvalidSyntax("invalid entity reference")
        }
        text_start = self.source_position()
      }
      Some(']') => {
        // Check for forbidden ]]> in content
        if self.check_string("]]>") {
          raise InvalidSyntax("']]>' is not allowed in element content")
        }
        buf.write_char(']')
        let _ = self.advance()
      }
      Some(c) => {
        // Check for invalid XML characters
        let code = c.to_int()
        if !is_valid_xml_content_char(code) {
          raise InvalidSyntax("invalid character in content")
        }
        // Normalize line endings: \r\n -> \n, \r -> \n
        if c == '\r' && !self.in_entity_content {
          let _ = self.advance()
          // Check if followed by \n
          if self.peek() == Some('\n') {
            let _ = self.advance()
          }
          buf.write_char('\n')
        } else {
          buf.write_char(c)
          let _ = self.advance()
        }
      }
      None => break
    }
  }
  // Push any remaining text
  let final_text = buf.to_string()
  if final_text.length() > 0 {
    self.push_pending_event({
      kind: Text(final_text),
      span: source_span(text_start, self.source_position()),
    })
  }
  // Return first pending event, or empty Text if none
  if self.pending_events.length() > 0 {
    self.pop_pending_event()
  } else {
    let position = self.source_position()
    { kind: Text(""), span: source_span(position, position), }
  }
}

///|
/// Read whitespace-only content (for prolog and misc after root)
/// Rejects any character references, entity references, or non-whitespace
fn Reader::read_whitespace_only(self : Reader) -> EventKind raise XmlErrorKind {
  let buf = StringBuilder()
  while !self.is_eof() {
    match self.peek() {
      Some('<') => break
      Some('&') =>
        // Character/entity references not allowed outside root element
        if self.root_closed {
          raise InvalidSyntax("character reference after root element")
        } else {
          raise InvalidSyntax("character reference before root element")
        }
      Some(c) =>
        if c == ' ' || c == '\t' || c == '\n' || c == '\r' {
          buf.write_char(c)
          let _ = self.advance()
        } else if self.root_closed {
          raise InvalidSyntax("content after root element")
        } else {
          raise InvalidSyntax("content before root element")
        }
      None => break
    }
  }
  Text(self.normalize_input_content(buf.to_string()))
}

///|
/// Check if character code is valid in XML content
/// Valid: #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
fn is_valid_xml_content_char(code : Int32) -> Bool {
  code == 0x9 ||
  code == 0xA ||
  code == 0xD ||
  (code >= 0x20 && code <= 0xD7FF) ||
  (code >= 0xE000 && code <= 0xFFFD) ||
  (code >= 0x10000 && code <= 0x10FFFF)
}

///|
/// Validate version: VersionNum ::= '1.' [0-9]+
fn is_valid_version(version : String) -> Bool {
  let chars = version.to_array()
  // Must be at least "1.0"
  if chars.length() < 3 {
    return false
  }
  // Must start with "1."
  if chars[0] != '1' || chars[1] != '.' {
    return false
  }
  // Must have at least one digit after "1."
  if chars.length() < 3 {
    return false
  }
  // Rest must be digits only
  for i = 2; i < chars.length(); i = i + 1 {
    let c = chars[i]
    if c < '0' || c > '9' {
      return false
    }
  }
  true
}

///|
/// Validate encoding name: [A-Za-z]([A-Za-z0-9._-])*
fn is_valid_encoding_name(name : String) -> Bool {
  let chars = name.to_array()
  if chars.length() == 0 {
    return false
  }
  // First character must be a letter
  let first = chars[0]
  if !((first >= 'A' && first <= 'Z') || (first >= 'a' && first <= 'z')) {
    return false
  }
  // Rest must be letters, digits, '.', '_', or '-'
  for i = 1; i < chars.length(); i = i + 1 {
    let c = chars[i]
    if !((c >= 'A' && c <= 'Z') ||
      (c >= 'a' && c <= 'z') ||
      (c >= '0' && c <= '9') ||
      c == '.' ||
      c == '_' ||
      c == '-') {
      return false
    }
  }
  true
}

///|
/// Skip a quoted string (single or double quotes)
fn Reader::skip_quoted_string(self : Reader) -> Unit raise XmlErrorKind {
  let quote = match self.peek() {
    Some('"') => '"'
    Some('\'') => '\''
    _ => raise InvalidSyntax("expected quoted literal")
  }
  let _ = self.advance()
  while self.peek() is Some(c) && c != quote {
    if !is_valid_xml_content_char(c.to_int()) {
      raise InvalidSyntax("invalid character in quoted literal")
    }
    let _ = self.advance()
  }
  if self.peek() != Some(quote) {
    raise UnexpectedEof
  }
  let _ = self.advance()
}

///|
/// Read and validate a Public ID literal
/// PubidChar ::= #x20 | #xD | #xA | [a-zA-Z0-9] | [-'()+,./:=?;!*#@$_%]
fn Reader::read_and_validate_public_id(
  self : Reader,
) -> Unit raise XmlErrorKind {
  let quote = match self.peek() {
    Some('"') => '"'
    Some('\'') => '\''
    _ => raise InvalidSyntax("expected quoted public ID")
  }
  let _ = self.advance() // consume opening quote
  while self.peek() is Some(c) && c != quote {
    if !is_pubid_char(c) {
      raise InvalidSyntax(
        "invalid character in public ID: '" + c.to_string() + "'",
      )
    }
    let _ = self.advance()
  }
  if self.peek() != Some(quote) {
    raise UnexpectedEof
  }
  let _ = self.advance()
}

///|
/// Check if a character is valid in a Public ID
fn is_pubid_char(c : Char) -> Bool {
  // PubidChar ::= #x20 | #xD | #xA | [a-zA-Z0-9] | [-'()+,./:=?;!*#@$_%]
  c == ' ' ||
  c == '\r' ||
  c == '\n' ||
  (c >= 'a' && c <= 'z') ||
  (c >= 'A' && c <= 'Z') ||
  (c >= '0' && c <= '9') ||
  c == '-' ||
  c == '\'' ||
  c == '(' ||
  c == ')' ||
  c == '+' ||
  c == ',' ||
  c == '.' ||
  c == '/' ||
  c == ':' ||
  c == '=' ||
  c == '?' ||
  c == ';' ||
  c == '!' ||
  c == '*' ||
  c == '#' ||
  c == '@' ||
  c == '$' ||
  c == '_' ||
  c == '%'
}

///|
/// Parse DTD internal subset content until ]
/// Extracts entity declarations and stores them
fn Reader::parse_internal_subset(self : Reader) -> Unit raise XmlErrorKind {
  while !self.is_eof() {
    self.skip_whitespace()
    match self.peek() {
      Some(']') => {
        let _ = self.advance()
        return
      }
      Some('<') => {
        let declaration_start = self.source_position()
        let _ = self.advance()
        if self.peek() == Some('!') {
          let _ = self.advance()
          if self.skip_string("ENTITY") {
            self.parse_entity_declaration()
          } else if self.skip_string("ATTLIST") {
            parse_attlist(self)
          } else if self.skip_string("ELEMENT") {
            parse_element_decl(self)
          } else if self.skip_string("--") {
            let content = self.read_comment_content()
            self.internal_subset_events.push({
              kind: Comment(content),
              span: source_span(declaration_start, self.source_position()),
            })
          } else if self.skip_string("NOTATION") {
            if self.skip_whitespace_count() == 0 {
              raise InvalidSyntax("whitespace required after NOTATION")
            }
            if self.read_non_element_name() == "" {
              raise InvalidSyntax("NOTATION requires a name")
            }
            if self.skip_whitespace_count() == 0 {
              raise InvalidSyntax(
                "whitespace required before notation identifier",
              )
            }
            self.read_external_id(false)
            self.finish_dtd_declaration()
          } else {
            raise InvalidSyntax("invalid declaration in internal DTD subset")
          }
        } else if self.peek() == Some('?') {
          let kind = self.read_pi()
          if kind is Decl(_) {
            raise InvalidSyntax("XML declaration not allowed in DTD")
          }
          self.internal_subset_events.push({
            kind,
            span: source_span(declaration_start, self.source_position()),
          })
        } else {
          raise InvalidSyntax("expected a declaration in internal DTD subset")
        }
      }
      Some('%') => {
        // A non-validating processor may leave parameter entities unread.
        let _ = self.advance()
        if self.read_name() == "" || self.peek() != Some(';') {
          raise InvalidSyntax("invalid parameter entity reference")
        }
        let _ = self.advance()
      }
      _ => raise InvalidSyntax("unexpected content in internal DTD subset")
    }
  }
  raise UnexpectedEof
}

///|
fn Reader::finish_dtd_declaration(self : Reader) -> Unit raise XmlErrorKind {
  self.skip_whitespace()
  if self.peek() != Some('>') {
    raise InvalidSyntax("expected '>' at end of DTD declaration")
  }
  let _ = self.advance()
}

///|
fn Reader::read_external_id(
  self : Reader,
  require_system : Bool,
) -> Unit raise XmlErrorKind {
  let keyword = self.read_name()
  if self.skip_whitespace_count() == 0 {
    raise InvalidSyntax(
      "whitespace required before external identifier literal",
    )
  }
  match keyword {
    "SYSTEM" => self.skip_quoted_string()
    "PUBLIC" => {
      self.read_and_validate_public_id()
      let had_whitespace = self.skip_whitespace_count() > 0
      if require_system || self.peek() is Some('"' | '\'') {
        if !had_whitespace {
          raise InvalidSyntax("whitespace required before system literal")
        }
        self.skip_quoted_string()
      }
    }
    _ => raise InvalidSyntax("expected SYSTEM or PUBLIC external identifier")
  }
}

///|
fn Reader::parse_entity_declaration(self : Reader) -> Unit raise XmlErrorKind {
  if self.skip_whitespace_count() == 0 {
    raise InvalidSyntax("whitespace required after ENTITY")
  }
  let parameter = self.peek() == Some('%')
  if parameter {
    let _ = self.advance()
    if self.skip_whitespace_count() == 0 {
      raise InvalidSyntax("whitespace required after % in parameter entity")
    }
  }
  let name = self.read_non_element_name()
  if name == "" {
    raise InvalidSyntax("ENTITY requires a name")
  }
  if self.skip_whitespace_count() == 0 {
    raise InvalidSyntax("whitespace required after entity name")
  }
  match self.peek() {
    Some('"' | '\'') => {
      let quote = self.advance().unwrap()
      let raw_value = self.read_entity_value(quote)
      let _ = self.advance()
      // Parameter entities and duplicate declarations also require valid syntax.
      validate_entity_value(raw_value, namespace_aware=self.namespace_aware)
      if !parameter {
        let value = expand_char_refs_only(raw_value)
        if !self.entities.contains(name) {
          self.entities.set(name, value)
        }
      }
    }
    _ => {
      self.read_external_id(true)
      let had_whitespace = self.skip_whitespace_count() > 0
      if self.skip_string("NDATA") {
        if parameter || !had_whitespace || self.skip_whitespace_count() == 0 {
          raise InvalidSyntax("invalid NDATA declaration")
        }
        if self.read_non_element_name() == "" {
          raise InvalidSyntax("NDATA requires a notation name")
        }
      }
    }
  }
  self.finish_dtd_declaration()
}

///|
/// Read entity value (handles nested entity references)
fn Reader::read_entity_value(
  self : Reader,
  quote : Char,
) -> String raise XmlErrorKind {
  let buf = StringBuilder()
  while self.peek() is Some(c) && c != quote {
    // Validate character
    let code = c.to_int()
    if !is_valid_xml_content_char(code) {
      raise InvalidSyntax("invalid character in entity value")
    }
    buf.write_char(c)
    let _ = self.advance()
  }
  if self.peek() != Some(quote) {
    raise UnexpectedEof
  }
  self.normalize_input_content(buf.to_string())
}

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

///|
/// Check if character is a valid XML NameStartChar (per XML 1.0 spec)
/// NameStartChar ::= ":" | [A-Z] | "_" | [a-z] | [#xC0-#xD6] | [#xD8-#xF6] |
///                   [#xF8-#x2FF] | [#x370-#x37D] | [#x37F-#x1FFF] |
///                   [#x200C-#x200D] | [#x2070-#x218F] | [#x2C00-#x2FEF] |
///                   [#x3001-#xD7FF] | [#xF900-#xFDCF] | [#xFDF0-#xFFFD] |
///                   [#x10000-#xEFFFF]
fn is_name_start_char(c : Char) -> Bool {
  let code = c.to_int()
  c == ':' ||
  c == '_' ||
  (c >= 'A' && c <= 'Z') ||
  (c >= 'a' && c <= 'z') ||
  (code >= 0xC0 && code <= 0xD6) ||
  (code >= 0xD8 && code <= 0xF6) ||
  (code >= 0xF8 && code <= 0x2FF) ||
  (code >= 0x370 && code <= 0x37D) ||
  (code >= 0x37F && code <= 0x1FFF) ||
  (code >= 0x200C && code <= 0x200D) ||
  (code >= 0x2070 && code <= 0x218F) ||
  (code >= 0x2C00 && code <= 0x2FEF) ||
  (code >= 0x3001 && code <= 0xD7FF) ||
  (code >= 0xF900 && code <= 0xFDCF) ||
  (code >= 0xFDF0 && code <= 0xFFFD) ||
  (code >= 0x10000 && code <= 0xEFFFF)
}

///|
/// Check if character is a valid XML NameChar (per XML 1.0 spec)
/// NameChar ::= NameStartChar | "-" | "." | [0-9] | #xB7 |
///              [#x0300-#x036F] | [#x203F-#x2040]
fn is_name_char(c : Char) -> Bool {
  if is_name_start_char(c) {
    return true
  }
  let code = c.to_int()
  c == '-' ||
  c == '.' ||
  (c >= '0' && c <= '9') ||
  code == 0xB7 ||
  (code >= 0x300 && code <= 0x36F) ||
  (code >= 0x203F && code <= 0x2040)
}