//! The range-to-regex algorithm.
//!
//! The flow follows the source structure. Detect padding, split into negative
//! and positive halves, turn each half into sub-range tokens, collate the two
//! halves with shared prefixes, then wrap.

use crate::number::Number;
use crate::options::Resolved;

/// A single sub-range token.
///
/// `pattern` is the body before any quantifier or zero prefix. `count` drives
/// the trailing quantifier. `string` is the finished fragment. `string` carries
/// the byte-exact output for a band, including padding and quantifier.
#[derive(Debug, Clone)]
struct Token {
    pattern: String,
    count: Vec<i64>,
    string: String,
}

/// Build the regex source for `[min, max]`.
///
/// `min_raw` and `max_raw` are the original argument strings. `min` and `max`
/// are their parsed values. Reordering, padding, and wrapping all happen here.
pub(crate) fn build(
    min_raw: &str,
    max_raw: &str,
    min: Number,
    max: Number,
    opts: &Resolved,
) -> String {
    let a = min.value.min(max.value);
    let b = min.value.max(max.value);

    let is_padded = has_padding(min_raw) || has_padding(max_raw);

    // Adjacent unpadded values: emit the two literals in original argument
    // order. Padded ranges fall through so the optional-zero logic applies. The
    // gap is computed in i128 because b - a can exceed i64 for wide ranges.
    if !is_padded && (b as i128 - a as i128) == 1 {
        let result = format!("{min_raw}|{max_raw}");
        if opts.capture {
            return format!("({result})");
        }
        if opts.wrap_false {
            return result;
        }
        return format!("(?:{result})");
    }

    let max_len = if is_padded {
        max_raw.chars().count()
    } else {
        0
    };

    let mut a = a;
    let mut negatives: Vec<Token> = Vec::new();
    let mut positives: Vec<Token> = Vec::new();

    if a < 0 {
        let new_min = if b < 0 { b.abs() } else { 1 };
        negatives = split_to_patterns(new_min, a.abs(), is_padded, max_len, opts);
        a = 0;
    }

    if b >= 0 {
        positives = split_to_patterns(a, b, is_padded, max_len, opts);
    }

    let mut result = collate_patterns(&negatives, &positives);

    if opts.capture {
        result = format!("({result})");
    } else if !opts.wrap_false && (positives.len() + negatives.len()) > 1 {
        result = format!("(?:{result})");
    }

    result
}

/// True when the string is zero-padded: an optional sign, a leading `0`, then at
/// least one more digit. The trailing digit may itself be `0`, so `"00"` and
/// `"-00"` are padded. A lone `"0"` or `"-0"` is not padded.
fn has_padding(s: &str) -> bool {
    let bytes = s.as_bytes();
    let mut i = 0;
    if bytes.first() == Some(&b'-') {
        i = 1;
    }
    bytes.get(i) == Some(&b'0') && bytes.get(i + 1).is_some_and(|b| b.is_ascii_digit())
}

/// Find break points for `[min, max]` at runs of nines and zeros.
///
/// Returns a sorted, de-duplicated list of sub-range upper bounds.
fn split_to_ranges(min: i64, max: i64) -> Vec<i64> {
    let mut stops: Vec<i64> = vec![max];

    // Nines pass. `count_nines` returns None once the run of nines exceeds i64.
    // The float-based source returns a value past max at that point, which ends
    // the loop. None ends it the same way.
    let mut nines = 1;
    while let Some(stop) = count_nines(min, nines) {
        if min > stop || stop > max {
            break;
        }
        push_unique(&mut stops, stop);
        nines += 1;
    }

    // Zeros pass. Work in i128 so `max + 1` and the power of ten cannot overflow
    // i64. `count_zeros` returns None once `10^zeros` exceeds the upper bound,
    // which floors the result to 0 and ends the loop.
    let upper = max as i128 + 1;
    let mut zeros = 1;
    while let Some(floored) = count_zeros(upper, zeros) {
        let stop = floored - 1;
        if (min as i128) >= stop || stop > max as i128 {
            break;
        }
        push_unique(&mut stops, stop as i64);
        zeros += 1;
    }

    stops.sort_unstable();
    stops.dedup();
    stops
}

/// Push a value only if absent, mirroring the source `Set` semantics.
fn push_unique(stops: &mut Vec<i64>, value: i64) {
    if !stops.contains(&value) {
        stops.push(value);
    }
}

/// Replace the last `len` digits of `min` with nines.
///
/// `count_nines(12, 1)` is `Some(19)`. `count_nines(12, 2)` is `Some(99)`. When
/// `len` reaches or exceeds the digit count, the leading part is empty, so
/// `count_nines(5, 2)` is `Some(99)`. Returns `None` when the result would
/// exceed i64, which lets the caller stop the nines loop.
fn count_nines(min: i64, len: usize) -> Option<i64> {
    let s = min.to_string();
    let keep = s.len().saturating_sub(len);
    let head = &s[..keep];
    let nines: String = "9".repeat(len);
    format!("{head}{nines}").parse().ok()
}

/// Floor `integer` to a multiple of `10^zeros`.
///
/// Returns `None` when `10^zeros` exceeds i128. For any in-scope bound the
/// modulus then equals `integer`, so the floor is 0 and the caller stops the
/// zeros loop.
fn count_zeros(integer: i128, zeros: u32) -> Option<i128> {
    let power = 10i128.checked_pow(zeros)?;
    Some(integer - (integer % power))
}

