///|
pub suberror IniParseError {
  UnexpectedEqualSign(line~ : Int, col~ : Int)
  EmptySection(line~ : Int, col~ : Int)
  UnclosedSection(line~ : Int, col~ : Int)
  ValueWithoutSection(line~ : Int, col~ : Int)
  QuoteMismatch(line~ : Int, col~ : Int)
  QuoteNotClosed(line~ : Int, col~ : Int)
}

///|
priv struct ParseContext {
  mut current_section : Section
  sections : @hashmap.HashMap[Section, @hashmap.HashMap[Key, Value]]
  mut line : Int
  mut col : Int
  mut key : String
  buffer : StringBuilder
}

///|
priv enum ReadingValueState {
  None
  SingleQuote
  DoubleQuote
} derive(Show)

///|
priv enum ParsePhase {
  Start
  ReadingSection
  ReadingKey
  ReadingValue(ReadingValueState)
  InComment
} derive(Show)

///|
priv struct EscapeStateHex {
  hex : StringBuilder
  mut left : Int
}

///|
priv enum EscapeState {
  None
  Escape
  Hex(EscapeStateHex)
}

///|
struct IniParseState {
  ctx : ParseContext
  mut phase : ParsePhase
  is_case_sensitive : Bool
  mut escape_state : EscapeState
  mut last_char_is_backslash : Bool // Track if last char is an unescaped backslash
}

///|
fn IniParseState::lower_if_needed(self : IniParseState, s : String) -> String {
  if self.is_case_sensitive {
    s
  } else {
    s.to_lower()
  }
}

///|
pub fn IniParseState::new(is_case_sensitive? : Bool = false) -> IniParseState {
  IniParseState::{
    ctx: ParseContext::{
      current_section: Section::Global,
      sections: @hashmap.from_array([(Section::Global, @hashmap.new())]),
      line: 1,
      col: 1,
      key: "",
      buffer: StringBuilder::new(),
    },
    phase: ParsePhase::Start,
    is_case_sensitive,
    escape_state: EscapeState::None,
    last_char_is_backslash: false,
  }
}

///|
pub fn parse(
  str : String,
  is_case_sensitive? : Bool = false,
) -> IniFile raise IniParseError {
  IniParseState::new(is_case_sensitive~).parse(str).finish()
}

///|
pub fn IniParseState::parse(
  self : IniParseState,
  chunk : String,
) -> IniParseState raise IniParseError {
  let mut self = self
  for c in chunk.iter() {
    self = self.process_char(c)
  }
  self
}

///|
pub fn IniParseState::finish(
  self : IniParseState,
) -> IniFile raise IniParseError {
  let self = self.process_char('\n')
  IniFile::new_with_sections(
    self.ctx.sections,
    is_case_sensitive=self.is_case_sensitive,
  )
}

///|
fn IniParseState::process_char(
  self : IniParseState,
  c : Char,
) -> IniParseState raise IniParseError {
  self.update_position(c)
  match self.phase {
    ParsePhase::Start => self.handle_start(c)
    ParsePhase::ReadingSection => self.handle_section_char(c)
    ParsePhase::ReadingKey => self.handle_key_char(c)
    ParsePhase::ReadingValue(_) => self.handle_value_char(c)
    ParsePhase::InComment => self.handle_comment(c)
  }
}

///|
fn IniParseState::update_position(self : IniParseState, c : Char) -> Unit {
  if c == '\n' {
    self.ctx.line += 1
    self.ctx.col = 1
  } else {
    self.ctx.col += 1
  }
}

///|
fn IniParseState::handle_start(
  self : IniParseState,
  c : Char,
) -> IniParseState raise IniParseError {
  match c {
    '[' => {
      self.phase = ReadingSection
      self.ctx.buffer.reset()
      self
    }
    '#' | ';' => {
      self.phase = InComment
      self
    }
    '=' =>
      raise IniParseError::UnexpectedEqualSign(
        line=self.ctx.line,
        col=self.ctx.col,
      )
    '\n' => self.reset_line()
    _ if c.is_whitespace() => self
    _ => {
      self.phase = ReadingKey
      self.ctx.buffer.write_char(c)
      self
    }
  }
}

