//! AST types produced by the Glyf parser.
//!
//! The core type is [`Element`], which represents a single node in the
//! parsed abbreviation tree. Elements are linked together via [`Node`],
//! an enum whose variant (`Sibling` or `Children`) carries the linked
//! element and describes the relationship.

use std::{fmt::Display, iter::repeat_n, sync::LazyLock};

use regex::Regex;

use crate::{
    config::{Config, ParserMode},
    parser::{parse_input, utils::has_node_operator},
};

use super::{
    attribute::{AttributeType, parse_attribute},
    error::GlyfError,
    snippet::parse_snippet,
};

static IDENTIFIER_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^[\w-]+").unwrap());

/// The relationship between an element and the next node in the chain.
///
/// Each variant carries the linked [`Element`] directly.
#[derive(Debug, Clone)]
pub enum Node {
    /// `+` — the linked element is a sibling (same indentation level).
    Sibling(Element),
    /// `>` — the linked element is a child (indented one level deeper).
    Children(Element),
}

impl Node {
    pub fn element(&self) -> &Element {
        match self {
            Node::Sibling(el) | Node::Children(el) => el,
        }
    }
}

#[derive(Debug, Clone)]
pub enum ElementKind {
    /// A concrete HTML/JSX tag: `<div class="foo">`
    Tag {
        /// HTML/JSX tag name.
        identifier: String,
        /// When `true` the element renders as `<tag />` (no closing tag, no children).
        self_closing: bool,
        /// Parsed attributes in declaration order (sorted by [`AttributeType`] at render time).
        attributes: Vec<AttributeType>,
    },
    /// A JSX fragment: `<></>`  (the `e` abbreviation)
    Fragment,
    /// A `(...)` group or multi-element snippet expansion
    Group(Box<Element>),
    /// JSX conditional rendering: `condition->true_part` or `condition->true_part->false_part`.
    ///
    /// `->` separates the JS condition from the true element and, optionally,
    /// the true element from the false element:
    /// - `isLoading->Spinner` → `{isLoading && <Spinner />}`
    /// - `isLoading->Spinner->div.empty` → `{isLoading ? <Spinner /> : <div className="empty" />}`
    ///
    /// Only active in JSX mode. The condition is any JavaScript expression;
    /// `->` cannot appear in valid JS so it is an unambiguous separator.
    Condition {
        condition: String,
        true_part: Box<Element>,
        false_part: Option<Box<Element>>,
    },
}

/// A parsed Glyf node — the fundamental unit of the output tree.
///
/// The type of node is described by [`ElementKind`]:
///
/// | `kind` | Meaning |
/// |---|---|
/// | `ElementKind::Tag { .. }` | A concrete HTML/JSX element: `<div>` |
/// | `ElementKind::Fragment` | A JSX fragment: `<></>` (JSX mode only, `e` abbreviation) |
/// | `ElementKind::Group(_)` | A `(...)` group or multi-element snippet expansion |
/// | `ElementKind::Condition { .. }` | JSX conditional rendering (JSX mode only) |
///
/// `Display` renders the element (and the full subtree rooted here) to
/// an indented HTML/JSX string.
#[derive(Debug, Clone)]
pub struct Element {
    pub kind: ElementKind,
    /// Number of times this element is repeated (`*N` in the abbreviation).
    pub multiplier: usize,
    /// The next element in the chain (child or sibling).
    pub node: Option<Box<Node>>,
    /// Indentation depth. `None` or `Some(0)` = root level, `Some(n)` = `n` tabs.
    pub level: Option<usize>,
    pub mode: ParserMode,
}

impl Default for Element {
    fn default() -> Self {
        Self {
            kind: ElementKind::Fragment,
            multiplier: 1,
            node: None,
            level: None,
            mode: ParserMode::HTML,
        }
    }
}

