///|
pub fn parse_line(
  line : String,
  substitution_data : @hashmap.T[String, String?]
) -> (String, String)?!ParseBufError {
  let parser = LineParser::new(line, substitution_data)
  parser.parse_line!()
}

///|
priv struct LineParser {
  original_line : String
  substitution_data : @hashmap.T[String, String?]
  mut line : String
  mut pos : UInt
}

///|
fn LineParser::new(
  line : String,
  substitution_data : @hashmap.T[String, String?]
) -> LineParser {
  {
    original_line: line,
    substitution_data,
    line: line.trim_space(), // we don’t want trailing whitespace
    pos: 0,
  }
}

///|
fn LineParser::err(self : LineParser) -> ParseBufError {
  ParseBufError::LineParse(self.original_line, self.pos)
}

///|
fn LineParser::parse_line(self : LineParser) -> (String, String)?!ParseBufError {
  self.skip_whitespace()
  // if its an empty line or a comment, skip it
  if self.line.is_empty() || self.line.starts_with("#") {
    return None
  }
  let mut key = self.parse_key!()
  self.skip_whitespace()

  // export can be either an optional prefix or a key itself
  if key == "export" {
    // here we check for an optional `=`, below we throw directly when it’s not found.
    if self.expect_equal().is_err() {
      key = self.parse_key!()
      self.skip_whitespace()
      self.expect_equal().unwrap_or_error!()
    }
  } else {
    self.expect_equal().unwrap_or_error!()
  }
  self.skip_whitespace()
  if self.line.is_empty() || self.line.starts_with("#") {
    self.substitution_data[key] = None
    return Some((key, ""))
  }
  let parsed_value = parse_value!(self.line, self.substitution_data)
  self.substitution_data[key] = Some(parsed_value)
  Some((key, parsed_value))
}

///|
fn LineParser::parse_key(self : LineParser) -> String!ParseBufError {
  fn string_starts_with(src : String, predicate : (Char) -> Bool) -> Bool {
    if src.length() == 0 {
      return false
    }
    predicate(src[0])
  }

  fn string_find(src : String, predicate : (Char) -> Bool) -> UInt? {
    let mut index = 0U
    while index < src.length().reinterpret_as_uint() {
      if predicate(src[index.reinterpret_as_int()]) {
        return Some(index)
      }
      index += 1
    }
    None
  }

  if not(
      string_starts_with(self.line, fn(c) {
        c.is_ascii_alphabetic() || c == '_'
      }),
    ) {
    raise self.err()
  }
  let index = match
    string_find(self.line, fn(c) {
      not(c.is_ascii_alphabetic() || c.is_numeric() || c == '_' || c == '.')
    }) {
    Some(index) => index
    None => self.line.length().reinterpret_as_uint()
  }
  self.pos += index
  let key = self.line.substring(start=0, end=index.reinterpret_as_int())
  self.line = self.line.substring(start=index.reinterpret_as_int())
  key
}

///|
fn LineParser::expect_equal(self : LineParser) -> Result[Unit, ParseBufError] {
  if not(self.line.starts_with("=")) {
    return Err(self.err())
  }
  self.line = self.line.substring(start=1)
  self.pos += 1
  Ok(())
}

///|
fn LineParser::skip_whitespace(self : LineParser) -> Unit {
  let mut first_space = 0
  while first_space < self.line.length() {
    if not(self.line[first_space].is_whitespace()) {
      break
    }
    first_space += 1
  }
  if first_space == self.line.length() {
    self.line = ""
  } else {
    self.line = self.line.substring(start=first_space)
  }
}

///|
priv enum SubstitutionMode {
  None
  Block
  EscapedBlock
} derive(Eq)

