//! A lexical analyzer for the Maat programming language.
//!
//! Converts raw source code into a stream of tokens, backed by a
//! [`compile-time DFA`] generated by [`logos`]. It handles whitespace, operators,
//! keywords, identifiers, and numeric literals while maintaining zero-copy
//! efficiency via string slices.
//!
//! [`compile-time DFA`]: https://en.wikipedia.org/wiki/Deterministic_finite_automaton
#![forbid(unsafe_code)]

mod num;
mod token;

use logos::{Filter, Lexer, Logos, Skip};
use maat_span::Span;
use num::{NumSuffix, NumToken};
pub use token::{KEYWORDS, Token, TokenKind};

/// The main lexer/tokenizer for Maat.
///
/// # Lifetime
///
/// The `'src` lifetime parameter ties the lexer to the source string it analyzes,
/// ensuring all produced tokens reference the original source without allocation.
///
/// # Examples
///
/// ```
/// # use maat_lexer::{MaatLexer, TokenKind};
/// let source = "let x = 42;";
/// let mut lexer = MaatLexer::new(source);
///
/// assert_eq!(lexer.next_token().kind, TokenKind::Let);
/// assert_eq!(lexer.next_token().kind, TokenKind::Ident);
/// assert_eq!(lexer.next_token().kind, TokenKind::Assign);
/// assert_eq!(lexer.next_token().kind, TokenKind::Int);
/// assert_eq!(lexer.next_token().kind, TokenKind::Semicolon);
/// assert_eq!(lexer.next_token().kind, TokenKind::Eof);
/// ```
pub struct MaatLexer<'src> {
    inner: Lexer<'src, RawToken>,
}

impl<'src> MaatLexer<'src> {
    /// Creates a new lexer for the given source code.
    ///
    /// The lexer is initialized at the beginning of the source string and is ready
    /// to produce tokens via [`next_token`](MaatLexer::next_token).
    ///
    /// # Parameters
    ///
    /// * `s` - The source code to tokenize.
    ///
    /// # Returns
    ///
    /// A new [`MaatLexer`] instance positioned at the start of the source.
    #[inline]
    pub fn new(s: &'src str) -> Self {
        Self {
            inner: RawToken::lexer(s),
        }
    }

    /// Advances the lexer and returns the next token from the source.
    ///
    /// This method consumes characters from the source stream and produces a single
    /// token. Whitespace and comments are automatically skipped. The method handles:
    ///
    /// - Single-character operators and delimiters
    /// - Multi-character operators (`==`, `!=`, `<=`, `>=`)
    /// - Keywords and identifiers (with Unicode support)
    /// - Integer literals with optional type suffixes
    /// - Invalid characters
    ///
    /// When the end of the source is reached, this method returns a token with
    /// kind [`TokenKind::Eof`].
    ///
    /// # Returns
    ///
    /// The next [`Token`] in the source stream.
    pub fn next_token(&mut self) -> Token<'src> {
        match self.inner.next() {
            Some(Ok(raw)) => self.emit_token(raw),
            Some(Err(())) => {
                let span = self.inner.span();
                Token::new(
                    TokenKind::Invalid,
                    self.inner.slice(),
                    Span::new(span.start, span.end),
                )
            }
            None => {
                let pos = self.inner.source().len();
                Token::new(TokenKind::Eof, "", Span::new(pos, pos))
            }
        }
    }

    /// Converts a successfully matched [`RawToken`] into a [`Token`].
    ///
    /// String literals have their surrounding quotes stripped from the `literal`
    /// field. Numeric literals have their type suffix excluded from `literal`
    /// while the full span (including suffix) is preserved.
    fn emit_token(&self, raw: RawToken) -> Token<'src> {
        let span = self.inner.span();
        let maat_span = Span::new(span.start, span.end);
        let cur_tok = self.inner.slice();

        match raw {
            RawToken::String => {
                let literal = &cur_tok[1..cur_tok.len() - 1];
                Token::new(TokenKind::String, literal, maat_span)
            }
            RawToken::Char => {
                let literal = &cur_tok[1..cur_tok.len() - 1];
                Token::new(TokenKind::Char, literal, maat_span)
            }
            RawToken::Number(num) => {
                let literal = &self.inner.source()[span.start..span.start + num.value_len as usize];
                Token::new(num.kind, literal, maat_span)
            }
            RawToken::Ident => Token::new(TokenKind::Ident, cur_tok, maat_span),
            RawToken::Label => Token::new(TokenKind::Label, &cur_tok[1..], maat_span),
            RawToken::DocComment => Token::new(TokenKind::DocComment, &cur_tok[3..], maat_span),
            other => Token::new(other.to_token_kind(), cur_tok, maat_span),
        }
    }
}

