// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html

use crate::err::{AnkiError, Result, TemplateError};
use crate::i18n::{tr_args, tr_strs, I18n, TR};
use crate::{cloze::add_cloze_numbers_in_string, template_filters::apply_filters};
use lazy_static::lazy_static;
use nom::branch::alt;
use nom::bytes::complete::{tag, take_until};
use nom::{
    combinator::{map, rest, verify},
    sequence::delimited,
};
use regex::Regex;
use std::collections::{HashMap, HashSet};
use std::fmt::Write;
use std::{borrow::Cow, iter};

pub type FieldMap<'a> = HashMap<&'a str, u16>;
type TemplateResult<T> = std::result::Result<T, TemplateError>;

static TEMPLATE_ERROR_LINK: &str =
    "https://anki.tenderapp.com/kb/problems/card-template-has-a-problem";
static TEMPLATE_BLANK_LINK: &str =
    "https://anki.tenderapp.com/kb/card-appearance/the-front-of-this-card-is-blank";
static TEMPLATE_BLANK_CLOZE_LINK: &str =
    "https://anki.tenderapp.com/kb/problems/no-cloze-found-on-card";

// Lexing
//----------------------------------------

#[derive(Debug)]
pub enum Token<'a> {
    Text(&'a str),
    Replacement(&'a str),
    OpenConditional(&'a str),
    OpenNegated(&'a str),
    CloseConditional(&'a str),
}

/// text outside handlebars
fn text_token(s: &str) -> nom::IResult<&str, Token> {
    map(
        verify(alt((take_until("{{"), rest)), |out: &str| !out.is_empty()),
        Token::Text,
    )(s)
}

/// text wrapped in handlebars
fn handlebar_token(s: &str) -> nom::IResult<&str, Token> {
    map(delimited(tag("{{"), take_until("}}"), tag("}}")), |out| {
        classify_handle(out)
    })(s)
}

fn next_token(input: &str) -> nom::IResult<&str, Token> {
    alt((handlebar_token, text_token))(input)
}

fn tokens<'a>(template: &'a str) -> Box<dyn Iterator<Item = TemplateResult<Token>> + 'a> {
    if template.trim_start().starts_with(ALT_HANDLEBAR_DIRECTIVE) {
        Box::new(legacy_tokens(
            template
                .trim_start()
                .trim_start_matches(ALT_HANDLEBAR_DIRECTIVE),
        ))
    } else {
        Box::new(new_tokens(template))
    }
}

fn new_tokens(mut data: &str) -> impl Iterator<Item = TemplateResult<Token>> {
    std::iter::from_fn(move || {
        if data.is_empty() {
            return None;
        }
        match next_token(data) {
            Ok((i, o)) => {
                data = i;
                Some(Ok(o))
            }
            Err(_e) => Some(Err(TemplateError::NoClosingBrackets(data.to_string()))),
        }
    })
}

/// classify handle based on leading character
fn classify_handle(s: &str) -> Token {
    let start = s.trim_start_matches('{').trim();
    if start.len() < 2 {
        return Token::Replacement(start);
    }
    if let Some(stripped) = start.strip_prefix('#') {
        Token::OpenConditional(stripped.trim_start())
    } else if let Some(stripped) = start.strip_prefix('/') {
        Token::CloseConditional(stripped.trim_start())
    } else if let Some(stripped) = start.strip_prefix('^') {
        Token::OpenNegated(stripped.trim_start())
    } else {
        Token::Replacement(start)
    }
}

// Legacy support
//----------------------------------------

static ALT_HANDLEBAR_DIRECTIVE: &str = "{{=<% %>=}}";

fn legacy_text_token(s: &str) -> nom::IResult<&str, Token> {
    if s.is_empty() {
        return Err(nom::Err::Error(nom::error::make_error(
            s,
            nom::error::ErrorKind::TakeUntil,
        )));
    }
    // if we locate a starting normal or alternate handlebar, use
    // whichever one we found first
    let normal_result: nom::IResult<&str, &str> = take_until("{{")(s);
    let (normal_remaining, normal_span) = normal_result.unwrap_or(("", s));
    let alt_result: nom::IResult<&str, &str> = take_until("<%")(s);
    let (alt_remaining, alt_span) = alt_result.unwrap_or(("", s));
    match (normal_span.len(), alt_span.len()) {
        (0, 0) => {
            // neither handlebar kind found
            map(rest, Token::Text)(s)
        }
        (n, a) => {
            if n < a {
                Ok((normal_remaining, Token::Text(normal_span)))
            } else {
                Ok((alt_remaining, Token::Text(alt_span)))
            }
        }
    }
}

fn legacy_next_token(input: &str) -> nom::IResult<&str, Token> {
    alt((
        handlebar_token,
        alternate_handlebar_token,
        legacy_text_token,
    ))(input)
}

/// text wrapped in <% %>
fn alternate_handlebar_token(s: &str) -> nom::IResult<&str, Token> {
    map(delimited(tag("<%"), take_until("%>"), tag("%>")), |out| {
        classify_handle(out)
    })(s)
}

fn legacy_tokens(mut data: &str) -> impl Iterator<Item = TemplateResult<Token>> {
    std::iter::from_fn(move || {
        if data.is_empty() {
            return None;
        }
        match legacy_next_token(data) {
            Ok((i, o)) => {
                data = i;
                Some(Ok(o))
            }
            Err(_e) => Some(Err(TemplateError::NoClosingBrackets(data.to_string()))),
        }
    })
}

// Parsing
//----------------------------------------

#[derive(Debug, PartialEq)]
enum ParsedNode {
    Text(String),
    Replacement {
        key: String,
        filters: Vec<String>,
    },
    Conditional {
        key: String,
        children: Vec<ParsedNode>,
    },
    NegatedConditional {
        key: String,
        children: Vec<ParsedNode>,
    },
}

#[derive(Debug)]
pub struct ParsedTemplate(Vec<ParsedNode>);

impl ParsedTemplate {
    /// Create a template from the provided text.
    pub fn from_text(template: &str) -> TemplateResult<ParsedTemplate> {
        let mut iter = tokens(template);
        Ok(Self(parse_inner(&mut iter, None)?))
    }
}

fn parse_inner<'a, I: Iterator<Item = TemplateResult<Token<'a>>>>(
    iter: &mut I,
    open_tag: Option<&'a str>,
) -> TemplateResult<Vec<ParsedNode>> {
    let mut nodes = vec![];

    while let Some(token) = iter.next() {
        use Token::*;
        nodes.push(match token? {
            Text(t) => ParsedNode::Text(t.into()),
            Replacement(t) => {
                let mut it = t.rsplit(':');
                ParsedNode::Replacement {
                    key: it.next().unwrap().into(),
                    filters: it.map(Into::into).collect(),
                }
            }
            OpenConditional(t) => ParsedNode::Conditional {
                key: t.into(),
                children: parse_inner(iter, Some(t))?,
            },
            OpenNegated(t) => ParsedNode::NegatedConditional {
                key: t.into(),
                children: parse_inner(iter, Some(t))?,
            },
            CloseConditional(t) => {
                let currently_open = if let Some(open) = open_tag {
                    if open == t {
                        // matching closing tag, move back to parent
                        return Ok(nodes);
                    } else {
                        Some(open.to_string())
                    }
                } else {
                    None
                };
                return Err(TemplateError::ConditionalNotOpen {
                    closed: t.to_string(),
                    currently_open,
                });
            }
        });
    }

    if let Some(open) = open_tag {
        Err(TemplateError::ConditionalNotClosed(open.to_string()))
    } else {
        Ok(nodes)
    }
}

fn template_error_to_anki_error(err: TemplateError, q_side: bool, i18n: &I18n) -> AnkiError {
    let header = i18n.tr(if q_side {
        TR::CardTemplateRenderingFrontSideProblem
    } else {
        TR::CardTemplateRenderingBackSideProblem
    });
    let details = localized_template_error(i18n, err);
    let more_info = i18n.tr(TR::CardTemplateRenderingMoreInfo);
    let info = format!(
        "{}<br>{}<br><a href='{}'>{}</a>",
        header, details, TEMPLATE_ERROR_LINK, more_info
    );

    AnkiError::TemplateError { info }
}

fn localized_template_error(i18n: &I18n, err: TemplateError) -> String {
    match err {
        TemplateError::NoClosingBrackets(tag) => i18n.trn(
            TR::CardTemplateRenderingNoClosingBrackets,
            tr_strs!("tag"=>tag, "missing"=>"}}"),
        ),
        TemplateError::ConditionalNotClosed(tag) => i18n.trn(
            TR::CardTemplateRenderingConditionalNotClosed,
            tr_strs!("missing"=>format!("{{{{/{}}}}}", tag)),
        ),
        TemplateError::ConditionalNotOpen {
            closed,
            currently_open,
        } => {
            if let Some(open) = currently_open {
                i18n.trn(
                    TR::CardTemplateRenderingWrongConditionalClosed,
                    tr_strs!(
                "found"=>format!("{{{{/{}}}}}", closed),
                "expected"=>format!("{{{{/{}}}}}", open)),
                )
            } else {
                i18n.trn(
                    TR::CardTemplateRenderingConditionalNotOpen,
                    tr_strs!(
                    "found"=>format!("{{{{/{}}}}}", closed),
                    "missing1"=>format!("{{{{#{}}}}}", closed),
                    "missing2"=>format!("{{{{^{}}}}}", closed)
                    ),
                )
            }
        }
        TemplateError::FieldNotFound { field, filters } => i18n.trn(
            TR::CardTemplateRenderingNoSuchField,
            tr_strs!(
            "found"=>format!("{{{{{}{}}}}}", filters, field),
            "field"=>field),
        ),
    }
}

// Checking if template is empty
//----------------------------------------

impl ParsedTemplate {
    /// true if provided fields are sufficient to render the template
    pub fn renders_with_fields(&self, nonempty_fields: &HashSet<&str>) -> bool {
        !template_is_empty(nonempty_fields, &self.0, true)
    }

    pub fn renders_with_fields_for_reqs(&self, nonempty_fields: &HashSet<&str>) -> bool {
        !template_is_empty(nonempty_fields, &self.0, false)
    }
}

/// If check_negated is false, negated conditionals resolve to their children, even
/// if the referenced key is non-empty. This allows the legacy required field cache to
/// generate results closer to older Anki versions.
fn template_is_empty(
    nonempty_fields: &HashSet<&str>,
    nodes: &[ParsedNode],
    check_negated: bool,
) -> bool {
    use ParsedNode::*;
    for node in nodes {
        match node {
            // ignore normal text
            Text(_) => (),
            Replacement { key, .. } => {
                if nonempty_fields.contains(key.as_str()) {
                    // a single replacement is enough
                    return false;
                }
            }
            Conditional { key, children } => {
                if !nonempty_fields.contains(key.as_str()) {
                    continue;
                }
                if !template_is_empty(nonempty_fields, children, check_negated) {
                    return false;
                }
            }
            NegatedConditional { key, children } => {
                if check_negated && nonempty_fields.contains(key.as_str()) {
                    continue;
                }

                if !template_is_empty(nonempty_fields, children, check_negated) {
                    return false;
                }
            }
        }
    }

    true
}

// Rendering
//----------------------------------------

#[derive(Debug, PartialEq)]
pub enum RenderedNode {
    Text {
        text: String,
    },
    Replacement {
        field_name: String,
        current_text: String,
        /// Filters are in the order they should be applied.
        filters: Vec<String>,
    },
}

pub(crate) struct RenderContext<'a> {
    pub fields: &'a HashMap<&'a str, Cow<'a, str>>,
    pub nonempty_fields: &'a HashSet<&'a str>,
    pub question_side: bool,
    pub card_ord: u16,
}

impl ParsedTemplate {
    /// Render the template with the provided fields.
    ///
    /// Replacements that use only standard filters will become part of
    /// a text node. If a non-standard filter is encountered, a partially
    /// rendered Replacement is returned for the calling code to complete.
    fn render(&self, context: &RenderContext) -> TemplateResult<Vec<RenderedNode>> {
        let mut rendered = vec![];

        render_into(&mut rendered, self.0.as_ref(), context)?;

        Ok(rendered)
    }
}

fn render_into(
    rendered_nodes: &mut Vec<RenderedNode>,
    nodes: &[ParsedNode],
    context: &RenderContext,
) -> TemplateResult<()> {
    use ParsedNode::*;
    for node in nodes {
        match node {
            Text(text) => {
                append_str_to_nodes(rendered_nodes, text);
            }
            Replacement { key, .. } if key == "FrontSide" => {
                // defer FrontSide rendering to Python, as extra
                // filters may be required
                rendered_nodes.push(RenderedNode::Replacement {
                    field_name: (*key).to_string(),
                    filters: vec![],
                    current_text: "".into(),
                });
            }
            Replacement { key, filters } if key == "" && !filters.is_empty() => {
                // if a filter is provided, we accept an empty field name to
                // mean 'pass an empty string to the filter, and it will add
                // its own text'
                rendered_nodes.push(RenderedNode::Replacement {
                    field_name: "".to_string(),
                    current_text: "".to_string(),
                    filters: filters.clone(),
                })
            }
            Replacement { key, filters } => {
                // apply built in filters if field exists
                let (text, remaining_filters) = match context.fields.get(key.as_str()) {
                    Some(text) => apply_filters(
                        text,
                        filters
                            .iter()
                            .map(|s| s.as_str())
                            .collect::<Vec<_>>()
                            .as_slice(),
                        key,
                        context,
                    ),
                    None => {
                        // unknown field encountered
                        let filters_str = filters
                            .iter()
                            .rev()
                            .cloned()
                            .chain(iter::once("".into()))
                            .collect::<Vec<_>>()
                            .join(":");
                        return Err(TemplateError::FieldNotFound {
                            field: (*key).to_string(),
                            filters: filters_str,
                        });
                    }
                };

                // fully processed?
                if remaining_filters.is_empty() {
                    append_str_to_nodes(rendered_nodes, text.as_ref())
                } else {
                    rendered_nodes.push(RenderedNode::Replacement {
                        field_name: (*key).to_string(),
                        filters: remaining_filters,
                        current_text: text.into(),
                    });
                }
            }
            Conditional { key, children } => {
                if context.nonempty_fields.contains(key.as_str()) {
                    render_into(rendered_nodes, children.as_ref(), context)?;
                }
            }
            NegatedConditional { key, children } => {
                if !context.nonempty_fields.contains(key.as_str()) {
                    render_into(rendered_nodes, children.as_ref(), context)?;
                }
            }
        };
    }

    Ok(())
}

/// Append to last node if last node is a string, else add new node.
fn append_str_to_nodes(nodes: &mut Vec<RenderedNode>, text: &str) {
    if let Some(RenderedNode::Text {
        text: ref mut existing_text,
    }) = nodes.last_mut()
    {
        // append to existing last node
        existing_text.push_str(text)
    } else {
        // otherwise, add a new string node
        nodes.push(RenderedNode::Text {
            text: text.to_string(),
        })
    }
}

/// True if provided text contains only whitespace and/or empty BR/DIV tags.
pub(crate) fn field_is_empty(text: &str) -> bool {
    lazy_static! {
        static ref RE: Regex = Regex::new(
            r#"(?xsi)
            ^(?:
            [[:space:]]
            |
            </?(?:br|div)\ ?/?>
            )*$
        "#
        )
        .unwrap();
    }
    RE.is_match(text)
}

fn nonempty_fields<'a, R>(fields: &'a HashMap<&str, R>) -> HashSet<&'a str>
where
    R: AsRef<str>,
{
    fields
        .iter()
        .filter_map(|(name, val)| {
            if !field_is_empty(val.as_ref()) {
                Some(*name)
            } else {
                None
            }
        })
        .collect()
}

