// Clippy does not agree with rustc here for some reason.
#![allow(clippy::needless_lifetimes)]

use std::{fmt, iter::Enumerate, num::NonZeroUsize, str::FromStr};

use anyhow::Result;
use parse_display::Display;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use tokeniser::Input;
use tower_lsp::lsp_types::SemanticTokenType;
use winnow::{
    self,
    error::ParseError,
    stream::{ContainsToken, Stream},
};

use crate::{
    errors::KclError,
    parsing::ast::types::{ItemVisibility, VariableKind},
    source_range::SourceRange,
    CompilationError, ModuleId,
};

mod tokeniser;

pub(crate) use tokeniser::RESERVED_WORDS;

// Note the ordering, it's important that `m` comes after `mm` and `cm`.
pub const NUM_SUFFIXES: [&str; 10] = ["mm", "cm", "m", "inch", "in", "ft", "yd", "deg", "rad", "?"];

#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, ts_rs::TS, JsonSchema)]
#[repr(u32)]
pub enum NumericSuffix {
    None,
    Count,
    Length,
    Angle,
    Mm,
    Cm,
    M,
    Inch,
    Ft,
    Yd,
    Deg,
    Rad,
    Unknown,
}

impl NumericSuffix {
    #[allow(dead_code)]
    pub fn is_none(self) -> bool {
        self == Self::None
    }

    pub fn is_some(self) -> bool {
        self != Self::None
    }

    pub fn digestable_id(&self) -> &[u8] {
        match self {
            NumericSuffix::None => &[],
            NumericSuffix::Count => b"_",
            NumericSuffix::Unknown => b"?",
            NumericSuffix::Length => b"Length",
            NumericSuffix::Angle => b"Angle",
            NumericSuffix::Mm => b"mm",
            NumericSuffix::Cm => b"cm",
            NumericSuffix::M => b"m",
            NumericSuffix::Inch => b"in",
            NumericSuffix::Ft => b"ft",
            NumericSuffix::Yd => b"yd",
            NumericSuffix::Deg => b"deg",
            NumericSuffix::Rad => b"rad",
        }
    }
}

impl FromStr for NumericSuffix {
    type Err = CompilationError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "_" | "Count" => Ok(NumericSuffix::Count),
            "Length" => Ok(NumericSuffix::Length),
            "Angle" => Ok(NumericSuffix::Angle),
            "mm" | "millimeters" => Ok(NumericSuffix::Mm),
            "cm" | "centimeters" => Ok(NumericSuffix::Cm),
            "m" | "meters" => Ok(NumericSuffix::M),
            "inch" | "in" => Ok(NumericSuffix::Inch),
            "ft" | "feet" => Ok(NumericSuffix::Ft),
            "yd" | "yards" => Ok(NumericSuffix::Yd),
            "deg" | "degrees" => Ok(NumericSuffix::Deg),
            "rad" | "radians" => Ok(NumericSuffix::Rad),
            "?" => Ok(NumericSuffix::Unknown),
            _ => Err(CompilationError::err(SourceRange::default(), "invalid unit of measure")),
        }
    }
}

impl fmt::Display for NumericSuffix {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            NumericSuffix::None => Ok(()),
            NumericSuffix::Count => write!(f, "_"),
            NumericSuffix::Unknown => write!(f, "_?"),
            NumericSuffix::Length => write!(f, "Length"),
            NumericSuffix::Angle => write!(f, "Angle"),
            NumericSuffix::Mm => write!(f, "mm"),
            NumericSuffix::Cm => write!(f, "cm"),
            NumericSuffix::M => write!(f, "m"),
            NumericSuffix::Inch => write!(f, "in"),
            NumericSuffix::Ft => write!(f, "ft"),
            NumericSuffix::Yd => write!(f, "yd"),
            NumericSuffix::Deg => write!(f, "deg"),
            NumericSuffix::Rad => write!(f, "rad"),
        }
    }
}

#[derive(Clone, Debug, PartialEq)]
pub(crate) struct TokenStream {
    tokens: Vec<Token>,
}

impl TokenStream {
    fn new(tokens: Vec<Token>) -> Self {
        Self { tokens }
    }