/// Internal token type produced by [`logos`].
///
/// Each variant maps to a [`TokenKind`]. Complex tokens (strings,
/// numbers) carry additional metadata extracted by lexer callbacks.
/// Comment and whitespace tokens are skipped automatically.
#[derive(Logos, Debug, Clone, Copy, PartialEq)]
#[logos(skip r"[ \t\r\n]+")]
enum RawToken {
    #[token("let")]
    Let,
    #[token("if")]
    If,
    #[token("else")]
    Else,
    #[token("true")]
    True,
    #[token("false")]
    False,
    #[token("fn")]
    Fn,
    #[token("return")]
    Return,
    #[token("macro")]
    Macro,
    #[token("as")]
    As,
    #[token("loop")]
    Loop,
    #[token("while")]
    While,
    #[token("for")]
    For,
    #[token("in")]
    In,
    #[token("break")]
    Break,
    #[token("continue")]
    Continue,
    #[token("where")]
    Where,
    #[token("struct")]
    Struct,
    #[token("enum")]
    Enum,
    #[token("match")]
    Match,
    #[token("impl")]
    Impl,
    #[token("trait")]
    Trait,
    #[token("self")]
    SelfValue,
    #[token("Self")]
    SelfType,
    #[token("mod")]
    Mod,
    #[token("use")]
    Use,
    #[token("pub")]
    Pub,
    #[token("mut")]
    Mut,

    #[token("==")]
    Equal,
    #[token("!=")]
    NotEqual,
    #[token("<=")]
    LessEqual,
    #[token(">=")]
    GreaterEqual,
    #[token("&&")]
    And,
    #[token("||")]
    Or,
    #[token("<<")]
    ShiftLeft,
    #[token(">>")]
    ShiftRight,
    #[token("+=")]
    AddAssign,
    #[token("-=")]
    SubAssign,
    #[token("*=")]
    MulAssign,
    #[token("/=")]
    DivAssign,
    #[token("%=")]
    RemAssign,
    #[token("->")]
    Arrow,
    #[token("=>")]
    FatArrow,
    #[token("::")]
    PathSep,
    #[token("..=")]
    DotDotEqual,
    #[token("..")]
    DotDot,

    #[token("=")]
    Assign,
    #[token("+")]
    Plus,
    #[token("-")]
    Minus,
    #[token("!")]
    Bang,
    #[token("*")]
    Asterisk,
    #[token("/")]
    Slash,
    #[token("%")]
    Percent,
    #[token("<")]
    Less,
    #[token(">")]
    Greater,
    #[token("&")]
    Ampersand,
    #[token("|")]
    Pipe,
    #[token("^")]
    Caret,
    #[token(".")]
    Dot,
    #[token("?")]
    Question,
    #[token("#")]
    Hash,
    #[token(",")]
    Comma,
    #[token(";")]
    Semicolon,
    #[token(":")]
    Colon,
    #[token("(")]
    LParen,
    #[token(")")]
    RParen,
    #[token("{")]
    LBrace,
    #[token("}")]
    RBrace,
    #[token("[")]
    LBracket,
    #[token("]")]
    RBracket,

    #[regex(r"//[^\n]*", lex_line_comment, allow_greedy = true)]
    DocComment,

    #[token("/*", skip_block_comment)]
    Comment,

    #[regex(r#""([^"\\]|\\.)*""#)]
    String,

    #[regex(r"0[bB][01]+", lex_radix_number)]
    #[regex(r"0[oO][0-7]+", lex_radix_number)]
    #[regex(r"0[xX][0-9a-fA-F]+", lex_radix_number)]
    #[regex(r"[0-9]+", lex_decimal_number)]
    Number(NumToken),

