//! Low-level utility functions used throughout the parser.
//! These functions have no dependencies on the parser's own types.
use std::sync::LazyLock;

use regex::Regex;

static MULTIPLIER_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^(\d+)").unwrap());

/// Returns the byte index of the first occurrence of `target` at nesting depth 0,
/// skipping characters inside `()` or `{}`.
///
/// When `target` is `>`, consecutive `>>` sequences are also skipped — `>>` is
/// the text content operator and must not be confused with the child operator `>`.
///
/// This is the core of the depth-aware operator parsing that prevents `>` or `+`
/// inside attribute values like `:onClick={a>b}` from being mistaken for operators.
pub(super) fn find_at_depth_zero(input: &str, target: char) -> Option<usize> {
    let mut depth = 0usize;
    let bytes = input.as_bytes();

    for (i, c) in input.char_indices() {
        match c {
            c if c == target && depth == 0 => {
                if target == '>'
                    && (bytes.get(i.saturating_add(1)) == Some(&b'>')
                        || i > 0 && bytes.get(i - 1) == Some(&b'>'))
                {
                    continue;
                }
                return Some(i);
            }
            '(' | '{' => depth += 1,
            ')' | '}' => depth = depth.saturating_sub(1),
            _ => {}
        }
    }
    None
}

/// Returns the byte index of the first occurrence of the byte sequence `seq`
/// at nesting depth 0, skipping occurrences inside `()` or `{}`.
///
/// Used by the JSX parser to locate `->` separators in conditional expressions
/// without being confused by the same characters inside function call arguments
/// or object literals.
pub(super) fn find_sequence_at_depth_zero(input: &str, seq: &str) -> Option<usize> {
    let bytes = input.as_bytes();
    let seq = seq.as_bytes();
    let mut depth = 0usize;
    for (i, &b) in bytes.iter().enumerate() {
        match b {
            b'(' | b'{' => depth += 1,
            b')' | b'}' => depth = depth.saturating_sub(1),
            _ if depth == 0 && bytes[i..].starts_with(seq) => return Some(i),
            _ => {}
        }
    }
    None
}

/// Extracts the `*N` repetition count from an element string.
///
/// Returns `None` when no `*` is present at depth 0, or when `*` is not
/// followed by a digit. The `N` value is everything up to the next
/// non-digit character, so `li*3+span` returns `Some(3)`.
pub(super) fn get_multiplier(element: &str) -> Option<usize> {
    let index = find_at_depth_zero(element, '*')?;
    let slice = element.split_at(index + 1).1;
    let capture = MULTIPLIER_REGEX.captures(slice)?;

    let multiplier = capture.get(0)?;

    Some(multiplier.as_str().parse::<usize>().unwrap_or(1))
}

/// Returns `true` if `element` contains a child (`>`) or sibling (`+`) operator
/// at nesting depth 0.
///
/// For `>`, consecutive `>>` sequences (the text-content operator) are not
/// counted — only a single `>` not preceded or followed by another `>` qualifies.
/// Returns at the first match to avoid unnecessary scanning.
pub(super) fn has_node_operator(element: &str) -> bool {
    find_at_depth_zero(element, '>').is_some() || find_at_depth_zero(element, '+').is_some()
}

/// Returns the byte index of the `n`-th `>` operator from the right at nesting
/// depth 0, skipping `>>` text-content operators and `>`s inside `()` or `{}`.
///
/// Used by [`normalize_climb_up`] to locate where to split the left-hand side
/// of a `^` expression. Returns `None` if fewer than `n` qualifying `>`s exist.
fn find_nth_node_from_right(input: &str, n: usize) -> Option<usize> {
    let bytes = input.as_bytes();
    let mut depth = 0usize;
    let mut positions = Vec::new();

    for (i, c) in input.char_indices().rev() {
        match c {
            '(' | '{' => depth = depth.saturating_sub(1),
            ')' | '}' => depth += 1,
            '>' if depth == 0 => {
                // Skip >> (text content operator)
                if bytes.get(i + 1) == Some(&b'>') || (i > 0 && bytes.get(i - 1) == Some(&b'>')) {
                    continue;
                }
                positions.push(i);
                if positions.len() == n {
                    break;
                }
            }
            _ => {}
        }
    }

    if n > positions.len() {
        return None;
    }

    positions.last().copied()
}

