#[cfg(feature = "ext-inheritance")]
use std::collections::HashMap;

use crate::{
    error::InternalError,
    lexer::{Lexer, Tag, Token},
    source::{Chunk, Span},
};

#[derive(Debug, Clone)]
pub struct Template<'src> {
    pub(crate) root: Vec<Node<'src>>,
    pub(crate) groups: Vec<Vec<Node<'src>>>,
}

impl<'src> Template<'src> {
    pub fn parse(source: &'src str) -> Result<Self, InternalError> {
        // dbg!(Lexer::new(source).collect::<Vec<_>>());
        let mut p = Parser {
            lexer: Lexer::new(source),
            source,
            groups: Vec::new(),
            prev_indentation: "",
            cut_indentation: 0,
        };
        let mut root = Vec::new();

        while p.lexer.peek().is_some() {
            let [fst, snd] = p.parse_node()?;
            if let Some(n) = fst {
                root.push(n);
            }
            if let Some(n) = snd {
                root.push(n);
            }
        }

        Ok(Self {
            root,
            groups: p.groups,
        })
    }
}

struct Parser<'src> {
    lexer: Lexer<'src>,
    source: &'src str,
    groups: Vec<Vec<Node<'src>>>,
    prev_indentation: &'src str,
    cut_indentation: usize,
}

#[derive(PartialEq, Eq, Debug, Clone)]
pub(crate) enum Node<'src> {
    Content(&'src str),
    Newline,
    Variable {
        key: &'src str,
        raw: bool,
    },
    Partial {
        name: &'src str,
        source: Span,
        indent: &'src str,
        #[cfg(feature = "ext-dynamic-names")]
        dynamic: bool,
    },

    Section {
        key: &'src str,
        group_id: usize,
        #[cfg(feature = "ext-lambdas")]
        span_inside: Span,
    },
    Inverted {
        key: &'src str,
        group_id: usize,
    },

    #[cfg(feature = "ext-inheritance")]
    Parent {
        name: &'src str,
        parameters: HashMap<&'src str, usize>,
        indent: &'src str,
        source: Span,
        #[cfg(feature = "ext-dynamic-names")]
        dynamic: bool,
    },
    #[cfg(feature = "ext-inheritance")]
    Block {
        name: &'src str,
        indent: &'src str,
        group_id: usize,
    },
}

impl<'src> Node<'src> {
    pub(crate) fn get_indent(&self) -> Option<&'src str> {
        match self {
            Node::Partial { indent, .. } => Some(indent),
            #[cfg(feature = "ext-inheritance")]
            Node::Parent { indent, .. } => Some(indent),
            #[cfg(feature = "ext-inheritance")]
            Node::Block { indent, .. } => Some(indent),
            _ => None,
        }
    }
}