// Rendering both sides
//----------------------------------------

#[allow(clippy::implicit_hasher)]
pub fn render_card(
    qfmt: &str,
    afmt: &str,
    field_map: &HashMap<&str, Cow<str>>,
    card_ord: u16,
    is_cloze: bool,
    i18n: &I18n,
) -> Result<(Vec<RenderedNode>, Vec<RenderedNode>)> {
    // prepare context
    let mut context = RenderContext {
        fields: field_map,
        nonempty_fields: &nonempty_fields(field_map),
        question_side: true,
        card_ord,
    };

    // question side
    let (mut qnodes, qtmpl) = ParsedTemplate::from_text(qfmt)
        .and_then(|tmpl| Ok((tmpl.render(&context)?, tmpl)))
        .map_err(|e| template_error_to_anki_error(e, true, i18n))?;

    // check if the front side was empty
    if is_cloze {
        if cloze_is_empty(field_map, card_ord) {
            let info = format!(
                "<div>{}<br><a href='{}'>{}</a></div>",
                i18n.trn(
                    TR::CardTemplateRenderingMissingCloze,
                    tr_args!["number"=>card_ord+1]
                ),
                TEMPLATE_BLANK_CLOZE_LINK,
                i18n.tr(TR::CardTemplateRenderingMoreInfo)
            );
            qnodes.push(RenderedNode::Text { text: info });
        }
    } else if !qtmpl.renders_with_fields(context.nonempty_fields) {
        let info = format!(
            "<div>{}<br><a href='{}'>{}</a></div>",
            i18n.tr(TR::CardTemplateRenderingEmptyFront),
            TEMPLATE_BLANK_LINK,
            i18n.tr(TR::CardTemplateRenderingMoreInfo)
        );
        qnodes.push(RenderedNode::Text { text: info });
    }

    // answer side
    context.question_side = false;
    let anodes = ParsedTemplate::from_text(afmt)
        .and_then(|tmpl| tmpl.render(&context))
        .map_err(|e| template_error_to_anki_error(e, false, i18n))?;

    Ok((qnodes, anodes))
}

