///|
pub struct ZoneToken {
  text_value : String
  quoted_value : Bool
  span_value : SourceSpan
} derive(Eq, Debug)

///|
pub struct ZoneStatement {
  token_values : Array[ZoneToken]
} derive(Eq, Debug)

///|
fn lexer_error(
  code : ZoneErrorCode,
  message : String,
  line : Int,
  column : Int,
) -> ZoneError {
  ZoneError::new(code, message, SourceSpan::point(line, column))
}

///|
fn ascii_character(code : Int) -> String {
  match code.to_char() {
    Some(value) => value.to_string()
    None => ""
  }
}

///|
fn is_zone_space(code : Int) -> Bool {
  code == 32 || code == 9
}

///|
fn is_decimal_digit(code : Int) -> Bool {
  code >= 48 && code <= 57
}

///|
fn finish_zone_token(
  tokens : Array[ZoneToken],
  text : String,
  quoted : Bool,
  start_line : Int,
  start_column : Int,
  end_line : Int,
  end_column : Int,
) -> Unit {
  tokens.push({
    text_value: text,
    quoted_value: quoted,
    span_value: SourceSpan::range(
      start_line, start_column, end_line, end_column,
    ),
  })
}

///|
/// Tokenize BIND-style zone text while preserving logical statements.
pub fn lex_zone(
  text : String,
  max_tokens? : Int = 100000,
  max_token_bytes? : Int = 65535,
) -> Result[Array[ZoneStatement], ZoneError] {
  if max_tokens < 0 || max_token_bytes < 0 {
    return Err(
      lexer_error(TokenLimitExceeded, "lexer limits cannot be negative", 0, 0),
    )
  }
  let statements : Array[ZoneStatement] = []
  let mut tokens : Array[ZoneToken] = []
  let mut current = ""
  let mut token_active = false
  let mut token_quoted = false
  let mut token_start_line = 0
  let mut token_start_column = 0
  let mut in_quote = false
  let mut parenthesis_depth = 0
  let mut line = 0
  let mut column = 0
  let mut index = 0
  let mut token_count = 0
  while index < text.length() {
    let code = text[index].to_int()
    if code > 127 {
      return Err(
        lexer_error(
          InvalidCharacter,
          "zone text must use ASCII syntax characters",
          line,
          column,
        ),
      )
    }
    if code == 92 {
      if !token_active {
        token_active = true
        token_start_line = line
        token_start_column = column
      }
      if index + 1 >= text.length() {
        return Err(
          lexer_error(
            UnterminatedEscape,
            "zone token ends with an incomplete escape",
            line,
            column,
          ),
        )
      }
      if index + 3 < text.length() &&
        is_decimal_digit(text[index + 1].to_int()) &&
        is_decimal_digit(text[index + 2].to_int()) &&
        is_decimal_digit(text[index + 3].to_int()) {
        let value = (text[index + 1].to_int() - 48) * 100 +
          (text[index + 2].to_int() - 48) * 10 +
          (text[index + 3].to_int() - 48)
        if value > 255 {
          return Err(
            lexer_error(
              InvalidCharacter,
              "decimal escape exceeds one octet",
              line,
              column,
            ),
          )
        }
        current = current + ascii_character(value)
        index = index + 4
        column = column + 4
      } else {
        let escaped = text[index + 1].to_int()
        if escaped == 13 || escaped == 10 {
          return Err(
            lexer_error(
              InvalidCharacter,
              "escaped line ending is not supported",
              line,
              column,
            ),
          )
        }
        current = current + ascii_character(escaped)
        index = index + 2
        column = column + 2
      }
      if current.length() > max_token_bytes {
        return Err(
          lexer_error(
            TokenLimitExceeded,
            "zone token exceeds the configured byte limit",
            line,
            column,
          ),
        )
      }
      continue
    }
    if in_quote {
      if code == 34 {
        in_quote = false
        index = index + 1
        column = column + 1
      } else if code == 13 || code == 10 {
        return Err(
          lexer_error(
            UnterminatedQuote,
            "quoted zone token crosses a physical line",
            line,
            column,
          ),
        )
      } else {
        current = current + ascii_character(code)
        index = index + 1
        column = column + 1
      }
      if current.length() > max_token_bytes {
        return Err(
          lexer_error(
            TokenLimitExceeded,
            "quoted token exceeds the configured byte limit",
            line,
            column,
          ),
        )
      }
      continue
    }
    if code == 34 {
      if !token_active {
        token_active = true
        token_start_line = line
        token_start_column = column
      }
      token_quoted = true
      in_quote = true
      index = index + 1
      column = column + 1
    } else if code == 59 {
      if token_active {
        finish_zone_token(
          tokens, current, token_quoted, token_start_line, token_start_column, line,
          column,
        )
        token_count = token_count + 1
        current = ""
        token_active = false
        token_quoted = false
      }
      while index < text.length() &&
            text[index].to_int() != 13 &&
            text[index].to_int() != 10 {
        index = index + 1
        column = column + 1
      }
    } else if code == 40 || code == 41 {
      if token_active {
        finish_zone_token(
          tokens, current, token_quoted, token_start_line, token_start_column, line,
          column,
        )
        token_count = token_count + 1
        current = ""
        token_active = false
        token_quoted = false
      }
      if code == 40 {
        parenthesis_depth = parenthesis_depth + 1
      } else if parenthesis_depth == 0 {
        return Err(
          lexer_error(
            UnbalancedParenthesis,
            "zone text has an unmatched closing parenthesis",
            line,
            column,
          ),
        )
      } else {
        parenthesis_depth = parenthesis_depth - 1
      }
      index = index + 1
      column = column + 1
    } else if is_zone_space(code) {
      if token_active {
        finish_zone_token(
          tokens, current, token_quoted, token_start_line, token_start_column, line,
          column,
        )
        token_count = token_count + 1
        current = ""
        token_active = false
        token_quoted = false
      }
      index = index + 1
      column = column + 1
    } else if code == 13 || code == 10 {
      if token_active {
        finish_zone_token(
          tokens, current, token_quoted, token_start_line, token_start_column, line,
          column,
        )
        token_count = token_count + 1
        current = ""
        token_active = false
        token_quoted = false
      }
      if parenthesis_depth == 0 && tokens.length() > 0 {
        statements.push({ token_values: tokens })
        tokens = []
      }
      if code == 13 &&
        index + 1 < text.length() &&
        text[index + 1].to_int() == 10 {
        index = index + 2
      } else {
        index = index + 1
      }
      line = line + 1
      column = 0
    } else {
      if !token_active {
        token_active = true
        token_start_line = line
        token_start_column = column
      }
      current = current + ascii_character(code)
      if current.length() > max_token_bytes {
        return Err(
          lexer_error(
            TokenLimitExceeded,
            "zone token exceeds the configured byte limit",
            line,
            column,
          ),
        )
      }
      index = index + 1
      column = column + 1
    }
    if token_count > max_tokens {
      return Err(
        lexer_error(
          TokenLimitExceeded,
          "zone text exceeds the configured token limit",
          line,
          column,
        ),
      )
    }
  }
  if in_quote {
    return Err(
      lexer_error(
        UnterminatedQuote,
        "zone text ends inside a quoted token",
        line,
        column,
      ),
    )
  }
  if parenthesis_depth != 0 {
    return Err(
      lexer_error(
        UnbalancedParenthesis,
        "zone text ends inside a parenthesized record",
        line,
        column,
      ),
    )
  }
  if token_active {
    finish_zone_token(
      tokens, current, token_quoted, token_start_line, token_start_column, line,
      column,
    )
    token_count = token_count + 1
  }
  if token_count > max_tokens {
    return Err(
      lexer_error(
        TokenLimitExceeded,
        "zone text exceeds the configured token limit",
        line,
        column,
      ),
    )
  }
  if tokens.length() > 0 {
    statements.push({ token_values: tokens })
  }
  Ok(statements)
}

///|
pub fn ZoneToken::text(self : ZoneToken) -> String {
  self.text_value
}

///|
pub fn ZoneToken::quoted(self : ZoneToken) -> Bool {
  self.quoted_value
}

///|
pub fn ZoneToken::span(self : ZoneToken) -> SourceSpan {
  self.span_value
}

///|
pub fn ZoneStatement::tokens(self : ZoneStatement) -> Array[ZoneToken] {
  self.token_values.copy()
}

///|
pub fn ZoneStatement::token_texts(self : ZoneStatement) -> Array[String] {
  let result : Array[String] = []
  for token in self.token_values {
    result.push(token.text())
  }
  result
}