///|
fn IniParseState::handle_section_char(
  self : IniParseState,
  c : Char,
) -> IniParseState raise IniParseError {
  match c {
    '#' | ';' =>
      raise IniParseError::UnclosedSection(line=self.ctx.line, col=self.ctx.col)
    ']' => {
      if self.ctx.buffer.is_empty() {
        raise IniParseError::EmptySection(line=self.ctx.line, col=self.ctx.col)
      }
      let section_name = self.ctx.buffer.to_string().trim()
      let section_name = self.lower_if_needed(section_name.to_string())
      self.ctx.current_section = Section::NamedSection(section_name)
      if not(self.ctx.sections.contains(self.ctx.current_section)) {
        self.ctx.sections.set(self.ctx.current_section, @hashmap.new())
      }
      self.phase = Start
      self
    }
    '\n' =>
      raise IniParseError::UnclosedSection(line=self.ctx.line, col=self.ctx.col)
    _ => {
      self.ctx.buffer.write_char(c)
      self
    }
  }
}

///|
fn IniParseState::handle_key_char(
  self : IniParseState,
  c : Char,
) -> IniParseState raise IniParseError {
  match c {
    '=' => {
      if self.ctx.buffer.is_empty() {
        raise IniParseError::UnexpectedEqualSign(
          line=self.ctx.line,
          col=self.ctx.col,
        )
      }
      let key = self.ctx.buffer.to_string().trim()
      let key = self.lower_if_needed(key.to_string())
      self.ctx.buffer.reset()
      self.phase = ReadingValue(ReadingValueState::None)
      self.ctx.key = key
      self
    }
    '\n' | ';' | '#' => {
      let key = self.ctx.buffer.to_string().trim()
      if not(key.is_empty()) {
        let key = self.lower_if_needed(key.to_string())
        self.ctx.key = key
        self.ctx.buffer.reset()
        ignore(self.commit_value())
      }
      self.reset_line()
    }
    _ if c.is_whitespace() =>
      match self.ctx.buffer.is_empty() {
        true => self
        false => {
          self.ctx.buffer.write_char(c)
          self
        }
      }
    _ => {
      self.ctx.buffer.write_char(c)
      self
    }
  }
}

///|
fn IniParseState::handle_value_char(
  self : IniParseState,
  c : Char,
) -> IniParseState raise IniParseError {
  match self.escape_state {
    EscapeState::Hex(state) =>
      if c.is_digit(16) {
        state.hex.write_char(c)
        state.left -= 1
        if state.left == 0 {
          // it must be 4 digits hex
          let code = try! @strconv.parse_int(state.hex.to_string(), base=16)
          self.ctx.buffer.write_char(Int::unsafe_to_char(code))
          self.escape_state = EscapeState::None
          self.last_char_is_backslash = false
        }
        return self
      } else {
        self.ctx.buffer.write_string("\\x" + state.hex.to_string())
        self.escape_state = EscapeState::None
        self.last_char_is_backslash = false
        // handle the current character
      }
    EscapeState::Escape => {
      match c {
        '\\' => self.ctx.buffer.write_char('\\')
        '\'' => self.ctx.buffer.write_char('\'')
        '"' => self.ctx.buffer.write_char('"')
        '0' => self.ctx.buffer.write_char('\u0000')
        'a' => self.ctx.buffer.write_char('\u0007')
        'b' => self.ctx.buffer.write_char('\b')
        't' => self.ctx.buffer.write_char('\t')
        'r' => self.ctx.buffer.write_char('\r')
        'n' => self.ctx.buffer.write_char('\n')
        ';' => self.ctx.buffer.write_char(';')
        '#' => self.ctx.buffer.write_char('#')
        '=' => self.ctx.buffer.write_char('=')
        ':' => self.ctx.buffer.write_char(':')
        'x' => {
          let hex = StringBuilder::new()
          self.escape_state = EscapeState::Hex(EscapeStateHex::{ hex, left: 4 })
          self.last_char_is_backslash = false
          return self
        }
        _ => self.ctx.buffer.write_char(c)
      }
      self.escape_state = EscapeState::None
      self.last_char_is_backslash = false
      return self
    }
    EscapeState::None => ()
  }
  if c == '\\' {
    self.escape_state = EscapeState::Escape
    self.last_char_is_backslash = not(self.last_char_is_backslash) // handle consecutive backslashes
    return self
  }
  match c {
    '\n' => {
      guard self.phase is ReadingValue(ReadingValueState::None) else {
        raise IniParseError::QuoteNotClosed(
          line=self.ctx.line,
          col=self.ctx.col,
        )
      }
      if self.last_char_is_backslash {
        // Remove the effect of the backslash, do not commit value, just continue
        self.last_char_is_backslash = false
        return self
      }
      ignore(self.commit_value())
      self.last_char_is_backslash = false
      self.reset_line()
    }
    ';' | '#' => {
      guard self.phase is ReadingValue(ReadingValueState::None) else {
        raise IniParseError::QuoteNotClosed(
          line=self.ctx.line,
          col=self.ctx.col,
        )
      }
      ignore(self.commit_value(drop_last_one_space=true))
      self.phase = InComment
      self.last_char_is_backslash = false
      self
    }
    '\'' => {
      if self.phase is ReadingValue(ReadingValueState::DoubleQuote) {
        raise IniParseError::QuoteMismatch(line=self.ctx.line, col=self.ctx.col)
      }
      self.phase = match self.phase {
        ReadingValue(ReadingValueState::None) =>
          ReadingValue(ReadingValueState::SingleQuote)
        ReadingValue(ReadingValueState::SingleQuote) =>
          ReadingValue(ReadingValueState::None)
        _ => self.phase
      }
      self.last_char_is_backslash = false
      self
    }
    '"' => {
      if self.phase is ReadingValue(ReadingValueState::SingleQuote) {
        raise IniParseError::QuoteMismatch(line=self.ctx.line, col=self.ctx.col)
      }
      self.phase = match self.phase {
        ReadingValue(ReadingValueState::None) =>
          ReadingValue(ReadingValueState::DoubleQuote)
        ReadingValue(ReadingValueState::DoubleQuote) =>
          ReadingValue(ReadingValueState::None)
        _ => self.phase
      }
      self.last_char_is_backslash = false
      self
    }
    _ if c.is_whitespace() => {
      match self.phase {
        ReadingValue(ReadingValueState::None) =>
          if not(self.ctx.buffer.is_empty()) {
            self.ctx.buffer.write_char(c)
          }
        ReadingValue(ReadingValueState::SingleQuote) =>
          self.ctx.buffer.write_char(c)
        ReadingValue(ReadingValueState::DoubleQuote) =>
          self.ctx.buffer.write_char(c)
        _ => ()
      }
      self.last_char_is_backslash = false
      self
    }
    _ => {
      self.ctx.buffer.write_char(c)
      self.last_char_is_backslash = false
      self
    }
  }
}

