///|
priv struct LexEnv {
  errors : Array[(Position, Position, LexicalError)]
  docstrings : Array[@list.List[(Location, Comment)]]
  comment : Bool
  file : String
  tokens : Array[(Token, Position, Position)]
  mut current_line : Int
  mut current_bol : Int
  last_unhandled_comment : Ref[(Comment, Int)?]
  asi_context : ASIContext
  start_cnum : Int
  is_interpolation : Bool
  enable_metavar : Bool
  mut surrogate_pair_count : Int
}

///|
fn LexEnv::add_lexing_error(
  self : LexEnv,
  start~ : Int,
  end~ : Int,
  err : LexicalError,
) -> Unit {
  let loc_start = self.make_pos(start)
  let loc_end = self.make_pos(end)
  self.errors.push((loc_start, loc_end, err))
}

///|
fn LexEnv::make_pos(self : LexEnv, cnum : Int) -> Position {
  {
    fname: self.file,
    lnum: self.current_line,
    cnum: self.start_cnum + cnum,
    bol: self.current_bol,
  }
}

///|
fn LexEnv::add_token(
  self : LexEnv,
  tok : Token,
  start : Position,
  end : Position,
) -> Unit {
  self.asi_context.add_token(
    tokens=self.tokens,
    last_unhandled_comment=self.last_unhandled_comment,
    tok,
  )
  // handle comment: add comments to token array
  self.tokens.push((tok, start, end))
  if self.comment {
    let tok_index = self.tokens.length() - 1
    fn prehandle_comment(c : Comment) -> Unit {
      self.last_unhandled_comment.val = Some((c, tok_index))
    }

    fn handle_comment(ci : (Comment, Int)) -> Unit {
      let (c, i) = ci
      let mut at_file_start = false
      fn count_newlines(start_index : Int, direction : Int) -> Int {
        let mut count = 0
        let mut index = start_index
        while true {
          let target_index = index + direction
          match self.tokens.get(target_index) {
            Some((COMMENT(_), _, _)) => return count
            Some((NEWLINE, _, _)) =>
              if count >= 2 {
                return count
              } else {
                count += 1
                index = target_index
              }
            None | Some((EOF, _, _)) => {
              if direction < 0 {
                at_file_start = true
              }
              return 0
            }
            Some(_) => return count
          }
        } nobreak {
          panic() // impossible
        }
      }

      let leading_newlines = @cmp.minimum(2, count_newlines(i, -1))
      // Make sure add trailing newline if the comment is at end of file
      let trailing_newlines = @cmp.minimum(2, count_newlines(i, 1))
      let (_, start, end) = self.tokens[i]
      let kind = if leading_newlines == 0 && !at_file_start {
        CommentKind::InlineTrailing
      } else {
        Ownline(
          leading_blank_line=leading_newlines == 2 && !at_file_start,
          trailing_blank_line=trailing_newlines == 2,
        )
      }
      self.tokens[i] = (COMMENT({ ..c, kind, }), start, end)
    }

    match (self.last_unhandled_comment.val, tok) {
      (_, NEWLINE) => ()
      (Some(ci), COMMENT(c)) => {
        handle_comment(ci)
        prehandle_comment(c)
      }
      (None, COMMENT(c)) => prehandle_comment(c)
      (Some(ci), _) => handle_comment(ci)
      (None, _) => ()
    }
  }
}

///|
fn LexEnv::add_token_with_loc(
  self : LexEnv,
  tok : Token,
  start~ : Int,
  end~ : Int,
  start_offset? : Int = 0,
) -> Unit {
  let start = self.make_pos(start + start_offset)
  let end = self.make_pos(end)
  self.add_token(tok, start, end)
}

///|
fn LexEnv::preserve_comment(self : LexEnv) -> (Comment, Int, Int) -> Unit {
  if self.comment {
    fn(comment, start, end) {
      let docstrings = self.docstrings
      if comment.content is [_, _, '/', ..] {
        let loc = Location::{
          start: self.make_pos(start),
          end: self.make_pos(end),
        }
        if docstrings.is_empty() {
          docstrings.push(@list.singleton((loc, comment)))
        } else {
          guard! docstrings.last() is Some(More(head, ..) as last)
          let last_idx = docstrings.length() - 1
          if loc.start.lnum - head.0.start.lnum > 1 {
            docstrings[last_idx] = last.rev()
            docstrings.push(@list.singleton((loc, comment)))
          } else {
            docstrings[last_idx] = docstrings[last_idx].add((loc, comment))
          }
        }
      }
    }
  } else {
    fn(_cmt, _start, _end) {  }
  }
}

///|
fn LexEnv::register_surrogate_pair(self : LexEnv, view : StringView) -> Unit {
  if !use_utf16_location.val {
    let mut count = 0
    for i = 0; i < view.length(); i = i + 1 {
      let c1 = view[i]
      if c1.is_leading_surrogate() && i + 1 < view.length() {
        let c2 = view[i + 1]
        if c2.is_trailing_surrogate() {
          count += 1
          continue i + 2
        }
      }
    }
    self.surrogate_pair_count += count
  }
}

///|
fn LexEnv::register_surrogate_pair_for_char(self : LexEnv, ch : Char) -> Unit {
  if !use_utf16_location.val {
    if ch.to_int() >= 0x10000 {
      self.surrogate_pair_count += 1
    }
  }
}

///|
fn LexEnv::calc_offset(
  self : LexEnv,
  base~ : StringView,
  view : StringView,
) -> Int {
  if use_utf16_location.val {
    view.start_offset() - base.start_offset()
  } else {
    view.start_offset() - base.start_offset() - self.surrogate_pair_count
  }
}