impl Element {
    /// Wraps an already-parsed [`Element`] tree in a group node.
    ///
    /// Used by `parse_group` and internally when snippet expansion
    /// produces a compound expression containing `>` or `+`.
    pub(super) fn from_group(
        group: Box<Element>,
        multiplier: usize,
        node: Option<Box<Node>>,
        level: Option<usize>,
        mode: ParserMode,
    ) -> Self {
        Self {
            kind: ElementKind::Group(group),
            multiplier,
            node,
            level,
            mode,
        }
    }

    /// Parses a raw Glyf abbreviation fragment into a concrete [`Element`].
    ///
    /// Pipeline:
    /// 1. Snippet expansion.
    /// 2. If the result contains `>` or `+`, re-parses via `parse_input`
    ///    and delegates to [`Element::from_group`].
    /// 3. Otherwise extracts the identifier, self-closing flag, and attributes.
    ///
    /// # Errors
    /// Returns [`GlyfError::NoIdentifier`] when no valid tag name is found.
    pub(super) fn from_abbr(
        value: &str,
        multiplier: usize,
        node: Option<Box<Node>>,
        level: Option<usize>,
        config: &Config,
    ) -> Result<Self, GlyfError> {
        let mode = config.mode.clone();

        // JSX fragment shorthand: bare "e" → <></>
        if mode.is_jsx() && value == "e" {
            return Ok(Self {
                kind: ElementKind::Fragment,
                multiplier,
                node,
                level,
                mode,
            });
        }

        let expanded = parse_snippet(value, &config.snippets);

        // Snippet expanded to a compound expression — re-parse as a tree
        if has_node_operator(&expanded) {
            let inner = parse_input(&expanded, level, config)?;
            return Ok(Self::from_group(
                Box::new(inner),
                multiplier,
                node,
                level,
                mode,
            ));
        }

        // Concrete element: extract identifier and attributes
        let identifier = IDENTIFIER_REGEX
            .find(&expanded)
            .ok_or(GlyfError::NoIdentifier)?
            .as_str()
            .to_string();

        let self_closing = expanded.ends_with('/');
        let attr_end = if self_closing {
            expanded.len().saturating_sub(1)
        } else {
            expanded.len()
        };
        let attributes = parse_attribute(&expanded[identifier.len()..attr_end]);

        Ok(Self {
            kind: ElementKind::Tag {
                identifier,
                self_closing,
                attributes,
            },
            multiplier,
            node,
            level,
            mode,
        })
    }

    /// Builds an [`Element`] with [`ElementKind::Condition`] from pre-split parts.
    ///
    /// Called by the JSX parser after splitting `condition->true_part->false_part`
    /// into its three components. Each string part is parsed via [`parse_input`]
    /// so the full Glyf syntax is supported on both sides of the `->` separator.
    ///
    /// # Errors
    /// Propagates any [`GlyfError`] from parsing `true_str` or `false_str`.
    pub(super) fn from_condition(
        condition: &str,
        true_str: &str,
        false_str: Option<&str>,
        node: Option<Box<Node>>,
        level: Option<usize>,
        config: &Config,
    ) -> Result<Self, GlyfError> {
        Ok(Self {
            kind: ElementKind::Condition {
                condition: condition.to_string(),
                true_part: Box::new(parse_input(true_str, None, config)?),
                false_part: false_str
                    .map(|s| parse_input(s, None, config))
                    .transpose()?
                    .map(Box::new),
            },
            multiplier: 1,
            node,
            level,
            mode: config.mode.clone(),
        })
    }

