//! Compile a numeric min/max range into the smallest matching regex source string.
//!
//! Given an inclusive integer range `[min, max]`, [`to_regex_range`] builds a
//! compact regex source string that matches the decimal representation of every
//! integer in the range and nothing outside it. The output is a source string,
//! not a compiled pattern. Wrap it in `^...$` and pass it to your regex engine
//! of choice.
//!
//! ```
//! use regex_range::{to_regex_range, Options};
//!
//! let src = to_regex_range("1", Some("5"), &Options::default()).unwrap();
//! assert_eq!(src, "[1-5]");
//!
//! let src = to_regex_range("-10", Some("10"), &Options::default()).unwrap();
//! assert_eq!(src, "(?:-[1-9]|-?10|[0-9])");
//! ```
//!
//! The generated strings use a small subset of regex syntax that is shared
//! across engines: character classes (`[a-b]`, `[ab]`), the `\d` shorthand,
//! alternation `|`, quantifiers `{n}` and `{n,m}` and `?`, and groups `(?:...)`
//! and `(...)`. No backreferences or lookaround appear, so the output works
//! directly with the `regex` crate.
//!
//! # Inputs
//!
//! Inputs are decimal integer strings. Leading zeros and a leading `-` are
//! allowed. Zero-padding affects the output: a padded bound makes the range
//! padded, and the generated pattern then allows leading zeros up to the width
//! of `max`. See [`Options::relax_zeros`].
//!
//! # Caching
//!
//! Results are memoized in a process-wide cache keyed by the raw inputs and the
//! resolved options. [`clear_cache`] empties it. The cache is an optimization.
//! It does not change the returned string.

#![forbid(unsafe_code)]
#![warn(missing_docs)]

mod cache;
mod number;
mod options;
mod pattern;

pub use cache::clear_cache;
pub use options::Options;

use number::Number;

/// Error returned when an argument is not a valid number.
///
/// The two variants mark the two validation points: the first argument and the
/// second argument. Read the message through [`Display`](std::fmt::Display), for
/// example with `to_string()`. The enum is `#[non_exhaustive]` so new variants
/// can be added without a breaking release.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Error {
    /// The first argument (`min`) was not a valid number.
    FirstArgument,
    /// The second argument (`max`) was not a valid number.
    SecondArgument,
}

impl std::fmt::Display for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let message = match self {
            Error::FirstArgument => "to_regex_range: expected the first argument to be a number",
            Error::SecondArgument => "to_regex_range: expected the second argument to be a number.",
        };
        f.write_str(message)
    }
}

impl std::error::Error for Error {}

/// Compile the inclusive range `[min, max]` into a regex source string.
///
/// `min` is required. `max` is optional. When `max` is `None` or equals `min`
/// by raw string value, the function returns `min` verbatim, keeping any zero
/// padding or sign. When `min` is greater than `max`, the bounds are swapped
/// for computation, but the adjacent-value fast path and the single-value short
/// circuit keep the original argument order in their literal output.
///
/// # Errors
///
/// Returns [`Error::FirstArgument`] when `min` is not a valid number. Returns
/// [`Error::SecondArgument`] when `max` is present and not a valid number. A
/// valid number is a decimal integer string, optionally signed and optionally
/// zero-padded. Surrounding whitespace is trimmed before parsing. Blank strings,
/// non-numeric strings, values outside `i64`, and `i64::MIN` are rejected.
///
/// # Examples
///
/// ```
/// use regex_range::{to_regex_range, Options};
///
/// let opts = Options::default();
/// assert_eq!(to_regex_range("5", None, &opts).unwrap(), "5");
/// assert_eq!(to_regex_range("0", Some("9"), &opts).unwrap(), "[0-9]");
/// assert_eq!(
///     to_regex_range("10", Some("50"), &opts).unwrap(),
///     "(?:1[0-9]|[2-4][0-9]|50)"
/// );
/// ```
pub fn to_regex_range(min: &str, max: Option<&str>, options: &Options) -> Result<String, Error> {
    let min_num = match Number::parse(min) {
        Some(n) => n,
        None => return Err(Error::FirstArgument),
    };

    // max omitted, or raw forms identical: return min verbatim.
    let max = match max {
        Some(m) if m != min => m,
        _ => return Ok(min.to_string()),
    };

    let max_num = match Number::parse(max) {
        Some(n) => n,
        None => return Err(Error::SecondArgument),
    };

    let opts = options.resolve();
    let key = cache::Key::new(min, max, &opts);
    if let Some(hit) = cache::get(&key) {
        return Ok(hit);
    }

    let result = pattern::build(min, max, min_num, max_num, &opts);
    cache::put(key, &result);
    Ok(result)
}
