use std::collections::VecDeque;

use crate::{
    error::InternalError,
    source::{Chunk, Span},
};

#[derive(Debug, PartialEq, Eq)]
pub(crate) enum Token<'src> {
    Start,
    Content,
    Newline { indent: &'src str },
    Tag(Tag, &'src str),
    SetDelimiter,
}

impl Token<'_> {
    pub(crate) fn is_standalone(&self) -> bool {
        match self {
            Self::SetDelimiter
            | Self::Tag(
                Tag::Partial | Tag::Section | Tag::Inverted | Tag::CloseTag | Tag::Comment,
                _,
            ) => true,
            #[cfg(feature = "ext-dynamic-names")]
            Self::Tag(Tag::DynPartial, _) => true,
            #[cfg(feature = "ext-inheritance")]
            Self::Tag(Tag::Parent, _) => true,
            #[cfg(all(feature = "ext-dynamic-names", feature = "ext-inheritance"))]
            Self::Tag(Tag::DynParent, _) => true,
            _ => false,
        }
    }
}

#[derive(Debug, PartialEq, Eq)]
pub(crate) enum Tag {
    Variable,
    RawVariable,
    Section,
    Inverted,
    Partial,
    #[cfg(feature = "ext-dynamic-names")]
    DynPartial,
    #[cfg(feature = "ext-inheritance")]
    Block,
    #[cfg(feature = "ext-inheritance")]
    Parent,
    #[cfg(all(feature = "ext-dynamic-names", feature = "ext-inheritance"))]
    DynParent,
    Comment,
    CloseTag,
}
impl Tag {
    pub(crate) fn is_partial(&self) -> bool {
        match self {
            Self::Partial => true,
            #[cfg(feature = "ext-dynamic-names")]
            Self::DynPartial => true,
            #[cfg(feature = "ext-inheritance")]
            Self::Parent => true,
            #[cfg(all(feature = "ext-dynamic-names", feature = "ext-inheritance"))]
            Self::DynParent => true,
            _ => false,
        }
    }

    pub(crate) fn is_open_tag(&self) -> bool {
        match self {
            Self::Section => true,
            Self::Inverted => true,
            #[cfg(feature = "ext-inheritance")]
            Self::Parent => true,
            #[cfg(feature = "ext-inheritance")]
            Self::Block => true,
            #[cfg(all(feature = "ext-dynamic-names", feature = "ext-inheritance"))]
            Self::DynParent => true,
            _ => false,
        }
    }
}

#[derive(Debug)]
pub(crate) struct Lexer<'src> {
    source: &'src str,
    offset: usize,
    open_tag: &'src str,
    close_tag: &'src str,
    peeked: VecDeque<<Self as Iterator>::Item>,
}

impl<'src> Lexer<'src> {
    pub(crate) fn new(source: &'src str) -> Self {
        // We collapse the indentation at the beginning of the file into a Start
        // token.
        let mut whitespace_before = true;
        let first_line_indent = source
            .char_indices()
            .take_while(|(_, c)| {
                if !whitespace_before {
                    false
                } else {
                    whitespace_before = char::is_whitespace(*c);
                    true
                }
            })
            .last()
            .map_or(0, |(i, _)| i);

        Self {
            source: &source[first_line_indent..],
            offset: first_line_indent,
            open_tag: "{{",
            close_tag: "}}",
            peeked: vec![Ok(Chunk::new(0..first_line_indent, Token::Start))].into(),
        }
    }

    pub(crate) fn loc(&self) -> Span {
        Span::from(self.offset..self.offset + 1)
    }