    /// Converts this element tree to its Glyf abbreviation string.
    ///
    /// The inverse of constructing an element via [`crate::expand`] — produces
    /// the Glyf abbreviation that would generate equivalent HTML/JSX.
    /// Attributes are emitted in compress order: `.class` before `#id` before
    /// `:prop` before `>>text`.
    ///
    /// # Examples
    ///
    /// ```
    /// use glyf_core::expand_to_tree;
    ///
    /// let el = expand_to_tree("div.foo>p", None, None).unwrap();
    /// assert_eq!(el.to_glyf(), "div.foo>p");
    /// ```
    pub fn to_glyf(&self) -> String {
        let mut result = match &self.kind {
            ElementKind::Condition {
                condition,
                true_part,
                false_part,
            } => match false_part {
                Some(f) => format!("{}->{}->{}", condition, true_part.to_glyf(), f.to_glyf()),
                None => format!("{}->{}", condition, true_part.to_glyf()),
            },
            ElementKind::Fragment => "e".into(),
            ElementKind::Group(group) => format!("({})", group.to_glyf()),
            ElementKind::Tag {
                identifier,
                self_closing,
                attributes,
            } => {
                let mut result = identifier.clone();

                let mut sorted = attributes.iter().collect::<Vec<&AttributeType>>();
                sorted.sort();
                let glyf_attribute = sorted
                    .iter()
                    .map(|attr| attr.to_glyf())
                    .collect::<Vec<String>>();
                result.push_str(&glyf_attribute.join(""));

                self_closing.then(|| {
                    result.push('/');
                });

                result
            }
        };

        if self.multiplier > 1 {
            result.push_str(&format!("*{}", self.multiplier));
        }

        if let Some(node) = &self.node {
            result.push_str(
                match node.as_ref() {
                    Node::Sibling(el) => format!("+{}", el.to_glyf()),
                    Node::Children(el) => format!(">{}", el.to_glyf()),
                }
                .as_str(),
            );
        }

        result
    }
}

impl Display for Element {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let value;
        let is_first_level = self.level.is_none_or(|v| v == 0);
        let level = self.level.unwrap_or(0);

        let prefix = format!(
            "{}{}",
            if !is_first_level { "\n" } else { "" },
            "\t".repeat(level)
        );

        let (child, sibling) = match self.node.as_ref().map(|n| n.as_ref()) {
            Some(Node::Children(e)) => (e.to_string(), String::new()),
            Some(Node::Sibling(e)) => (String::new(), e.to_string()),
            None => (String::new(), String::new()),
        };

        let suffix = if !child.is_empty() {
            format!("\n{}", "\t".repeat(level))
        } else {
            String::new()
        };

        match &self.kind {
            ElementKind::Tag {
                identifier,
                self_closing,
                attributes,
            } => {
                let mut attributes = attributes.clone();
                attributes.sort();

                let classes = &attributes
                    .iter()
                    .filter_map(|a| match a {
                        AttributeType::Class(name) => Some(name.as_str()),
                        _ => None,
                    })
                    .collect::<Vec<&str>>()
                    .join(" ");

                let props_attributes = &attributes
                    .iter()
                    .filter_map(|a| {
                        if matches!(a, AttributeType::Id(_) | AttributeType::Props(_, _)) {
                            Some(a.render(&self.mode))
                        } else {
                            None
                        }
                    })
                    .collect::<Vec<String>>()
                    .join("");

                let text_attribute = &attributes
                    .iter()
                    .find_map(|a| {
                        if matches!(a, AttributeType::Text(_)) {
                            Some(a.render(&self.mode))
                        } else {
                            None
                        }
                    })
                    .unwrap_or(String::new());

                let class_attribute = if !classes.is_empty() {
                    match &self.mode {
                        ParserMode::HTML => format!(" class=\"{}\"", classes),
                        ParserMode::JSX(value) => format!(
                            " {}=\"{}\"",
                            value.as_deref().unwrap_or("className"),
                            classes
                        ),
                    }
                } else {
                    String::new()
                };

                let main = format!("{}{}{}", identifier, props_attributes, class_attribute);

                if *self_closing && child.is_empty() && text_attribute.is_empty() {
                    value = format!("{}<{} />", prefix, main);
                } else {
                    value = format!(
                        "{}<{}>{}{}{}</{}>",
                        prefix, main, text_attribute, child, suffix, identifier
                    );
                }
            }
            ElementKind::Group(group) => {
                value = group.to_string();
            }
            ElementKind::Fragment => {
                value = format!("{}<>{}{}</>", prefix, child, suffix);
            }
            ElementKind::Condition {
                condition,
                true_part,
                false_part,
            } => {
                value = match false_part {
                    Some(f) => format!("{}{{{} ? {} : {}}}", prefix, condition, true_part, f),
                    None => format!("{}{{{} && {}}}", prefix, condition, true_part),
                };
            }
        }

