///|
priv struct DotenvIter {
  cursor : Cursor
  substitution_data : @hashmap.T[String, String?]
}

///|
fn DotenvIter::new(data : String) -> DotenvIter {
  let cursor = Cursor::new(data)
  { cursor, substitution_data: @hashmap.new() }
}

///|
fn DotenvIter::internal_load(
  self : DotenvIter,
  load_fn : (String, String, EnvMap) -> Unit,
) -> EnvMap raise ParseBufError {
  self.remove_bom()
  let map = EnvMap::new()
  for {
    let item = self.next()
    if item.is_empty() {
      break
    }
    let item = item.unwrap().unwrap_or_error()
    let (k, v) = item
    load_fn(k, v, map)
  }
  map
}

///|
fn DotenvIter::load(self : DotenvIter) -> EnvMap raise ParseBufError {
  self.internal_load(fn(k, v, map) { map.map[k] = v })
}

///|
fn DotenvIter::load_and_modify(self : DotenvIter) -> EnvMap raise ParseBufError {
  // Not thread-safe
  let vars = @sys.get_env_vars()
  self.internal_load(fn(k, v, map) {
    if not(vars.contains(k)) {
      @sys.set_env_var(k, v)
    }
    map.map[k] = v
  })
}

///|
fn DotenvIter::load_and_modify_override(
  self : DotenvIter,
) -> EnvMap raise ParseBufError {
  self.internal_load(fn(k, v, map) {
    @sys.set_env_var(k, v)
    map.map[k] = v
  })
}

///| Removes the BOM if it exists.
///
/// For more info, see the [Unicode BOM character](https://www.compart.com/en/unicode/U+FEFF).
fn DotenvIter::remove_bom(self : DotenvIter) -> Unit {
  guard self.cursor.length() >= 3 else { return }
  let origin_pos = self.cursor.pos()
  if self.cursor.next() is Some(0xfeff) {
    // The BOM is present, so exit early
    return
  }
  // The BOM is not present, so we reset the cursor position
  self.cursor.set_pos(origin_pos)
}

///|
priv enum ParseState {
  Complete
  Escape
  StrongOpen
  StrongOpenEscape
  WeakOpen
  WeakOpenEscape
  Comment
  WhiteSpace
} derive(Eq)

///|
fn ParseState::eval_end(self : ParseState, buf : String) -> (UInt, ParseState) {
  let mut cur_state = self
  let mut cur_pos : UInt = 0
  for pos, c in buf.iter2() {
    cur_pos = pos.reinterpret_as_uint()
    cur_state = match cur_state {
      ParseState::WhiteSpace =>
        match c {
          '#' => return (cur_pos, ParseState::Comment)
          '\\' => ParseState::Escape
          '"' => ParseState::WeakOpen
          '\'' => ParseState::StrongOpen
          _ => ParseState::Complete
        }
      ParseState::Escape => ParseState::Complete
      ParseState::Complete =>
        match c {
          c if c.is_whitespace() && c != '\n' && c != '\r' =>
            ParseState::WhiteSpace
          '\\' => ParseState::Escape
          '"' => ParseState::WeakOpen
          '\'' => ParseState::StrongOpen
          _ => ParseState::Complete
        }
      ParseState::WeakOpen =>
        match c {
          '\\' => ParseState::WeakOpenEscape
          '"' => ParseState::Complete
          _ => ParseState::WeakOpen
        }
      ParseState::WeakOpenEscape => ParseState::WeakOpen
      ParseState::StrongOpen =>
        match c {
          '\\' => ParseState::StrongOpenEscape
          '\'' => ParseState::Complete
          _ => ParseState::StrongOpen
        }
      ParseState::StrongOpenEscape => ParseState::StrongOpen
      // Comments last the entire line.
      ParseState::Comment => abort("should have returned already")
    }
  }
  (cur_pos, cur_state)
}

///|
fn DotenvIter::next(
  self : DotenvIter,
) -> Result[(String, String), ParseBufError]? {
  for {
    let line = match self.cursor.next_line() {
      Some(Ok(line)) => line
      Some(Err(e)) => return Some(Err(e))
      None => return None
    }
    match (try? parse_line(line, self.substitution_data)) {
      Ok(Some(res)) => return Some(Ok(res))
      Ok(None) => continue
      Err(e) => return Some(Err(e))
    }
  }
}

///|
pub suberror ParseBufError {
  LineParse(String, UInt)
}