///|
/// Parses INI text using default tolerant options.
pub fn parse_ini(name : String, text : String) -> ParseResult {
  parse_ini_with_options(name, text, default_parse_options())
}

///|
/// Parses Java-style properties text using defaults close to INI mode.
pub fn parse_properties(name : String, text : String) -> ParseResult {
  let options = default_parse_options()
  parse_properties_with_options(name, text, options)
}

///|
/// Parses INI text with explicit options.
pub fn parse_ini_with_options(
  name : String,
  text : String,
  options : ParseOptions,
) -> ParseResult {
  parse_lines(name, split_lines(normalize_newlines(text)), options, false)
}

///|
/// Parses properties text with explicit options.
pub fn parse_properties_with_options(
  name : String,
  text : String,
  options : ParseOptions,
) -> ParseResult {
  parse_lines(
    name,
    join_property_continuations(split_lines(normalize_newlines(text))),
    options,
    true,
  )
}

///|
fn parse_lines(
  name : String,
  lines : Array[String],
  options : ParseOptions,
  properties_mode : Bool,
) -> ParseResult {
  let entries : Array[ConfigEntry] = []
  let diagnostics : Array[Diagnostic] = []
  let mut section = ""
  let mut pending_comments : Array[String] = []
  let seen : Array[String] = []
  let mut line_number = 1
  for raw_line in lines {
    let trimmed = raw_line.trim()
    if trimmed == "" {
      if !options.preserve_comments {
        pending_comments = []
      }
    } else if is_comment(trimmed.to_owned(), properties_mode) {
      if options.preserve_comments {
        pending_comments.push(strip_comment_marker(trimmed.to_owned()))
      }
    } else if !properties_mode && is_section_line(trimmed.to_owned()) {
      let parsed = parse_section_name(trimmed.to_owned())
      if parsed == "" && options.strict_sections {
        diagnostics.push(
          error(
            "empty-section",
            "empty section names are not allowed",
            span=Some(span(name, line_number, 1)),
          ),
        )
      }
      section = parsed
      pending_comments = []
    } else {
      let content = if properties_mode {
        raw_line.trim().to_owned()
      } else {
        raw_line
      }
      let sep = find_separator(
        content,
        options.allow_colon_separator,
        properties_mode,
      )
      match sep {
        None =>
          if options.allow_empty_value {
            let key = trimmed.to_owned()
            add_entry(
              entries, diagnostics, seen, section, key, "", name, line_number, pending_comments,
            )
            pending_comments = []
          } else {
            diagnostics.push(
              error(
                "missing-separator",
                "expected '=' or ':' between key and value",
                span=Some(span(name, line_number, 1)),
              ),
            )
          }
        Some(index) => {
          let value_start = if properties_mode {
            properties_value_start(content, index)
          } else {
            index + 1
          }
          let raw_key = content[0:index].to_owned().trim().to_owned()
          let key = if properties_mode {
            unescape_properties_value(raw_key)
          } else {
            raw_key
          }
          let raw_value = content[value_start:content.length()]
            .to_owned()
            .trim()
            .to_owned()
          let value = if properties_mode {
            unescape_properties_value(unquote_value(raw_value))
          } else {
            unquote_value(raw_value)
          }
          if key == "" {
            diagnostics.push(
              error(
                "empty-key",
                "configuration keys must not be empty",
                span=Some(span(name, line_number, 1)),
              ),
            )
          } else {
            add_entry(
              entries, diagnostics, seen, section, key, value, name, line_number,
              pending_comments,
            )
          }
          pending_comments = []
        }
      }
    }
    line_number = line_number + 1
  }
  let document = { name, entries, diagnostics }
  { document, ok: diagnostics_ok(diagnostics) }
}

