// 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.

///|
/// HTML5 Tokenizer per WHATWG Living Standard
pub struct Tokenizer {
  // Input
  input : Array[Char]
  mut pos : Int
  mut line : Int
  mut column : Int
  mut last_pos : Int
  mut last_line : Int
  mut last_column : Int
  mut saw_eof : Bool // Track if last consume returned EOF
  input_error_reported : Array[Bool]

  // State machine
  mut state : State
  mut return_state : State // For character reference

  // Error collection
  errors : Array[ParseError]

  // Current tag being built
  current_tag_name : StringBuilder
  mut current_tag_is_end : Bool
  mut current_tag_self_closing : Bool
  current_attrs : Array[Attribute]
  current_attr_name : StringBuilder
  current_attr_value : StringBuilder
  mut current_attr_duplicate : Bool

  // Current comment/doctype being built
  current_comment : StringBuilder
  current_pi_target : StringBuilder
  current_pi_data : StringBuilder
  current_doctype_name : StringBuilder
  current_doctype_public_id : StringBuilder
  current_doctype_system_id : StringBuilder
  mut current_doctype_force_quirks : Bool
  mut current_doctype_public_id_set : Bool
  mut current_doctype_system_id_set : Bool

  // Character reference
  mut char_ref_code : Int
  temp_buffer : StringBuilder

  // For appropriate end tag check
  mut last_start_tag_name : String

  // Pending tokens (some states emit multiple)
  pending_tokens : Array[Token]

  // Foreign content flag (set by tree builder for CDATA handling)
  mut in_foreign_content : Bool
}

///|
/// Create a tokenizer from a string
pub fn Tokenizer::new(input : String) -> Tokenizer {
  {
    input: input.to_array(),
    pos: 0,
    line: 1,
    column: 1,
    last_pos: 0,
    last_line: 1,
    last_column: 1,
    saw_eof: false,
    input_error_reported: Array::make(input.to_array().length(), false),
    state: Data,
    return_state: Data,
    errors: [],
    current_tag_name: StringBuilder(),
    current_tag_is_end: false,
    current_tag_self_closing: false,
    current_attrs: [],
    current_attr_name: StringBuilder(),
    current_attr_value: StringBuilder(),
    current_attr_duplicate: false,
    current_comment: StringBuilder(),
    current_pi_target: StringBuilder(),
    current_pi_data: StringBuilder(),
    current_doctype_name: StringBuilder(),
    current_doctype_public_id: StringBuilder(),
    current_doctype_system_id: StringBuilder(),
    current_doctype_force_quirks: false,
    current_doctype_public_id_set: false,
    current_doctype_system_id_set: false,
    char_ref_code: 0,
    temp_buffer: StringBuilder(),
    last_start_tag_name: "",
    pending_tokens: [],
    in_foreign_content: false,
  }
}

///|
/// Get current source position
pub fn Tokenizer::get_position(self : Tokenizer) -> SourcePosition {
  { line: self.line, column: self.column, offset: self.pos, }
}

///|
/// Peek at current character without consuming
/// Returns LF for CR (normalization)
fn Tokenizer::peek(self : Tokenizer) -> Char? {
  if self.pos < self.input.length() {
    self.report_input_error_at(self.pos)
    let c = self.input[self.pos]
    // Normalize CR to LF when peeking
    if c == '\r' {
      Some('\n')
    } else {
      Some(c)
    }
  } else {
    None
  }
}

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

///|
/// Consume and return current character
/// Normalizes CR and CRLF to LF per WHATWG spec
fn Tokenizer::consume(self : Tokenizer) -> Char? {
  if self.pos < self.input.length() {
    self.saw_eof = false
    self.last_pos = self.pos
    self.last_line = self.line
    self.last_column = self.column
    let mut c = self.input[self.pos]
    let raw_pos = self.pos
    self.pos += 1
    self.report_input_error_at(raw_pos)
    // Normalize CR to LF (WHATWG preprocessing)
    if c == '\r' {
      c = '\n'
      // Skip following LF if present (CRLF -> single LF)
      if self.pos < self.input.length() && self.input[self.pos] == '\n' {
        self.pos += 1
      }
    }
    if c == '\n' {
      self.line += 1
      self.column = 1
    } else {
      self.column += 1
    }
    Some(c)
  } else {
    self.saw_eof = true
    None
  }
}

