///|
priv enum QuoteMode {
  Unquoted
  SingleQuoted
  DoubleQuoted
} derive(Eq)

///|
priv struct Lexeme {
  value : String
  span : SourceSpan
}

///|
let max_line_characters : Int = 65_536

///|
let max_source_bytes : Int = 8 * 1024 * 1024

///|
fn lower_ascii(value : String) -> String {
  let output = StringBuilder()
  for char in value {
    output.write_char(char.to_ascii_lowercase())
  }
  output.to_string()
}

///|
fn location(path : String, line : Int, column : Int) -> SourceLocation {
  { path, line, column }
}

///|
fn span_from(start : SourceLocation, end_ : SourceLocation) -> SourceSpan {
  { start, end_ }
}

///|
fn invalid_control(char : Char) -> Bool {
  char.is_control() && char != '\t' && char != '\r'
}

///|
fn validate_character(
  char : Char,
  path : String,
  line : Int,
  column : Int,
) -> Unit raise ParseError {
  let at = location(path, line, column)
  if char == '\u{00}' {
    raise UnexpectedNul(location=at)
  }
  if invalid_control(char) {
    raise InvalidDirective(location=at, message="unsupported control character")
  }
}

///|
fn finish_lexeme(
  tokens : Array[Lexeme],
  output : StringBuilder,
  start : SourceLocation,
  end_ : SourceLocation,
) -> Unit {
  tokens.push({ value: output.to_string(), span: span_from(start, end_) })
  output.reset()
}

///|
fn write_backslash(
  characters : Array[Char],
  index : Int,
  output : StringBuilder,
) -> Int {
  if index + 1 < characters.length() &&
    (characters[index + 1] == '\'' || characters[index + 1] == '"') {
    output.write_char(characters[index + 1])
    2
  } else {
    output.write_char('\\')
    1
  }
}

///|
fn utf8_width(char : Char) -> Int {
  let codepoint = char.to_int()
  if codepoint <= 0x7f {
    1
  } else if codepoint <= 0x7ff {
    2
  } else if codepoint <= 0xffff {
    3
  } else {
    4
  }
}

///|
fn source_exceeds_byte_limit(source : String) -> Bool {
  let mut byte_count = 0
  for char in source {
    let width = utf8_width(char)
    if byte_count > max_source_bytes - width {
      return true
    }
    byte_count += width
  }
  false
}

///|
/// Decode one physical configuration line. A `#` begins a comment only at a
/// token boundary. OpenSSH preserves ordinary backslashes; only a backslash
/// directly before either quote character quotes that character.
fn lex_line(
  raw_line : String,
  path : String,
  line_number : Int,
) -> Array[Lexeme] raise ParseError {
  let characters = raw_line.to_array()
  let first = location(path, line_number, 1)
  if characters.length() > max_line_characters {
    raise LineTooLong(location=first, limit=max_line_characters)
  }
  let tokens : Array[Lexeme] = []
  let output = StringBuilder()
  let mut index = 0
  let mut column = 1
  let mut active = false
  let mut start = first
  let mut quote_start = first
  let mut mode = Unquoted
  while index < characters.length() {
    let char = characters[index]
    validate_character(char, path, line_number, column)
    if !active {
      if char == '#' {
        break
      }
      if char == ' ' || char == '\t' {
        index += 1
        column += 1
        continue
      }
      active = true
      start = location(path, line_number, column)
      if char == '\'' {
        mode = SingleQuoted
        quote_start = start
        index += 1
        column += 1
        continue
      }
      if char == '"' {
        mode = DoubleQuoted
        quote_start = start
        index += 1
        column += 1
        continue
      }
      if char == '\\' {
        let consumed = write_backslash(characters, index, output)
        index += consumed
        column += consumed
        continue
      }
      output.write_char(char)
      index += 1
      column += 1
      continue
    }
    match mode {
      Unquoted => {
        if char == ' ' || char == '\t' {
          finish_lexeme(
            tokens,
            output,
            start,
            location(path, line_number, column),
          )
          active = false
          index += 1
          column += 1
          continue
        }
        if char == '\'' {
          mode = SingleQuoted
          quote_start = location(path, line_number, column)
          index += 1
          column += 1
          continue
        }
        if char == '"' {
          mode = DoubleQuoted
          quote_start = location(path, line_number, column)
          index += 1
          column += 1
          continue
        }
        if char == '\\' {
          let consumed = write_backslash(characters, index, output)
          index += consumed
          column += consumed
          continue
        }
        output.write_char(char)
        index += 1
        column += 1
      }
      SingleQuoted => {
        if char == '\\' {
          let consumed = write_backslash(characters, index, output)
          index += consumed
          column += consumed
          continue
        } else if char == '\'' {
          mode = Unquoted
        } else {
          output.write_char(char)
        }
        index += 1
        column += 1
      }
      DoubleQuoted => {
        if char == '\\' {
          let consumed = write_backslash(characters, index, output)
          index += consumed
          column += consumed
          continue
        } else if char == '"' {
          mode = Unquoted
          index += 1
          column += 1
          continue
        }
        output.write_char(char)
        index += 1
        column += 1
      }
    }
  }
  if mode != Unquoted {
    raise UnterminatedQuote(location=quote_start)
  }
  if active {
    finish_lexeme(tokens, output, start, location(path, line_number, column))
  }
  tokens
}