fn cloze_is_empty(field_map: &HashMap<&str, Cow<str>>, card_ord: u16) -> bool {
    let mut set = HashSet::with_capacity(4);
    for field in field_map.values() {
        add_cloze_numbers_in_string(field.as_ref(), &mut set);
    }
    !set.contains(&(card_ord + 1))
}

// Field requirements
//----------------------------------------

#[derive(Debug, Clone, PartialEq)]
pub enum FieldRequirements {
    Any(HashSet<u16>),
    All(HashSet<u16>),
    None,
}

impl ParsedTemplate {
    /// Return fields required by template.
    ///
    /// This is not able to represent negated expressions or combinations of
    /// Any and All, but is compatible with older Anki clients.
    ///
    /// In the future, it may be feasible to calculate the requirements
    /// when adding cards, instead of caching them up front, which would mean
    /// the above restrictions could be lifted. We would probably
    /// want to add a cache of non-zero fields -> available cards to avoid
    /// slowing down bulk operations like importing too much.
    pub fn requirements(&self, field_map: &FieldMap) -> FieldRequirements {
        let mut nonempty: HashSet<_> = Default::default();
        let mut ords = HashSet::new();
        for (name, ord) in field_map {
            nonempty.clear();
            nonempty.insert(*name);
            if self.renders_with_fields_for_reqs(&nonempty) {
                ords.insert(*ord);
            }
        }
        if !ords.is_empty() {
            return FieldRequirements::Any(ords);
        }

        nonempty.extend(field_map.keys());
        ords.extend(field_map.values().copied());
        for (name, ord) in field_map {
            // can we remove this field and still render?
            nonempty.remove(name);
            if self.renders_with_fields_for_reqs(&nonempty) {
                ords.remove(ord);
            }
            nonempty.insert(*name);
        }
        if !ords.is_empty() && self.renders_with_fields_for_reqs(&nonempty) {
            FieldRequirements::All(ords)
        } else {
            FieldRequirements::None
        }
    }
}