        let repeated = repeat_n(value.as_str(), self.multiplier)
            .collect::<Vec<&str>>()
            .join(if is_first_level { "\n" } else { "" });

        let sibling_output = if is_first_level && !sibling.is_empty() {
            format!("\n{}", sibling)
        } else {
            sibling
        };

        write!(f, "{}{}", repeated, sibling_output)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::collections::HashMap;

    fn html_config(snippets_list: &[(&str, &str)]) -> Config {
        Config::new(ParserMode::HTML, snippets(snippets_list))
    }

    fn snippets(pairs: &[(&str, &str)]) -> HashMap<String, String> {
        pairs
            .iter()
            .map(|(k, v)| (k.to_string(), v.to_string()))
            .collect()
    }

    // -------------------------------------------------------------------------
    // Element::from_abbr — basic tests
    // -------------------------------------------------------------------------
    mod element_new_tests {
        use super::*;

        fn tag_id(e: &Element) -> &str {
            match &e.kind {
                ElementKind::Tag { identifier, .. } => identifier,
                _ => panic!("not a Tag"),
            }
        }
        fn tag_attrs(e: &Element) -> Option<&Vec<AttributeType>> {
            match &e.kind {
                ElementKind::Tag { attributes, .. } if !attributes.is_empty() => Some(attributes),
                ElementKind::Tag { .. } => None,
                _ => panic!("not a Tag"),
            }
        }
        fn is_self_closing(e: &Element) -> bool {
            matches!(
                &e.kind,
                ElementKind::Tag {
                    self_closing: true,
                    ..
                }
            )
        }
        fn group_inner(e: &Element) -> &Element {
            match &e.kind {
                ElementKind::Group(g) => g,
                _ => panic!("not a Group"),
            }
        }

        #[test]
        fn simple_identifier() {
            let e = Element::from_abbr("div", 1, None, None, &Config::default()).unwrap();
            assert_eq!(tag_id(&e), "div");
            assert!(!is_self_closing(&e));
            assert!(tag_attrs(&e).is_none());
        }

        #[test]
        fn snippet_expands_self_closing_tag() {
            let config = html_config(&[("br", "br/")]);
            let e = Element::from_abbr("br", 1, None, None, &config).unwrap();
            assert_eq!(tag_id(&e), "br");
            assert!(is_self_closing(&e));
        }

        #[test]
        fn explicit_self_closing_slash() {
            let e = Element::from_abbr("Input/", 1, None, None, &Config::default()).unwrap();
            assert_eq!(tag_id(&e), "Input");
            assert!(is_self_closing(&e));
        }

        #[test]
        fn snippet_expands_and_parses_attributes() {
            let config = html_config(&[("img", "img:src:alt")]);
            let e = Element::from_abbr("img", 1, None, None, &config).unwrap();
            assert_eq!(tag_id(&e), "img");
            let attrs = tag_attrs(&e).expect("img should have attributes");
            assert_eq!(attrs.len(), 2);
            assert_eq!(attrs[0], AttributeType::Props("src".into(), None));
            assert_eq!(attrs[1], AttributeType::Props("alt".into(), None));
        }

        #[test]
        fn class_attribute_is_parsed() {
            let e = Element::from_abbr("div.container", 1, None, None, &Config::default()).unwrap();
            assert_eq!(tag_id(&e), "div");
            let attrs = tag_attrs(&e).expect("should have attributes");
            assert_eq!(attrs.len(), 1);
            assert_eq!(attrs[0], AttributeType::Class("container".into()));
        }

