use std::f64;

use thiserror::Error;

use crate::{Error, Result};

#[derive(PartialEq, Debug, Clone)]
pub enum Token {
    Iden(String),
    Fun(String),
    Op(String),
    Lparen,
    Rparen,
    Number(f64),
    Eof,
}

#[derive(Debug, Clone, Copy, Error)]
pub enum LexerError {
    #[error("encountered unknown character \'{0}\' during lexing")]
    UnknownCharacter(char),
}

#[derive(Debug)]
pub struct Lexer {
    tokens: Vec<Token>,
}
impl Lexer {
    const FUNCTIONS: [&'static str; 6] = ["sin", "cos", "ln", "sqrt", "exp", "tan"];
    const CONSTANTS: [&'static str; 2] = ["pi", "e"];

    pub fn new(input: &str) -> Result<Self> {
        let mut tokens = Vec::new();
        let mut chars = input.chars().peekable();

        while let Some(&c) = chars.peek() {
            match c {
                ' ' => {
                    chars.next();
                }
                '(' => {
                    tokens.push(Token::Lparen);
                    chars.next();
                }
                ')' => {
                    tokens.push(Token::Rparen);
                    chars.next();
                }
                '+' => {
                    tokens.push(Token::Op(String::from(c)));
                    chars.next();
                }
                '-' => {
                    tokens.push(Token::Op(String::from(c)));
                    chars.next();
                }
                '*' => {
                    tokens.push(Token::Op(String::from(c)));
                    chars.next();
                }
                '/' => {
                    tokens.push(Token::Op(String::from(c)));
                    chars.next();
                }
                '^' => {
                    tokens.push(Token::Op(String::from(c)));
                    chars.next();
                }
                'a'..='z' | 'A'..='Z' => {
                    let mut ident = String::new();
                    while let Some(&c) = chars.peek() {
                        if c.is_ascii_alphabetic() {
                            ident.push(c);
                            chars.next();
                        } else {
                            break;
                        }
                    }
                    if Self::FUNCTIONS.iter().any(|fun| *fun == ident) {
                        tokens.push(Token::Fun(ident.to_string()));
                    } else if Self::CONSTANTS.iter().any(|con| *con == ident) {
                        let index = Self::CONSTANTS
                            .iter()
                            .position(|con| *con == ident)
                            .unwrap(); //unwrap safe due to if check
                        let con_str = Self::CONSTANTS[index];
                        if con_str == "pi" {
                            tokens.push(Token::Number(f64::consts::PI));
                        } else if con_str == "e" {
                            tokens.push(Token::Number(f64::consts::E));
                        }
                    } else {
                        tokens.push(Token::Iden(ident))
                    }
                }
                '0'..='9' => {
                    let mut num_str = String::new();
                    while let Some(&c) = chars.peek() {
                        if c.is_ascii_digit() || c == '.' {
                            num_str.push(c);
                            chars.next();
                        } else {
                            break;
                        }
                    }
                    match num_str.parse() {
                        Ok(num) => tokens.push(Token::Number(num)),
                        Err(e) => return Err(Error::ParseFloat(e)),
                    }
                }
                c => return Err(Error::Lexer(LexerError::UnknownCharacter(c))),
            }
        }
        tokens.push(Token::Eof);
        tokens.reverse();
        Ok(Lexer { tokens })
    }

    pub fn get_next_token(&mut self) -> Token {
        self.tokens.pop().unwrap_or(Token::Eof)
    }

    pub fn peek(&self) -> Token {
        self.tokens.last().unwrap_or(&Token::Eof).to_owned()
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn parse_sin() {
        let lex = Lexer::new("sin(x)").unwrap();
        let mut expected = vec![
            Token::Fun("sin".into()),
            Token::Lparen,
            Token::Iden("x".into()),
            Token::Rparen,
            Token::Eof,
        ];
        expected.reverse();
        assert_eq!(lex.tokens, expected);
    }

    #[test]
    fn parse_exp() {
        let lex = Lexer::new("exp(x)").unwrap();
        let mut expected = vec![
            Token::Fun("exp".into()),
            Token::Lparen,
            Token::Iden("x".into()),
            Token::Rparen,
            Token::Eof,
        ];
        expected.reverse();
        assert_eq!(lex.tokens, expected);
    }

    #[test]
    fn parse_num() {
        let lex = Lexer::new("sin(2x)").unwrap();
        let mut expected = vec![
            Token::Fun("sin".into()),
            Token::Lparen,
            Token::Number(2.0),
            Token::Iden("x".into()),
            Token::Rparen,
            Token::Eof,
        ];
        expected.reverse();
        assert_eq!(lex.tokens, expected)
    }

    #[test]
    fn opertators() {
        let lex = Lexer::new("cos(x) + sin(2x)").unwrap();
        let mut expected = vec![
            Token::Fun("cos".into()),
            Token::Lparen,
            Token::Iden("x".into()),
            Token::Rparen,
            Token::Op("+".into()),
            Token::Fun("sin".into()),
            Token::Lparen,
            Token::Number(2.0),
            Token::Iden("x".into()),
            Token::Rparen,
            Token::Eof,
        ];
        expected.reverse();
        assert_eq!(lex.tokens, expected)
    }
    #[test]
    fn next() {
        let mut lex = Lexer::new("cos(x) + sin(2x)").unwrap();
        let token = lex.get_next_token();
        assert_eq!(token, Token::Fun("cos".into()));
        assert_eq!(lex.tokens.len(), 10);
    }
    #[test]
    fn peek() {
        let lex = Lexer::new("cos(x) + sin(2x)").unwrap();
        let token = lex.peek();
        assert_eq!(token, Token::Fun("cos".into()));
        assert_eq!(lex.tokens.len(), 11);
    }
}
