use crate::{Kind, Lex, Token};

/// Responsible for tokenizing source code.
#[derive(Copy, Clone, Debug)]
pub struct Lexer {
    token_buffer_size: usize,
}

impl Lexer {
    //! Constructors

    /// Creates a new lexer.
    pub const fn new() -> Self {
        Self {
            token_buffer_size: 4096,
        }
    }
}

impl Lexer {
    //! Lexing

    /// Tokenizes the source code.
    pub fn lex<'a>(&self, source: &'a str) -> Lex<'a> {
        let mut tokens: Vec<Token<'a>> = Vec::with_capacity(self.token_buffer_size);
        let mut line: usize = 0;
        let mut position: usize = 0;
        let mut remaining: &'a str = source;
        while !remaining.is_empty() {
            let (len, kind) = Lexer::next(remaining);
            let value: &'a str = &remaining[..len];
            remaining = &remaining[len..];
            tokens.push(Token::new(kind, value, line, position));
            if kind == Kind::LineEnding {
                line += 1;
                position = 0;
            } else {
                position += len;
            }
        }
        Lex::new(source, tokens)
    }
}

impl Lexer {
    //! Private Utilities

    /// Gets the next token length and kind. The string must be non-empty.
    fn next(s: &str) -> (usize, Kind) {
        assert!(!s.is_empty());
        let b: &[u8] = s.as_bytes();
        match b[0] {
            c if c > 127 => (Lexer::prefix_len(b, |c| c > 127), Kind::NonAscii),
            b'\r' | b'\n' => (Lexer::line_ending_len(b), Kind::LineEnding),
            b' ' | b'\t' => (
                Lexer::prefix_len(b, |c| c == b' ' || c == b'\t'),
                Kind::Whitespace,
            ),
            c if c <= 31 || c == 127 => (
                Lexer::prefix_len(b, |c| {
                    (c <= 31 && c != b'\r' && c != b'\n' && c != b'\t') || c == 127
                }),
                Kind::Controls,
            ),
            c if c.is_ascii_alphanumeric() || c == b'_' => (
                Lexer::prefix_len(b, |c| c.is_ascii_alphanumeric() || c == b'_'),
                Kind::Symbol,
            ),
            c => (1, Kind::Special(c)),
        }
    }

    /// Gets the number of prefix bytes matching the filter.
    fn prefix_len<F>(b: &[u8], f: F) -> usize
    where
        F: Fn(u8) -> bool,
    {
        for (i, c) in b.iter().enumerate() {
            if !f(*c) {
                return i;
            }
        }
        b.len()
    }

    /// Gets the length of the line-ending. The bytes must be non-empty.
    fn line_ending_len(b: &[u8]) -> usize {
        assert!(!b.is_empty());
        if b.len() == 1 || b[0] == b'\n' || b[1] != b'\n' {
            1
        } else {
            2
        }
    }
}