/// Rewrites a climb-up (`^`) expression into its equivalent grouping form.
///
/// `before` is the left side of the first `^`, `rest` starts at the `^`.
/// Consecutive `^`s are all consumed at once — `rest = "^^button"` climbs
/// two levels. The right side of the carets is left unchanged for the caller
/// to re-parse, allowing sequential `^` like `a>b^c^d` to be resolved
/// iteratively through recursive calls to `parse_input`.
///
/// Returns the transformed string, e.g.:
/// - `("div>p>span", "^button")` → `"div>(p>span)+button"`
/// - `("div>p>span", "^^button")` → `"(div>p>span)+button"`
pub(super) fn normalize_climb_up(before: &str, rest: &str) -> String {
    // Count consecutive ^
    let n = rest.chars().take_while(|&c| c == '^').count();
    let after = &rest[n..];

    match find_nth_node_from_right(before, n + 1) {
        Some(pos) => {
            let prefix = &before[..pos];
            let segment = &before[pos + 1..];

            if prefix.is_empty() {
                return format!("({})+{}", segment, after);
            }

            format!("{}>({})+{}", prefix, segment, after)
        }
        None => format!("({})+{}", before, after),
    }
}

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

    // -------------------------------------------------------------------------
    // find_at_depth_zero
    // -------------------------------------------------------------------------
    mod find_at_depth_zero_tests {
        use super::*;

        #[test]
        fn finds_target_in_flat_string() {
            assert_eq!(find_at_depth_zero("div>p", '>'), Some(3));
        }

        #[test]
        fn returns_none_when_target_absent() {
            assert_eq!(find_at_depth_zero("div", '>'), None);
        }

        #[test]
        fn returns_none_for_empty_string() {
            assert_eq!(find_at_depth_zero("", '>'), None);
        }

        #[test]
        fn finds_target_at_position_zero() {
            assert_eq!(find_at_depth_zero(">div", '>'), Some(0));
        }

        #[test]
        fn skips_target_inside_parentheses() {
            // '>' at index 4 is inside (div>p) and must be skipped
            // '+' at index 7 is at depth 0 and must be returned
            assert_eq!(find_at_depth_zero("(div>p)+span", '+'), Some(7));
        }

        #[test]
        fn skips_target_inside_braces() {
            // '>' at index 14 is inside {val>2} and must be skipped
            // '+' at index 17 is at depth 0 and must be returned
            assert_eq!(find_at_depth_zero("a:onClick={val>2}+b", '+'), Some(17));
        }

        #[test]
        fn handles_nested_groups() {
            // everything before index 10 is inside one or two '('
            assert_eq!(find_at_depth_zero("(p>(span))+div", '+'), Some(10));
        }

        #[test]
        fn returns_first_occurrence_at_depth_zero() {
            assert_eq!(find_at_depth_zero("div+p+span", '+'), Some(3));
        }

        #[test]
        fn does_not_find_target_only_inside_parens() {
            assert_eq!(find_at_depth_zero("(div>p)", '>'), None);
        }

        #[test]
        fn should_skip_inner_text_call() {
            assert_eq!(find_at_depth_zero("div>>Hello", '>'), None);
        }
    }

    // -------------------------------------------------------------------------
    // get_multiplier
    // -------------------------------------------------------------------------
    mod get_multiplier_tests {
        use super::*;

        #[test]
        fn returns_none_without_star() {
            assert_eq!(get_multiplier("div"), None);
        }

        #[test]
        fn extracts_multiplier_from_element_with_prefix() {
            assert_eq!(get_multiplier("div*3"), Some(3));
        }

        #[test]
        fn extracts_multiplier_from_star_only_prefix() {
            assert_eq!(get_multiplier("*3"), Some(3));
        }

        #[test]
        fn extracts_multi_digit_multiplier() {
            assert_eq!(get_multiplier("li*12"), Some(12));
        }

        #[test]
        fn extracts_from_star_prefix_with_suffix() {
            // parse_group calls get_multiplier("*3+span") on the text after ')'
            assert_eq!(get_multiplier("*3+span"), Some(3));
        }

        #[test]
        fn returns_none_when_star_not_followed_by_digit() {
            assert_eq!(get_multiplier("div*"), None);
        }

        #[test]
        fn returns_none_when_star_inside_braces() {
            // '*' is at depth 1 inside {}, find_at_depth_zero skips it
            assert_eq!(get_multiplier("div:x={val*3}"), None);
        }

        #[test]
        fn multiplier_stops_at_non_digit_char() {
            assert_eq!(get_multiplier("div*3+p"), Some(3));
        }
    }

    mod has_node_operator_tests {
        use super::*;

        #[test]
        fn detects_child_operator() {
            assert!(has_node_operator("div>p"));
        }

        #[test]
        fn detects_sibling_operator() {
            assert!(has_node_operator("h1+p"));
        }

        #[test]
        fn skips_text_content_operator() {
            assert!(!has_node_operator("div>>Hello"));
        }

        #[test]
        fn detects_single_gt_before_text_operator() {
            assert!(has_node_operator("div>p>>text"));
        }

        #[test]
        fn group_child_is_detected() {
            assert!(has_node_operator("(div)>p"));
        }

        #[test]
        fn plain_identifier_is_false() {
            assert!(!has_node_operator("a:href"));
        }
    }

    #[cfg(test)]
    mod find_nth_gt_from_right_tests {
        use super::super::find_nth_node_from_right;

        #[test]
        fn returns_none_on_empty() {
            assert_eq!(find_nth_node_from_right("", 1), None);
        }

        #[test]
        fn returns_none_when_fewer_gts_than_n() {
            assert_eq!(find_nth_node_from_right("a>b", 2), None);
        }

        #[test]
        fn returns_last_gt_for_n_of_1() {
            assert_eq!(find_nth_node_from_right("a>b>c", 1), Some(3)); // before c
        }

        #[test]
        fn returns_second_from_right_for_n_of_2() {
            assert_eq!(find_nth_node_from_right("a>b>c", 2), Some(1)); // before b
        }

        #[test]
        fn skips_gt_inside_braces() {
            assert_eq!(find_nth_node_from_right("a:x={b>c}>d", 1), Some(9));
        }

        #[test]
        fn skips_text_content_operator() {
            assert_eq!(find_nth_node_from_right("div>>text", 1), None);
        }
    }

    #[cfg(test)]
    mod normalize_climb_up_tests {
        use super::*;

        #[test]
        fn single_caret_three_levels_deep() {
            // div>p>span^button  ≡  div>(p>span)+button
            assert_eq!(
                normalize_climb_up("div>p>span", "^button"),
                "div>(p>span)+button"
            );
        }

        #[test]
        fn double_caret_climbs_past_root() {
            // div>p>span^^button  ≡  (div>p>span)+button  (only 2 >'s, climbs to root)
            assert_eq!(
                normalize_climb_up("div>p>span", "^^button"),
                "(div>p>span)+button"
            );
        }

        #[test]
        fn single_caret_one_level_climbs_to_root() {
            // a>b^c — only one > in before, no room to climb inside → root level
            assert_eq!(normalize_climb_up("a>b", "^c"), "(a>b)+c");
        }

        #[test]
        fn single_caret_no_depth_in_before() {
            // div^p — no > at all in before
            assert_eq!(normalize_climb_up("div", "^p"), "(div)+p");
        }

        #[test]
        fn double_caret_four_levels_deep() {
            // a>b>c>d^^e — 3 >'s, 3rd from right is before b
            assert_eq!(normalize_climb_up("a>b>c>d", "^^e"), "a>(b>c>d)+e");
        }

        #[test]
        fn triple_caret_consumes_all_depth() {
            // a>b>c>d^^^e — triple caret, only 3 >'s → climbs past root
            assert_eq!(normalize_climb_up("a>b>c>d", "^^^e"), "(a>b>c>d)+e");
        }

        #[test]
        fn right_side_preserves_sibling_chain() {
            // a>b>c^d+e — d+e both land at b's level (sibling of b's children)
            assert_eq!(normalize_climb_up("a>b>c", "^d+e"), "a>(b>c)+d+e");
        }

        #[test]
        fn sequential_carets_only_first_batch_consumed() {
            // a>b^c^d — first ^ only consumed here; second ^ left in right side
            // for parse_input to handle on the next recursive call
            assert_eq!(normalize_climb_up("a>b", "^c^d"), "(a>b)+c^d");
        }

        #[test]
        fn gt_inside_braces_not_counted_as_depth() {
            // a:x={b>c}>d^e — the > inside {} must be ignored, only outer > counts
            assert_eq!(normalize_climb_up("a:x={b>c}>d", "^e"), "(a:x={b>c}>d)+e");
        }

        #[test]
        fn before_with_sibling_operator() {
            // div>p+span^button — + doesn't affect depth, still one > in before
            assert_eq!(
                normalize_climb_up("div>p+span", "^button"),
                "(div>p+span)+button"
            );
        }

        #[test]
        fn right_side_with_child_operator() {
            // a>b>c^d>e — right side has its own child
            assert_eq!(normalize_climb_up("a>b>c", "^d>e"), "a>(b>c)+d>e");
        }
    }
}