        #[test]
        fn prop_with_value_is_parsed() {
            let e = Element::from_abbr("div:role=main", 1, None, None, &Config::default()).unwrap();
            let attrs = tag_attrs(&e).expect("should have attributes");
            assert_eq!(
                attrs[0],
                AttributeType::Props("role".into(), Some("main".into()))
            );
        }

        #[test]
        fn multiplier_and_level_are_passed_through() {
            let e = Element::from_abbr("li", 5, None, Some(2), &Config::default()).unwrap();
            assert_eq!(e.multiplier, 5);
            assert_eq!(e.level, Some(2));
        }

        // -------------------------------------------------------------------------
        // Element::from_abbr — multi-element snippet expansion
        // -------------------------------------------------------------------------
        mod multi_element_snippet_tests {
            use super::*;

            #[test]
            fn child_operator_in_expansion_produces_group() {
                let config = html_config(&[("card", "div.card>p")]);
                let e = Element::from_abbr("card", 1, None, None, &config).unwrap();
                assert!(
                    matches!(e.kind, ElementKind::Group(_)),
                    "group wrapper must be a Group"
                );
                assert_eq!(tag_id(group_inner(&e)), "div");
            }

            #[test]
            fn sibling_operator_in_expansion_produces_group() {
                let config = html_config(&[("duo", "h1+p")]);
                let e = Element::from_abbr("duo", 1, None, None, &config).unwrap();
                assert!(matches!(e.kind, ElementKind::Group(_)));
                let inner = group_inner(&e);
                assert_eq!(tag_id(inner), "h1");
                let sibling = inner.node.as_deref().expect("h1 should have sibling p");
                assert!(matches!(sibling, Node::Sibling(_)));
                assert_eq!(tag_id(sibling.element()), "p");
            }

            #[test]
            fn complex_expansion_builds_nested_tree() {
                let config = html_config(&[("card", "div.card>p.card-header+p.card-body")]);
                let e = Element::from_abbr("card", 1, None, None, &config).unwrap();
                assert!(matches!(e.kind, ElementKind::Group(_)));
                let inner = group_inner(&e);
                assert_eq!(tag_id(inner), "div");
                let div_attrs = tag_attrs(inner).expect("div should have class");
                assert!(
                    div_attrs
                        .iter()
                        .any(|a| a == &AttributeType::Class("card".into()))
                );
                let child_node = inner.node.as_deref().expect("div should have a child node");
                assert!(matches!(child_node, Node::Children(_)));
                assert_eq!(tag_id(child_node.element()), "p");
                let sibling_node = child_node
                    .element()
                    .node
                    .as_deref()
                    .expect("should have sibling");
                assert!(matches!(sibling_node, Node::Sibling(_)));
                assert_eq!(tag_id(sibling_node.element()), "p");
            }

            #[test]
            fn multiplier_is_preserved_on_group_expansion() {
                let config = html_config(&[("card", "div.card>p")]);
                let e = Element::from_abbr("card", 3, None, None, &config).unwrap();
                assert_eq!(e.multiplier, 3);
            }

            #[test]
            fn outer_sibling_node_is_preserved_on_group_expansion() {
                let config = html_config(&[("card", "div.card>p")]);
                let footer = Element::from_abbr("footer", 1, None, None, &config).unwrap();
                let node = Box::new(Node::Sibling(footer));
                let e = Element::from_abbr("card", 1, Some(node), None, &config).unwrap();
                let sibling = e
                    .node
                    .as_deref()
                    .expect("wrapper must carry the sibling node");
                assert!(matches!(sibling, Node::Sibling(_)));
                assert_eq!(tag_id(sibling.element()), "footer");
            }

            #[test]
            fn child_expansion_renders_correctly() {
                let config = html_config(&[("card", "div.card>p")]);
                let e = Element::from_abbr("card", 1, None, None, &config).unwrap();
                assert_eq!(e.to_string(), "<div class=\"card\">\n\t<p></p>\n</div>");
            }

            #[test]
            fn sibling_expansion_renders_correctly() {
                let config = html_config(&[("duo", "h1+p")]);
                let e = Element::from_abbr("duo", 1, None, None, &config).unwrap();
                assert_eq!(e.to_string(), "<h1></h1>\n<p></p>");
            }

