use std::collections::{HashMap, VecDeque};
use std::iter::Peekable;
use std::str::FromStr;

use pest::error::LineColLocation;
use pest::iterators::Pair;
use pest::{Parser, Position, Span};
use serde_json::value::Value as Json;

use crate::error::{TemplateError, TemplateErrorReason};
use crate::grammar::{HandlebarsParser, Rule};
use crate::json::path::{parse_json_path_from_iter, Path};
use crate::support;

use derive_builder::Builder;

use self::TemplateElement::{
    Comment, DecoratorBlock, DecoratorExpression, Expression, HelperBlock, HtmlExpression,
    PartialBlock, PartialExpression, RawString,
};

#[non_exhaustive]
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct TemplateMapping(pub usize, pub usize);

/// A handlebars template
#[non_exhaustive]
#[derive(Builder, PartialEq, Eq, Clone, Debug, Default)]
pub struct Template {
    #[builder(setter(into, strip_option), default)]
    pub name: Option<String>,
    pub elements: Vec<TemplateElement>,
    pub mapping: Vec<TemplateMapping>,
}

#[derive(Default)]
pub(crate) struct TemplateOptions {
    pub(crate) prevent_indent: bool,
    pub(crate) is_partial: bool,
    pub(crate) name: Option<String>,
}

impl TemplateOptions {
    fn name(&self) -> String {
        self.name.clone().unwrap_or_else(|| "Unnamed".to_owned())
    }
}

#[non_exhaustive]
#[derive(Builder, PartialEq, Eq, Clone, Debug)]
pub struct Subexpression {
    // we use box here avoid resursive struct definition
    pub element: Box<TemplateElement>,
}

impl Subexpression {
    pub fn new(
        name: Parameter,
        params: Vec<Parameter>,
        hash: HashMap<String, Parameter>,
    ) -> Subexpression {
        Subexpression {
            element: Box::new(Expression(Box::new(HelperTemplate {
                name,
                params,
                hash,
                template: None,
                inverse: None,
                block_param: None,
                block: false,
                chain: false,
                indent_before_write: false,
            }))),
        }
    }

    pub fn is_helper(&self) -> bool {
        match *self.as_element() {
            TemplateElement::Expression(ref ht) => !ht.is_name_only(),
            _ => false,
        }
    }

    pub fn as_element(&self) -> &TemplateElement {
        self.element.as_ref()
    }

    pub fn name(&self) -> &str {
        match *self.as_element() {
            // FIXME: avoid unwrap here
            Expression(ref ht) => ht.name.as_name().unwrap(),
            _ => unreachable!(),
        }
    }

    pub fn params(&self) -> Option<&Vec<Parameter>> {
        match *self.as_element() {
            Expression(ref ht) => Some(&ht.params),
            _ => None,
        }
    }

    pub fn hash(&self) -> Option<&HashMap<String, Parameter>> {
        match *self.as_element() {
            Expression(ref ht) => Some(&ht.hash),
            _ => None,
        }
    }
}

#[non_exhaustive]
#[derive(PartialEq, Eq, Clone, Debug)]
pub enum BlockParam {
    Single(Parameter),
    Pair((Parameter, Parameter)),
}

#[non_exhaustive]
#[derive(Builder, PartialEq, Eq, Clone, Debug)]
pub struct ExpressionSpec {
    pub name: Parameter,
    pub params: Vec<Parameter>,
    pub hash: HashMap<String, Parameter>,
    #[builder(setter(strip_option), default)]
    pub block_param: Option<BlockParam>,
    pub omit_pre_ws: bool,
    pub omit_pro_ws: bool,
}

#[non_exhaustive]
#[derive(PartialEq, Eq, Clone, Debug)]
pub enum Parameter {
    // for helper name only
    Name(String),
    // for expression, helper param and hash
    Path(Path),
    Literal(Json),
    Subexpression(Subexpression),
}

#[non_exhaustive]
#[derive(Builder, PartialEq, Eq, Clone, Debug)]
pub struct HelperTemplate {
    pub name: Parameter,
    pub params: Vec<Parameter>,
    pub hash: HashMap<String, Parameter>,
    #[builder(setter(strip_option), default)]
    pub block_param: Option<BlockParam>,
    #[builder(setter(strip_option), default)]
    pub template: Option<Template>,
    #[builder(setter(strip_option), default)]
    pub inverse: Option<Template>,
    pub block: bool,
    pub chain: bool,
    pub(crate) indent_before_write: bool,
}

impl HelperTemplate {
    pub fn new(exp: ExpressionSpec, block: bool, indent_before_write: bool) -> HelperTemplate {
        HelperTemplate {
            name: exp.name,
            params: exp.params,
            hash: exp.hash,
            block_param: exp.block_param,
            block,
            template: None,
            inverse: None,
            chain: false,
            indent_before_write,
        }
    }

    pub fn new_chain(
        exp: ExpressionSpec,
        block: bool,
        indent_before_write: bool,
    ) -> HelperTemplate {
        HelperTemplate {
            name: exp.name,
            params: exp.params,
            hash: exp.hash,
            block_param: exp.block_param,
            block,
            template: None,
            inverse: None,
            chain: true,
            indent_before_write,
        }
    }

    // test only
    pub(crate) fn with_path(path: Path) -> HelperTemplate {
        HelperTemplate {
            name: Parameter::Path(path),
            params: Vec::with_capacity(5),
            hash: HashMap::new(),
            block_param: None,
            template: None,
            inverse: None,
            block: false,
            chain: false,
            indent_before_write: false,
        }
    }

    pub(crate) fn is_name_only(&self) -> bool {
        !self.block && self.params.is_empty() && self.hash.is_empty()
    }