impl<'src> Parser<'src> {
    fn parse_node(&mut self) -> Result<[Option<Node<'src>>; 2], InternalError> {
        let Some(t) = self.lexer.next() else {
            return Err(InternalError::new("expected a node", self.lexer.loc()));
        };
        let t = t?;

        match t.data {
            Token::Start => {
                #[cfg(feature = "inline-indentation")]
                {
                    self.prev_indentation = &self.source[t.span.start..t.span.end];
                }

                if self.lexer.peek_token().is_some_and(Token::is_standalone)
                    && let Some(standalone) = self.lexer.find_standalone()
                {
                    #[cfg(not(feature = "inline-indentation"))]
                    {
                        self.prev_indentation = &self.source[t.span.start..t.span.end];
                    }

                    // Push the start of the newline token forward so that it will
                    // skip the actual newline.
                    if let Some(span) = standalone.span {
                        span.start += 1;
                    }
                    if let Some(Token::Tag(tag, _)) = self.lexer.peek_token()
                        && tag.is_partial()
                    {
                        self.parse_content(t.span).map(|c| [c, None])
                    } else {
                        Ok([const { None }; 2])
                    }
                } else {
                    self.parse_content(t.span).map(|c| [c, None])
                }
            }
            Token::Content => self.parse_content(t.span).map(|c| [c, None]),
            Token::Newline { indent } => {
                #[cfg(feature = "inline-indentation")]
                {
                    self.prev_indentation = indent;
                }

                let mut start = t.span.start + self.cut_indentation;
                let nl_node = if t.span.end > t.span.start + indent.len() {
                    start += 1;
                    Some(Node::Newline)
                } else {
                    None
                };
                if let Some(standalone) = self.lexer.find_standalone() {
                    #[cfg(not(feature = "inline-indentation"))]
                    {
                        self.prev_indentation = indent;
                    }

                    // its a standalone tag! we will skip the newline character after the tag.
                    if let Some(span) = standalone.span {
                        span.start += 1;
                    }
                    Ok([nl_node, None])
                } else {
                    #[cfg(not(feature = "inline-indentation"))]
                    cfg_select! {
                        feature = "ext-inheritance" => {
                            if matches!(self.lexer.peek_token(), Some(Token::Tag(Tag::Block, _))) {
                                self.prev_indentation = indent;
                            } else {
                                self.prev_indentation = "";
                            }
                        }
                        _ => {
                            self.prev_indentation = "";
                        }
                    }

                    // Do not put the newline character into the content node
                    self.parse_content(Span::from(start..t.span.end))
                        .map(|c| [nl_node, c])
                }
            }
            Token::Tag(tag, content) => Ok([
                Some(match tag {
                    Tag::Variable => Node::Variable {
                        key: content,
                        raw: false,
                    },
                    Tag::RawVariable => Node::Variable {
                        key: content,
                        raw: true,
                    },
                    Tag::Section => {
                        #[cfg(feature = "ext-lambdas")]
                        let (group, span_inside) = self.parse_group_to_span(t.span, content)?;
                        #[cfg(not(feature = "ext-lambdas"))]
                        let group = self.parse_group(t.span, content)?;
                        let group_id = self.groups.len();
                        self.groups.push(group);
                        Node::Section {
                            key: content,
                            group_id,
                            #[cfg(feature = "ext-lambdas")]
                            span_inside,
                        }
                    }
                    Tag::Inverted => {
                        let group = self.parse_group(t.span, content)?;
                        let group_id = self.groups.len();
                        self.groups.push(group);
                        Node::Inverted {
                            key: content,
                            group_id,
                        }
                    }
                    Tag::Partial => Node::Partial {
                        name: content,
                        source: t.span,
                        indent: self.prev_indentation,
                        #[cfg(feature = "ext-dynamic-names")]
                        dynamic: false,
                    },
                    #[cfg(feature = "ext-dynamic-names")]
                    Tag::DynPartial => Node::Partial {
                        name: content,
                        source: t.span,
                        indent: self.prev_indentation,
                        dynamic: true,
                    },
                    #[cfg(feature = "ext-inheritance")]
                    Tag::Block => {
                        use std::mem;

                        let (cut_indentation, block_indent) = self.cut_indentation()?;
                        let old_ci =
                            mem::replace(&mut self.cut_indentation, cut_indentation.unwrap_or(0));
                        let group = self.parse_group(t.span, content)?;
                        self.cut_indentation = old_ci;

                        let group_id = self.groups.len();
                        self.groups.push(group);

                        let indent = if cut_indentation.is_some() {
                            block_indent
                        } else {
                            self.prev_indentation
                        };
                        self.prev_indentation = "";

                        Node::Block {
                            name: content,
                            indent,
                            group_id,
                        }
                    }
                    #[cfg(feature = "ext-inheritance")]
                    Tag::Parent => Node::Parent {
                        name: content,
                        parameters: self.parse_parent(t.span, content)?,
                        indent: self.prev_indentation,
                        source: t.span,
                        #[cfg(feature = "ext-dynamic-names")]
                        dynamic: false,
                    },
                    #[cfg(all(feature = "ext-dynamic-names", feature = "ext-inheritance"))]
                    Tag::DynParent => Node::Parent {
                        name: content,
                        parameters: self.parse_parent(t.span, content)?,
                        indent: self.prev_indentation,
                        source: t.span,
                        dynamic: true,
                    },
                    Tag::Comment => return Ok([None, None]),
                    Tag::CloseTag => {
                        return Err(InternalError::new("disconnected closing tag", t.span));
                    }
                }),
                None,
            ]),
            Token::SetDelimiter => Ok([None, None]),
        }
    }

