use crate::parser::template;
use crate::parser::Body;
use crate::parser::DustTag;
use crate::parser::KVPair;
use crate::parser::PartialNameElement;
use crate::parser::Path;
use crate::parser::RValue;
use crate::parser::Special;
use crate::parser::Template;
use crate::parser::{Filter, TemplateElement};
use crate::renderer::context_element::ContextElement;
use crate::renderer::errors::CompileError;
use crate::renderer::errors::RenderError;
use crate::renderer::errors::WalkError;
use crate::renderer::inline_partial_tree::extract_inline_partials;
use crate::renderer::inline_partial_tree::InlinePartialTreeElement;
use crate::renderer::iteration_context::IterationContext;
use crate::renderer::parameters_context::ParametersContext;
use crate::renderer::walking::walk_path;
use std::borrow::Borrow;
use std::collections::HashMap;

#[derive(Clone, Debug)]
pub struct CompiledTemplate<'a> {
    template: Template<'a>,
    pub name: String,
}

#[derive(Clone, Debug)]
pub struct DustRenderer<'a> {
    templates: HashMap<String, &'a Template<'a>>,
}

pub fn compile_template<'a>(
    source: &'a str,
    name: String,
) -> Result<CompiledTemplate<'a>, CompileError> {
    // TODO: This could use better error management
    let (_remaining, parsed_template) = template(source).expect("Failed to compile template");
    Ok(CompiledTemplate {
        template: parsed_template,
        name: name,
    })
}