// Renaming & deleting fields
//----------------------------------------

impl ParsedTemplate {
    /// Given a map of old to new field names, update references to the new names.
    /// Returns true if any changes made.
    pub(crate) fn rename_and_remove_fields(
        self,
        fields: &HashMap<String, Option<String>>,
    ) -> ParsedTemplate {
        let out = rename_and_remove_fields(self.0, fields);
        ParsedTemplate(out)
    }
}

fn rename_and_remove_fields(
    nodes: Vec<ParsedNode>,
    fields: &HashMap<String, Option<String>>,
) -> Vec<ParsedNode> {
    let mut out = vec![];
    for node in nodes {
        match node {
            ParsedNode::Text(text) => out.push(ParsedNode::Text(text)),
            ParsedNode::Replacement { key, filters } => {
                match fields.get(&key) {
                    // delete the field
                    Some(None) => (),
                    // rename it
                    Some(Some(new_name)) => out.push(ParsedNode::Replacement {
                        key: new_name.into(),
                        filters,
                    }),
                    // or leave it alone
                    None => out.push(ParsedNode::Replacement { key, filters }),
                }
            }
            ParsedNode::Conditional { key, children } => {
                let children = rename_and_remove_fields(children, fields);
                match fields.get(&key) {
                    // remove the field, preserving children
                    Some(None) => out.extend(children),
                    // rename it
                    Some(Some(new_name)) => out.push(ParsedNode::Conditional {
                        key: new_name.into(),
                        children,
                    }),
                    // or leave it alone
                    None => out.push(ParsedNode::Conditional { key, children }),
                }
            }
            ParsedNode::NegatedConditional { key, children } => {
                let children = rename_and_remove_fields(children, fields);
                match fields.get(&key) {
                    // remove the field, preserving children
                    Some(None) => out.extend(children),
                    // rename it
                    Some(Some(new_name)) => out.push(ParsedNode::NegatedConditional {
                        key: new_name.into(),
                        children,
                    }),
                    // or leave it alone
                    None => out.push(ParsedNode::NegatedConditional { key, children }),
                }
            }
        }
    }
    out
}