    pub(super) fn remove_unknown(&mut self) -> Vec<Token> {
        let tokens = std::mem::take(&mut self.tokens);
        let (tokens, unknown_tokens): (Vec<Token>, Vec<Token>) = tokens
            .into_iter()
            .partition(|token| token.token_type != TokenType::Unknown);
        self.tokens = tokens;
        unknown_tokens
    }

    pub fn iter(&self) -> impl Iterator<Item = &Token> {
        self.tokens.iter()
    }

    pub fn is_empty(&self) -> bool {
        self.tokens.is_empty()
    }

    pub fn as_slice(&self) -> TokenSlice {
        TokenSlice::from(self)
    }
}

impl<'a> From<&'a TokenStream> for TokenSlice<'a> {
    fn from(stream: &'a TokenStream) -> Self {
        TokenSlice {
            start: 0,
            end: stream.tokens.len(),
            stream,
        }
    }
}

impl IntoIterator for TokenStream {
    type Item = Token;

    type IntoIter = std::vec::IntoIter<Token>;

    fn into_iter(self) -> Self::IntoIter {
        self.tokens.into_iter()
    }
}

#[derive(Debug, Clone)]
pub(crate) struct TokenSlice<'a> {
    stream: &'a TokenStream,
    /// Current position of the leading Token in the stream
    start: usize,
    /// The number of total Tokens in the stream
    end: usize,
}

impl<'a> std::ops::Deref for TokenSlice<'a> {
    type Target = [Token];

    fn deref(&self) -> &Self::Target {
        &self.stream.tokens[self.start..self.end]
    }
}

impl<'a> TokenSlice<'a> {
    pub fn token(&self, i: usize) -> &Token {
        &self.stream.tokens[i + self.start]
    }

    pub fn iter(&self) -> impl Iterator<Item = &Token> {
        (**self).iter()
    }

    pub fn without_ends(&self) -> Self {
        Self {
            start: self.start + 1,
            end: self.end - 1,
            stream: self.stream,
        }
    }

    pub fn as_source_range(&self) -> SourceRange {
        let stream_len = self.stream.tokens.len();
        let first_token = if stream_len == self.start {
            &self.stream.tokens[self.start - 1]
        } else {
            self.token(0)
        };
        let last_token = if stream_len == self.end {
            &self.stream.tokens[stream_len - 1]
        } else {
            self.token(self.end - self.start)
        };
        SourceRange::new(first_token.start, last_token.end, last_token.module_id)
    }
}

impl<'a> IntoIterator for TokenSlice<'a> {
    type Item = &'a Token;

    type IntoIter = std::slice::Iter<'a, Token>;

    fn into_iter(self) -> Self::IntoIter {
        self.stream.tokens[self.start..self.end].iter()
    }
}

impl<'a> Stream for TokenSlice<'a> {
    type Token = Token;
    type Slice = Self;
    type IterOffsets = Enumerate<std::vec::IntoIter<Token>>;
    type Checkpoint = Checkpoint;

    fn iter_offsets(&self) -> Self::IterOffsets {
        #[allow(clippy::unnecessary_to_owned)]
        self.to_vec().into_iter().enumerate()
    }

    fn eof_offset(&self) -> usize {
        self.len()
    }

    fn next_token(&mut self) -> Option<Self::Token> {
        let token = self.first()?.clone();
        self.start += 1;
        Some(token)
    }

    fn offset_for<P>(&self, predicate: P) -> Option<usize>
    where
        P: Fn(Self::Token) -> bool,
    {
        self.iter().position(|b| predicate(b.clone()))
    }

    fn offset_at(&self, tokens: usize) -> Result<usize, winnow::error::Needed> {
        if let Some(needed) = tokens.checked_sub(self.len()).and_then(NonZeroUsize::new) {
            Err(winnow::error::Needed::Size(needed))
        } else {
            Ok(tokens)
        }
    }

    fn next_slice(&mut self, offset: usize) -> Self::Slice {
        assert!(self.start + offset <= self.end);

        let next = TokenSlice {
            stream: self.stream,
            start: self.start,
            end: self.start + offset,
        };
        self.start += offset;
        next
    }

    fn checkpoint(&self) -> Self::Checkpoint {
        Checkpoint(self.start, self.end)
    }

    fn reset(&mut self, checkpoint: &Self::Checkpoint) {
        self.start = checkpoint.0;
        self.end = checkpoint.1;
    }

    fn raw(&self) -> &dyn fmt::Debug {
        self
    }
}