    #[regex(r"[\p{L}_][\p{L}\p{N}_]*")]
    Ident,

    #[regex(r"'([^'\\]|\\[nrt0\\'\x22]|\\u\{[0-9a-fA-F]{1,6}\})'")]
    Char,

    #[regex(r"'[a-zA-Z_][a-zA-Z0-9_]*")]
    Label,
}

impl RawToken {
    /// Maps this internal token variant to the corresponding [`TokenKind`].
    #[inline]
    const fn to_token_kind(self) -> TokenKind {
        match self {
            Self::Let => TokenKind::Let,
            Self::If => TokenKind::If,
            Self::Else => TokenKind::Else,
            Self::True => TokenKind::True,
            Self::False => TokenKind::False,
            Self::Fn => TokenKind::Fn,
            Self::Return => TokenKind::Return,
            Self::Macro => TokenKind::Macro,
            Self::As => TokenKind::As,
            Self::Loop => TokenKind::Loop,
            Self::While => TokenKind::While,
            Self::For => TokenKind::For,
            Self::In => TokenKind::In,
            Self::Break => TokenKind::Break,
            Self::Continue => TokenKind::Continue,
            Self::Where => TokenKind::Where,
            Self::Struct => TokenKind::Struct,
            Self::Enum => TokenKind::Enum,
            Self::Match => TokenKind::Match,
            Self::Impl => TokenKind::Impl,
            Self::Trait => TokenKind::Trait,
            Self::SelfValue => TokenKind::SelfValue,
            Self::SelfType => TokenKind::SelfType,
            Self::Mod => TokenKind::Mod,
            Self::Use => TokenKind::Use,
            Self::Pub => TokenKind::Pub,
            Self::Mut => TokenKind::Mut,

            Self::Equal => TokenKind::Equal,
            Self::NotEqual => TokenKind::NotEqual,
            Self::LessEqual => TokenKind::LessEqual,
            Self::GreaterEqual => TokenKind::GreaterEqual,
            Self::And => TokenKind::And,
            Self::Or => TokenKind::Or,
            Self::ShiftLeft => TokenKind::ShiftLeft,
            Self::ShiftRight => TokenKind::ShiftRight,
            Self::AddAssign => TokenKind::AddAssign,
            Self::SubAssign => TokenKind::SubAssign,
            Self::MulAssign => TokenKind::MulAssign,
            Self::DivAssign => TokenKind::DivAssign,
            Self::RemAssign => TokenKind::RemAssign,
            Self::Arrow => TokenKind::Arrow,
            Self::FatArrow => TokenKind::FatArrow,
            Self::PathSep => TokenKind::PathSep,
            Self::DotDotEqual => TokenKind::DotDotEqual,
            Self::DotDot => TokenKind::DotDot,

            Self::Assign => TokenKind::Assign,
            Self::Plus => TokenKind::Plus,
            Self::Minus => TokenKind::Minus,
            Self::Bang => TokenKind::Bang,
            Self::Asterisk => TokenKind::Asterisk,
            Self::Slash => TokenKind::Slash,
            Self::Percent => TokenKind::Percent,
            Self::Less => TokenKind::Less,
            Self::Greater => TokenKind::Greater,
            Self::Ampersand => TokenKind::Ampersand,
            Self::Pipe => TokenKind::Pipe,
            Self::Caret => TokenKind::Caret,
            Self::Dot => TokenKind::Dot,
            Self::Question => TokenKind::Question,
            Self::Hash => TokenKind::Hash,
            Self::Comma => TokenKind::Comma,
            Self::Semicolon => TokenKind::Semicolon,
            Self::Colon => TokenKind::Colon,
            Self::LParen => TokenKind::LParen,
            Self::RParen => TokenKind::RParen,
            Self::LBrace => TokenKind::LBrace,
            Self::RBrace => TokenKind::RBrace,
            Self::LBracket => TokenKind::LBracket,
            Self::RBracket => TokenKind::RBracket,

            Self::Comment
            | Self::DocComment
            | Self::String
            | Self::Char
            | Self::Number(_)
            | Self::Ident
            | Self::Label => {
                unreachable!()
            }
        }
    }
}