///|
/// Report preprocessing errors when a raw input character is first observed.
fn Tokenizer::report_input_error_at(self : Tokenizer, raw_pos : Int) -> Unit {
  if self.input_error_reported[raw_pos] {
    return
  }
  self.input_error_reported[raw_pos] = true
  let code = self.input[raw_pos].to_int()
  let error = if code != 0 &&
    is_control_char(code) &&
    !is_ascii_whitespace_code(code) {
    Some(ControlCharacterInInputStream)
  } else if is_noncharacter(code) {
    Some(NoncharacterInInputStream)
  } else {
    None
  }
  match error {
    Some(code) => {
      let mut line = 1
      let mut column = 1
      let mut i = 0
      while i < raw_pos {
        if self.input[i] == '\r' {
          if i + 1 < raw_pos && self.input[i + 1] == '\n' {
            i += 1
          }
          line += 1
          column = 1
        } else if self.input[i] == '\n' {
          line += 1
          column = 1
        } else {
          column += 1
        }
        i += 1
      }
      self.errors.push({ code, position: { line, column, offset: raw_pos, }, })
    }
    None => ()
  }
}

///|
/// Reconsume the current preprocessed character.
/// If we're at EOF, stay at EOF (don't go back to last char)
fn Tokenizer::reconsume(self : Tokenizer) -> Unit {
  // If we just saw EOF, don't go back - we want to stay at EOF
  if self.saw_eof {
    return
  }
  self.pos = self.last_pos
  self.line = self.last_line
  self.column = self.last_column
}

///|
/// Get current source position
fn Tokenizer::current_position(self : Tokenizer) -> SourcePosition {
  { line: self.line, column: self.column, offset: self.pos, }
}

///|
/// Emit a parse error
fn Tokenizer::emit_error(self : Tokenizer, code : ParseErrorCode) -> Unit {
  let position = if self.saw_eof {
    self.current_position()
  } else {
    { line: self.last_line, column: self.last_column, offset: self.last_pos, }
  }
  self.errors.push({ code, position, })
}

///|
/// Emit an error at the next input position after the current character.
fn Tokenizer::emit_error_at_next_position(
  self : Tokenizer,
  code : ParseErrorCode,
) -> Unit {
  self.errors.push({ code, position: self.current_position(), })
}

///|
/// Get all parse errors
pub fn Tokenizer::get_errors(self : Tokenizer) -> Array[ParseError] {
  self.errors
}

///|
/// Reset tag building state for a new tag
fn Tokenizer::reset_tag(self : Tokenizer, is_end : Bool) -> Unit {
  self.current_tag_name.reset()
  self.current_tag_is_end = is_end
  self.current_tag_self_closing = false
  self.current_attrs.clear()
  self.current_attr_name.reset()
  self.current_attr_value.reset()
  self.current_attr_duplicate = false
}

///|
/// Start a new attribute
fn Tokenizer::start_new_attribute(self : Tokenizer) -> Unit {
  self.current_attr_name.reset()
  self.current_attr_value.reset()
  self.current_attr_duplicate = false
}

///|
/// Record whether the current attribute name duplicates an earlier one.
fn Tokenizer::finish_attribute_name(self : Tokenizer) -> Unit {
  let name = self.current_attr_name.to_string()
  if !name.is_empty() &&
    self.current_attrs.iter().any(fn(attr) { attr.name == name }) {
    self.current_attr_duplicate = true
    self.emit_error(DuplicateAttribute)
  }
}

///|
/// Finish current attribute and add to list
fn Tokenizer::finish_attribute(self : Tokenizer) -> Unit {
  let name = self.current_attr_name.to_string()
  // Don't add empty attributes
  if name.is_empty() {
    return
  }
  let value = self.current_attr_value.to_string()
  if !self.current_attr_duplicate {
    self.current_attrs.push({ name, value, })
  }
  self.current_attr_name.reset()
  self.current_attr_value.reset()
  self.current_attr_duplicate = false
}