    pub fn peek(&mut self) -> Option<&<Self as Iterator>::Item> {
        self.peek_nth(0)
    }
    pub fn peek_nth(&mut self, n: usize) -> Option<&<Self as Iterator>::Item> {
        while self.peeked.len() <= n {
            let next = self.force_next()?;
            self.peeked.push_back(next);
        }

        Some(&self.peeked[n])
    }
    pub fn peek_token(&mut self) -> Option<&Token<'src>> {
        self.peek_nth_token(0)
    }
    pub fn peek_nth_token(&mut self, n: usize) -> Option<&Token<'src>> {
        Some(&self.peek_nth(n)?.as_ref().ok()?.data)
    }

    #[allow(dead_code)]
    pub fn peek_mut(&mut self) -> Option<&mut <Self as Iterator>::Item> {
        self.peek_nth_mut(0)
    }
    pub fn peek_nth_mut(&mut self, n: usize) -> Option<&mut <Self as Iterator>::Item> {
        while self.peeked.len() <= n {
            let next = self.force_next()?;
            self.peeked.push_back(next);
        }

        Some(&mut self.peeked[n])
    }
    #[allow(dead_code)]
    pub fn peek_token_mut(&mut self) -> Option<&mut Token<'src>> {
        self.peek_nth_token_mut(0)
    }
    #[allow(dead_code)]
    pub fn peek_nth_token_mut(&mut self, n: usize) -> Option<&mut Token<'src>> {
        Some(&mut self.peek_nth_mut(n)?.as_mut().ok()?.data)
    }

    pub fn find_standalone(&mut self) -> Option<StandaloneReport<'_>> {
        let mut i = 0;
        while let Some(Ok(tk)) = self.peek_nth(i) {
            match &tk.data {
                Token::Newline { .. } => {
                    if i == 0 {
                        return None;
                    } else {
                        // unwraps to fix borrowing issue
                        let span = &mut self.peek_nth_mut(i).unwrap().as_mut().unwrap().span;
                        return Some(StandaloneReport { span: Some(span) });
                    }
                }
                t if t.is_standalone() => (),
                _ => return None,
            }
            i += 1;
        }
        if i > 0 {
            // Standalone before EOF
            Some(StandaloneReport { span: None })
        } else {
            None
        }
    }

    fn consume_tag(&mut self) -> Option<<Self as Iterator>::Item> {
        let Some(end) = &self.source[self.open_tag.len()..].find(self.close_tag) else {
            self.source = "";
            return Some(Err(InternalError::new(
                "unclosed tag",
                self.offset..self.offset + self.open_tag.len(),
            )));
        };
        let end = end + self.open_tag.len();
        let (mut tag, mut rest) = self.source.split_at(end + self.close_tag.len());
        let mut content = &tag[self.open_tag.len()..tag.len() - self.close_tag.len()];
        let mut content_chars = content.char_indices();

        macro_rules! tag_kind {
            ($kind:expr) => {{
                content = &content[1..];
                $kind
            }};
            ($kind:expr, dyn $dyn_kind:expr) => {{
                #[allow(unused)]
                if let Some((i, '*')) = content_chars.filter(|(_, c)| !c.is_whitespace()).next() {
                    cfg_select! {
                        feature = "ext-dynamic-names" => {
                            content = &content[i + 1..];
                            $dyn_kind
                        }
                        _ => {
                            let source = self.offset..self.offset + tag.len();
                            self.source = rest;
                            self.offset += tag.len();
                            return Some(Err(InternalError::new("dynamic names are disabled", source)));
                        }
                    }
                } else {
                    tag_kind!($kind)
                }
            }};
        }
        let kind = match content_chars.next().map(|(_, c)| c) {
            Some('{') => {
                if self.close_tag == "}}" {
                    if rest.chars().next() != Some('}') {
                        let source = Span {
                            start: self.offset + content.len() + 2,
                            end: self.offset + tag.len(),
                        };
                        self.source = rest;
                        self.offset += tag.len();
                        return Some(Err(InternalError::new(
                            "expected a triple mustache",
                            source,
                        )));
                    }
                    (tag, rest) = self.source.split_at(end + 3);
                    content = &tag[self.open_tag.len()..tag.len() - 3];
                } else if content.chars().last() != Some('}') {
                    let source = Span {
                        start: self.offset + content.len() + self.open_tag.len(),
                        end: self.offset + tag.len(),
                    };
                    self.source = rest;
                    self.offset += tag.len();
                    return Some(Err(InternalError::new(
                        "expected a triple mustache",
                        source,
                    )));
                } else {
                    content = &content[..content.len() - 1]
                }
                content = &content[1..];
                Tag::RawVariable
            }
            Some('=') => {
                if content.chars().last() != Some('=') {
                    let source = Span {
                        start: self.offset + content.len() + self.open_tag.len(),
                        end: self.offset + tag.len(),
                    };
                    self.source = rest;
                    self.offset += tag.len();
                    return Some(Err(InternalError::new("expected an equals sign", source)));
                }
                content = &content[1..content.len() - 1].trim();
                let Some((open, close)) = content.split_once(" ") else {
                    self.source = rest;
                    self.offset += tag.len();
                    return Some(Err(InternalError::new(
                        "expected a space to separate delimiters",
                        Span {
                            start: self.offset,
                            end: self.offset + tag.len(),
                        },
                    )));
                };
                self.open_tag = open.trim();
                self.close_tag = close.trim();

                let span = Span {
                    start: self.offset,
                    end: self.offset + tag.len(),
                };
                self.source = rest;
                self.offset += tag.len();

                return Some(Ok(Chunk::new(span, Token::SetDelimiter)));
            }
            Some('&') => tag_kind!(Tag::RawVariable),
            Some('#') => tag_kind!(Tag::Section),
            Some('^') => tag_kind!(Tag::Inverted),
            Some('>') => tag_kind!(Tag::Partial, dyn Tag::DynPartial),
            #[cfg(not(feature = "ext-inheritance"))]
            Some('<') | Some('$') => {
                let source = Span {
                    start: self.offset,
                    end: self.offset + tag.len(),
                };
                self.source = rest;
                self.offset += tag.len();
                return Some(Err(InternalError::new("inheritance is disabled", source)));
            }
            #[cfg(feature = "ext-inheritance")]
            Some('<') => tag_kind!(Tag::Parent, dyn Tag::DynParent),
            #[cfg(feature = "ext-inheritance")]
            Some('$') => tag_kind!(Tag::Block),
            Some('/') => tag_kind!(Tag::CloseTag),
            Some('!') => tag_kind!(Tag::Comment),
            _ => Tag::Variable,
        };

        let span = Span {
            start: self.offset,
            end: self.offset + tag.len(),
        };
        self.source = rest;
        self.offset += tag.len();

        Some(Ok(Chunk::new(span, Token::Tag(kind, content.trim()))))
    }

    fn consume_newline(&mut self) -> Option<<Self as Iterator>::Item> {
        // Count whitespace characters.
        let mut whitespace_before = true;
        let len = self
            .source
            .char_indices()
            .take_while(|(i, c)| {
                if !whitespace_before {
                    false
                } else {
                    whitespace_before = (*c != '\n' || *i == 0) && char::is_whitespace(*c);
                    true
                }
            })
            .last()?
            .0;
        let indent = if len > 1 { &self.source[1..len] } else { "" };
        // If the newline is directly at the end of the file, len = 0, which causes
        // an infinite loop
        let len = len.max(1);
        let span = Span {
            start: self.offset,
            end: self.offset + len,
        };

        self.offset += len;
        self.source = &self.source[len..];

        Some(Ok(Chunk::new(span, Token::Newline { indent })))
    }

    fn force_next(&mut self) -> Option<<Self as Iterator>::Item> {
        if self.source.is_empty() {
            return None;
        }

        if self.source.starts_with('\n') {
            self.consume_newline()
        } else if self.source.starts_with(self.open_tag) {
            self.consume_tag()
        } else {
            let next = match (self.source.find(self.open_tag), self.source.find('\n')) {
                (Some(a), Some(b)) => a.min(b),
                (Some(next), None) | (None, Some(next)) => next,
                (None, None) => self.source.len(),
            };
            let (content, rest) = self.source.split_at(next);

            let span = Span {
                start: self.offset,
                end: self.offset + content.len(),
            };
            self.source = rest;
            self.offset += content.len();

            Some(Ok(Chunk::new(span, Token::Content)))
        }
    }
}

