//! Line reference support for CalcPad.
//!
//! Provides line references (`line1`, `#1`) that resolve to the result of a
//! specific line by number, and renumbering logic for when lines are inserted
//! or deleted.
//!
//! Line references are 1-indexed (matching what users see in the editor).
//! Internally they are stored in the EvalContext as `__line_N` variables.

use std::collections::HashSet;

/// Extract all line reference numbers from an expression string.
/// Recognizes both `lineN` and `#N` syntax (case-insensitive for "line").
pub fn extract_line_refs(input: &str) -> Vec<usize> {
    let mut refs = Vec::new();
    let bytes = input.as_bytes();
    let len = bytes.len();
    let mut i = 0;

    while i < len {
        // Check for #N syntax
        if bytes[i] == b'#' && i + 1 < len && bytes[i + 1].is_ascii_digit() {
            i += 1;
            let start = i;
            while i < len && bytes[i].is_ascii_digit() {
                i += 1;
            }
            if let Ok(n) = input[start..i].parse::<usize>() {
                if !refs.contains(&n) {
                    refs.push(n);
                }
            }
            continue;
        }

        // Check for lineN syntax (case-insensitive)
        if i + 4 < len {
            let word = &input[i..i + 4];
            if word.eq_ignore_ascii_case("line") {
                let after = i + 4;
                if after < len && bytes[after].is_ascii_digit() {
                    // Check that the character before is not alphanumeric (word boundary)
                    if i == 0 || !bytes[i - 1].is_ascii_alphanumeric() {
                        let num_start = after;
                        let mut j = after;
                        while j < len && bytes[j].is_ascii_digit() {
                            j += 1;
                        }
                        // Check that the character after is not alphanumeric (word boundary)
                        if j >= len || !bytes[j].is_ascii_alphanumeric() {
                            if let Ok(n) = input[num_start..j].parse::<usize>() {
                                if !refs.contains(&n) {
                                    refs.push(n);
                                }
                            }
                        }
                        i = j;
                        continue;
                    }
                }
            }
        }

        i += 1;
    }

    refs
}

/// Update line references in an expression string after a line insertion.
///
/// When a new line is inserted at position `insert_at` (1-indexed),
/// all references to lines at or after that position are incremented by 1.
pub fn renumber_after_insert(input: &str, insert_at: usize) -> String {
    renumber_refs(input, |line_num| {
        if line_num >= insert_at {
            line_num + 1
        } else {
            line_num
        }
    })
}

/// Update line references in an expression string after a line deletion.
///
/// When a line is deleted at position `delete_at` (1-indexed),
/// references to the deleted line become 0 (invalid).
/// References to lines after the deleted one are decremented by 1.
pub fn renumber_after_delete(input: &str, delete_at: usize) -> String {
    renumber_refs(input, |line_num| {
        if line_num == delete_at {
            0 // mark as invalid
        } else if line_num > delete_at {
            line_num - 1
        } else {
            line_num
        }
    })
}

/// Apply a renumbering function to all line references in an expression string.
/// Handles both `lineN` and `#N` syntax.
fn renumber_refs<F>(input: &str, transform: F) -> String
where
    F: Fn(usize) -> usize,
{
    let mut result = String::with_capacity(input.len());
    let bytes = input.as_bytes();
    let len = bytes.len();
    let mut i = 0;

    while i < len {
        // Check for #N syntax
        if bytes[i] == b'#' && i + 1 < len && bytes[i + 1].is_ascii_digit() {
            result.push('#');
            i += 1;
            let start = i;
            while i < len && bytes[i].is_ascii_digit() {
                i += 1;
            }
            if let Ok(n) = input[start..i].parse::<usize>() {
                let new_n = transform(n);
                result.push_str(&new_n.to_string());
            } else {
                result.push_str(&input[start..i]);
            }
            continue;
        }

        // Check for lineN syntax (case-insensitive)
        if i + 4 < len {
            let word = &input[i..i + 4];
            if word.eq_ignore_ascii_case("line") {
                let after = i + 4;
                if after < len && bytes[after].is_ascii_digit() {
                    if i == 0 || !bytes[i - 1].is_ascii_alphanumeric() {
                        let prefix = &input[i..i + 4]; // preserve original case
                        let num_start = after;
                        let mut j = after;
                        while j < len && bytes[j].is_ascii_digit() {
                            j += 1;
                        }
                        if j >= len || !bytes[j].is_ascii_alphanumeric() {
                            if let Ok(n) = input[num_start..j].parse::<usize>() {
                                let new_n = transform(n);
                                result.push_str(prefix);
                                result.push_str(&new_n.to_string());
                                i = j;
                                continue;
                            }
                        }
                    }
                }
            }
        }

        // Safe: input is valid UTF-8, push the character
        let ch = input[i..].chars().next().unwrap();
        result.push(ch);
        i += ch.len_utf8();
    }

    result
}

