///|
priv struct ParserState {
  options : ParseOptions
  preamble : Array[Property]
  sections : Array[Section]
  diagnostics : Array[Diagnostic]
  mut current_section : Section?
  mut root : Bool
  mut offset : Int
}

///|
fn ParserState::diagnose(
  self : ParserState,
  line : Int,
  column : Int,
  end_column : Int,
  severity : Severity,
  code : String,
  message : String,
  hint : String,
) -> Unit {
  self.diagnostics.push(
    make_diagnostic(
      self.options.path,
      line,
      column,
      end_column,
      self.offset,
      severity,
      code,
      message,
      hint,
    ),
  )
}

///|
fn ParserState::finish_section(self : ParserState) -> Unit {
  if self.current_section is Some(section) {
    self.sections.push(section)
    self.current_section = None
  }
}

///|
fn ParserState::has_key_in_current_scope(
  self : ParserState,
  key : String,
) -> Bool {
  match self.current_section {
    Some(section) => section.properties.any(property => property.key == key)
    None => self.preamble.any(property => property.key == key)
  }
}

///|
fn ParserState::push_property(self : ParserState, property : Property) -> Unit {
  if self.options.warn_duplicate_keys &&
    self.has_key_in_current_scope(property.key) {
    self.diagnose(
      property.source.line,
      property.source.column,
      property.span.end.column,
      Warning,
      "EC1006",
      "duplicate property in the same scope: " + property.key,
      "The later value wins; remove the earlier assignment if this is intentional.",
    )
  }
  match self.current_section {
    Some(section) => section.properties.push(property)
    None => {
      if property.key == "root" {
        let root_value = lower_ascii(property.value)
        if root_value == "true" {
          self.root = true
        } else if root_value == "false" {
          self.root = false
        } else {
          self.diagnose(
            property.source.line,
            property.source.column,
            property.span.end.column,
            Error,
            "EC1007",
            "root must be true or false",
            "Use `root = true` to stop searching parent directories.",
          )
        }
      } else if self.options.warn_unknown_preamble {
        self.diagnose(
          property.source.line,
          property.source.column,
          property.span.end.column,
          Warning,
          "EC1008",
          "property outside a section has no effect: " + property.key,
          "Move the property below a section header such as `[*]`.",
        )
      }
      self.preamble.push(property)
    }
  }
}

///|
fn ParserState::parse_section(
  self : ParserState,
  line : String,
  line_number : Int,
) -> Bool {
  guard line.has_prefix("[") else { return false }
  if !line.has_suffix("]") {
    self.diagnose(
      line_number,
      1,
      line.length() + 1,
      Error,
      "EC1001",
      "section header must end with ]",
      "Add a closing bracket to the section header.",
    )
    return true
  }
  let pattern = line[1:line.length() - 1].to_owned()
  if pattern == "" {
    self.diagnose(
      line_number,
      1,
      line.length() + 1,
      Error,
      "EC1002",
      "section pattern cannot be empty",
      "Provide a glob pattern, for example `[*.mbt]`.",
    )
    return true
  }
  self.finish_section()
  self.current_section = Some({
    pattern,
    properties: [],
    source: make_location(self.options.path, line_number, 1, self.offset),
    span: make_span(
      self.options.path,
      line_number,
      1,
      line.length() + 1,
      self.offset,
    ),
  })
  true
}

///|
fn ParserState::parse_assignment(
  self : ParserState,
  line : String,
  line_number : Int,
) -> Unit {
  match find_unescaped_separator(line, self.options.allow_colon_separator) {
    None =>
      self.diagnose(
        line_number,
        1,
        line.length() + 1,
        Error,
        "EC1003",
        "expected a key/value assignment",
        "Separate the property name and value with `=`.",
      )
    Some(separator) => {
      let raw_key = line[:separator].to_owned()
      let raw_value = line[separator + 1:].to_owned()
      let key_start = first_non_space(raw_key)
      let key_end = last_non_space_end(raw_key)
      let value_start = first_non_space(raw_value)
      let value_end = last_non_space_end(raw_value)
      let key = lower_ascii(
        unescape_assignment(raw_key[key_start:key_end].to_owned()),
      )
      let value = unescape_assignment(
        raw_value[value_start:value_end].to_owned(),
      )
      if key == "" {
        self.diagnose(
          line_number,
          1,
          separator + 1,
          Error,
          "EC1004",
          "property name cannot be empty",
          "Add a property name before the separator.",
        )
      } else if value == "" {
        self.diagnose(
          line_number,
          separator + 2,
          line.length() + 1,
          Warning,
          "EC1005",
          "property value is empty",
          "Use `unset` to remove an inherited value explicitly.",
        )
        self.push_property({
          key,
          value,
          raw_key: raw_key[key_start:key_end].to_owned(),
          raw_value: raw_value[value_start:value_end].to_owned(),
          source: make_location(
            self.options.path,
            line_number,
            key_start + 1,
            self.offset + key_start,
          ),
          span: make_span(
            self.options.path,
            line_number,
            key_start + 1,
            line.length() + 1,
            self.offset + key_start,
          ),
        })
      } else {
        self.push_property({
          key,
          value,
          raw_key: raw_key[key_start:key_end].to_owned(),
          raw_value: raw_value[value_start:value_end].to_owned(),
          source: make_location(
            self.options.path,
            line_number,
            key_start + 1,
            self.offset + key_start,
          ),
          span: make_span(
            self.options.path,
            line_number,
            key_start + 1,
            line.length() + 1,
            self.offset + key_start,
          ),
        })
      }
    }
  }
}

///|
fn ParserState::parse_line(
  self : ParserState,
  raw_line : String,
  line_number : Int,
) -> Unit {
  let without_bom = strip_utf8_bom(raw_line, line_number == 1)
  let line = trim_owned(without_bom.trim_end().to_owned())
  if line == "" || line.has_prefix("#") || line.has_prefix(";") {
    return
  }
  if !self.parse_section(line, line_number) {
    self.parse_assignment(line, line_number)
  }
}

///|
/// Parse an EditorConfig document with explicit options.
pub fn parse_with_options(
  source : String,
  options : ParseOptions,
) -> ParseResult {
  let state = {
    options,
    preamble: [],
    sections: [],
    diagnostics: [],
    current_section: None,
    root: false,
    offset: 0,
  }
  let mut line_number = 0
  for raw_line in source.split("\n") {
    line_number += 1
    state.parse_line(raw_line.to_owned(), line_number)
    state.offset = state.offset + raw_line.length() + 1
  }
  state.finish_section()
  {
    config: {
      path: options.path,
      root: state.root,
      preamble: state.preamble,
      sections: state.sections,
      source_text: source,
    },
    diagnostics: state.diagnostics,
  }
}

///|
/// Parse an in-memory EditorConfig document using compatibility defaults.
pub fn parse(source : String) -> ParseResult {
  parse_with_options(source, ParseOptions::default())
}

///|
/// Parse a document while retaining its source path in diagnostics.
pub fn parse_document(path : String, source : String) -> ParseResult {
  let options = ParseOptions::default()
  parse_with_options(source, { ..options, path, })
}