//! Reconstruct a selector string from its syntax tree.
//!
//! Used for the combinator group key in the decision tree and for the
//! canonical ordering inside `normalize`.

use super::ast::{CompoundSelector, SimpleSelector};

/// Serialize a compound selector.
///
/// A combinator node prints before the rest of the compound; other simple
/// selectors append in list order.
pub fn serialize_compound(c: &CompoundSelector) -> String {
    let mut acc = String::new();
    for node in &c.list {
        if let SimpleSelector::Combinator { .. } = node {
            acc = format!("{}{}", serialize_simple(node), acc);
        } else {
            acc.push_str(&serialize_simple(node));
        }
    }
    acc
}

/// Serialize one simple selector node.
pub fn serialize_simple(sel: &SimpleSelector) -> String {
    match sel {
        SimpleSelector::Universal { namespace, .. } => {
            format!("{}*", ser_ns(namespace))
        }
        SimpleSelector::Tag {
            name, namespace, ..
        } => {
            format!("{}{}", ser_ns(namespace), ser_ident(name))
        }
        SimpleSelector::Class { name, .. } => format!(".{}", ser_ident(name)),
        SimpleSelector::Id { name, .. } => format!("#{}", ser_ident(name)),
        SimpleSelector::AttrPresence {
            name, namespace, ..
        } => {
            format!("[{}{}]", ser_ns(namespace), ser_ident(name))
        }
        SimpleSelector::AttrValue {
            name,
            namespace,
            matcher,
            value,
            modifier,
            ..
        } => {
            let m = modifier.map(|m| m.as_str()).unwrap_or("");
            format!(
                "[{}{}{}\"{}\"{}]",
                ser_ns(namespace),
                ser_ident(name),
                matcher.as_str(),
                ser_str(value),
                m
            )
        }
        SimpleSelector::PseudoClass { name, .. } => format!(":{}", ser_ident(name)),
        SimpleSelector::FunctionalPseudoClass { name, list, .. } => {
            let inner: Vec<String> = list.iter().map(serialize_compound).collect();
            format!(":{}({})", ser_ident(name), inner.join(","))
        }
        SimpleSelector::Combinator {
            combinator, left, ..
        } => {
            format!("{}{}", serialize_compound(left), combinator)
        }
    }
}

/// Serialize a namespace prefix into `prefix|`, or empty when absent.
fn ser_ns(ns: &Option<String>) -> String {
    match ns {
        Some(p) => format!("{}|", ser_ident(p)),
        None => String::new(),
    }
}

/// Escape one char as a code-point escape: backslash, lowercase hex, space.
fn code_point(c: char) -> String {
    format!("\\{:x} ", c as u32)
}

/// Escape an identifier for output.
fn ser_ident(s: &str) -> String {
    let chars: Vec<char> = s.chars().collect();
    let mut out = String::new();
    for (i, &c) in chars.iter().enumerate() {
        // A leading digit, or a digit right after a leading hyphen, escapes.
        if (i == 0 && c.is_ascii_digit()) || (i == 1 && chars[0] == '-' && c.is_ascii_digit()) {
            out.push_str(&code_point(c));
        } else if c == '-' && chars.len() == 1 {
            out.push_str("\\-");
        } else if c == '-' || c == '_' || c.is_ascii_alphanumeric() || !c.is_ascii() {
            out.push(c);
        } else if c == '\0' {
            out.push('\u{FFFD}');
        } else if ((c as u32) >= 0x01 && (c as u32) <= 0x1f) || c == '\u{7f}' {
            out.push_str(&code_point(c));
        } else {
            out.push('\\');
            out.push(c);
        }
    }
    out
}

/// Escape a string value for output between double quotes.
fn ser_str(s: &str) -> String {
    let mut out = String::new();
    for c in s.chars() {
        match c {
            '"' => out.push_str("\\\""),
            '\\' => out.push_str("\\\\"),
            '\0' => out.push('\u{FFFD}'),
            c if ((c as u32) >= 0x01 && (c as u32) <= 0x1f) || c == '\u{7f}' => {
                out.push_str(&code_point(c))
            }
            c => out.push(c),
        }
    }
    out
}