// Writing back to a string
//----------------------------------------

impl ParsedTemplate {
    pub(crate) fn template_to_string(&self) -> String {
        let mut buf = String::new();
        nodes_to_string(&mut buf, &self.0);
        buf
    }
}

fn nodes_to_string(buf: &mut String, nodes: &[ParsedNode]) {
    for node in nodes {
        match node {
            ParsedNode::Text(text) => buf.push_str(text),
            ParsedNode::Replacement { key, filters } => {
                write!(
                    buf,
                    "{{{{{}}}}}",
                    filters
                        .iter()
                        .rev()
                        .chain(iter::once(key))
                        .map(|s| s.to_string())
                        .collect::<Vec<_>>()
                        .join(":")
                )
                .unwrap();
            }
            ParsedNode::Conditional { key, children } => {
                write!(buf, "{{{{#{}}}}}", key).unwrap();
                nodes_to_string(buf, &children);
                write!(buf, "{{{{/{}}}}}", key).unwrap();
            }
            ParsedNode::NegatedConditional { key, children } => {
                write!(buf, "{{{{^{}}}}}", key).unwrap();
                nodes_to_string(buf, &children);
                write!(buf, "{{{{/{}}}}}", key).unwrap();
            }
        }
    }
}

// Detecting cloze fields
//----------------------------------------