///|
fn IniParseState::handle_comment(
  self : IniParseState,
  c : Char,
) -> IniParseState {
  match c {
    '\n' => self.reset_line()
    _ => self
  }
}

///|
fn IniParseState::reset_line(self : IniParseState) -> IniParseState {
  self.phase = Start
  self.ctx.buffer.reset()
  self
}

///|
fn IniParseState::commit_value(
  self : IniParseState,
  // If true, drop the last space in the value
  // this is used to handle the case when a comment is at the end of the line
  drop_last_one_space? : Bool = false,
) -> IniParseState raise IniParseError {
  let mut value = self.ctx.buffer.to_string()
  if drop_last_one_space && value is [.. rest, ch] && ch.is_whitespace() {
    value = rest.to_string()
  }
  let section = self.ctx.sections.get(self.ctx.current_section).unwrap()
  let key = self.ctx.key
  guard not(key.is_blank()) else {
    raise IniParseError::ValueWithoutSection(
      line=self.ctx.line,
      col=self.ctx.col,
    )
  }
  section.set(key, value)
  self.ctx.buffer.reset()
  self
}

///|
test "IniParseState::new/default_settings" {
  let state = IniParseState::new()
  inspect(state.is_case_sensitive, content="false")
  inspect(state.ctx.line, content="1")
  inspect(state.ctx.col, content="1")
  inspect(state.ctx.key, content="")
  inspect(state.phase, content="Start")
  inspect(state.ctx.current_section, content="Global")
}

///|
test "IniParseState::new/case_sensitive" {
  let state = IniParseState::new(is_case_sensitive=true)
  inspect(state.is_case_sensitive, content="true")
  inspect(state.ctx.current_section, content="Global")
}

///|
test "IniParseState::new/buffer_empty" {
  let state = IniParseState::new()
  inspect(state.ctx.buffer.is_empty(), content="true")
}

///|
test "IniParseState::process_char/start_phase" {
  let state = IniParseState::new()
  // Test transition from Start to ReadingSection
  let state = state.process_char('[')
  inspect(state.phase, content="ReadingSection")
  // Test transition from Start to InComment
  let state = IniParseState::new().process_char(';')
  inspect(state.phase, content="InComment")
  // Test transition from Start to ReadingKey
  let state = IniParseState::new().process_char('a')
  inspect(state.phase, content="ReadingKey")
}