    fn insert_inverse_node(&mut self, mut node: Box<HelperTemplate>) {
        // Create a list in "inverse" member to hold the else-chain.
        // Here we create the new template to save the else-chain node.
        // The template render could render it successfully without any code add.
        let mut new_chain_template = Template::new();
        node.inverse = self.inverse.take();
        new_chain_template.elements.push(HelperBlock(node));
        self.inverse = Some(new_chain_template);
    }

    fn ref_chain_head_mut(&mut self) -> Option<&mut Box<HelperTemplate>> {
        if self.chain {
            if let Some(inverse_tmpl) = &mut self.inverse {
                assert_eq!(inverse_tmpl.elements.len(), 1);
                if let HelperBlock(helper) = &mut inverse_tmpl.elements[0] {
                    return Some(helper);
                }
            }
        }
        None
    }

    fn set_chain_template(&mut self, tmpl: Option<Template>) {
        if let Some(hepler) = self.ref_chain_head_mut() {
            hepler.template = tmpl;
        } else {
            self.template = tmpl;
        }
    }

    fn revert_chain_and_set(&mut self, inverse: Option<Template>) {
        if self.chain {
            let mut prev = None;

            if let Some(head) = self.ref_chain_head_mut() {
                if head.template.is_some() {
                    // Here the prev will hold the head inverse template.
                    // It will be set when reverse the chain.
                    prev = inverse;
                } else {
                    // If the head already has template. set the inverse template.
                    head.template = inverse;
                }
            }

            // Reverse the else chain, to the normal list order.
            while let Some(mut node) = self.inverse.take() {
                assert_eq!(node.elements.len(), 1);
                if let HelperBlock(c) = &mut node.elements[0] {
                    self.inverse = c.inverse.take();
                    c.inverse = prev;
                    prev = Some(node);
                }
            }

            self.inverse = prev;
        } else {
            // If the helper has no else chain.
            // set the template to self.
            if self.template.is_some() {
                self.inverse = inverse;
            } else {
                self.template = inverse;
            }
        }
    }

    fn set_chained(&mut self) {
        self.chain = true;
    }

    pub fn is_chained(&self) -> bool {
        self.chain
    }
}

#[non_exhaustive]
#[derive(Builder, PartialEq, Eq, Clone, Debug)]
pub struct DecoratorTemplate {
    pub name: Parameter,
    pub params: Vec<Parameter>,
    pub hash: HashMap<String, Parameter>,
    #[builder(setter(strip_option), default)]
    pub template: Option<Template>,
    // for partial indent
    #[builder(setter(into, strip_option), default)]
    pub indent: Option<String>,
    pub(crate) indent_before_write: bool,
}

impl DecoratorTemplate {
    pub fn new(exp: ExpressionSpec, indent_before_write: bool) -> DecoratorTemplate {
        DecoratorTemplate {
            name: exp.name,
            params: exp.params,
            hash: exp.hash,
            template: None,
            indent: None,
            indent_before_write,
        }
    }
}

impl Parameter {
    pub fn as_name(&self) -> Option<&str> {
        match self {
            Parameter::Name(ref n) => Some(n),
            Parameter::Path(ref p) => Some(p.raw()),
            _ => None,
        }
    }

    pub fn parse(s: &str) -> Result<Parameter, TemplateError> {
        let parser = HandlebarsParser::parse(Rule::parameter, s)
            .map_err(|_| TemplateError::of(TemplateErrorReason::InvalidParam(s.to_owned())))?;

        let mut it = parser.flatten().peekable();
        Template::parse_param(s, &mut it, s.len() - 1)
    }

    fn debug_name(&self) -> String {
        if let Some(name) = self.as_name() {
            name.to_owned()
        } else {
            format!("{self:?}")
        }
    }
}

impl Template {
    pub fn new() -> Template {
        Template::default()
    }

    fn push_element(&mut self, e: TemplateElement, line: usize, col: usize) {
        self.elements.push(e);
        self.mapping.push(TemplateMapping(line, col));
    }