impl<'a> DustRenderer<'a> {
    pub fn new() -> DustRenderer<'a> {
        DustRenderer {
            templates: HashMap::new(),
        }
    }

    pub fn load_source(&mut self, template: &'a CompiledTemplate) {
        self.templates
            .insert(template.name.clone(), &template.template);
    }

    pub fn render(
        &'a self,
        name: &str,
        breadcrumbs: &Vec<&'a dyn ContextElement>,
    ) -> Result<String, RenderError> {
        self.render_template(name, breadcrumbs, None)
    }

    fn render_template(
        &'a self,
        name: &str,
        breadcrumbs: &Vec<&'a dyn ContextElement>,
        blocks: Option<&'a InlinePartialTreeElement<'a>>,
    ) -> Result<String, RenderError> {
        let main_template = match self.templates.get(name) {
            Some(tmpl) => tmpl,
            None => {
                return Err(RenderError::TemplateNotFound(name.to_owned()));
            }
        };
        let extracted_inline_partials = extract_inline_partials(main_template);
        let new_blocks = InlinePartialTreeElement::new(blocks, extracted_inline_partials);
        let new_block_context = BlockContext {
            breadcrumbs: breadcrumbs,
            blocks: &new_blocks,
        };
        self.render_body(&main_template.contents, breadcrumbs, &new_block_context)
    }

    fn render_maybe_body(
        &'a self,
        body: &'a Option<Body>,
        breadcrumbs: &Vec<&'a dyn ContextElement>,
        blocks: &'a BlockContext<'a>,
    ) -> Result<String, RenderError> {
        match body {
            None => Ok("".to_owned()),
            Some(body) => Ok(self.render_body(body, breadcrumbs, blocks)?),
        }
    }

    fn render_body(
        &'a self,
        body: &'a Body,
        breadcrumbs: &Vec<&'a dyn ContextElement>,
        blocks: &'a BlockContext<'a>,
    ) -> Result<String, RenderError> {
        let mut output = String::new();
        for elem in &body.elements {
            match elem {
                TemplateElement::TEIgnoredWhitespace(_) => {}
                TemplateElement::TESpan(span) => output.push_str(span.contents),
                TemplateElement::TETag(dt) => {
                    output.push_str(&self.render_tag(dt, breadcrumbs, blocks)?);
                }
            }
        }
        Ok(output)
    }

    /// For rendering a dynamic partial's name
    fn render_partial_name(
        &'a self,
        body: &'a Vec<PartialNameElement>,
        breadcrumbs: &Vec<&'a dyn ContextElement>,
        blocks: &'a BlockContext<'a>,
    ) -> Result<String, RenderError> {
        let converted_to_template_elements: Vec<TemplateElement<'a>> =
            body.into_iter().map(|e| e.into()).collect();
        self.render_body(
            &Body {
                elements: converted_to_template_elements,
            },
            breadcrumbs,
            blocks,
        )
    }

    fn render_tag(
        &'a self,
        tag: &'a DustTag,
        breadcrumbs: &Vec<&'a dyn ContextElement>,
        blocks: &'a BlockContext<'a>,
    ) -> Result<String, RenderError> {
        match tag {
            DustTag::DTComment(_comment) => (),
            DustTag::DTSpecial(special) => {
                return Ok(match special {
                    Special::Space => " ",
                    Special::NewLine => "\n",
                    Special::CarriageReturn => "\r",
                    Special::LeftCurlyBrace => "{",
                    Special::RightCurlyBrace => "}",
                }
                .to_owned())
            }
            DustTag::DTLiteralStringBlock(literal) => return Ok((*literal).to_owned()),
            DustTag::DTReference(reference) => {
                let val = walk_path(breadcrumbs, &reference.path.keys);
                match val {
                    Err(WalkError::CantWalk) => return Ok("".to_owned()),
                    Ok(final_val) => {
                        return if final_val.is_truthy() {
                            final_val.render(&Self::preprocess_filters(&reference.filters))
                        } else {
                            Ok("".to_owned())
                        };
                    }
                }
            }
            DustTag::DTSection(container) => {
                let injected_context = ParametersContext::new(breadcrumbs, &container.params);
                let val = walk_path(breadcrumbs, &container.path.keys);
                match val {
                    Err(WalkError::CantWalk) => {
                        let new_breadcrumbs = Self::new_breadcrumbs_section(
                            breadcrumbs,
                            None,
                            Some(&injected_context),
                            &container.explicit_context,
                            None,
                        );
                        return self.render_maybe_body(
                            &container.else_contents,
                            new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
                            blocks,
                        );
                    }
                    Ok(final_val) => {
                        return if final_val.is_truthy() {
                            match &container.contents {
                                // If the body is empty, just shortcut
                                // to an empty string now rather than
                                // generating intermediate contexts
                                // and iterating for nothing.
                                None => Ok("".to_owned()),
                                Some(body) => {
                                    let loop_elements: Vec<&dyn ContextElement> =
                                        final_val.get_loop_elements();
                                    if loop_elements.is_empty() {
                                        // Scalar value
                                        let new_breadcrumbs = Self::new_breadcrumbs_section(
                                            breadcrumbs,
                                            None,
                                            Some(&injected_context),
                                            &container.explicit_context,
                                            Some(final_val),
                                        );
                                        self.render_body(
                                            body,
                                            new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
                                            blocks,
                                        )
                                    } else {
                                        // Array-like value
                                        let total_length = loop_elements.len();
                                        let rendered_results: Result<Vec<String>, RenderError> =
                                            loop_elements
                                                .into_iter()
                                                .enumerate()
                                                .map(|(i, array_elem)| {
                                                    let index_context =
                                                        IterationContext::new(i, total_length);
                                                    let new_breadcrumbs =
                                                        Self::new_breadcrumbs_section(
                                                            breadcrumbs,
                                                            Some(&index_context),
                                                            Some(&injected_context),
                                                            &container.explicit_context,
                                                            Some(array_elem),
                                                        );
                                                    self.render_body(
                                                        &body,
                                                        new_breadcrumbs
                                                            .as_ref()
                                                            .unwrap_or(breadcrumbs),
                                                        blocks,
                                                    )
                                                })
                                                .collect();
                                        let rendered_slice: &[String] = &rendered_results?;
                                        return Ok(rendered_slice.join(""));
                                    }
                                }
                            }
                        } else {
                            // Oddly enough if the value is falsey (like
                            // an empty array or null), Dust uses the
                            // original context before walking the path as
                            // the context for rendering the else block
                            let new_breadcrumbs = Self::new_breadcrumbs_section(
                                breadcrumbs,
                                None,
                                Some(&injected_context),
                                &container.explicit_context,
                                None,
                            );
                            return self.render_maybe_body(
                                &container.else_contents,
                                new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
                                blocks,
                            );
                        };
                    }
                }
            }
            DustTag::DTExists(container) => {
                let new_breadcrumbs = Self::new_breadcrumbs_partial(
                    breadcrumbs,
                    breadcrumbs,
                    None,
                    &container.explicit_context,
                );
                let val = walk_path(breadcrumbs, &container.path.keys);
                return if val.map(|v| v.is_truthy()).unwrap_or(false) {
                    self.render_maybe_body(
                        &container.contents,
                        new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
                        blocks,
                    )
                } else {
                    self.render_maybe_body(
                        &container.else_contents,
                        new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
                        blocks,
                    )
                };
            }
            DustTag::DTNotExists(container) => {
                let new_breadcrumbs = Self::new_breadcrumbs_partial(
                    breadcrumbs,
                    breadcrumbs,
                    None,
                    &container.explicit_context,
                );
                let val = walk_path(breadcrumbs, &container.path.keys);
                return if !val.map(|v| v.is_truthy()).unwrap_or(false) {
                    self.render_maybe_body(
                        &container.contents,
                        new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
                        blocks,
                    )
                } else {
                    self.render_maybe_body(
                        &container.else_contents,
                        new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
                        blocks,
                    )
                };
            }
            DustTag::DTPartial(partial) => {
                let partial_name = self.render_partial_name(&partial.name, breadcrumbs, blocks)?;
                if partial.params.is_empty() {
                    let new_breadcrumbs = Self::new_breadcrumbs_partial(
                        breadcrumbs,
                        breadcrumbs,
                        None,
                        &partial.explicit_context,
                    );
                    let rendered_content = self.render_template(
                        &partial_name,
                        new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
                        Some(blocks.blocks),
                    )?;
                    return Ok(rendered_content);
                } else {
                    let injected_context = ParametersContext::new(breadcrumbs, &partial.params);
                    let new_breadcrumbs = Self::new_breadcrumbs_partial(
                        breadcrumbs,
                        breadcrumbs,
                        Some(&injected_context),
                        &partial.explicit_context,
                    );
                    let rendered_content = self.render_template(
                        &partial_name,
                        new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
                        Some(blocks.blocks),
                    )?;
                    return Ok(rendered_content);
                }
            }
            DustTag::DTInlinePartial(_named_block) => {
                // Inline partials are blank during rendering (they get injected into blocks)
                return Ok("".to_owned());
            }
            DustTag::DTBlock(named_block) => {
                let new_breadcrumbs = Self::new_breadcrumbs_partial(
                    breadcrumbs,
                    blocks.breadcrumbs,
                    None,
                    &named_block.explicit_context,
                );
                return match blocks.blocks.get_block(named_block.name) {
                    None => self.render_maybe_body(
                        &named_block.contents,
                        new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
                        blocks,
                    ),
                    Some(inline_partial) => self.render_maybe_body(
                        inline_partial,
                        new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
                        blocks,
                    ),
                };
            }
            DustTag::DTHelperEquals(parameterized_block) => {
                let new_breadcrumbs = Self::new_breadcrumbs_partial(
                    breadcrumbs,
                    breadcrumbs,
                    None,
                    &parameterized_block.explicit_context,
                );
                let param_map: HashMap<&str, &RValue<'a>> =
                    Self::get_rval_map(&parameterized_block.params);
                // Special case: when comparing two RVPaths, if the
                // path is equal then dust assumes the values are
                // equal (otherwise, non-scalar values are
                // automatically not equal)
                if Self::are_paths_identical(&param_map) {
                    return match &parameterized_block.contents {
                        None => Ok("".to_owned()),
                        Some(body) => {
                            let rendered_content = self.render_body(
                                body,
                                new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
                                blocks,
                            )?;
                            Ok(rendered_content)
                        }
                    };
                }

                let left_side: Result<&dyn ContextElement, WalkError> =
                    match Self::get_rval(breadcrumbs, &param_map, "key") {
                        None => return Ok("".to_owned()),
                        Some(res) => res,
                    };
                let right_side: Result<&dyn ContextElement, WalkError> =
                    Self::get_rval(breadcrumbs, &param_map, "value")
                        .unwrap_or(Err(WalkError::CantWalk));

                if left_side == right_side {
                    return self.render_maybe_body(
                        &parameterized_block.contents,
                        new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
                        blocks,
                    );
                } else {
                    return self.render_maybe_body(
                        &parameterized_block.else_contents,
                        new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
                        blocks,
                    );
                }
            }
            DustTag::DTHelperNotEquals(parameterized_block) => {
                let new_breadcrumbs = Self::new_breadcrumbs_partial(
                    breadcrumbs,
                    breadcrumbs,
                    None,
                    &parameterized_block.explicit_context,
                );
                let param_map: HashMap<&str, &RValue<'a>> =
                    Self::get_rval_map(&parameterized_block.params);
                // Special case: when comparing two RVPaths, if the
                // path is equal then dust assumes the values are
                // equal (otherwise, non-scalar values are
                // automatically not equal)
                if Self::are_paths_identical(&param_map) {
                    return match &parameterized_block.else_contents {
                        None => Ok("".to_owned()),
                        Some(body) => {
                            let rendered_content = self.render_body(
                                body,
                                new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
                                blocks,
                            )?;
                            Ok(rendered_content)
                        }
                    };
                }

                let left_side: Result<&dyn ContextElement, WalkError> =
                    match Self::get_rval(breadcrumbs, &param_map, "key") {
                        None => return Ok("".to_owned()),
                        Some(res) => res,
                    };
                let right_side: Result<&dyn ContextElement, WalkError> =
                    Self::get_rval(breadcrumbs, &param_map, "value")
                        .unwrap_or(Err(WalkError::CantWalk));
                if left_side != right_side {
                    return self.render_maybe_body(
                        &parameterized_block.contents,
                        new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
                        blocks,
                    );
                } else {
                    return self.render_maybe_body(
                        &parameterized_block.else_contents,
                        new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
                        blocks,
                    );
                }
            }
            DustTag::DTHelperGreaterThan(parameterized_block) => {
                let new_breadcrumbs = Self::new_breadcrumbs_partial(
                    breadcrumbs,
                    breadcrumbs,
                    None,
                    &parameterized_block.explicit_context,
                );
                let param_map: HashMap<&str, &RValue<'a>> =
                    Self::get_rval_map(&parameterized_block.params);
                let left_side: Result<&dyn ContextElement, WalkError> =
                    match Self::get_rval(breadcrumbs, &param_map, "key") {
                        None => return Ok("".to_owned()),
                        Some(res) => res,
                    };
                let right_side: Result<&dyn ContextElement, WalkError> =
                    Self::get_rval(breadcrumbs, &param_map, "value")
                        .unwrap_or(Err(WalkError::CantWalk));
                match (left_side, right_side) {
                    (Err(_), _) | (_, Err(_)) => {
                        return self.render_maybe_body(
                            &parameterized_block.else_contents,
                            new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
                            blocks,
                        )
                    }
                    (Ok(left_side_unwrapped), Ok(right_side_unwrapped)) => {
                        if left_side_unwrapped > right_side_unwrapped {
                            return self.render_maybe_body(
                                &parameterized_block.contents,
                                new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
                                blocks,
                            );
                        } else {
                            return self.render_maybe_body(
                                &parameterized_block.else_contents,
                                new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
                                blocks,
                            );
                        }
                    }
                }
            }
            DustTag::DTHelperGreaterThanOrEquals(parameterized_block) => {
                let new_breadcrumbs = Self::new_breadcrumbs_partial(
                    breadcrumbs,
                    breadcrumbs,
                    None,
                    &parameterized_block.explicit_context,
                );
                let param_map: HashMap<&str, &RValue<'a>> =
                    Self::get_rval_map(&parameterized_block.params);
                let left_side: Result<&dyn ContextElement, WalkError> =
                    match Self::get_rval(breadcrumbs, &param_map, "key") {
                        None => return Ok("".to_owned()),
                        Some(res) => res,
                    };
                let right_side: Result<&dyn ContextElement, WalkError> =
                    Self::get_rval(breadcrumbs, &param_map, "value")
                        .unwrap_or(Err(WalkError::CantWalk));
                match (left_side, right_side) {
                    (Err(_), _) | (_, Err(_)) => {
                        return self.render_maybe_body(
                            &parameterized_block.else_contents,
                            new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
                            blocks,
                        )
                    }
                    (Ok(left_side_unwrapped), Ok(right_side_unwrapped)) => {
                        if left_side_unwrapped >= right_side_unwrapped {
                            return self.render_maybe_body(
                                &parameterized_block.contents,
                                new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
                                blocks,
                            );
                        } else {
                            return self.render_maybe_body(
                                &parameterized_block.else_contents,
                                new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
                                blocks,
                            );
                        }
                    }
                }
            }
            DustTag::DTHelperLessThan(parameterized_block) => {
                let new_breadcrumbs = Self::new_breadcrumbs_partial(
                    breadcrumbs,
                    breadcrumbs,
                    None,
                    &parameterized_block.explicit_context,
                );
                let param_map: HashMap<&str, &RValue<'a>> =
                    Self::get_rval_map(&parameterized_block.params);
                let left_side: Result<&dyn ContextElement, WalkError> =
                    match Self::get_rval(breadcrumbs, &param_map, "key") {
                        None => return Ok("".to_owned()),
                        Some(res) => res,
                    };
                let right_side: Result<&dyn ContextElement, WalkError> =
                    Self::get_rval(breadcrumbs, &param_map, "value")
                        .unwrap_or(Err(WalkError::CantWalk));
                match (left_side, right_side) {
                    (Err(_), _) | (_, Err(_)) => {
                        return self.render_maybe_body(
                            &parameterized_block.else_contents,
                            new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
                            blocks,
                        )
                    }
                    (Ok(left_side_unwrapped), Ok(right_side_unwrapped)) => {
                        if left_side_unwrapped < right_side_unwrapped {
                            return self.render_maybe_body(
                                &parameterized_block.contents,
                                new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
                                blocks,
                            );
                        } else {
                            return self.render_maybe_body(
                                &parameterized_block.else_contents,
                                new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
                                blocks,
                            );
                        }
                    }
                }
            }
            DustTag::DTHelperLessThanOrEquals(parameterized_block) => {
                let new_breadcrumbs = Self::new_breadcrumbs_partial(
                    breadcrumbs,
                    breadcrumbs,
                    None,
                    &parameterized_block.explicit_context,
                );
                let param_map: HashMap<&str, &RValue<'a>> =
                    Self::get_rval_map(&parameterized_block.params);
                let left_side: Result<&dyn ContextElement, WalkError> =
                    match Self::get_rval(breadcrumbs, &param_map, "key") {
                        None => return Ok("".to_owned()),
                        Some(res) => res,
                    };
                let right_side: Result<&dyn ContextElement, WalkError> =
                    Self::get_rval(breadcrumbs, &param_map, "value")
                        .unwrap_or(Err(WalkError::CantWalk));
                match (left_side, right_side) {
                    (Err(_), _) | (_, Err(_)) => {
                        return self.render_maybe_body(
                            &parameterized_block.else_contents,
                            new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
                            blocks,
                        )
                    }
                    (Ok(left_side_unwrapped), Ok(right_side_unwrapped)) => {
                        if left_side_unwrapped <= right_side_unwrapped {
                            return self.render_maybe_body(
                                &parameterized_block.contents,
                                new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
                                blocks,
                            );
                        } else {
                            return self.render_maybe_body(
                                &parameterized_block.else_contents,
                                new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
                                blocks,
                            );
                        }
                    }
                }
            }
        }
        Ok("".to_owned())
    }

    /// Gets the elements to loop over for a section.
    ///
    /// If the value is falsey, and therefore should render the else
    /// block, this will return an empty vector.
    fn get_loop_elements<'b>(
        walk_result: Result<&'b dyn ContextElement, WalkError>,
    ) -> Vec<&'b dyn ContextElement> {
        match walk_result {
            Err(WalkError::CantWalk) => Vec::new(),
            Ok(walk_target) => walk_target.get_loop_elements(),
        }
    }

    fn are_paths_identical<'b>(param_map: &HashMap<&str, &RValue<'b>>) -> bool {
        match (param_map.get("key"), param_map.get("value")) {
            (None, _) => false,
            (_, None) => false,
            (Some(key_rval), Some(value_rval)) => match (key_rval, value_rval) {
                (RValue::RVPath(key_path), RValue::RVPath(value_path)) => {
                    key_path.keys == value_path.keys
                }
                _ => false,
            },
        }
    }

    fn get_rval_map<'b>(params: &'b Vec<KVPair<'b>>) -> HashMap<&'b str, &'b RValue<'b>> {
        params
            .iter()
            .map(|pair: &KVPair<'b>| (pair.key, &pair.value))
            .collect()
    }

    fn get_rval<'b>(
        breadcrumbs: &'b Vec<&'b dyn ContextElement>,
        param_map: &HashMap<&str, &'b RValue<'b>>,
        key: &str,
    ) -> Option<Result<&'b dyn ContextElement, WalkError>> {
        match param_map.get(key) {
            None => None,
            Some(rval) => match rval {
                RValue::RVLiteral(literal) => Some(Ok(literal)),
                RValue::RVPath(path) => Some(walk_path(breadcrumbs, &path.keys)),
            },
        }
    }

    fn preprocess_filters(filters: &Vec<Filter>) -> Vec<Filter> {
        let mut final_filters: Vec<Filter> = filters
            .into_iter()
            .filter(|f| f != &&Filter::DisableHtmlEncode)
            .map(|f| f.clone())
            .collect();

        // If the user has not specified any escaping filter (|s or
        // |h), automatically add an html escape filter
        if !filters.iter().any(|f| f == &Filter::DisableHtmlEncode) {
            final_filters.push(Filter::HtmlEncode);
        }
        final_filters
    }

    /// Generate a new breadcrumbs object
    ///
    /// This function generates a new breadcrumbs object based on the
    /// new context information provided.
    ///
    /// breadcrumbs are the breadcrumbs that will be used in the final
    /// breadcrumbs (unless omitted due to an explicit context)
    ///
    /// explicit_context_breadcrumbs are the breadcrumbs used to
    /// evaluate an explicit context path. Most of the time the two
    /// breadcrumbs parameters will be identical, but for
    /// blocks/inline partials the explicit_context_breadcrumbs will
    /// be the breadcrumbs from the start of the partial containing
    /// the block.
    ///
    /// explicit_context is for contexts specified with a `:path`
    /// inside a dust tag.
    ///
    /// injected_context is for any generated context. This includes
    /// both parameters on a tag and also the handling of $idx and
    /// $len.
    ///
    /// New context element is the element is an element to append to
    /// the end, generally for use in section tags which walk to a new
    /// context.
    ///
    /// If explicit_context is not None, then the final breadcrumb stack will be:
    ///
    /// ```text
    /// breadcrumbs
    /// injected_context
    /// new_context_element
    /// ```
    ///
    /// However, if explicit_context is not None, then the old
    /// breadcrumbs are omitted, leading to the new breadcrumb stack
    /// as:
    ///
    /// ```text
    /// injected_context
    /// explicit_context
    /// new_context_element
    /// ```
    fn new_breadcrumbs_deprecated<'b>(
        breadcrumbs: &'b Vec<&'b dyn ContextElement>,
        explicit_context_breadcrumbs: &'b Vec<&'b dyn ContextElement>,
        injected_context: Option<&'b dyn ContextElement>,
        explicit_context: &Option<Path<'b>>,
        new_context_element: Option<&'b dyn ContextElement>,
    ) -> Option<Vec<&'b dyn ContextElement>> {
        // If none of the additional contexts are present, return None
        // to signal that the original breadcrumbs should be used
        // rather than incurring a copy here.
        match (injected_context, explicit_context, new_context_element) {
            (None, None, None) => return None,
            _ => (),
        };
        let mut new_stack = match explicit_context {
            Some(_) => Vec::with_capacity(3),
            None => breadcrumbs.clone(),
        };
        // TODO: Can sections have parameters, and if so, what happens then? Currently when there is an injected context or an explicit context it gets inserted behind the current context, so 1->2->3 becomes 1->2->injected->3 or explicit->3. When there is a new context(4) with injected we're doing 1->2->3->injected->4. When there is an explicit context and a new context we're doing explicit->4. But what happens if there is a section with parameters and an explicit context, hitting all the categories? Would it be parameters->explicit->4? I would definitely have to change the parameters to this function since right now iteration variables and parameters are both sharing injected_context.
        injected_context.map(|ctx| {
            // Special case: when there is no explicit context or new
            // context element, the injected context gets inserted 1
            // spot behind the current context. Otherwise, the
            // injected context gets added after the current context
            // but before the explicit context and new context
            // element.
            match (explicit_context, new_context_element) {
                (None, None) => new_stack.insert(std::cmp::max(new_stack.len() - 1, 0), ctx),
                _ => new_stack.push(ctx),
            }
        });
        explicit_context.as_ref().map(|path| {
            walk_path(explicit_context_breadcrumbs, &path.keys).map(|val| {
                if val.is_truthy() {
                    new_stack.push(val)
                }
            });
        });
        new_context_element.map(|ctx| new_stack.push(ctx));
        Some(new_stack)
    }

    fn new_breadcrumbs_section<'b>(
        breadcrumbs: &'b Vec<&'b dyn ContextElement>,
        index_context: Option<&'b dyn ContextElement>,
        injected_context: Option<&'b dyn ContextElement>,
        explicit_context: &Option<Path<'b>>,
        new_context_element: Option<&'b dyn ContextElement>,
    ) -> Option<Vec<&'b dyn ContextElement>> {
        // If none of the additional contexts are present, return None
        // to signal that the original breadcrumbs should be used
        // rather than incurring a copy here.
        match (
            index_context,
            injected_context,
            explicit_context,
            new_context_element,
        ) {
            (None, None, None, None) => return None,
            _ => (),
        }
        let mut new_stack = match explicit_context {
            Some(_) => Vec::with_capacity(4),
            None => breadcrumbs.clone(),
        };
        explicit_context.as_ref().map(|path| {
            walk_path(breadcrumbs, &path.keys).map(|val| {
                if val.is_truthy() {
                    new_stack.push(val)
                }
            });
        });
        injected_context.map(|ctx| new_stack.push(ctx));
        new_context_element.map(|ctx| new_stack.push(ctx));
        index_context.map(|ctx| new_stack.push(ctx));
        Some(new_stack)
    }

    fn get_index_of_first_non_pseudo_element<'b, B>(breadcrumbs: &'b Vec<B>) -> Option<usize>
    where
        B: Borrow<dyn ContextElement + 'a>,
    {
        breadcrumbs
            .iter()
            .rposition(|b| !(*b).borrow().is_pseudo_element())
    }

    fn new_breadcrumbs_partial<'b>(
        breadcrumbs: &'b Vec<&'b dyn ContextElement>,
        explicit_context_breadcrumbs: &'b Vec<&'b dyn ContextElement>,
        injected_context: Option<&'b dyn ContextElement>,
        explicit_context: &Option<Path<'b>>,
    ) -> Option<Vec<&'b dyn ContextElement>> {
        // If none of the additional contexts are present, return None
        // to signal that the original breadcrumbs should be used
        // rather than incurring a copy here.
        match (injected_context, explicit_context) {
            (None, None) => return None,
            _ => (),
        };
        let mut new_stack = match explicit_context {
            Some(_) => Vec::with_capacity(3),
            None => breadcrumbs.clone(),
        };
        injected_context.map(|ctx| {
            // Special case: when there is no explicit context, the
            // injected context gets inserted 1 spot behind the
            // current context. Otherwise, the injected context gets
            // added after the current context but before the explicit
            // context.
            match explicit_context {
                None => new_stack.insert(
                    Self::get_index_of_first_non_pseudo_element(&new_stack).unwrap_or(0),
                    ctx,
                ),
                _ => new_stack.push(ctx),
            }
        });
        explicit_context.as_ref().map(|path| {
            walk_path(explicit_context_breadcrumbs, &path.keys).map(|val| {
                if val.is_truthy() {
                    new_stack.push(val)
                }
            });
        });
        Some(new_stack)
    }
}