///|
fn parse_value(
  input : String,
  substitution_data : @hashmap.T[String, String?]
) -> String!ParseBufError {
  // '
  let mut strong_quote = false
  // "
  let mut weak_quote = false
  let mut escaped = false
  let mut expecting_end = false

  //FIXME (from Rust) can this be done without yet another allocation per line?
  let output = StringBuilder::new()
  let mut substitution_mode = SubstitutionMode::None
  let substitution_name = StringBuilder::new()
  for index, c in input.iter2() {
    //the regex _should_ already trim whitespace off the end
    //expecting_end is meant to permit: k=v #comment
    //without affecting: k=v#comment
    //and throwing on: k=v w
    if expecting_end {
      if c == ' ' || c == '\t' {
        continue
      } else if c == '#' {
        break
      }
      raise ParseBufError::LineParse(input, index.reinterpret_as_uint())
    } else if escaped {
      //TODO (from Rust) I tried handling literal \r but various issues
      //imo not worth worrying about until there's a use case
      //(actually handling backslash 0x10 would be a whole other matter)
      //then there's \v \f bell hex... etc
      match c {
        '\\' | '\'' | '"' | '$' | ' ' => output.write_char(c)
        // handle \n case
        'n' => output.write_char('\n')
        _ => raise ParseBufError::LineParse(input, index.reinterpret_as_uint())
      }
      escaped = false
    } else if strong_quote {
      if c == '\'' {
        strong_quote = false
      } else {
        output.write_char(c)
      }
    } else if substitution_mode != SubstitutionMode::None {
      if c.is_ascii_alphabetic() || c.is_numeric() {
        substitution_name.write_char(c)
      } else {
        match substitution_mode {
          SubstitutionMode::None => abort("unreachable")
          SubstitutionMode::Block =>
            if c == '{' && substitution_name.is_empty() {
              substitution_mode = SubstitutionMode::EscapedBlock
            } else {
              apply_substitution(
                substitution_data,
                substitution_name.to_string(),
                output,
              )
              substitution_name.reset()
              if c == '$' {
                substitution_mode = if not(strong_quote) && not(escaped) {
                  SubstitutionMode::Block
                } else {
                  SubstitutionMode::None
                }
              } else {
                substitution_mode = SubstitutionMode::None
                output.write_char(c)
              }
            }
          SubstitutionMode::EscapedBlock =>
            if c == '}' {
              substitution_mode = SubstitutionMode::None
              apply_substitution(
                substitution_data,
                substitution_name.to_string(),
                output,
              )
              substitution_name.reset()
            } else {
              substitution_name.write_char(c)
            }
        }
      }
    } else if c == '$' {
      substitution_mode = if not(strong_quote) && not(escaped) {
        SubstitutionMode::Block
      } else {
        SubstitutionMode::None
      }
    } else if weak_quote {
      if c == '"' {
        weak_quote = false
      } else if c == '\\' {
        escaped = true
      } else {
        output.write_char(c)
      }
    } else if c == '\'' {
      strong_quote = true
    } else if c == '"' {
      weak_quote = true
    } else if c == '\\' {
      escaped = true
    } else if c == ' ' || c == '\t' {
      expecting_end = true
    } else {
      output.write_char(c)
    }
  }

  //XXX also fail if escaped? or...
  if substitution_mode == SubstitutionMode::EscapedBlock ||
    strong_quote ||
    weak_quote {
    let value_length = input.length()
    raise ParseBufError::LineParse(
      input,
      if value_length == 0 {
        0U
      } else {
        (value_length - 1).reinterpret_as_uint()
      },
    )
  } else {
    apply_substitution(substitution_data, substitution_name.to_string(), output)
    substitution_name.reset()
    output.to_string()
  }
}

///|
fn apply_substitution(
  substitution_data : @hashmap.T[String, String?],
  substitution_name : String,
  output : StringBuilder
) -> Unit {
  let env_vars = @sys.get_env_vars()
  if env_vars.contains(substitution_name) {
    output.write_string(env_vars[substitution_name].unwrap())
  } else {
    let stored_value = substitution_data[substitution_name].or(None)
    output.write_string(stored_value.or_default())
  }
}