/// Distinguishes documentation comments (`///`) from regular line comments (`//`).
fn lex_line_comment(src: &mut Lexer<'_, RawToken>) -> Filter<()> {
    let slice = src.slice();
    if slice.starts_with("///") && !slice.starts_with("////") {
        Filter::Emit(())
    } else {
        Filter::Skip
    }
}

/// Skips a nested block comment (`/* ... */`), supporting arbitrary nesting depth.
fn skip_block_comment(src: &mut Lexer<'_, RawToken>) -> Skip {
    let remainder = src.remainder().as_bytes();
    let mut depth = 1usize;
    let mut i = 0;
    while i < remainder.len() && depth > 0 {
        match (remainder[i], remainder.get(i + 1).copied()) {
            (b'/', Some(b'*')) => {
                depth += 1;
                i += 2;
            }
            (b'*', Some(b'/')) => {
                depth -= 1;
                i += 2;
            }
            _ => i += 1,
        }
    }
    src.bump(i);
    Skip
}

/// Tries to read and consume a type suffix from the remaining source.
///
/// Recognizes integer suffixes (`i8`..`i128`, `isize`, `u8`..`u128`, `usize`) and
/// the field-element suffix `fe`, each with an optional leading underscore
/// (`_i64`, `_fe`). Advances the lexer past the suffix if one is found.
fn try_read_suffix(src: &mut Lexer<'_, RawToken>) -> Option<NumSuffix> {
    let remainder = src.remainder().as_bytes();
    let underscore = usize::from(remainder.first() == Some(&b'_'));
    let (suffix, len) = num::match_num_suffix(&remainder[underscore..])?;
    src.bump(underscore + len);
    Some(suffix)
}

/// Lexes a radix-prefixed number (binary, octal, hex) with an optional type suffix.
fn lex_radix_number(src: &mut Lexer<'_, RawToken>) -> NumToken {
    let value_len = src.slice().len() as u32;
    let kind = try_read_suffix(src)
        .map(NumSuffix::to_token_kind)
        .unwrap_or(TokenKind::Int);
    NumToken { kind, value_len }
}

/// Lexes a decimal number, detecting invalid float literals and optional type suffixes.
///
/// Floating-point literals are illegal in Maat. If a fractional part (`.digits`)
/// or exponent (`e`/`E`) is detected, the entire float is consumed and emitted
/// as [`TokenKind::Invalid`].
fn lex_decimal_number(src: &mut Lexer<'_, RawToken>) -> NumToken {
    let value_len = src.slice().len() as u32;

    // Reject floating-point literals
    if let Some(float_len) = try_lex_float(src) {
        return NumToken {
            kind: TokenKind::Invalid,
            value_len: value_len + float_len,
        };
    }

    let kind = try_read_suffix(src)
        .map(NumSuffix::to_token_kind)
        .unwrap_or(TokenKind::Int);
    NumToken { kind, value_len }
}

/// Attempts to read the remaining source as a floating-point number,
/// returning the length of the number (as `Some(u32)`) if successful, or
/// None otherwise.
fn try_lex_float(src: &mut Lexer<'_, RawToken>) -> Option<u32> {
    let remainder = src.remainder().as_bytes();
    let has_frac =
        remainder.first() == Some(&b'.') && remainder.get(1).is_some_and(u8::is_ascii_digit);
    let has_exp = matches!(remainder.first(), Some(b'e' | b'E'));

    if has_frac || has_exp {
        let mut float_len = 0;
        if has_frac {
            float_len += 1;
            while remainder.get(float_len).is_some_and(u8::is_ascii_digit) {
                float_len += 1;
            }
        }
        if remainder
            .get(float_len)
            .is_some_and(|&b| b == b'e' || b == b'E')
        {
            float_len += 1;
            if remainder
                .get(float_len)
                .is_some_and(|&b| b == b'+' || b == b'-')
            {
                float_len += 1;
            }
            while remainder.get(float_len).is_some_and(u8::is_ascii_digit) {
                float_len += 1;
            }
        }
        src.bump(float_len);
        return Some(float_len as u32);
    }
    None
}