/// Turn one equal-length sub-range into a pattern body and count.
///
/// Equal digits emit the literal. A differing pair emits a character class,
/// except a full `0` to `9` pair which is deferred to a single trailing class
/// counted by `count`.
fn range_to_pattern(start: &str, stop: &str, opts: &Resolved) -> Token {
    if start == stop {
        return Token {
            pattern: start.to_string(),
            count: Vec::new(),
            string: String::new(),
        };
    }

    let start_bytes = start.as_bytes();
    let stop_bytes = stop.as_bytes();
    let mut pattern = String::new();
    let mut count: i64 = 0;

    for i in 0..start_bytes.len() {
        let sd = start_bytes[i];
        let td = stop_bytes[i];
        if sd == td {
            pattern.push(sd as char);
        } else if sd != b'0' || td != b'9' {
            pattern.push_str(&to_character_class(sd, td));
        } else {
            count += 1;
        }
    }

    if count > 0 {
        pattern.push_str(if opts.shorthand { "\\d" } else { "[0-9]" });
    }

    Token {
        pattern,
        count: vec![count],
        string: String::new(),
    }
}

/// Build `[a-b]`, or `[ab]` when the two digits are adjacent.
fn to_character_class(a: u8, b: u8) -> String {
    let adjacent = (b as i32 - a as i32) == 1;
    if adjacent {
        format!("[{}{}]", a as char, b as char)
    } else {
        format!("[{}-{}]", a as char, b as char)
    }
}

/// Turn a count list into a quantifier suffix.
///
/// A second element or a first element above one yields `{...}`. Otherwise the
/// suffix is empty, leaving a single class or literal to stand alone.
fn to_quantifier(digits: &[i64]) -> String {
    let start = digits.first().copied().unwrap_or(0);
    let stop = digits.get(1).copied();

    let has_stop = matches!(stop, Some(s) if s != 0);
    if has_stop || start > 1 {
        match stop {
            Some(s) if s != 0 => format!("{{{start},{s}}}"),
            _ => format!("{{{start}}}"),
        }
    } else {
        String::new()
    }
}

/// Walk the sub-ranges, apply padding, and collapse adjacent identical patterns.
fn split_to_patterns(
    min: i64,
    max: i64,
    is_padded: bool,
    max_len: usize,
    opts: &Resolved,
) -> Vec<Token> {
    let ranges = split_to_ranges(min, max);
    let mut tokens: Vec<Token> = Vec::new();
    let mut start = min;

    for &upper in &ranges {
        let mut obj = range_to_pattern(&start.to_string(), &upper.to_string(), opts);

        if !is_padded {
            if let Some(prev) = tokens.last_mut() {
                if prev.pattern == obj.pattern {
                    if prev.count.len() > 1 {
                        prev.count.pop();
                    }
                    prev.count.push(obj.count.first().copied().unwrap_or(0));
                    prev.string = format!("{}{}", prev.pattern, to_quantifier(&prev.count));
                    start = upper.saturating_add(1);
                    continue;
                }
            }
        }

        let zeros = if is_padded {
            pad_zeros(upper, max_len, opts.relax_zeros)
        } else {
            String::new()
        };

        obj.string = format!("{}{}{}", zeros, obj.pattern, to_quantifier(&obj.count));
        tokens.push(obj);
        // The final band can end at i64::MAX. `start` only seeds the next
        // iteration, so saturate rather than overflow.
        start = upper.saturating_add(1);
    }

    tokens
}

/// Build the leading zero prefix for a padded band.
///
/// `diff` is how many zeros the band needs to reach the max width. Relaxed
/// output makes those zeros optional. Strict output makes them mandatory.
fn pad_zeros(value: i64, max_len: usize, relax: bool) -> String {
    let value_len = value.to_string().chars().count();
    // A band wider than the padded max needs no leading zeros. Saturate so the
    // width comparison never wraps into spurious padding.
    let diff = max_len.saturating_sub(value_len);

    match diff {
        0 => String::new(),
        1 => {
            if relax {
                "0?".to_string()
            } else {
                "0".to_string()
            }
        }
        2 => {
            if relax {
                "0{0,2}".to_string()
            } else {
                "00".to_string()
            }
        }
        n => {
            if relax {
                format!("0{{0,{n}}}")
            } else {
                format!("0{{{n}}}")
            }
        }
    }
}

/// Merge negative and positive halves into one alternation.
///
/// Three buckets in fixed order: only-negative (prefix `-`), shared (prefix
/// `-?`, iterated over the negatives), then only-positive (no prefix). Joined by
/// `|`.
fn collate_patterns(neg: &[Token], pos: &[Token]) -> String {
    let mut subpatterns: Vec<String> = Vec::new();

    for ele in neg {
        if !contains(pos, &ele.string) {
            subpatterns.push(format!("-{}", ele.string));
        }
    }
    for ele in neg {
        if contains(pos, &ele.string) {
            subpatterns.push(format!("-?{}", ele.string));
        }
    }
    for ele in pos {
        if !contains(neg, &ele.string) {
            subpatterns.push(ele.string.clone());
        }
    }

    subpatterns.join("|")
}

/// True when any token in `arr` has the given `string`.
fn contains(arr: &[Token], string: &str) -> bool {
    arr.iter().any(|ele| ele.string == string)
}