///|
test "panic IniParseState::process_char/invalid_equal_sign" {
  let state = IniParseState::new()
  ignore(state.process_char('='))
}

///|
test "panic IniParseState::process_char/unclosed_section" {
  let state = IniParseState::new()
  let state = state.process_char('[')
  ignore(state.process_char('\n'))
}

///|
test "panic IniParseState::process_char/unexpected_equal_sign" {
  let state = IniParseState::new()
  ignore(state.process_char('='))
}

///|
test "IniParseState::process_char/global_section_edge_cases" {
  let state = IniParseState::new()
  inspect(state.process_char('[').phase, content="ReadingSection")
  inspect(state.process_char('G').phase, content="ReadingSection")
  let tmp = state.process_char(']')
  inspect(tmp.phase, content="Start")
  inspect(tmp.ctx.current_section, content="NamedSection(\"g\")")
  inspect(state.process_char('[').phase, content="ReadingSection")
  try {
    let _ = tmp.process_char('#')
    assert_true(false) // This shouldn't happen according to the test expectation
  } catch {
    IniParseError::UnclosedSection(_) => () // Expected behavior
    _ => assert_true(false) // Unexpected error type
  }
}

///|
test "panic IniParseState::process_char/unclosed_section_newline" {
  let state = IniParseState::new()
  ignore(state.process_char('['))
  ignore(state.process_char('\n'))
}

///|
test "IniParseState::handle_start/reading_section" {
  let state = IniParseState::new()
  let updated_state = state.handle_start('[')
  inspect(updated_state.phase, content="ReadingSection")
  inspect(updated_state.ctx.buffer.is_empty(), content="true")
}

///|
test "panic IniParseState::handle_start/unexpected_equal_sign" {
  let state = IniParseState::new()
  ignore(state.handle_start('='))
}

///|
test "IniParseState::handle_start/in_comment" {
  let state = IniParseState::new()
  let updated_state = state.handle_start('#')
  inspect(updated_state.phase, content="InComment")
}

///|
test "IniParseState::handle_section_char/basic" {
  let state = IniParseState::new()
  // Test normal section name
  let state = state
    .handle_section_char('t')
    .handle_section_char('e')
    .handle_section_char('s')
    .handle_section_char('t')
    .handle_section_char(']')
  inspect(state.ctx.current_section, content="NamedSection(\"test\")")
  inspect(state.phase, content="Start")
}

///|
test "panic IniParseState::handle_section_char/empty_section" {
  let state = IniParseState::new()
  ignore(state.handle_section_char(']'))
}

///|
test "IniParseState::handle_section_char/unclosed_section" {
  fn new_state() -> IniParseState raise IniParseError {
    IniParseState::new().handle_section_char('t')
  }

  // Test both comment characters
  assert_true(
    try {
      let _ = new_state().handle_section_char('#')
      false
    } catch {
      IniParseError::UnclosedSection(_) => true
      _ => false
    },
  )
  assert_true(
    try {
      let _ = new_state().handle_section_char(';')
      false
    } catch {
      IniParseError::UnclosedSection(_) => true
      _ => false
    },
  )

  // Test newline
  assert_true(
    try {
      let _ = new_state().handle_section_char('\n')
      false
    } catch {
      IniParseError::UnclosedSection(_) => true
      _ => false
    },
  )
}

///|
test "IniParseState::handle_section_char/duplicate_section" {
  let state = IniParseState::new()
  let state = state.parse(
    (
      #|[test]
      #|key=foo
      #|key2=value
      #|
      #|[test]
      #|key=bar
      #|
    ),
  )
  let section = state.ctx.sections.get(Section::NamedSection("test")).unwrap()
  inspect(section.get("key"), content="Some(Value(\"bar\"))")
  inspect(section.get("key2"), content="Some(Value(\"value\"))")
}

///|
test "IniParseState::handle_key_char/basic" {
  let state = IniParseState::new()
  let state = state.handle_key_char('k')
  let state = state.handle_key_char('e')
  let state = state.handle_key_char('y')
  let state = state.handle_key_char('=')
  inspect(state.ctx.key, content="key")
  inspect(state.phase, content="ReadingValue(None)")
  inspect(state.ctx.buffer.is_empty(), content="true")
}

///|
test "panic IniParseState::handle_key_char/empty_buffer" {
  let state = IniParseState::new()
  ignore(state.handle_key_char('='))
}