impl ParsedTemplate {
    /// A set of field names with a cloze filter attached.
    /// Field names may not be valid.
    pub(crate) fn cloze_fields(&self) -> HashSet<&str> {
        let mut set = HashSet::new();
        find_fields_with_filter(&self.0, &mut set, "cloze");
        set
    }
}

fn find_fields_with_filter<'a>(
    nodes: &'a [ParsedNode],
    fields: &mut HashSet<&'a str>,
    filter: &str,
) {
    for node in nodes {
        match node {
            ParsedNode::Text(_) => {}
            ParsedNode::Replacement { key, filters } => {
                if filters.iter().any(|f| f == filter) {
                    fields.insert(key);
                }
            }
            ParsedNode::Conditional { children, .. } => {
                find_fields_with_filter(&children, fields, filter);
            }
            ParsedNode::NegatedConditional { children, .. } => {
                find_fields_with_filter(&children, fields, filter);
            }
        }
    }
}

// Tests
//---------------------------------------

#[cfg(test)]
mod test {
    use super::{FieldMap, ParsedNode::*, ParsedTemplate as PT};
    use crate::err::TemplateError;
    use crate::{
        i18n::I18n,
        log,
        template::{field_is_empty, nonempty_fields, FieldRequirements, RenderContext},
    };
    use std::collections::{HashMap, HashSet};
    use std::iter::FromIterator;

    #[test]
    fn field_empty() {
        assert_eq!(field_is_empty(""), true);
        assert_eq!(field_is_empty(" "), true);
        assert_eq!(field_is_empty("x"), false);
        assert_eq!(field_is_empty("<BR>"), true);
        assert_eq!(field_is_empty("<div />"), true);
        assert_eq!(field_is_empty(" <div> <br> </div>\n"), true);
        assert_eq!(field_is_empty(" <div>x</div>\n"), false);
    }

    #[test]
    fn parsing() {
        let orig = "foo {{bar}} {{#baz}} quux {{/baz}}";
        let tmpl = PT::from_text(orig).unwrap();
        assert_eq!(
            tmpl.0,
            vec![
                Text("foo ".into()),
                Replacement {
                    key: "bar".into(),
                    filters: vec![]
                },
                Text(" ".into()),
                Conditional {
                    key: "baz".into(),
                    children: vec![Text(" quux ".into())]
                }
            ]
        );
        assert_eq!(orig, &tmpl.template_to_string());

        let tmpl = PT::from_text("{{^baz}}{{/baz}}").unwrap();
        assert_eq!(
            tmpl.0,
            vec![NegatedConditional {
                key: "baz".into(),
                children: vec![]
            }]
        );

        PT::from_text("{{#mis}}{{/matched}}").unwrap_err();
        PT::from_text("{{/matched}}").unwrap_err();
        PT::from_text("{{#mis}}").unwrap_err();

        // whitespace
        assert_eq!(
            PT::from_text("{{ tag }}").unwrap().0,
            vec![Replacement {
                key: "tag".into(),
                filters: vec![]
            }]
        );

        // stray closing characters (like in javascript) are ignored
        assert_eq!(
            PT::from_text("text }} more").unwrap().0,
            vec![Text("text }} more".into())]
        );

        PT::from_text("{{").unwrap_err();
        PT::from_text(" {{").unwrap_err();
        PT::from_text(" {{ ").unwrap_err();

        // make sure filters and so on are round-tripped correctly
        let orig = "foo {{one:two}} {{one:two:three}} {{^baz}} {{/baz}} {{foo:}}";
        let tmpl = PT::from_text(orig).unwrap();
        assert_eq!(orig, &tmpl.template_to_string());
    }

