///|
/// Strip UTF-8 BOM (U+FEFF) from the beginning of a string if present.
fn strip_bom(src : String) -> String {
  if src.length() >= 1 && src.code_unit_at(0).to_int() == 0xFEFF {
    src[1:src.length()].to_owned()
  } else {
    src
  }
}

///|
/// Parse a `.env` file string into a Map of key-value pairs.
///
/// Supports:
/// - UTF-8 BOM (U+FEFF) is silently stripped
/// - `KEY=value` and `KEY: value` syntax
/// - `export KEY=value` prefix
/// - Single-quoted values (literal, no escape processing)
/// - Double-quoted values (with `\n`, `\r`, `\t`, `\\` escape sequences)
/// - Backtick-quoted values (literal, no escape processing)
/// - Inline comments with `#`
/// - Multiline values within quotes
/// - Empty values
/// - Keys and values surrounded by whitespace
///
/// Last duplicate key wins. Malformed quotes are silently ignored.
pub fn parse(src : String) -> Map[String, String] {
  let result = Map([], capacity=16)
  let p = Parser::new(strip_bom(src))
  while !p.is_eof() {
    p.skip_whitespace()
    if p.is_eof() {
      break
    }
    match p.peek() {
      Some('\n') | Some('\r') => {
        p.skip_eol()
        continue
      }
      Some('#') => {
        p.skip_to_eol()
        p.skip_eol()
        continue
      }
      _ => ()
    }
    try_skip_export(p)
    let key = parse_key(p)
    if key == "" {
      p.skip_to_eol()
      p.skip_eol()
      continue
    }
    p.skip_whitespace()
    match p.peek() {
      Some('=') | Some(':') => p.pos += 1
      _ => {
        p.skip_to_eol()
        p.skip_eol()
        continue
      }
    }
    let value = parse_value(p)
    result[key] = value
  }
  result
}

///|
/// Like `parse`, but returns `Err(ParseError)` on malformed input
/// (e.g. unclosed quotes).
pub fn try_parse(src : String) -> Result[Map[String, String], ParseError] {
  let result = Map([], capacity=16)
  let p = Parser::new(strip_bom(src))
  while !p.is_eof() {
    p.skip_whitespace()
    if p.is_eof() {
      break
    }
    match p.peek() {
      Some('\n') | Some('\r') => {
        p.skip_eol()
        continue
      }
      Some('#') => {
        p.skip_to_eol()
        p.skip_eol()
        continue
      }
      _ => ()
    }
    try_skip_export(p)
    let key = parse_key(p)
    if key == "" {
      p.skip_to_eol()
      p.skip_eol()
      continue
    }
    p.skip_whitespace()
    match p.peek() {
      Some('=') | Some(':') => p.pos += 1
      _ => {
        p.skip_to_eol()
        p.skip_eol()
        continue
      }
    }
    match try_parse_value(p) {
      Ok(value) => result[key] = value
      Err(e) => return Err(e)
    }
  }
  Ok(result)
}