//! Sort/filter **specs** — plain, representable data, never closures.
//!
//! The core owns the spec (view state: sorted-by-what, filtered-how); the store
//! *executes* it — in memory for resident stores, pushed down to a query for
//! out-of-core ones. A representable spec is the only kind a store
//! can push down or an app can persist as a preset; an opaque `Fn` can be
//! neither. With the optional `serde` feature the spec types derive
//! `Serialize`/`Deserialize` (the round-trip the presets primitive builds on);
//! the default build keeps `oriel-core` dependency-free.
//!
//! [`FilterSpec::matches`] / [`Predicate::matches`] are **public helpers** for
//! resident stores (the same tools the built-in [`VecStore`] uses — no
//! privileged internals); a pushdown store translates the spec instead of
//! calling them.
//!
//! [`VecStore`]: crate::data::VecStore

use core::cmp::Ordering;

use crate::columns::ColumnId;

/// Sort order for a [`SortSpec`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum SortDirection {
    /// Smallest first.
    Ascending,
    /// Largest first.
    Descending,
}

impl SortDirection {
    /// The opposite direction.
    #[must_use]
    pub fn toggled(self) -> Self {
        match self {
            SortDirection::Ascending => SortDirection::Descending,
            SortDirection::Descending => SortDirection::Ascending,
        }
    }
}

/// A single-column sort (multi-column sorting is held open, not built).
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SortSpec {
    /// The column whose values order the rows.
    pub column: ColumnId,
    /// The order.
    pub direction: SortDirection,
}

/// A field value used in predicates and as a sort key.
///
/// Stores resolve `(row, column) -> Option<FilterValue>`; `None` means the field
/// doesn't exist for that row/column.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum FilterValue {
    /// Textual value.
    Text(String),
    /// Integer value.
    Int(i64),
    /// Floating-point value (compared by total order, so `NaN` sorts stably).
    Float(f64),
    /// Boolean value.
    Bool(bool),
}

impl FilterValue {
    /// A total order over values, used for sorting: same-variant values compare
    /// naturally (floats by [`f64::total_cmp`]); different variants order by
    /// variant (`Text < Int < Float < Bool`) so mixed columns sort stably rather
    /// than panicking.
    #[must_use]
    pub fn compare(&self, other: &FilterValue) -> Ordering {
        use FilterValue::{Bool, Float, Int, Text};
        match (self, other) {
            (Text(a), Text(b)) => a.cmp(b),
            (Int(a), Int(b)) => a.cmp(b),
            (Float(a), Float(b)) => a.total_cmp(b),
            (Bool(a), Bool(b)) => a.cmp(b),
            _ => self.variant_rank().cmp(&other.variant_rank()),
        }
    }

    fn variant_rank(&self) -> u8 {
        match self {
            FilterValue::Text(_) => 0,
            FilterValue::Int(_) => 1,
            FilterValue::Float(_) => 2,
            FilterValue::Bool(_) => 3,
        }
    }
}

/// One representable predicate. Deliberately **not** `#[non_exhaustive]`: when a
/// variant is added, every store's `match` must break so it decides how to
/// execute (or push down) the new predicate, rather than silently ignoring it.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Predicate {
    /// Case-insensitive substring match on a **text** field. A row matches only
    /// if the field resolves to [`FilterValue::Text`] containing `needle`.
    Contains {
        /// The column whose value is searched.
        column: ColumnId,
        /// The substring to look for (case-insensitive).
        needle: String,
    },
    /// Exact equality with a value. A row matches only if the field resolves to
    /// an equal value of the same variant.
    Equals {
        /// The column whose value is compared.
        column: ColumnId,
        /// The value to compare against.
        value: FilterValue,
    },
}

impl Predicate {
    /// Evaluates this predicate for one row, given a resolver from [`ColumnId`]
    /// to that row's field value. Unresolvable fields (`None`) never match —
    /// loud (rows disappear) rather than silently ignoring a clause.
    pub fn matches(&self, resolve: impl Fn(ColumnId) -> Option<FilterValue>) -> bool {
        match self {
            Predicate::Contains { column, needle } => match resolve(*column) {
                Some(FilterValue::Text(text)) => {
                    text.to_lowercase().contains(&needle.to_lowercase())
                }
                _ => false,
            },
            Predicate::Equals { column, value } => {
                resolve(*column).is_some_and(|v| v == *value)
            }
        }
    }
}

/// A conjunction of predicates: a row passes if **every** clause matches. An
/// empty clause list passes everything (no filter).
#[derive(Debug, Clone, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct FilterSpec {
    /// The ANDed clauses.
    pub clauses: Vec<Predicate>,
}

impl FilterSpec {
    /// A spec that filters nothing.
    #[must_use]
    pub fn none() -> Self {
        Self::default()
    }

    /// Whether this spec filters nothing.
    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.clauses.is_empty()
    }

    /// Evaluates all clauses for one row (see [`Predicate::matches`]).
    pub fn matches(&self, resolve: impl Fn(ColumnId) -> Option<FilterValue> + Copy) -> bool {
        self.clauses.iter().all(|clause| clause.matches(resolve))
    }
}