impl<'src> Iterator for Lexer<'src> {
    type Item = Result<Chunk<Token<'src>>, InternalError>;

    fn next(&mut self) -> Option<Self::Item> {
        self.peeked.pop_front().or_else(|| self.force_next())
    }
}

pub struct StandaloneReport<'a> {
    pub span: Option<&'a mut Span>,
}

#[cfg(test)]
mod test {
    use crate::{
        error::InternalError,
        lexer::{Lexer, Tag, Token},
        source::Chunk,
    };

    fn check(src: &str, expected: &[Result<Chunk<Token>, InternalError>]) {
        let lexer = Lexer::new(src);
        assert_eq!(lexer.collect::<Vec<_>>(), expected);
    }

    #[test]
    fn variable() {
        check(
            "Hello, {{subject}}",
            &[
                Ok(Chunk::new(0..0, Token::Start)),
                Ok(Chunk::new(0..7, Token::Content)),
                Ok(Chunk::new(7..18, Token::Tag(Tag::Variable, "subject"))),
            ],
        );
    }

    #[test]
    fn single_braces() {
        check(
            "Hello, {Mustache}",
            &[
                Ok(Chunk::new(0..0, Token::Start)),
                Ok(Chunk::new(0..17, Token::Content)),
            ],
        )
    }

    #[test]
    fn triple_mustache() {
        check(
            "Hello, {{{subject}}}",
            &[
                Ok(Chunk::new(0..0, Token::Start)),
                Ok(Chunk::new(0..7, Token::Content)),
                Ok(Chunk::new(7..20, Token::Tag(Tag::RawVariable, "subject"))),
            ],
        )
    }

    #[test]
    fn triple_mustache_recovery() {
        check(
            "Hello, {{{subject}}!",
            &[
                Ok(Chunk::new(0..0, Token::Start)),
                Ok(Chunk::new(0..7, Token::Content)),
                Err(InternalError::new("expected a triple mustache", 17..19)),
                Ok(Chunk::new(19..20, Token::Content)),
            ],
        )
    }

    #[test]
    fn set_delimiter() {
        check(
            "{{=| |=}} |value|",
            &[
                Ok(Chunk::new(0..0, Token::Start)),
                Ok(Chunk::new(0..9, Token::SetDelimiter)),
                Ok(Chunk::new(9..10, Token::Content)),
                Ok(Chunk::new(10..17, Token::Tag(Tag::Variable, "value"))),
            ],
        )
    }

    #[test]
    fn set_delimiter_triple() {
        check(
            "{{=| |=}} |{value}|",
            &[
                Ok(Chunk::new(0..0, Token::Start)),
                Ok(Chunk::new(0..9, Token::SetDelimiter)),
                Ok(Chunk::new(9..10, Token::Content)),
                Ok(Chunk::new(10..19, Token::Tag(Tag::RawVariable, "value"))),
            ],
        )
    }

    #[test]
    fn newlines() {
        check(
            "Before
            After",
            &[
                Ok(Chunk::new(0..0, Token::Start)),
                Ok(Chunk::new(0..6, Token::Content)),
                Ok(Chunk::new(
                    6..19,
                    Token::Newline {
                        indent: "            ",
                    },
                )),
                Ok(Chunk::new(19..24, Token::Content)),
            ],
        );
    }
}