impl<'a> winnow::stream::Offset for TokenSlice<'a> {
    fn offset_from(&self, start: &Self) -> usize {
        self.start - start.start
    }
}

impl<'a> winnow::stream::Offset<Checkpoint> for TokenSlice<'a> {
    fn offset_from(&self, start: &Checkpoint) -> usize {
        self.start - start.0
    }
}

impl winnow::stream::Offset for Checkpoint {
    fn offset_from(&self, start: &Self) -> usize {
        self.0 - start.0
    }
}

impl<'a> winnow::stream::StreamIsPartial for TokenSlice<'a> {
    type PartialState = ();

    fn complete(&mut self) -> Self::PartialState {}

    fn restore_partial(&mut self, _: Self::PartialState) {}

    fn is_partial_supported() -> bool {
        false
    }
}

impl<'a> winnow::stream::FindSlice<&str> for TokenSlice<'a> {
    fn find_slice(&self, substr: &str) -> Option<std::ops::Range<usize>> {
        self.iter()
            .enumerate()
            .find_map(|(i, b)| if b.value == substr { Some(i..self.end) } else { None })
    }
}

#[derive(Clone, Debug)]
pub struct Checkpoint(usize, usize);

/// The types of tokens.
#[derive(Debug, PartialEq, Eq, Copy, Clone, Display)]
#[display(style = "camelCase")]
pub enum TokenType {
    /// A number.
    Number,
    /// A word.
    Word,
    /// An operator.
    Operator,
    /// A string.
    String,
    /// A keyword.
    Keyword,
    /// A type.
    Type,
    /// A brace.
    Brace,
    /// A hash.
    Hash,
    /// A bang.
    Bang,
    /// A dollar sign.
    Dollar,
    /// Whitespace.
    Whitespace,
    /// A comma.
    Comma,
    /// A colon.
    Colon,
    /// A double colon: `::`
    DoubleColon,
    /// A period.
    Period,
    /// A double period: `..`.
    DoublePeriod,
    /// A line comment.
    LineComment,
    /// A block comment.
    BlockComment,
    /// A function name.
    Function,
    /// Unknown lexemes.
    Unknown,
    /// The ? symbol, used for optional values.
    QuestionMark,
    /// The @ symbol.
    At,
    /// `;`
    SemiColon,
}

/// Most KCL tokens correspond to LSP semantic tokens (but not all).
impl TryFrom<TokenType> for SemanticTokenType {
    type Error = anyhow::Error;
    fn try_from(token_type: TokenType) -> Result<Self> {
        // If you return a new kind of `SemanticTokenType`, make sure to update `SEMANTIC_TOKEN_TYPES`
        // in the LSP implementation.
        Ok(match token_type {
            TokenType::Number => Self::NUMBER,
            TokenType::Word => Self::VARIABLE,
            TokenType::Keyword => Self::KEYWORD,
            TokenType::Type => Self::TYPE,
            TokenType::Operator => Self::OPERATOR,
            TokenType::QuestionMark => Self::OPERATOR,
            TokenType::String => Self::STRING,
            TokenType::Bang => Self::OPERATOR,
            TokenType::LineComment => Self::COMMENT,
            TokenType::BlockComment => Self::COMMENT,
            TokenType::Function => Self::FUNCTION,
            TokenType::Whitespace
            | TokenType::Brace
            | TokenType::Comma
            | TokenType::Colon
            | TokenType::DoubleColon
            | TokenType::Period
            | TokenType::DoublePeriod
            | TokenType::Hash
            | TokenType::Dollar
            | TokenType::At
            | TokenType::SemiColon
            | TokenType::Unknown => {
                anyhow::bail!("unsupported token type: {:?}", token_type)
            }
        })
    }
}

impl TokenType {
    pub fn is_whitespace(&self) -> bool {
        matches!(self, Self::Whitespace)
    }

    pub fn is_comment(&self) -> bool {
        matches!(self, Self::LineComment | Self::BlockComment)
    }
}

#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Token {
    pub token_type: TokenType,
    /// Offset in the source code where this token begins.
    pub start: usize,
    /// Offset in the source code where this token ends.
    pub end: usize,
    pub(super) module_id: ModuleId,
    pub(super) value: String,
}

impl ContainsToken<Token> for (TokenType, &str) {
    fn contains_token(&self, token: Token) -> bool {
        self.0 == token.token_type && self.1 == token.value
    }
}