struct BlockContext<'a> {
    /// The breadcrumbs at the time of entering the current partial
    breadcrumbs: &'a Vec<&'a dyn ContextElement>,
    blocks: &'a InlinePartialTreeElement<'a>,
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::parser::Filter;
    use crate::renderer::context_element::Loopable;
    use crate::renderer::context_element::Renderable;
    use crate::renderer::context_element::Truthiness;
    use crate::renderer::context_element::Walkable;
    use crate::renderer::CompareContextElement;
    use std::cmp::Ordering;

    impl ContextElement for String {}

    impl Truthiness for String {
        fn is_truthy(&self) -> bool {
            !self.is_empty()
        }
    }

    impl Renderable for String {
        fn render(&self, _filters: &Vec<Filter>) -> Result<String, RenderError> {
            Ok(self.clone())
        }
    }

    impl Loopable for String {
        fn get_loop_elements(&self) -> Vec<&dyn ContextElement> {
            Vec::new()
        }
    }

    impl Walkable for String {
        fn walk(&self, segment: &str) -> Result<&dyn ContextElement, WalkError> {
            Err(WalkError::CantWalk)
        }
    }

    impl CompareContextElement for String {
        fn equals(&self, other: &dyn ContextElement) -> bool {
            match other.to_any().downcast_ref::<Self>() {
                None => false,
                Some(other_string) => self == other_string,
            }
        }

        fn partial_compare(&self, other: &dyn ContextElement) -> Option<Ordering> {
            match other.to_any().downcast_ref::<Self>() {
                None => None,
                Some(other_string) => self.partial_cmp(other_string),
            }
        }
    }
    impl ContextElement for u64 {}

    impl Truthiness for u64 {
        fn is_truthy(&self) -> bool {
            true
        }
    }

    impl Renderable for u64 {
        fn render(&self, _filters: &Vec<Filter>) -> Result<String, RenderError> {
            Ok(self.to_string())
        }
    }

    impl Loopable for u64 {
        fn get_loop_elements(&self) -> Vec<&dyn ContextElement> {
            Vec::new()
        }
    }

    impl Walkable for u64 {
        fn walk(&self, segment: &str) -> Result<&dyn ContextElement, WalkError> {
            Err(WalkError::CantWalk)
        }
    }

    impl CompareContextElement for u64 {
        fn equals(&self, other: &dyn ContextElement) -> bool {
            match other.to_any().downcast_ref::<Self>() {
                None => false,
                Some(other_num) => self == other_num,
            }
        }

        fn partial_compare(&self, other: &dyn ContextElement) -> Option<Ordering> {
            match other.to_any().downcast_ref::<Self>() {
                None => None,
                Some(other_num) => self.partial_cmp(other_num),
            }
        }
    }

    impl<I: 'static + ContextElement + Clone> ContextElement for HashMap<String, I> {}

    impl<I: ContextElement> Truthiness for HashMap<String, I> {
        fn is_truthy(&self) -> bool {
            true
        }
    }

    impl<I: ContextElement> Renderable for HashMap<String, I> {
        fn render(&self, _filters: &Vec<Filter>) -> Result<String, RenderError> {
            // TODO: handle the filters
            Ok("[object Object]".to_owned())
        }
    }

    impl<I: ContextElement> Walkable for HashMap<String, I> {
        fn walk(&self, segment: &str) -> Result<&dyn ContextElement, WalkError> {
            let child = self.get(segment).ok_or(WalkError::CantWalk)?;
            Ok(child)
        }
    }

    impl<I: 'static + ContextElement + Clone> Loopable for HashMap<String, I> {
        fn get_loop_elements(&self) -> Vec<&dyn ContextElement> {
            Vec::new()
        }
    }

    impl<I: 'static + ContextElement + Clone> CompareContextElement for HashMap<String, I> {
        fn equals(&self, other: &dyn ContextElement) -> bool {
            false
        }

        fn partial_compare(&self, other: &dyn ContextElement) -> Option<Ordering> {
            // TODO: Implement
            None
        }
    }

    #[test]
    fn test_walk_path() {
        let context: HashMap<String, String> = [
            ("cat".to_string(), "kitty".to_string()),
            ("dog".to_string(), "doggy".to_string()),
            ("tiger".to_string(), "murderkitty".to_string()),
        ]
        .iter()
        .cloned()
        .collect();
        let number_context: HashMap<String, u64> = [
            ("cat".to_string(), 1),
            ("dog".to_string(), 2),
            ("tiger".to_string(), 3),
        ]
        .iter()
        .cloned()
        .collect();
        let deep_context: HashMap<String, HashMap<String, String>> = [
            (
                "cat".to_string(),
                [("food".to_string(), "meat".to_string())]
                    .iter()
                    .cloned()
                    .collect(),
            ),
            (
                "dog".to_string(),
                [("food".to_string(), "meat".to_string())]
                    .iter()
                    .cloned()
                    .collect(),
            ),
            (
                "tiger".to_string(),
                [("food".to_string(), "people".to_string())]
                    .iter()
                    .cloned()
                    .collect(),
            ),
        ]
        .iter()
        .cloned()
        .collect();

        assert_eq!(
            walk_path(&vec![&context as &dyn ContextElement], &vec!["cat"])
                .unwrap()
                .render(&Vec::new())
                .unwrap(),
            "kitty".to_owned()
        );
        assert_eq!(
            walk_path(
                &vec![&number_context as &dyn ContextElement],
                &vec!["tiger"]
            )
            .unwrap()
            .render(&Vec::new())
            .unwrap(),
            "3".to_owned()
        );
        assert_eq!(
            walk_path(
                &vec![&deep_context as &dyn ContextElement],
                &vec!["tiger", "food"]
            )
            .unwrap()
            .render(&Vec::new())
            .unwrap(),
            "people".to_owned()
        );
    }
}