    fn parse_subexpression<'a, I>(
        source: &'a str,
        it: &mut Peekable<I>,
        limit: usize,
    ) -> Result<Parameter, TemplateError>
    where
        I: Iterator<Item = Pair<'a, Rule>>,
    {
        let espec = Template::parse_expression(source, it.by_ref(), limit)?;
        Ok(Parameter::Subexpression(Subexpression::new(
            espec.name,
            espec.params,
            espec.hash,
        )))
    }

    fn parse_name<'a, I>(
        source: &'a str,
        it: &mut Peekable<I>,
        _: usize,
    ) -> Result<Parameter, TemplateError>
    where
        I: Iterator<Item = Pair<'a, Rule>>,
    {
        let name_node = it.next().unwrap();
        let rule = name_node.as_rule();
        let name_span = name_node.as_span();
        match rule {
            Rule::identifier
            | Rule::partial_identifier
            | Rule::opt_identifier
            | Rule::opt_partial_identifier
            | Rule::invert_tag_item => Ok(Parameter::Name(name_span.as_str().to_owned())),
            Rule::reference => {
                let paths = parse_json_path_from_iter(it, name_span.end());
                Ok(Parameter::Path(Path::new(name_span.as_str(), paths)))
            }
            Rule::subexpression => {
                Template::parse_subexpression(source, it.by_ref(), name_span.end())
            }
            other => unreachable!("{other:?}"),
        }
    }

    fn parse_param<'a, I>(
        source: &'a str,
        it: &mut Peekable<I>,
        _: usize,
    ) -> Result<Parameter, TemplateError>
    where
        I: Iterator<Item = Pair<'a, Rule>>,
    {
        let mut param = it.next().unwrap();
        if param.as_rule() == Rule::helper_parameter {
            param = it.next().unwrap();
        }
        let param_rule = param.as_rule();
        let param_span = param.as_span();
        let result = match param_rule {
            Rule::reference => {
                let path_segs = parse_json_path_from_iter(it, param_span.end());
                Parameter::Path(Path::new(param_span.as_str(), path_segs))
            }
            Rule::literal => {
                // Parse the parameter as a JSON literal
                let param_literal = it.next().unwrap();
                let json_result = match param_literal.as_rule() {
                    Rule::string_literal
                        if it.peek().unwrap().as_rule() == Rule::string_inner_single_quote =>
                    {
                        // ...unless the parameter is a single-quoted string.
                        // In that case, transform it to a double-quoted string
                        // and then parse it as a JSON literal.
                        let string_inner_single_quote = it.next().unwrap();
                        let double_quoted = format!(
                            "\"{}\"",
                            string_inner_single_quote
                                .as_str()
                                .replace("\\'", "'")
                                .replace('"', "\\\"")
                        );
                        Json::from_str(&double_quoted)
                    }
                    _ => Json::from_str(param_span.as_str()),
                };
                if let Ok(json) = json_result {
                    Parameter::Literal(json)
                } else {
                    return Err(TemplateError::of(TemplateErrorReason::InvalidParam(
                        param_span.as_str().to_owned(),
                    )));
                }
            }
            Rule::subexpression => {
                Template::parse_subexpression(source, it.by_ref(), param_span.end())?
            }
            _ => unreachable!(),
        };

        while let Some(n) = it.peek() {
            let n_span = n.as_span();
            if n_span.end() > param_span.end() {
                break;
            }
            it.next();
        }

        Ok(result)
    }

    fn parse_hash<'a, I>(
        source: &'a str,
        it: &mut Peekable<I>,
        limit: usize,
    ) -> Result<(String, Parameter), TemplateError>
    where
        I: Iterator<Item = Pair<'a, Rule>>,
    {
        let name = it.next().unwrap();
        let name_node = name.as_span();
        // identifier
        let key = name_node.as_str().to_owned();

        let value = Template::parse_param(source, it.by_ref(), limit)?;
        Ok((key, value))
    }

    fn parse_block_param<'a, I>(_: &'a str, it: &mut Peekable<I>, limit: usize) -> BlockParam
    where
        I: Iterator<Item = Pair<'a, Rule>>,
    {
        let p1_name = it.next().unwrap();
        let p1_name_span = p1_name.as_span();
        // identifier
        let p1 = p1_name_span.as_str().to_owned();

        let p2 = it.peek().and_then(|p2_name| {
            let p2_name_span = p2_name.as_span();
            if p2_name_span.end() <= limit {
                Some(p2_name_span.as_str().to_owned())
            } else {
                None
            }
        });

        if let Some(p2) = p2 {
            it.next();
            BlockParam::Pair((Parameter::Name(p1), Parameter::Name(p2)))
        } else {
            BlockParam::Single(Parameter::Name(p1))
        }
    }

    fn parse_expression<'a, I>(
        source: &'a str,
        it: &mut Peekable<I>,
        limit: usize,
    ) -> Result<ExpressionSpec, TemplateError>
    where
        I: Iterator<Item = Pair<'a, Rule>>,
    {
        let mut params: Vec<Parameter> = Vec::new();
        let mut hashes: HashMap<String, Parameter> = HashMap::new();
        let mut omit_pre_ws = false;
        let mut omit_pro_ws = false;
        let mut block_param = None;

        if it.peek().unwrap().as_rule() == Rule::leading_tilde_to_omit_whitespace {
            omit_pre_ws = true;
            it.next();
        }

        let name = Template::parse_name(source, it.by_ref(), limit)?;

        loop {
            let rule;
            let end;
            if let Some(pair) = it.peek() {
                let pair_span = pair.as_span();
                if pair_span.end() < limit {
                    rule = pair.as_rule();
                    end = pair_span.end();
                } else {
                    break;
                }
            } else {
                break;
            }

            it.next();

            match rule {
                Rule::helper_parameter => {
                    params.push(Template::parse_param(source, it.by_ref(), end)?);
                }
                Rule::hash => {
                    let (key, value) = Template::parse_hash(source, it.by_ref(), end)?;
                    hashes.insert(key, value);
                }
                Rule::block_param => {
                    block_param = Some(Template::parse_block_param(source, it.by_ref(), end));
                }
                Rule::trailing_tilde_to_omit_whitespace => {
                    omit_pro_ws = true;
                }
                _ => {}
            }
        }
        Ok(ExpressionSpec {
            name,
            params,
            hash: hashes,
            block_param,
            omit_pre_ws,
            omit_pro_ws,
        })
    }

    fn remove_previous_whitespace(template_stack: &mut VecDeque<Template>) {
        let t = template_stack.front_mut().unwrap();
        if let Some(RawString(ref mut text)) = t.elements.last_mut() {
            text.trim_end().to_owned().clone_into(text);
        }
    }

    // in handlebars, the whitespaces around statement are
    // automatically trimed.
    // this function checks if current span has both leading and
    // trailing whitespaces, which we treat as a standalone statement.
    //
    //
    fn process_standalone_statement(
        template_stack: &mut VecDeque<Template>,
        source: &str,
        current_span: &Span<'_>,
        prevent_indent: bool,
        is_partial: bool,
    ) -> bool {
        let continuation = &source[current_span.end()..];

        let mut with_trailing_newline = support::str::starts_with_empty_line(continuation);

        // For full templates, we behave as if there was a trailing newline if we encounter
        // the end of input. See #611.
        with_trailing_newline |= !is_partial && continuation.is_empty();

        if with_trailing_newline {
            let with_leading_newline =
                support::str::ends_with_empty_line(&source[..current_span.start()]);

            // prevent_indent: a special toggle for partial expression
            // (>) that leading whitespaces are kept
            if prevent_indent && with_leading_newline {
                let t = template_stack.front_mut().unwrap();
                // check the last element before current
                if let Some(RawString(ref mut text)) = t.elements.last_mut() {
                    // trim leading space for standalone statement
                    text.trim_end_matches(support::str::whitespace_matcher)
                        .to_owned()
                        .clone_into(text);
                }
            }

            // return true when the item is the first element in root template
            current_span.start() == 0 || with_leading_newline
        } else {
            false
        }
    }

    fn raw_string<'a>(
        source: &'a str,
        pair: Option<Pair<'a, Rule>>,
        trim_start: bool,
        trim_start_line: bool,
    ) -> TemplateElement {
        let mut s = String::from(source);

        if let Some(pair) = pair {
            // the source may contains leading space because of pest's limitation
            // we calculate none space start here in order to correct the offset
            let pair_span = pair.as_span();

            let current_start = pair_span.start();
            let span_length = pair_span.end() - current_start;
            let leading_space_offset = s.len() - span_length;

            // we would like to iterate pair reversely in order to remove certain
            // index from our string buffer so here we convert the inner pairs to
            // a vector.
            for sub_pair in pair.into_inner().rev() {
                // remove escaped backslash
                if sub_pair.as_rule() == Rule::escape {
                    let escape_span = sub_pair.as_span();

                    let backslash_pos = escape_span.start();
                    let backslash_rel_pos = leading_space_offset + backslash_pos - current_start;
                    s.remove(backslash_rel_pos);
                }
            }
        }

        if trim_start {
            RawString(s.trim_start().to_owned())
        } else if trim_start_line {
            let s = s.trim_start_matches(support::str::whitespace_matcher);
            RawString(support::str::strip_first_newline(s).to_owned())
        } else {
            RawString(s)
        }
    }

    pub(crate) fn compile2(
        source: &str,
        options: TemplateOptions,
    ) -> Result<Template, TemplateError> {
        let mut helper_stack: VecDeque<HelperTemplate> = VecDeque::new();
        let mut decorator_stack: VecDeque<DecoratorTemplate> = VecDeque::new();
        let mut template_stack: VecDeque<Template> = VecDeque::new();

        let mut omit_pro_ws = false;
        // flag for newline removal of standalone statements
        // this option is marked as true when standalone statement is detected
        // then the leading whitespaces and newline of next rawstring will be trimed
        let mut trim_line_required = false;

        let parser_queue = HandlebarsParser::parse(Rule::handlebars, source).map_err(|e| {
            let (line_no, col_no) = match e.line_col {
                LineColLocation::Pos(line_col) => line_col,
                LineColLocation::Span(line_col, _) => line_col,
            };
            TemplateError::of(TemplateErrorReason::InvalidSyntax(
                e.variant.message().to_string(),
            ))
            .at(source, line_no, col_no)
            .in_template(options.name())
        })?;

        // dbg!(parser_queue.clone().flatten());

        // remove escape from our pair queue
        let mut it = parser_queue
            .flatten()
            .filter(|p| {
                // remove rules that should be silent but not for now due to pest limitation
                !matches!(p.as_rule(), Rule::escape)
            })
            .peekable();
        let mut end_pos: Option<Position<'_>> = None;
        loop {
            if let Some(pair) = it.next() {
                let prev_end = end_pos.as_ref().map_or(0, pest::Position::pos);
                let rule = pair.as_rule();
                let span = pair.as_span();

                let is_trailing_string = rule != Rule::template
                    && span.start() != prev_end
                    && !omit_pro_ws
                    && rule != Rule::raw_text
                    && rule != Rule::raw_block_text;

                if is_trailing_string {
                    // trailing string check
                    let (line_no, col_no) = span.start_pos().line_col();
                    if rule == Rule::raw_block_end {
                        let mut t = Template::new();
                        t.push_element(
                            Template::raw_string(
                                &source[prev_end..span.start()],
                                None,
                                false,
                                trim_line_required,
                            ),
                            line_no,
                            col_no,
                        );
                        template_stack.push_front(t);
                    } else {
                        let t = template_stack.front_mut().unwrap();
                        t.push_element(
                            Template::raw_string(
                                &source[prev_end..span.start()],
                                None,
                                false,
                                trim_line_required,
                            ),
                            line_no,
                            col_no,
                        );
                    }

                    // reset standalone statement marker
                    trim_line_required = false;
                }

                let (line_no, col_no) = span.start_pos().line_col();
                match rule {
                    Rule::template => {
                        template_stack.push_front(Template::new());
                    }
                    Rule::raw_text => {
                        // leading space fix
                        let start = if span.start() != prev_end {
                            prev_end
                        } else {
                            span.start()
                        };

                        let t = template_stack.front_mut().unwrap();

                        t.push_element(
                            Template::raw_string(
                                &source[start..span.end()],
                                Some(pair.clone()),
                                omit_pro_ws,
                                trim_line_required,
                            ),
                            line_no,
                            col_no,
                        );

                        // reset standalone statement marker
                        trim_line_required = false;
                    }
                    Rule::helper_block_start
                    | Rule::raw_block_start
                    | Rule::decorator_block_start
                    | Rule::partial_block_start => {
                        let exp = Template::parse_expression(source, it.by_ref(), span.end())?;

                        if exp.omit_pre_ws {
                            Template::remove_previous_whitespace(&mut template_stack);
                        }
                        omit_pro_ws = exp.omit_pro_ws;

                        // standalone statement check, it also removes leading whitespaces of
                        // previous rawstring when standalone statement detected
                        trim_line_required = Template::process_standalone_statement(
                            &mut template_stack,
                            source,
                            &span,
                            true,
                            options.is_partial,
                        );

                        let indent_before_write = trim_line_required && !exp.omit_pre_ws;

                        match rule {
                            Rule::helper_block_start | Rule::raw_block_start => {
                                let helper_template =
                                    HelperTemplate::new(exp.clone(), true, indent_before_write);
                                helper_stack.push_front(helper_template);
                            }
                            Rule::decorator_block_start | Rule::partial_block_start => {
                                let decorator =
                                    DecoratorTemplate::new(exp.clone(), indent_before_write);
                                decorator_stack.push_front(decorator);
                            }
                            _ => unreachable!(),
                        }

                        let t = template_stack.front_mut().unwrap();
                        t.mapping.push(TemplateMapping(line_no, col_no));
                    }
                    Rule::invert_tag | Rule::invert_chain_tag => {
                        // hack: invert_tag structure is similar to ExpressionSpec, so I
                        // use it here to represent the data

                        if rule == Rule::invert_chain_tag {
                            let _ = Template::parse_name(source, &mut it, span.end())?;
                        }
                        let exp = Template::parse_expression(source, it.by_ref(), span.end())?;

                        if exp.omit_pre_ws {
                            Template::remove_previous_whitespace(&mut template_stack);
                        }
                        omit_pro_ws = exp.omit_pro_ws;

                        // standalone statement check, it also removes leading whitespaces of
                        // previous rawstring when standalone statement detected
                        trim_line_required = Template::process_standalone_statement(
                            &mut template_stack,
                            source,
                            &span,
                            true,
                            options.is_partial,
                        );

                        let indent_before_write = trim_line_required && !exp.omit_pre_ws;

                        let t = template_stack.pop_front().unwrap();
                        let h = helper_stack.front_mut().unwrap();

                        if rule == Rule::invert_chain_tag {
                            h.set_chained();
                        }

                        h.set_chain_template(Some(t));
                        if rule == Rule::invert_chain_tag {
                            h.insert_inverse_node(Box::new(HelperTemplate::new_chain(
                                exp,
                                true,
                                indent_before_write,
                            )));
                        }
                    }

                    Rule::raw_block_text => {
                        let mut t = Template::new();
                        t.push_element(
                            Template::raw_string(
                                span.as_str(),
                                Some(pair.clone()),
                                omit_pro_ws,
                                trim_line_required,
                            ),
                            line_no,
                            col_no,
                        );
                        template_stack.push_front(t);
                    }
                    Rule::expression
                    | Rule::html_expression
                    | Rule::decorator_expression
                    | Rule::partial_expression
                    | Rule::helper_block_end
                    | Rule::raw_block_end
                    | Rule::decorator_block_end
                    | Rule::partial_block_end => {
                        let exp = Template::parse_expression(source, it.by_ref(), span.end())?;

                        if exp.omit_pre_ws {
                            Template::remove_previous_whitespace(&mut template_stack);
                        }
                        omit_pro_ws = exp.omit_pro_ws;

                        match rule {
                            Rule::expression | Rule::html_expression => {
                                let helper_template =
                                    HelperTemplate::new(exp.clone(), false, false);
                                let el = if rule == Rule::expression {
                                    Expression(Box::new(helper_template))
                                } else {
                                    HtmlExpression(Box::new(helper_template))
                                };
                                let t = template_stack.front_mut().unwrap();
                                t.push_element(el, line_no, col_no);
                            }
                            Rule::decorator_expression | Rule::partial_expression => {
                                // do not auto trim ident spaces for
                                // partial_expression(>)
                                let prevent_indent =
                                    !(rule == Rule::partial_expression && options.prevent_indent);
                                trim_line_required = Template::process_standalone_statement(
                                    &mut template_stack,
                                    source,
                                    &span,
                                    prevent_indent,
                                    options.is_partial,
                                );

                                // indent for partial expression >
                                let mut indent = None;
                                if rule == Rule::partial_expression
                                    && !options.prevent_indent
                                    && !exp.omit_pre_ws
                                {
                                    indent = support::str::find_trailing_whitespace_chars(
                                        &source[..span.start()],
                                    );
                                }

                                let mut decorator = DecoratorTemplate::new(
                                    exp.clone(),
                                    trim_line_required && !exp.omit_pre_ws,
                                );
                                decorator.indent = indent.map(std::borrow::ToOwned::to_owned);

                                let el = if rule == Rule::decorator_expression {
                                    DecoratorExpression(Box::new(decorator))
                                } else {
                                    PartialExpression(Box::new(decorator))
                                };
                                let t = template_stack.front_mut().unwrap();
                                t.push_element(el, line_no, col_no);
                            }
                            Rule::helper_block_end | Rule::raw_block_end => {
                                // standalone statement check, it also removes leading whitespaces of
                                // previous rawstring when standalone statement detected
                                trim_line_required = Template::process_standalone_statement(
                                    &mut template_stack,
                                    source,
                                    &span,
                                    true,
                                    options.is_partial,
                                );

                                let mut h = helper_stack.pop_front().unwrap();
                                let close_tag_name = exp.name.as_name();
                                if h.name.as_name() == close_tag_name {
                                    let prev_t = template_stack.pop_front().unwrap();
                                    h.revert_chain_and_set(Some(prev_t));

                                    let t = template_stack.front_mut().unwrap();
                                    t.elements.push(HelperBlock(Box::new(h)));
                                } else {
                                    return Err(TemplateError::of(
                                        TemplateErrorReason::MismatchingClosedHelper(
                                            h.name.debug_name(),
                                            exp.name.debug_name(),
                                        ),
                                    )
                                    .at(source, line_no, col_no)
                                    .in_template(options.name()));
                                }
                            }
                            Rule::decorator_block_end | Rule::partial_block_end => {
                                // standalone statement check, it also removes leading whitespaces of
                                // previous rawstring when standalone statement detected
                                trim_line_required = Template::process_standalone_statement(
                                    &mut template_stack,
                                    source,
                                    &span,
                                    true,
                                    options.is_partial,
                                );

                                let mut d = decorator_stack.pop_front().unwrap();
                                let close_tag_name = exp.name.as_name();
                                let empty_close_tag = close_tag_name
                                    .map(|name| name.is_empty())
                                    .unwrap_or_default();
                                if empty_close_tag || d.name.as_name() == close_tag_name {
                                    let prev_t = template_stack.pop_front().unwrap();
                                    d.template = Some(prev_t);
                                    let t = template_stack.front_mut().unwrap();
                                    if rule == Rule::decorator_block_end {
                                        t.elements.push(DecoratorBlock(Box::new(d)));
                                    } else {
                                        t.elements.push(PartialBlock(Box::new(d)));
                                    }
                                } else {
                                    return Err(TemplateError::of(
                                        TemplateErrorReason::MismatchingClosedDecorator(
                                            d.name.debug_name(),
                                            exp.name.debug_name(),
                                        ),
                                    )
                                    .at(source, line_no, col_no)
                                    .in_template(options.name()));
                                }
                            }
                            _ => unreachable!(),
                        }
                    }
                    Rule::hbs_comment_compact => {
                        trim_line_required = Template::process_standalone_statement(
                            &mut template_stack,
                            source,
                            &span,
                            true,
                            options.is_partial,
                        );

                        let text = span
                            .as_str()
                            .trim_start_matches("{{!")
                            .trim_end_matches("}}");
                        let t = template_stack.front_mut().unwrap();
                        t.push_element(Comment(text.to_owned()), line_no, col_no);
                    }
                    Rule::hbs_comment => {
                        trim_line_required = Template::process_standalone_statement(
                            &mut template_stack,
                            source,
                            &span,
                            true,
                            options.is_partial,
                        );

                        let text = span
                            .as_str()
                            .trim_start_matches("{{!--")
                            .trim_end_matches("--}}");
                        let t = template_stack.front_mut().unwrap();
                        t.push_element(Comment(text.to_owned()), line_no, col_no);
                    }
                    _ => {}
                }

                if rule != Rule::template {
                    end_pos = Some(span.end_pos());
                }
            } else {
                let prev_end = end_pos.as_ref().map_or(0, pest::Position::pos);
                if prev_end < source.len() {
                    let text = &source[prev_end..source.len()];
                    // is some called in if check
                    let (line_no, col_no) = end_pos.unwrap().line_col();
                    let t = template_stack.front_mut().unwrap();
                    t.push_element(RawString(text.to_owned()), line_no, col_no);
                }
                let mut root_template = template_stack.pop_front().unwrap();
                root_template.name = options.name;
                return Ok(root_template);
            }
        }
    }

    // These two compile functions are kept for compatibility with 4.x
    // Template APIs in case that some developers are using them
    // without registry.

    pub fn compile(source: &str) -> Result<Template, TemplateError> {
        Self::compile2(source, TemplateOptions::default())
    }

    pub fn compile_with_name<S: AsRef<str>>(
        source: S,
        name: String,
    ) -> Result<Template, TemplateError> {
        Self::compile2(
            source.as_ref(),
            TemplateOptions {
                name: Some(name),
                ..Default::default()
            },
        )
    }
}

