//! Render a decision tree as a Unicode box-drawing string.
//!
//! Only accepts `String` associated values. Map your input collection first if
//! you need another value type. The output is meant for tests and debugging.

use crate::ast::{DecisionTreeNode, MatcherNode, VariantNode};

type Template = [[&'static str; 2]; 2];

const THIN_LINES: Template = [["├─", "│ "], ["└─", "  "]];
const HEAVY_LINES: Template = [["┠─", "┃ "], ["┖─", "  "]];
const DOUBLE_LINES: Template = [["╟─", "║ "], ["╙─", "  "]];

/// Render the root node array as a string.
///
/// The output starts with the root marker and a newline, then the tree.
pub fn treeify(nodes: &[DecisionTreeNode<String>]) -> String {
    format!("▽\n{}", treeify_array(&nodes_to_any(nodes), THIN_LINES))
}

/// A node, variant, or matcher, so one renderer can walk all three.
enum AnyNode<'a> {
    Node(&'a DecisionTreeNode<String>),
    Variant(&'a VariantNode<String>),
    Matcher(&'a MatcherNode<String>),
}

fn nodes_to_any(nodes: &[DecisionTreeNode<String>]) -> Vec<AnyNode<'_>> {
    nodes.iter().map(AnyNode::Node).collect()
}

fn treeify_array(nodes: &[AnyNode<'_>], tpl: Template) -> String {
    let items: Vec<String> = nodes.iter().map(treeify_node).collect();
    prefix_items(tpl, &items)
}

fn treeify_node(node: &AnyNode<'_>) -> String {
    match node {
        AnyNode::Node(n) => treeify_tree_node(n),
        AnyNode::Variant(v) => {
            let any: Vec<AnyNode<'_>> = v.cont.iter().map(AnyNode::Node).collect();
            format!("◇ = {}\n{}", v.value, treeify_array(&any, HEAVY_LINES))
        }
        AnyNode::Matcher(m) => {
            let modifier = m.modifier.map(|x| x.as_str()).unwrap_or("");
            let any: Vec<AnyNode<'_>> = m.cont.iter().map(AnyNode::Node).collect();
            format!(
                "◈ {} \"{}\"{}\n{}",
                m.matcher.as_str(),
                m.value,
                modifier,
                treeify_array(&any, HEAVY_LINES)
            )
        }
    }
}

fn treeify_tree_node(node: &DecisionTreeNode<String>) -> String {
    match node {
        DecisionTreeNode::Terminal { value_container } => {
            format!(
                "◁ #{} {} {}",
                value_container.index,
                stringify_specificity(value_container.specificity),
                value_container.value
            )
        }
        DecisionTreeNode::TagName { variants } => {
            let any: Vec<AnyNode<'_>> = variants.iter().map(AnyNode::Variant).collect();
            format!("◻ Tag name\n{}", treeify_array(&any, DOUBLE_LINES))
        }
        DecisionTreeNode::AttrValue { name, matchers } => {
            let any: Vec<AnyNode<'_>> = matchers.iter().map(AnyNode::Matcher).collect();
            format!(
                "▣ Attr value: {}\n{}",
                name,
                treeify_array(&any, DOUBLE_LINES)
            )
        }
        DecisionTreeNode::AttrPresence { name, cont } => {
            let any: Vec<AnyNode<'_>> = cont.iter().map(AnyNode::Node).collect();
            format!(
                "◨ Attr presence: {}\n{}",
                name,
                treeify_array(&any, HEAVY_LINES)
            )
        }
        DecisionTreeNode::PseudoClass { name, cont } => {
            let any: Vec<AnyNode<'_>> = cont.iter().map(AnyNode::Node).collect();
            format!(
                "◪ Pseudo-class: {}\n{}",
                name,
                treeify_array(&any, HEAVY_LINES)
            )
        }
        DecisionTreeNode::PushElement { combinator, cont } => {
            let any: Vec<AnyNode<'_>> = cont.iter().map(AnyNode::Node).collect();
            format!(
                "◉ Push element: {}\n{}",
                combinator,
                treeify_array(&any, THIN_LINES)
            )
        }
        DecisionTreeNode::PopElement { cont } => {
            let any: Vec<AnyNode<'_>> = cont.iter().map(AnyNode::Node).collect();
            format!("◌ Pop element\n{}", treeify_array(&any, THIN_LINES))
        }
    }
}

/// Format a specificity like JSON: `[0,1,0]` with no spaces.
fn stringify_specificity(s: [u32; 3]) -> String {
    format!("[{},{},{}]", s[0], s[1], s[2])
}

fn prefix_items(tpl: Template, items: &[String]) -> String {
    let len = items.len();
    let mut out = Vec::with_capacity(len);
    for (i, item) in items.iter().enumerate() {
        out.push(prefix_item(tpl, item, i == len - 1));
    }
    out.join("\n")
}

fn prefix_item(tpl: Template, item: &str, tail: bool) -> String {
    let tpl1 = if tail { tpl[1] } else { tpl[0] };
    let joined = item
        .split('\n')
        .collect::<Vec<_>>()
        .join(&format!("\n{}", tpl1[1]));
    format!("{}{}", tpl1[0], joined)
}