            #[test]
            fn complex_card_expansion_renders_correctly() {
                let config = html_config(&[("card", "div.card>p.card-header+p.card-body")]);
                let e = Element::from_abbr("card", 1, None, None, &config).unwrap();
                assert_eq!(
                    e.to_string(),
                    "<div class=\"card\">\n\t<p class=\"card-header\"></p>\n\t<p class=\"card-body\"></p>\n</div>"
                );
            }

            #[test]
            fn multiplied_group_expansion_renders_correctly() {
                let config = html_config(&[("duo", "h1+p")]);
                let e = Element::from_abbr("duo", 3, None, None, &config).unwrap();
                assert_eq!(
                    e.to_string(),
                    "<h1></h1>\n<p></p>\n<h1></h1>\n<p></p>\n<h1></h1>\n<p></p>"
                );
            }

            #[test]
            fn group_expansion_with_outer_sibling_renders_correctly() {
                let config = html_config(&[("card", "div.card>p")]);
                let footer = Element::from_abbr("footer", 1, None, None, &config).unwrap();
                let node = Box::new(Node::Sibling(footer));
                let e = Element::from_abbr("card", 1, Some(node), None, &config).unwrap();
                assert_eq!(
                    e.to_string(),
                    "<div class=\"card\">\n\t<p></p>\n</div>\n<footer></footer>"
                );
            }
        }
    }

    // -------------------------------------------------------------------------
    // Element::to_glyf
    // -------------------------------------------------------------------------
    mod element_to_glyf_tests {
        use super::*;

        fn cfg() -> Config {
            Config::default()
        }

        #[test]
        fn simple_element() {
            let e = Element::from_abbr("div", 1, None, None, &cfg()).unwrap();
            assert_eq!(e.to_glyf(), "div");
        }

        #[test]
        fn element_with_class() {
            let e = Element::from_abbr("div.foo", 1, None, None, &cfg()).unwrap();
            assert_eq!(e.to_glyf(), "div.foo");
        }

        #[test]
        fn element_with_id() {
            let e = Element::from_abbr("div#main", 1, None, None, &cfg()).unwrap();
            assert_eq!(e.to_glyf(), "div#main");
        }

        #[test]
        fn element_with_prop() {
            let e = Element::from_abbr("a:href=url", 1, None, None, &cfg()).unwrap();
            assert_eq!(e.to_glyf(), "a:href=url");
        }

        #[test]
        fn element_with_text_content() {
            let e = Element::from_abbr("p>>Hello", 1, None, None, &cfg()).unwrap();
            assert_eq!(e.to_glyf(), "p>>Hello");
        }

        #[test]
        fn self_closing_element() {
            let e = Element::from_abbr("br/", 1, None, None, &cfg()).unwrap();
            assert_eq!(e.to_glyf(), "br/");
        }

        #[test]
        fn element_with_multiplier() {
            let e = Element::from_abbr("li", 3, None, None, &cfg()).unwrap();
            assert_eq!(e.to_glyf(), "li*3");
        }

        #[test]
        fn element_with_child() {
            let e = parse_input("div>p", None, &cfg()).unwrap();
            assert_eq!(e.to_glyf(), "div>p");
        }

        #[test]
        fn element_with_sibling() {
            let e = parse_input("div+span", None, &cfg()).unwrap();
            assert_eq!(e.to_glyf(), "div+span");
        }

        #[test]
        fn attributes_sorted_class_before_id() {
            // to_glyf uses compress order: class first, then id
            let e = Element::from_abbr("div#main.foo", 1, None, None, &cfg()).unwrap();
            assert_eq!(e.to_glyf(), "div.foo#main");
        }

        #[test]
        fn chained_children() {
            let e = parse_input("div>p>span", None, &cfg()).unwrap();
            assert_eq!(e.to_glyf(), "div>p>span");
        }
    }
}