///|
/// Emit current tag as token
fn Tokenizer::emit_current_tag(self : Tokenizer) -> Token {
  let name = self.current_tag_name.to_string().to_lower()
  if self.current_tag_is_end {
    // End tags shouldn't have attributes or self-closing flag
    if self.current_attrs.length() > 0 {
      self.emit_error(EndTagWithAttributes)
    }
    if self.current_tag_self_closing {
      self.emit_error(EndTagWithTrailingSolidus)
    }
    EndTag(name~)
  } else {
    self.last_start_tag_name = name
    StartTag(
      name~,
      attrs=self.current_attrs.copy(),
      self_closing=self.current_tag_self_closing,
    )
  }
}

///|
/// Emit current comment as token
fn Tokenizer::emit_current_comment(self : Tokenizer) -> Token {
  Comment(self.current_comment.to_string())
}

///|
/// Emit current DOCTYPE as token
fn Tokenizer::emit_current_doctype(self : Tokenizer) -> Token {
  let name = if self.current_doctype_name.to_string().length() > 0 {
    Some(self.current_doctype_name.to_string())
  } else {
    None
  }
  let public_id = if self.current_doctype_public_id_set {
    Some(self.current_doctype_public_id.to_string())
  } else {
    None
  }
  let system_id = if self.current_doctype_system_id_set {
    Some(self.current_doctype_system_id.to_string())
  } else {
    None
  }
  DOCTYPE(
    name~,
    public_id~,
    system_id~,
    force_quirks=self.current_doctype_force_quirks,
  )
}

///|
/// Reset DOCTYPE building state
fn Tokenizer::reset_doctype(self : Tokenizer) -> Unit {
  self.current_doctype_name.reset()
  self.current_doctype_public_id.reset()
  self.current_doctype_system_id.reset()
  self.current_doctype_force_quirks = false
  self.current_doctype_public_id_set = false
  self.current_doctype_system_id_set = false
}

///|
/// Reset comment building state
fn Tokenizer::reset_comment(self : Tokenizer) -> Unit {
  self.current_comment.reset()
}

///|
/// Check if current end tag is appropriate (matches last start tag)
fn Tokenizer::is_appropriate_end_tag(self : Tokenizer) -> Bool {
  self.current_tag_name.to_string().to_lower() == self.last_start_tag_name
}

///|
/// ASCII lowercase a character
fn to_ascii_lower(c : Char) -> Char {
  if c >= 'A' && c <= 'Z' {
    Int::unsafe_to_char(c.to_int() + 32)
  } else {
    c
  }
}

///|
/// Check if character is ASCII alpha
fn is_ascii_alpha(c : Char) -> Bool {
  (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')
}

///|
/// Check if character is ASCII alphanumeric
fn is_ascii_alphanumeric(c : Char) -> Bool {
  is_ascii_alpha(c) || (c >= '0' && c <= '9')
}

///|
/// Check if character is ASCII upper alpha
fn is_ascii_upper_alpha(c : Char) -> Bool {
  c >= 'A' && c <= 'Z'
}

///|
/// Check if character is ASCII lower alpha
fn is_ascii_lower_alpha(c : Char) -> Bool {
  c >= 'a' && c <= 'z'
}

///|
/// Check if character is ASCII digit
fn is_ascii_digit(c : Char) -> Bool {
  c >= '0' && c <= '9'
}

///|
/// Check if character is ASCII upper hex digit
fn is_ascii_upper_hex_digit(c : Char) -> Bool {
  c >= 'A' && c <= 'F'
}

///|
/// Check if character is ASCII lower hex digit
fn is_ascii_lower_hex_digit(c : Char) -> Bool {
  c >= 'a' && c <= 'f'
}

///|
/// Check if character is ASCII hex digit
fn is_ascii_hex_digit(c : Char) -> Bool {
  is_ascii_digit(c) ||
  is_ascii_upper_hex_digit(c) ||
  is_ascii_lower_hex_digit(c)
}

///|
/// Check if character is whitespace per HTML spec
fn is_whitespace(c : Char) -> Bool {
  c == '\t' || c == '\n' || c == '\u{0C}' || c == ' '
}

///|
/// Switch tokenizer to RCDATA mode (for