    fn cut_indentation(&mut self) -> Result<(Option<usize>, &'src str), InternalError> {
        let mut cut_indentation = None::<usize>;
        let mut use_tabs = None::<bool>;
        let mut tag_level = 0;
        let mut block_indent = self.prev_indentation;
        let mut i = 0_usize;
        while let Some(Ok(Chunk { data: tk, span })) = self.lexer.peek_nth(i) {
            match tk {
                Token::Newline { indent } => {
                    let span = *span;
                    let indent = *indent;

                    if tag_level == 0
                        && matches!(
                            self.lexer.peek_nth_token(i + 1),
                            Some(Token::Tag(Tag::CloseTag, _))
                        )
                    {
                        break;
                    }
                    if indent.contains(" ") && indent.contains("\t") {
                        return Err(InternalError::new(
                            "tabs and spaces may not be used in the same block for indentation",
                            span,
                        ));
                    } else if let Some(use_tabs) = use_tabs {
                        if indent.contains("\t") != use_tabs {
                            return Err(InternalError::new(
                                "tabs and spaces may not be used in the same block for indentation",
                                span,
                            ));
                        }
                    } else if indent.contains(" ") {
                        use_tabs = Some(false);
                    } else if indent.contains("\t") {
                        use_tabs = Some(true);
                    }

                    let len = indent.len();
                    let ci = cut_indentation.map_or(len, |o| o.min(len));
                    block_indent = &indent[0..ci.saturating_sub(self.cut_indentation)];
                    cut_indentation = Some(ci);
                }
                Token::Tag(t, _) => {
                    if *t == Tag::CloseTag {
                        if tag_level == 0 {
                            break;
                        } else {
                            tag_level -= 1;
                        }
                    } else if t.is_open_tag() {
                        tag_level += 1;
                    }
                }
                _ => (),
            }
            i += 1;
        }
        Ok((cut_indentation, block_indent))
    }

    fn parse_content(&mut self, t: Span) -> Result<Option<Node<'src>>, InternalError> {
        let mut end = t.end;

        loop {
            let t = match self.lexer.peek_token() {
                Some(Token::Tag(_, _))
                | Some(Token::SetDelimiter)
                | Some(Token::Newline { .. })
                | None => break,
                Some(Token::Start | Token::Content) => self.lexer.next().unwrap(),
            };
            end = t?.span.end;
        }