///|
test "IniParseState::handle_key_char/whitespace" {
  let state = IniParseState::new()
  // Leading whitespace should be ignored
  let state = state.handle_key_char(' ')
  inspect(state.ctx.buffer.is_empty(), content="true")

  // Key characters should be recorded
  let state = state.handle_key_char('k')
  let state = state.handle_key_char('e')
  let state = state.handle_key_char('y')

  // Trailing whitespace should be recorded after first non-whitespace
  let state = state.handle_key_char(' ')
  inspect(state.ctx.buffer.to_string(), content="key ")
}

///|
test "IniParseState::handle_key_char/valueless_key" {
  let state = IniParseState::new()
  let state = state.handle_key_char('k')
  let state = state.handle_key_char('e')
  let state = state.handle_key_char('y')
  let state = state.handle_key_char('\n')
  inspect(state.ctx.key, content="key")
  inspect(state.phase, content="Start")
  let section = state.ctx.sections.get(state.ctx.current_section)
  inspect(section is Some(_), content="true")
  let value = section.unwrap().get("key")
  inspect(value, content="Some(Value(\"\"))")
}

///|
test "IniParseState::handle_key_char/comment_in_key" {
  let state = IniParseState::new()
  let state = state.handle_key_char('k')
  let state = state.handle_key_char('e')
  let state = state.handle_key_char('y')
  let state = state.handle_key_char(';')
  inspect(state.phase, content="Start")
  inspect(
    state.ctx.sections.get(Section::Global).unwrap().get("key").unwrap(),
    content="Value(\"\")",
  )
}

///|
test "panic IniParseState::handle_value_char/quote_not_closed_nl" {
  let state = IniParseState::new()
  let state = state.handle_value_char('\n')
  ignore(state)
}

///|
test "panic IniParseState::handle_value_char/quote_not_closed_comment" {
  let state = IniParseState::new()
  let state = state.handle_value_char(';')
  ignore(state)
}

///|
test "panic IniParseState::handle_value_char/quote_mismatch_single_double" {
  let state = IniParseState::new()
  let state = state.parse("[section]\nkey=")
  let state = state.handle_value_char('"')
  let state = state.handle_value_char('\'')
  ignore(state)
}

///|
test "panic IniParseState::handle_value_char/quote_mismatch_double_single" {
  let state = IniParseState::new()
  let state = state.parse("[section]\nkey=")
  let state = state.handle_value_char('\'')
  let state = state.handle_value_char('"')
  ignore(state)
}

///|
test "IniParseState::handle_comment/basic" {
  let state = IniParseState::new().parse("[section]\nkey=value#")
  // Test non-newline characters should keep the state unchanged
  let state = state.handle_comment('a')
  inspect(state.phase, content="InComment")
  let state = IniParseState::new().parse("[section]\nkey=value#")
  // Test newline should reset the line
  let state = state.handle_comment('\n')
  inspect(state.phase, content="Start")
  inspect(state.ctx.buffer.is_empty(), content="true")
}

///|
test "IniParseState::handle_comment/special_chars" {
  let state = IniParseState::new().parse("[section]\nkey=value#")
  // Test special characters should not affect the state
  inspect(state.handle_comment('#').phase, content="InComment")
  inspect(state.handle_comment(';').phase, content="InComment")
  inspect(state.handle_comment('=').phase, content="InComment")
  inspect(state.handle_comment('[').phase, content="InComment")
  inspect(state.handle_comment(']').phase, content="InComment")
}

///|
test "IniParseState::commit_value/basic" {
  let state = IniParseState::new()
  let state = state.parse("[section]\nkey=value")
  let state = state.commit_value()
  inspect(state.ctx.buffer.is_empty(), content="true")
  // Verify the section exists
  let section = state.ctx.sections.get(Section::NamedSection("section"))
  inspect(section is Some(_), content="true")
  // Verify the key exists in the section
  let value = section.unwrap().get("key")
  inspect(value is Some(_), content="true")
}

///|
test "panic IniParseState::commit_value/no_section" {
  let state = IniParseState::new()
  state.ctx.buffer.write_string("value")
  ignore(state.commit_value())
}

///|
test "IniParseState::commit_value/empty_value" {
  let state = IniParseState::new()
  let state = state.parse("[section]\nkey=")
  let state = state.commit_value()
  // Verify the section exists
  let section = state.ctx.sections.get(Section::NamedSection("section"))
  inspect(section is Some(_), content="true")
  // Verify the key exists in the section with empty value
  let value = section.unwrap().get("key")
  inspect(value is Some(_), content="true")
}