use crate::scanner::token::{Token, TokenKind};
use std::{iter::Cloned, iter::Peekable, slice};

/// Cursor for tokens in a token sequence, providing direct value access and advanced peeking.
#[derive(Debug)]
pub struct Cursor<'a> {
    tokens: Peekable<Cloned<slice::Iter<'a, Token>>>,
    value: Option<Token>,
}

impl<'a> Cursor<'a> {
    /// Create a new token cursor.
    ///
    /// The cursor is initialized with the first token of the sequence.
    #[must_use]
    pub fn new(tokens: &'a [Token]) -> Self {
        let mut cursor = Cursor {
            tokens: tokens.iter().cloned().peekable(),
            value: None,
        };

        cursor.advance();
        cursor
    }

    /// Get the current cursor value.
    #[must_use]
    pub fn value(&self) -> Option<&Token> {
        self.value.as_ref()
    }

    /// Check if the current token matches one of a list of kinds.
    pub fn matches(&mut self, kinds: &[TokenKind]) -> bool {
        self.value.as_ref().is_some_and(|t| kinds.contains(&t.kind))
    }

    /// Peek at the next token (one past the current position) without advancing.
    ///
    /// Returns `None` if there is no next token.
    #[must_use]
    pub fn peek(&mut self) -> Option<&Token> {
        self.tokens.peek()
    }

    /// Advance the cursor to the next token.
    pub fn advance(&mut self) -> Option<&Token> {
        self.value = self.tokens.next();
        self.value.as_ref()
    }

    /// Conditionally advance the cursor if the current token matches one of a list of kinds.
    pub fn advance_if_matches(&mut self, kinds: &[TokenKind]) -> bool {
        self.value
            .as_ref()
            .is_some_and(|t| kinds.contains(&t.kind))
            .then(|| self.advance())
            .is_some()
    }

    /// Check current token kind and consume current token.
    #[allow(clippy::needless_pass_by_value)]
    pub fn consume(&mut self, kind: TokenKind) -> bool {
        self.value
            .as_ref()
            .is_some_and(|t| t.kind == kind)
            .then(|| self.advance())
            .is_some()
    }

    /// Check if cursor is at end-of-stream (EOS).
    #[must_use]
    pub fn is_eos(&self) -> bool {
        self.value.is_none()
    }
}

#[cfg(test)]
mod tests {
    use super::Cursor;
    use crate::Location;
    use crate::scanner::token::{Token, TokenKind, Tokens};
    use pretty_assertions::assert_eq;

    /// Generates token sequence for `let x = 42;`.
    fn create_token_sequence() -> Tokens {
        vec![
            Token::new(TokenKind::Let, "let", Location::new(0, 1, 1)),
            Token::new(
                TokenKind::Identifier("x".to_owned()),
                "x",
                Location::new(0, 1, 5),
            ),
            Token::new(TokenKind::Is, "=", Location::new(0, 1, 7)),
            Token::new(TokenKind::Number(42.0), "42", Location::new(0, 1, 9)),
            Token::new(TokenKind::Semicolon, ";", Location::new(0, 1, 11)),
        ]
    }

    #[test]
    fn new() {
        let tokens = create_token_sequence();
        let cursor = Cursor::new(&tokens);

        assert_eq!(cursor.value(), create_token_sequence().first());
    }

    #[test]
    fn value() {
        let tokens = create_token_sequence();
        let mut cursor = Cursor::new(&tokens);

        assert_eq!(cursor.value(), create_token_sequence().first());
        cursor.advance();
        assert_eq!(cursor.value(), create_token_sequence().get(1));
    }

    #[test]
    fn matches() {
        let tokens = vec![
            Token::new(TokenKind::Plus, "+", Location::new(0, 1, 3)),
            Token::new(TokenKind::Minus, "-", Location::new(0, 1, 5)),
        ];
        let mut cursor = Cursor::new(&tokens);

        assert_eq!(cursor.matches(&[TokenKind::Minus]), false);
        assert_eq!(cursor.matches(&[TokenKind::Plus]), true);

        cursor.advance();

        assert_eq!(cursor.matches(&[TokenKind::Dot, TokenKind::For]), false);
        assert_eq!(cursor.matches(&[TokenKind::Minus]), true);
    }

    #[test]
    fn advance() {
        let tokens = create_token_sequence();
        let mut cursor = Cursor::new(&tokens);

        for i in 1..10 {
            cursor.advance();
            assert_eq!(cursor.value(), create_token_sequence().get(i));
        }

        assert_eq!(cursor.value(), None);
    }

    #[test]
    fn advance_if_matches() {
        let tokens = vec![
            Token::new(TokenKind::Plus, "+", Location::new(0, 1, 1)),
            Token::new(TokenKind::Minus, "-", Location::new(0, 1, 3)),
            Token::new(TokenKind::Star, "*", Location::new(0, 1, 5)),
        ];
        let mut cursor = Cursor::new(&tokens);

        assert_eq!(cursor.advance_if_matches(&[TokenKind::Comma]), false);
        assert_eq!(cursor.advance_if_matches(&[TokenKind::Minus]), false);
        assert_eq!(cursor.advance_if_matches(&[TokenKind::Plus]), true);
        assert_eq!(
            cursor.advance_if_matches(&[TokenKind::Dot, TokenKind::For]),
            false
        );
        assert_eq!(cursor.advance_if_matches(&[TokenKind::Minus]), true);
        assert_eq!(cursor.advance_if_matches(&[TokenKind::Dot]), false);
        assert_eq!(cursor.advance_if_matches(&[TokenKind::Star]), true);
    }

    #[test]
    fn is_eos_empty() {
        let tokens = Tokens::new();
        let cursor = Cursor::new(&tokens);

        assert_eq!(cursor.is_eos(), true);
    }

    #[test]
    fn consume() {
        let tokens = vec![
            Token::new(TokenKind::Number(1.0), "1", Location::new(0, 1, 1)),
            Token::new(TokenKind::Plus, "+", Location::new(0, 1, 3)),
            Token::new(TokenKind::Number(2.0), "2", Location::new(0, 1, 5)),
            Token::new(TokenKind::Semicolon, ";", Location::new(0, 1, 7)),
        ];
        let mut cursor = Cursor::new(&tokens);

        // Should fail to consume wrong token kind
        assert_eq!(cursor.consume(TokenKind::Plus), false);
        assert_eq!(cursor.consume(TokenKind::Minus), false);

        // Should successfully consume correct token kind
        assert_eq!(cursor.consume(TokenKind::Number(1.0)), true);
        assert_eq!(cursor.consume(TokenKind::Plus), true);
        assert_eq!(cursor.consume(TokenKind::Number(2.0)), true);
        assert_eq!(cursor.consume(TokenKind::Semicolon), true);

        // Should fail when at end of stream
        assert_eq!(cursor.consume(TokenKind::Plus), false);
    }

    #[test]
    fn is_eos() {
        let tokens = create_token_sequence();
        let num_tokens = tokens.len();
        let mut cursor = Cursor::new(&tokens);

        for _ in 0..num_tokens {
            assert_eq!(cursor.is_eos(), false);
            cursor.advance();
        }

        assert_eq!(cursor.is_eos(), true);
    }
}