impl ContainsToken<Token> for TokenType {
    fn contains_token(&self, token: Token) -> bool {
        *self == token.token_type
    }
}

impl Token {
    pub fn from_range(
        range: std::ops::Range<usize>,
        module_id: ModuleId,
        token_type: TokenType,
        value: String,
    ) -> Self {
        Self {
            start: range.start,
            end: range.end,
            module_id,
            value,
            token_type,
        }
    }
    pub fn is_code_token(&self) -> bool {
        !matches!(
            self.token_type,
            TokenType::Whitespace | TokenType::LineComment | TokenType::BlockComment
        )
    }

    pub fn as_source_range(&self) -> SourceRange {
        SourceRange::new(self.start, self.end, self.module_id)
    }

    pub fn as_source_ranges(&self) -> Vec<SourceRange> {
        vec![self.as_source_range()]
    }

    pub fn visibility_keyword(&self) -> Option<ItemVisibility> {
        if !matches!(self.token_type, TokenType::Keyword) {
            return None;
        }
        match self.value.as_str() {
            "export" => Some(ItemVisibility::Export),
            _ => None,
        }
    }

    pub fn numeric_value(&self) -> Option<f64> {
        if self.token_type != TokenType::Number {
            return None;
        }
        let value = &self.value;
        let value = value
            .split_once(|c: char| c == '_' || c.is_ascii_alphabetic())
            .map(|(s, _)| s)
            .unwrap_or(value);
        value.parse().ok()
    }

    pub fn uint_value(&self) -> Option<u32> {
        if self.token_type != TokenType::Number {
            return None;
        }
        let value = &self.value;
        let value = value
            .split_once(|c: char| c == '_' || c.is_ascii_alphabetic())
            .map(|(s, _)| s)
            .unwrap_or(value);
        value.parse().ok()
    }

    pub fn numeric_suffix(&self) -> NumericSuffix {
        if self.token_type != TokenType::Number {
            return NumericSuffix::None;
        }

        if self.value.ends_with('_') {
            return NumericSuffix::Count;
        }

        for suffix in NUM_SUFFIXES {
            if self.value.ends_with(suffix) {
                return suffix.parse().unwrap();
            }
        }

        NumericSuffix::None
    }

    /// Is this token the beginning of a variable/function declaration?
    /// If so, what kind?
    /// If not, returns None.
    pub fn declaration_keyword(&self) -> Option<VariableKind> {
        if !matches!(self.token_type, TokenType::Keyword) {
            return None;
        }
        Some(match self.value.as_str() {
            "fn" => VariableKind::Fn,
            "var" | "let" | "const" => VariableKind::Const,
            _ => return None,
        })
    }
}

impl From<Token> for SourceRange {
    fn from(token: Token) -> Self {
        Self::new(token.start, token.end, token.module_id)
    }
}

impl From<&Token> for SourceRange {
    fn from(token: &Token) -> Self {
        Self::new(token.start, token.end, token.module_id)
    }
}

pub fn lex(s: &str, module_id: ModuleId) -> Result<TokenStream, KclError> {
    tokeniser::lex(s, module_id).map_err(From::from)
}

impl From<ParseError<Input<'_>, winnow::error::ContextError>> for KclError {
    fn from(err: ParseError<Input<'_>, winnow::error::ContextError>) -> Self {
        let (input, offset): (Vec<char>, usize) = (err.input().chars().collect(), err.offset());
        let module_id = err.input().state.module_id;

        if offset >= input.len() {
            // From the winnow docs:
            //
            // This is an offset, not an index, and may point to
            // the end of input (input.len()) on eof errors.

            return KclError::Lexical(crate::errors::KclErrorDetails::new(
                "unexpected EOF while parsing".to_owned(),
                vec![SourceRange::new(offset, offset, module_id)],
            ));
        }

        // TODO: Add the Winnow tokenizer context to the error.
        // See https://github.com/KittyCAD/modeling-app/issues/784
        let bad_token = &input[offset];
        // TODO: Add the Winnow parser context to the error.
        // See https://github.com/KittyCAD/modeling-app/issues/784
        KclError::Lexical(crate::errors::KclErrorDetails::new(
            format!("found unknown token '{}'", bad_token),
            vec![SourceRange::new(offset, offset + 1, module_id)],
        ))
    }
}