#[non_exhaustive]
#[derive(PartialEq, Eq, Clone, Debug)]
pub enum TemplateElement {
    RawString(String),
    HtmlExpression(Box<HelperTemplate>),
    Expression(Box<HelperTemplate>),
    HelperBlock(Box<HelperTemplate>),
    DecoratorExpression(Box<DecoratorTemplate>),
    DecoratorBlock(Box<DecoratorTemplate>),
    PartialExpression(Box<DecoratorTemplate>),
    PartialBlock(Box<DecoratorTemplate>),
    Comment(String),
}

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

    #[test]
    fn test_parse_escaped_tag_raw_string() {
        let source = r"foo \{{bar}}";
        let t = Template::compile(source).ok().unwrap();
        assert_eq!(t.elements.len(), 1);
        assert_eq!(
            *t.elements.first().unwrap(),
            RawString("foo {{bar}}".to_string())
        );
    }

    #[test]
    fn test_pure_backslash_raw_string() {
        let source = r"\\\\";
        let t = Template::compile(source).ok().unwrap();
        assert_eq!(t.elements.len(), 1);
        assert_eq!(*t.elements.first().unwrap(), RawString(source.to_string()));
    }

    #[test]
    fn test_parse_escaped_block_raw_string() {
        let source = r"\{{{{foo}}}} bar";
        let t = Template::compile(source).ok().unwrap();
        assert_eq!(t.elements.len(), 1);
        assert_eq!(
            *t.elements.first().unwrap(),
            RawString("{{{{foo}}}} bar".to_string())
        );
    }

    #[test]
    fn test_parse_template() {
        let source = "<h1>{{title}} 你好</h1> {{{content}}}
{{#if date}}<p>good</p>{{else}}<p>bad</p>{{/if}}<img>{{foo bar}}中文你好
{{#unless true}}kitkat{{^}}lollipop{{/unless}}";
        let t = Template::compile(source).ok().unwrap();

        assert_eq!(t.elements.len(), 10);

        assert_eq!(*t.elements.first().unwrap(), RawString("<h1>".to_string()));
        assert_eq!(
            *t.elements.get(1).unwrap(),
            Expression(Box::new(HelperTemplate::with_path(Path::with_named_paths(
                &["title"]
            ))))
        );

        assert_eq!(
            *t.elements.get(3).unwrap(),
            HtmlExpression(Box::new(HelperTemplate::with_path(Path::with_named_paths(
                &["content"],
            ))))
        );

        match *t.elements.get(5).unwrap() {
            HelperBlock(ref h) => {
                assert_eq!(h.name.as_name().unwrap(), "if".to_string());
                assert_eq!(h.params.len(), 1);
                assert_eq!(h.template.as_ref().unwrap().elements.len(), 1);
            }
            _ => {
                panic!("Helper expected here.");
            }
        };

        match *t.elements.get(7).unwrap() {
            Expression(ref h) => {
                assert_eq!(h.name.as_name().unwrap(), "foo".to_string());
                assert_eq!(h.params.len(), 1);
                assert_eq!(
                    *(h.params.first().unwrap()),
                    Parameter::Path(Path::with_named_paths(&["bar"]))
                );
            }
            _ => {
                panic!("Helper expression here");
            }
        };

        match *t.elements.get(9).unwrap() {
            HelperBlock(ref h) => {
                assert_eq!(h.name.as_name().unwrap(), "unless".to_string());
                assert_eq!(h.params.len(), 1);
                assert_eq!(h.inverse.as_ref().unwrap().elements.len(), 1);
            }
            _ => {
                panic!("Helper expression here");
            }
        };
    }

    #[test]
    fn test_parse_block_partial_path_identifier() {
        let source = "{{#> foo/bar}}{{/foo/bar}}";
        assert!(Template::compile(source).is_ok());
    }

    #[test]
    fn test_parse_block_empty_end() {
        let source = "{{#> foo/bar}}{{/}}";
        Template::compile(source).unwrap();
    }

    #[test]
    fn test_parse_dynamic_block() {
        let source = "{{#> (foo)}}{{/}}";
        Template::compile(source).unwrap();
    }

    #[test]
    fn test_parse_error() {
        let source = "{{#ifequals name compare=\"hello\"}}\nhello\n\t{{else}}\ngood";

        let terr = Template::compile(source).unwrap_err();

        assert!(matches!(
            terr.reason(),
            TemplateErrorReason::InvalidSyntax(_)
        ));
        assert_eq!(terr.pos(), Some((4, 5)));
    }

    #[test]
    fn test_subexpression() {
        let source =
            "{{foo (bar)}}{{foo (bar baz)}} hello {{#if (baz bar) then=(bar)}}world{{/if}}";
        let t = Template::compile(source).ok().unwrap();

        assert_eq!(t.elements.len(), 4);
        match *t.elements.first().unwrap() {
            Expression(ref h) => {
                assert_eq!(h.name.as_name().unwrap(), "foo".to_owned());
                assert_eq!(h.params.len(), 1);
                if let Parameter::Subexpression(t) = h.params.first().unwrap() {
                    assert_eq!(t.name(), "bar".to_owned());
                } else {
                    panic!("Subexpression expected");
                }
            }
            _ => {
                panic!("Helper expression expected");
            }
        };

        match *t.elements.get(1).unwrap() {
            Expression(ref h) => {
                assert_eq!(h.name.as_name().unwrap(), "foo".to_string());
                assert_eq!(h.params.len(), 1);
                if let Parameter::Subexpression(t) = h.params.first().unwrap() {
                    assert_eq!(t.name(), "bar".to_owned());
                    if let Some(Parameter::Path(p)) = t.params().unwrap().first() {
                        assert_eq!(p, &Path::with_named_paths(&["baz"]));
                    } else {
                        panic!("non-empty param expected ");
                    }
                } else {
                    panic!("Subexpression expected");
                }
            }
            _ => {
                panic!("Helper expression expected");
            }
        };

        match *t.elements.get(3).unwrap() {
            HelperBlock(ref h) => {
                assert_eq!(h.name.as_name().unwrap(), "if".to_string());
                assert_eq!(h.params.len(), 1);
                assert_eq!(h.hash.len(), 1);

                if let Parameter::Subexpression(t) = h.params.first().unwrap() {
                    assert_eq!(t.name(), "baz".to_owned());
                    if let Some(Parameter::Path(p)) = t.params().unwrap().first() {
                        assert_eq!(p, &Path::with_named_paths(&["bar"]));
                    } else {
                        panic!("non-empty param expected ");
                    }
                } else {
                    panic!("Subexpression expected (baz bar)");
                }

                if let Parameter::Subexpression(t) = h.hash.get("then").unwrap() {
                    assert_eq!(t.name(), "bar".to_owned());
                } else {
                    panic!("Subexpression expected (bar)");
                }
            }
            _ => {
                panic!("HelperBlock expected");
            }
        }
    }

    #[test]
    fn test_white_space_omitter() {
        let source = "hello~     {{~world~}} \n  !{{~#if true}}else{{/if~}}";
        let t = Template::compile(source).ok().unwrap();

        assert_eq!(t.elements.len(), 4);

        assert_eq!(t.elements[0], RawString("hello~".to_string()));
        assert_eq!(
            t.elements[1],
            Expression(Box::new(HelperTemplate::with_path(Path::with_named_paths(
                &["world"]
            ))))
        );
        assert_eq!(t.elements[2], RawString("!".to_string()));

        let t2 = Template::compile("{{#if true}}1  {{~ else ~}} 2 {{~/if}}")
            .ok()
            .unwrap();
        assert_eq!(t2.elements.len(), 1);
        match t2.elements[0] {
            HelperBlock(ref h) => {
                assert_eq!(
                    h.template.as_ref().unwrap().elements[0],
                    RawString("1".to_string())
                );
                assert_eq!(
                    h.inverse.as_ref().unwrap().elements[0],
                    RawString("2".to_string())
                );
            }
            _ => unreachable!(),
        }
    }

    #[test]
    fn test_unclosed_expression() {
        let sources = ["{{invalid", "{{{invalid", "{{invalid}", "{{!hello"];
        for s in &sources {
            let result = Template::compile(s.to_owned());
            let err = result.expect_err("expected a syntax error");
            let syntax_error_msg = match err.reason() {
                TemplateErrorReason::InvalidSyntax(s) => s,
                _ => panic!("InvalidSyntax expected"),
            };
            assert!(
                syntax_error_msg.contains("expected identifier"),
                "{}",
                syntax_error_msg
            );
        }
    }

    #[test]
    fn test_raw_helper() {
        let source = "hello{{{{raw}}}}good{{night}}{{{{/raw}}}}world";
        match Template::compile(source) {
            Ok(t) => {
                assert_eq!(t.elements.len(), 3);
                assert_eq!(t.elements[0], RawString("hello".to_owned()));
                assert_eq!(t.elements[2], RawString("world".to_owned()));
                match t.elements[1] {
                    HelperBlock(ref h) => {
                        assert_eq!(h.name.as_name().unwrap(), "raw".to_owned());
                        if let Some(ref ht) = h.template {
                            assert_eq!(ht.elements.len(), 1);
                            assert_eq!(
                                *ht.elements.first().unwrap(),
                                RawString("good{{night}}".to_owned())
                            );
                        } else {
                            panic!("helper template not found");
                        }
                    }
                    _ => {
                        panic!("Unexpected element type");
                    }
                }
            }
            Err(e) => {
                panic!("{}", e);
            }
        }
    }

    #[test]
    fn test_literal_parameter_parser() {
        match Template::compile("{{hello 1 name=\"value\" valid=false ref=someref}}") {
            Ok(t) => {
                if let Expression(ref ht) = t.elements[0] {
                    assert_eq!(ht.params[0], Parameter::Literal(json!(1)));
                    assert_eq!(
                        ht.hash["name"],
                        Parameter::Literal(Json::String("value".to_owned()))
                    );
                    assert_eq!(ht.hash["valid"], Parameter::Literal(Json::Bool(false)));
                    assert_eq!(
                        ht.hash["ref"],
                        Parameter::Path(Path::with_named_paths(&["someref"]))
                    );
                }
            }
            Err(e) => panic!("{}", e),
        }
    }

    #[test]
    fn test_template_mapping() {
        match Template::compile("hello\n  {{~world}}\n{{#if nice}}\n\thello\n{{/if}}") {
            Ok(t) => {
                assert_eq!(t.mapping.len(), t.elements.len());
                assert_eq!(t.mapping[0], TemplateMapping(1, 1));
                assert_eq!(t.mapping[1], TemplateMapping(2, 3));
                assert_eq!(t.mapping[3], TemplateMapping(3, 1));
            }
            Err(e) => panic!("{}", e),
        }
    }

    #[test]
    fn test_whitespace_elements() {
        let c = Template::compile(
            "  {{elem}}\n\t{{#if true}} \
         {{/if}}\n{{{{raw}}}} {{{{/raw}}}}\n{{{{raw}}}}{{{{/raw}}}}\n",
        );
        let r = c.unwrap();
        // the \n after last raw block is dropped by pest
        assert_eq!(r.elements.len(), 9);
    }

    #[test]
    fn test_block_param() {
        match Template::compile("{{#each people as |person|}}{{person}}{{/each}}") {
            Ok(t) => {
                if let HelperBlock(ref ht) = t.elements[0] {
                    if let Some(BlockParam::Single(Parameter::Name(ref n))) = ht.block_param {
                        assert_eq!(n, "person");
                    } else {
                        panic!("block param expected.")
                    }
                } else {
                    panic!("Helper block expected");
                }
            }
            Err(e) => panic!("{}", e),
        }

        match Template::compile("{{#each people as |val key|}}{{person}}{{/each}}") {
            Ok(t) => {
                if let HelperBlock(ref ht) = t.elements[0] {
                    if let Some(BlockParam::Pair((
                        Parameter::Name(ref n1),
                        Parameter::Name(ref n2),
                    ))) = ht.block_param
                    {
                        assert_eq!(n1, "val");
                        assert_eq!(n2, "key");
                    } else {
                        panic!("helper block param expected.");
                    }
                } else {
                    panic!("Helper block expected");
                }
            }
            Err(e) => panic!("{}", e),
        }
    }

    #[test]
    fn test_decorator() {
        match Template::compile("hello {{* ssh}} world") {
            Err(e) => panic!("{}", e),
            Ok(t) => {
                if let DecoratorExpression(ref de) = t.elements[1] {
                    assert_eq!(de.name.as_name(), Some("ssh"));
                    assert_eq!(de.template, None);
                }
            }
        }

        match Template::compile("hello {{> ssh}} world") {
            Err(e) => panic!("{}", e),
            Ok(t) => {
                if let PartialExpression(ref de) = t.elements[1] {
                    assert_eq!(de.name.as_name(), Some("ssh"));
                    assert_eq!(de.template, None);
                }
            }
        }

        match Template::compile("{{#*inline \"hello\"}}expand to hello{{/inline}}{{> hello}}") {
            Err(e) => panic!("{}", e),
            Ok(t) => {
                if let DecoratorBlock(ref db) = t.elements[0] {
                    assert_eq!(db.name, Parameter::Name("inline".to_owned()));
                    assert_eq!(
                        db.params[0],
                        Parameter::Literal(Json::String("hello".to_owned()))
                    );
                    assert_eq!(
                        db.template.as_ref().unwrap().elements[0],
                        TemplateElement::RawString("expand to hello".to_owned())
                    );
                }
            }
        }

        match Template::compile("{{#> layout \"hello\"}}expand to hello{{/layout}}{{> hello}}") {
            Err(e) => panic!("{}", e),
            Ok(t) => {
                if let PartialBlock(ref db) = t.elements[0] {
                    assert_eq!(db.name, Parameter::Name("layout".to_owned()));
                    assert_eq!(
                        db.params[0],
                        Parameter::Literal(Json::String("hello".to_owned()))
                    );
                    assert_eq!(
                        db.template.as_ref().unwrap().elements[0],
                        TemplateElement::RawString("expand to hello".to_owned())
                    );
                }
            }
        }
    }

    #[test]
    fn test_panic_with_tag_name() {
        let s = "{{#>(X)}}{{/X}}";
        let result = Template::compile(s);
        assert!(result.is_err());
        assert_eq!("decorator \"Subexpression(Subexpression { element: Expression(HelperTemplate { name: Path(Relative(([Named(\\\"X\\\")], \\\"X\\\"))), params: [], hash: {}, block_param: None, template: None, inverse: None, block: false, chain: false, indent_before_write: false }) })\" was opened, but \"X\" is closing", format!("{}", result.unwrap_err().reason()));
    }
}