    #[test]
    fn nonempty() {
        let fields = HashSet::from_iter(vec!["1", "3"].into_iter());
        let mut tmpl = PT::from_text("{{2}}{{1}}").unwrap();
        assert_eq!(tmpl.renders_with_fields(&fields), true);
        tmpl = PT::from_text("{{2}}").unwrap();
        assert_eq!(tmpl.renders_with_fields(&fields), false);
        tmpl = PT::from_text("{{2}}{{4}}").unwrap();
        assert_eq!(tmpl.renders_with_fields(&fields), false);
        tmpl = PT::from_text("{{#3}}{{^2}}{{1}}{{/2}}{{/3}}").unwrap();
        assert_eq!(tmpl.renders_with_fields(&fields), true);

        tmpl = PT::from_text("{{^1}}{{3}}{{/1}}").unwrap();
        assert_eq!(tmpl.renders_with_fields(&fields), false);
        assert_eq!(tmpl.renders_with_fields_for_reqs(&fields), true);
    }

    #[test]
    fn requirements() {
        let field_map: FieldMap = vec!["a", "b", "c"]
            .iter()
            .enumerate()
            .map(|(a, b)| (*b, a as u16))
            .collect();

        let mut tmpl = PT::from_text("{{a}}{{b}}").unwrap();
        assert_eq!(
            tmpl.requirements(&field_map),
            FieldRequirements::Any(HashSet::from_iter(vec![0, 1].into_iter()))
        );

        tmpl = PT::from_text("{{#a}}{{b}}{{/a}}").unwrap();
        assert_eq!(
            tmpl.requirements(&field_map),
            FieldRequirements::All(HashSet::from_iter(vec![0, 1].into_iter()))
        );

        tmpl = PT::from_text("{{z}}").unwrap();
        assert_eq!(tmpl.requirements(&field_map), FieldRequirements::None);

        tmpl = PT::from_text("{{^a}}{{b}}{{/a}}").unwrap();
        assert_eq!(
            tmpl.requirements(&field_map),
            FieldRequirements::Any(HashSet::from_iter(vec![1].into_iter()))
        );

        tmpl = PT::from_text("{{^a}}{{#b}}{{c}}{{/b}}{{/a}}").unwrap();
        assert_eq!(
            tmpl.requirements(&field_map),
            FieldRequirements::All(HashSet::from_iter(vec![1, 2].into_iter()))
        );

        tmpl = PT::from_text("{{#a}}{{#b}}{{a}}{{/b}}{{/a}}").unwrap();
        assert_eq!(
            tmpl.requirements(&field_map),
            FieldRequirements::All(HashSet::from_iter(vec![0, 1].into_iter()))
        );

        tmpl = PT::from_text(
            r#"
{{^a}}
    {{b}}
{{/a}}

{{#a}}
    {{a}}
    {{b}}
{{/a}}
"#,
        )
        .unwrap();

        assert_eq!(
            tmpl.requirements(&field_map),
            FieldRequirements::Any(HashSet::from_iter(vec![0, 1].into_iter()))
        );
    }

    #[test]
    fn alt_syntax() {
        let input = "
{{=<% %>=}}
<%Front%>
<% #Back %>
<%/Back%>";
        assert_eq!(
            PT::from_text(input).unwrap().0,
            vec![
                Text("\n".into()),
                Replacement {
                    key: "Front".into(),
                    filters: vec![]
                },
                Text("\n".into()),
                Conditional {
                    key: "Back".into(),
                    children: vec![Text("\n".into())]
                }
            ]
        );
        let input = "
{{=<% %>=}}
{{#foo}}
<%Front%>
{{/foo}}
";
        assert_eq!(
            PT::from_text(input).unwrap().0,
            vec![
                Text("\n".into()),
                Conditional {
                    key: "foo".into(),
                    children: vec![
                        Text("\n".into()),
                        Replacement {
                            key: "Front".into(),
                            filters: vec![]
                        },
                        Text("\n".into())
                    ]
                },
                Text("\n".into())
            ]
        );
    }

    #[test]
    fn render_single() {
        let map: HashMap<_, _> = vec![("F", "f"), ("B", "b"), ("E", " ")]
            .into_iter()
            .map(|r| (r.0, r.1.into()))
            .collect();

        let ctx = RenderContext {
            fields: &map,
            nonempty_fields: &nonempty_fields(&map),
            question_side: true,
            card_ord: 1,
        };

        use crate::template::RenderedNode as FN;
        let mut tmpl = PT::from_text("{{B}}A{{F}}").unwrap();
        assert_eq!(
            tmpl.render(&ctx).unwrap(),
            vec![FN::Text {
                text: "bAf".to_owned()
            },]
        );

        // empty
        tmpl = PT::from_text("{{#E}}A{{/E}}").unwrap();
        assert_eq!(tmpl.render(&ctx).unwrap(), vec![]);

        // missing
        tmpl = PT::from_text("{{^M}}A{{/M}}").unwrap();
        assert_eq!(
            tmpl.render(&ctx).unwrap(),
            vec![FN::Text {
                text: "A".to_owned()
            },]
        );

        // nested
        tmpl = PT::from_text("{{^E}}1{{#F}}2{{#B}}{{F}}{{/B}}{{/F}}{{/E}}").unwrap();
        assert_eq!(
            tmpl.render(&ctx).unwrap(),
            vec![FN::Text {
                text: "12f".to_owned()
            },]
        );

        // unknown filters
        tmpl = PT::from_text("{{one:two:B}}").unwrap();
        assert_eq!(
            tmpl.render(&ctx).unwrap(),
            vec![FN::Replacement {
                field_name: "B".to_owned(),
                filters: vec!["two".to_string(), "one".to_string()],
                current_text: "b".to_owned()
            },]
        );

        // partially unknown filters
        // excess colons are ignored
        tmpl = PT::from_text("{{one::text:B}}").unwrap();
        assert_eq!(
            tmpl.render(&ctx).unwrap(),
            vec![FN::Replacement {
                field_name: "B".to_owned(),
                filters: vec!["one".to_string()],
                current_text: "b".to_owned()
            },]
        );

        // known filter
        tmpl = PT::from_text("{{text:B}}").unwrap();
        assert_eq!(
            tmpl.render(&ctx).unwrap(),
            vec![FN::Text {
                text: "b".to_owned()
            }]
        );

        // unknown field
        tmpl = PT::from_text("{{X}}").unwrap();
        assert_eq!(
            tmpl.render(&ctx).unwrap_err(),
            TemplateError::FieldNotFound {
                field: "X".to_owned(),
                filters: "".to_owned()
            }
        );

        // unknown field with filters
        tmpl = PT::from_text("{{foo:text:X}}").unwrap();
        assert_eq!(
            tmpl.render(&ctx).unwrap_err(),
            TemplateError::FieldNotFound {
                field: "X".to_owned(),
                filters: "foo:text:".to_owned()
            }
        );

        // a blank field is allowed if it has filters
        tmpl = PT::from_text("{{filter:}}").unwrap();
        assert_eq!(
            tmpl.render(&ctx).unwrap(),
            vec![FN::Replacement {
                field_name: "".to_string(),
                current_text: "".to_string(),
                filters: vec!["filter".to_string()]
            }]
        );
    }

    #[test]
    fn render_card() {
        let map: HashMap<_, _> = vec![("E", "")]
            .into_iter()
            .map(|r| (r.0, r.1.into()))
            .collect();

        let i18n = I18n::new(&[""], "", log::terminal());
        use crate::template::RenderedNode as FN;

        let qnodes = super::render_card("test{{E}}", "", &map, 1, false, &i18n)
            .unwrap()
            .0;
        assert_eq!(
            qnodes[0],
            FN::Text {
                text: "test".into()
            }
        );
        if let FN::Text { ref text } = qnodes[1] {
            assert!(text.contains("card is blank"));
        } else {
            unreachable!();
        }
    }
}