/// Detect circular line references given a set of lines and their line reference dependencies.
///
/// Returns the set of line numbers (1-indexed) that are involved in circular references.
pub fn detect_circular_line_refs(
    line_refs: &[(usize, Vec<usize>)], // (line_number, referenced_lines)
) -> HashSet<usize> {
    use std::collections::HashMap;

    let mut adj: HashMap<usize, Vec<usize>> = HashMap::new();
    for (line_num, refs) in line_refs {
        adj.insert(*line_num, refs.clone());
    }

    let mut circular = HashSet::new();

    #[derive(Clone, Copy, PartialEq)]
    enum State {
        Unvisited,
        InProgress,
        Done,
    }

    let mut state: HashMap<usize, State> = HashMap::new();
    for (line_num, _) in line_refs {
        state.insert(*line_num, State::Unvisited);
    }

    fn dfs(
        node: usize,
        adj: &HashMap<usize, Vec<usize>>,
        state: &mut HashMap<usize, State>,
        path: &mut Vec<usize>,
        circular: &mut HashSet<usize>,
    ) {
        if let Some(&s) = state.get(&node) {
            if s == State::Done {
                return;
            }
            if s == State::InProgress {
                // Found a cycle — mark all nodes in the cycle
                if let Some(start_idx) = path.iter().position(|&n| n == node) {
                    for &n in &path[start_idx..] {
                        circular.insert(n);
                    }
                }
                circular.insert(node);
                return;
            }
        }

        state.insert(node, State::InProgress);
        path.push(node);

        if let Some(deps) = adj.get(&node) {
            for &dep in deps {
                if adj.contains_key(&dep) {
                    dfs(dep, adj, state, path, circular);
                }
            }
        }

        path.pop();
        state.insert(node, State::Done);
    }

    for (line_num, _) in line_refs {
        if state.get(line_num) == Some(&State::Unvisited) {
            let mut path = Vec::new();
            dfs(*line_num, &adj, &mut state, &mut path, &mut circular);
        }
    }

    circular
}

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

    // --- extract_line_refs ---

    #[test]
    fn test_extract_hash_ref() {
        assert_eq!(extract_line_refs("#1 * 2"), vec![1]);
    }

    #[test]
    fn test_extract_line_ref() {
        assert_eq!(extract_line_refs("line1 * 2"), vec![1]);
    }

    #[test]
    fn test_extract_line_ref_case_insensitive() {
        assert_eq!(extract_line_refs("Line3 + Line1"), vec![3, 1]);
    }

    #[test]
    fn test_extract_multiple_refs() {
        assert_eq!(extract_line_refs("#1 + #2 * line3"), vec![1, 2, 3]);
    }

    #[test]
    fn test_extract_no_refs() {
        assert_eq!(extract_line_refs("x + 5"), Vec::<usize>::new());
    }

    #[test]
    fn test_extract_dedup() {
        assert_eq!(extract_line_refs("#1 + #1"), vec![1]);
    }

    // --- renumber_after_insert ---

    #[test]
    fn test_renumber_insert_shifts_refs_at_or_after() {
        assert_eq!(renumber_after_insert("#1 + #2", 1), "#2 + #3");
    }

    #[test]
    fn test_renumber_insert_no_shift_before() {
        assert_eq!(renumber_after_insert("#1 + #2", 3), "#1 + #2");
    }

    #[test]
    fn test_renumber_insert_line_syntax() {
        assert_eq!(renumber_after_insert("line2 * 3", 1), "line3 * 3");
    }

    #[test]
    fn test_renumber_insert_mixed() {
        assert_eq!(renumber_after_insert("#1 + line3", 2), "#1 + line4");
    }

    // --- renumber_after_delete ---

    #[test]
    fn test_renumber_delete_marks_deleted_as_zero() {
        assert_eq!(renumber_after_delete("#2 * 3", 2), "#0 * 3");
    }

    #[test]
    fn test_renumber_delete_shifts_after() {
        assert_eq!(renumber_after_delete("#1 + #3", 2), "#1 + #2");
    }

    #[test]
    fn test_renumber_delete_line_syntax() {
        assert_eq!(renumber_after_delete("line3 + 5", 2), "line2 + 5");
    }

    #[test]
    fn test_renumber_delete_no_change_before() {
        assert_eq!(renumber_after_delete("#1 + #2", 5), "#1 + #2");
    }

    // --- detect_circular_line_refs ---

    #[test]
    fn test_no_cycles() {
        let refs = vec![(1, vec![]), (2, vec![1]), (3, vec![1, 2])];
        let circular = detect_circular_line_refs(&refs);
        assert!(circular.is_empty());
    }

    #[test]
    fn test_direct_cycle() {
        let refs = vec![(1, vec![2]), (2, vec![1])];
        let circular = detect_circular_line_refs(&refs);
        assert!(circular.contains(&1));
        assert!(circular.contains(&2));
    }

    #[test]
    fn test_transitive_cycle() {
        let refs = vec![(1, vec![2]), (2, vec![3]), (3, vec![1])];
        let circular = detect_circular_line_refs(&refs);
        assert!(circular.contains(&1));
        assert!(circular.contains(&2));
        assert!(circular.contains(&3));
    }

    #[test]
    fn test_self_reference() {
        let refs = vec![(1, vec![1])];
        let circular = detect_circular_line_refs(&refs);
        assert!(circular.contains(&1));
    }

    #[test]
    fn test_partial_cycle() {
        // Line 4 depends on line 2 which is in a cycle, but line 4 is not in the cycle itself
        let refs = vec![(1, vec![2]), (2, vec![1]), (3, vec![]), (4, vec![2])];
        let circular = detect_circular_line_refs(&refs);
        assert!(circular.contains(&1));
        assert!(circular.contains(&2));
        assert!(!circular.contains(&3));
        // Line 4 is not in the cycle (it just references a cyclic line)
        assert!(!circular.contains(&4));
    }
}