        Ok(if end > t.start {
            Some(Node::Content(&self.source[t.start..end]))
        } else {
            None
        })
    }

    fn parse_group(
        &mut self,
        open_tag: Span,
        expected_close_tag: &str,
    ) -> Result<Vec<Node<'src>>, InternalError> {
        self.parse_group_to_span(open_tag, expected_close_tag)
            .map(|(v, _)| v)
    }
    fn parse_group_to_span(
        &mut self,
        open_tag: Span,
        expected_close_tag: &str,
    ) -> Result<(Vec<Node<'src>>, Span), InternalError> {
        let mut g = Vec::new();

        while !matches!(
            self.lexer.peek_token(),
            Some(Token::Tag(Tag::CloseTag, _)) | None
        ) {
            let [fst, snd] = self.parse_node()?;
            if let Some(n) = fst {
                g.push(n);
            }
            if let Some(n) = snd {
                g.push(n);
            }
        }

        let Some(close_tag) = self.lexer.next() else {
            return Err(InternalError::new("unclosed group", open_tag));
        };
        let close_tag = close_tag?;
        match close_tag.data {
            Token::Tag(Tag::CloseTag, name) if name == expected_close_tag => (),
            Token::Tag(Tag::CloseTag, name) => {
                return Err(InternalError::new(
                    format!("expected '{{{{/{expected_close_tag}}}}}', but found {{{{/{name}}}}}"),
                    close_tag.span,
                ));
            }
            _ => return Err(InternalError::new("unclosed group", open_tag)),
        }

        Ok((
            g,
            Span {
                start: open_tag.end,
                end: close_tag.span.start,
            },
        ))
    }

    #[cfg(feature = "ext-inheritance")]
    fn parse_parent(
        &mut self,
        open_tag: Span,
        expected_close_tag: &str,
    ) -> Result<HashMap<&'src str, usize>, InternalError> {
        let mut params = HashMap::new();

        let close_tag = loop {
            match self.lexer.next() {
                Some(Ok(t)) => match t.data {
                    Token::Tag(Tag::Block, name) => {
                        // standalone tag check

                        if let Some(Ok(Chunk {
                            data: Token::Newline { .. },
                            span,
                        })) = self.lexer.peek_mut()
                        {
                            span.start += 1;
                        }

                        use std::mem;
                        let (cut_indentation, _) = self.cut_indentation()?;
                        let old_ci =
                            mem::replace(&mut self.cut_indentation, cut_indentation.unwrap_or(0));
                        let group = self.parse_group(t.span, name)?;
                        self.cut_indentation = old_ci;

                        params.insert(name, self.groups.len());
                        self.groups.push(group);
                    }
                    Token::Tag(Tag::CloseTag, _) => break t,
                    Token::Newline { .. } => {
                        if let Some(standalone) = self.lexer.find_standalone() {
                            // its a standalone tag! we will skip the newline character after the tag.
                            if let Some(span) = standalone.span {
                                span.start += 1;
                            }
                        }
                    }
                    _ => (),
                },
                Some(Err(e)) => return Err(e),
                None => return Err(InternalError::new("unclosed parent", open_tag)),
            }
        };

        match close_tag.data {
            Token::Tag(Tag::CloseTag, name) if name == expected_close_tag => (),
            Token::Tag(Tag::CloseTag, name) => {
                return Err(InternalError::new(
                    format!("expected '{{{{/{expected_close_tag}}}}}', but found {{{{/{name}}}}}"),
                    close_tag.span,
                ));
            }
            _ => return Err(InternalError::new("unclosed parent", open_tag)),
        }

        Ok(params)
    }
}

#[cfg(test)]
mod test {
    use crate::parser::{Node, Template};
    #[cfg(feature = "ext-lambdas")]
    use crate::source::Span;

    fn check(src: &str, expected: &[Node<'static>]) {
        let template = Template::parse(src).unwrap();
        assert_eq!(template.root, expected);
    }
    fn check_groups(src: &str, expected: &[Node<'static>], groups: &[Vec<Node<'static>>]) {
        let template = Template::parse(src).unwrap();
        assert_eq!(template.root, expected);
        assert_eq!(template.groups, groups)
    }

    #[test]
    fn variable() {
        check(
            "Hello, {{subject}}",
            &[
                Node::Content("Hello, "),
                Node::Variable {
                    key: "subject",
                    raw: false,
                },
            ],
        )
    }

    #[test]
    fn section() {
        check_groups(
            "{{#blog}}
                hello
            {{/blog}}",
            &[Node::Section {
                key: "blog",
                group_id: 0,
                #[cfg(feature = "ext-lambdas")]
                span_inside: Span::from(9..44),
            }],
            &[vec![Node::Content("                hello"), Node::Newline]],
        );
    }
}