///|
fn add_entry(
  entries : Array[ConfigEntry],
  diagnostics : Array[Diagnostic],
  seen : Array[String],
  section : String,
  key : String,
  value : String,
  source : String,
  line_number : Int,
  comments : Array[String],
) -> Unit {
  let path = entry_path(section, key)
  if contains_string(seen, path) {
    diagnostics.push(
      warning(
        "duplicate-key",
        "duplicate key '" + path + "' overrides an earlier value",
        span=Some(span(source, line_number, 1)),
      ),
    )
  }
  seen.push(path)
  entries.push({
    section,
    key,
    value,
    source,
    span: span(source, line_number, 1),
    comments: comments.copy(),
  })
}

///|
fn normalize_newlines(text : String) -> String {
  text.replace(old="\r\n", new="\n").replace(old="\r", new="\n")
}

///|
fn split_lines(text : String) -> Array[String] {
  text.split("\n").map(line => line.to_owned()).collect()
}

///|
fn is_comment(text : String, properties_mode : Bool) -> Bool {
  text.has_prefix(";") ||
  text.has_prefix("#") ||
  (properties_mode && text.has_prefix("!"))
}

///|
fn strip_comment_marker(text : String) -> String {
  if text.length() <= 1 {
    ""
  } else {
    text[1:text.length()].to_owned().trim().to_owned()
  }
}

///|
fn is_section_line(text : String) -> Bool {
  text.has_prefix("[") && text.has_suffix("]")
}

///|
fn parse_section_name(text : String) -> String {
  if text.length() <= 2 {
    ""
  } else {
    text[1:text.length() - 1].to_owned().trim().to_owned()
  }
}

///|
fn find_separator(
  line : String,
  allow_colon : Bool,
  properties_mode : Bool,
) -> Int? {
  let mut index = 0
  let mut escaped = false
  while index < line.length() {
    let ch = line[index]
    if escaped {
      escaped = false
    } else if ch == '\\' {
      escaped = true
    } else if ch == '=' ||
      (allow_colon && ch == ':') ||
      (properties_mode && is_properties_whitespace(ch)) {
      return Some(index)
    }
    index = index + 1
  }
  None
}

///|
fn properties_value_start(line : String, separator : Int) -> Int {
  let mut index = separator
  while index < line.length() && is_properties_whitespace(line[index]) {
    index = index + 1
  }
  if index < line.length() && (line[index] == '=' || line[index] == ':') {
    index = index + 1
  }
  while index < line.length() && is_properties_whitespace(line[index]) {
    index = index + 1
  }
  index
}

///|
fn is_properties_whitespace(ch : UInt16) -> Bool {
  ch == ' ' || ch == '\t'
}

///|
fn unquote_value(value : String) -> String {
  if value.length() >= 2 {
    if (value.has_prefix("\"") && value.has_suffix("\"")) ||
      (value.has_prefix("'") && value.has_suffix("'")) {
      value[1:value.length() - 1].to_owned()
    } else {
      value
    }
  } else {
    value
  }
}

///|
fn contains_string(values : Array[String], target : String) -> Bool {
  for value in values {
    if value == target {
      return true
    }
  }
  false
}

///|
fn join_property_continuations(lines : Array[String]) -> Array[String] {
  let joined : Array[String] = []
  let mut current = ""
  let mut active = false
  for line in lines {
    if active {
      current = current + line.trim().to_owned()
    } else {
      current = line
      active = true
    }
    if ends_with_unescaped_backslash(current) {
      current = current[0:current.length() - 1].to_owned()
    } else {
      joined.push(current)
      current = ""
      active = false
    }
  }
  if active {
    joined.push(current)
  }
  joined
}

///|
fn ends_with_unescaped_backslash(text : String) -> Bool {
  if text.length() == 0 || !text.has_suffix("\\") {
    false
  } else {
    let mut count = 0
    let mut index = text.length() - 1
    while index >= 0 && text[index] == '\\' {
      count = count + 1
      index = index - 1
    }
    count % 2 == 1
  }
}