/// The full view query: what the rows are sorted by and filtered how. This is
/// the value the grid gets/sets and presets persist.
#[derive(Debug, Clone, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct QuerySpec {
    /// Single-column sort, or `None` for the store's natural order.
    pub sort: Option<SortSpec>,
    /// The predicate filter.
    pub filter: FilterSpec,
}

impl QuerySpec {
    /// Whether this is the identity query (no sort, no filter).
    #[must_use]
    pub fn is_identity(&self) -> bool {
        self.sort.is_none() && self.filter.is_empty()
    }

    /// The sort that results from a header click on `column` — the market
    /// three-state cycle: unsorted → ascending → descending → **unsorted**.
    /// The third state matters: without it the store's natural order is
    /// unreachable once any header has ever been clicked. View-interaction
    /// policy, owned by the core so every integrator cycles identically.
    #[must_use]
    pub fn cycled_sort(&self, column: ColumnId) -> Option<SortSpec> {
        match self.sort {
            Some(current) if current.column == column => match current.direction {
                SortDirection::Ascending => {
                    Some(SortSpec { column, direction: SortDirection::Descending })
                }
                SortDirection::Descending => None,
            },
            _ => Some(SortSpec { column, direction: SortDirection::Ascending }),
        }
    }
}

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

    const TEXT_COL: ColumnId = ColumnId(1);
    const CH_COL: ColumnId = ColumnId(2);

    fn resolver(text: &str, ch: i64) -> impl Fn(ColumnId) -> Option<FilterValue> + Copy + '_ {
        move |col| match col {
            TEXT_COL => Some(FilterValue::Text(text.to_string())),
            CH_COL => Some(FilterValue::Int(ch)),
            _ => None,
        }
    }

    #[test]
    fn contains_is_case_insensitive_and_text_only() {
        let p = Predicate::Contains {
            column: TEXT_COL,
            needle: "7df".into(),
        };
        assert!(p.matches(resolver("ID 7DF DLC 8", 0)));
        assert!(!p.matches(resolver("ID 7E8 DLC 8", 0)));
        // Contains against a non-text field never matches.
        let on_int = Predicate::Contains {
            column: CH_COL,
            needle: "1".into(),
        };
        assert!(!on_int.matches(resolver("anything", 1)));
    }

    #[test]
    fn equals_and_conjunction() {
        let spec = FilterSpec {
            clauses: vec![
                Predicate::Contains { column: TEXT_COL, needle: "id".into() },
                Predicate::Equals { column: CH_COL, value: FilterValue::Int(1) },
            ],
        };
        assert!(spec.matches(resolver("ID 7DF", 1)));
        assert!(!spec.matches(resolver("ID 7DF", 2))); // wrong channel
        assert!(!spec.matches(resolver("nope", 1))); // text miss
        assert!(FilterSpec::none().matches(resolver("anything", 9)));
    }

    #[test]
    fn unresolvable_fields_never_match() {
        let p = Predicate::Equals { column: ColumnId(99), value: FilterValue::Int(1) };
        assert!(!p.matches(resolver("x", 1)));
    }

    #[test]
    fn value_compare_is_total() {
        use FilterValue::{Float, Int, Text};
        assert_eq!(Text("a".into()).compare(&Text("b".into())), Ordering::Less);
        assert_eq!(Int(2).compare(&Int(2)), Ordering::Equal);
        assert_eq!(Float(1.5).compare(&Float(f64::NAN)), Ordering::Less);
        // Cross-variant: stable variant order, no panic.
        assert_eq!(Text("z".into()).compare(&Int(0)), Ordering::Less);
    }

    #[test]
    fn direction_toggles() {
        assert_eq!(SortDirection::Ascending.toggled(), SortDirection::Descending);
        assert_eq!(SortDirection::Descending.toggled(), SortDirection::Ascending);
    }

    #[test]
    fn identity_query() {
        assert!(QuerySpec::default().is_identity());
        let sorted = QuerySpec {
            sort: Some(SortSpec { column: TEXT_COL, direction: SortDirection::Ascending }),
            filter: FilterSpec::none(),
        };
        assert!(!sorted.is_identity());
    }
}

/// The spec round-trip: get/set the spec **through serialization**
/// (run with `cargo test -p oriel-core --features serde`).
#[cfg(all(test, feature = "serde"))]
mod serde_tests {
    use super::*;

    #[test]
    fn query_spec_roundtrips_through_json() {
        let spec = QuerySpec {
            sort: Some(SortSpec {
                column: ColumnId(7),
                direction: SortDirection::Descending,
            }),
            filter: FilterSpec {
                clauses: vec![
                    Predicate::Contains { column: ColumnId(1), needle: "7DF".into() },
                    Predicate::Equals { column: ColumnId(2), value: FilterValue::Int(1) },
                    Predicate::Equals { column: ColumnId(3), value: FilterValue::Bool(true) },
                    Predicate::Equals { column: ColumnId(4), value: FilterValue::Float(2.5) },
                ],
            },
        };

        let json = serde_json::to_string(&spec).expect("serialize");
        let back: QuerySpec = serde_json::from_str(&json).expect("deserialize");
        assert_eq!(back, spec);
    }
}
