//! The render seam (column model) and the shareable column-layout state.
//!
//! GUI-free: the [`Column`] trait carries an **opaque**
//! `type Element` the core never constructs or inspects — the integrator sets it
//! to an iced `Element` and drives rendering. This module is a *column model*,
//! not a renderer/backend abstraction.

/// Stable column identity — survives reorder, hide/show, and persisted layout
/// A positional index is **not** identity. Unlike [`RowId`], a
/// `ColumnId` is `Copy` (columns are few and integrator-assigned).
///
/// [`RowId`]: crate::data::DataSource::RowId
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ColumnId(pub u64);

/// A column's sizing policy. (Continuous content-fit sizing is held open,
/// not built — grip double-click auto-size covers the use case.)
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ColumnSizing {
    /// Non-resizable; exactly this many logical pixels.
    ///
    /// Field-review rule: resizability is **all-or-nothing per grid** —
    /// resize grips on only some headers read as broken, not configured.
    /// Mix `Fixed` and `Resizable` in one grid only with that in mind.
    Fixed(f32),
    /// Resizable within `[min, max]`, starting at `default`.
    Resizable {
        /// Initial width.
        default: f32,
        /// Smallest allowed width.
        min: f32,
        /// Largest allowed width.
        max: f32,
    },
}

impl ColumnSizing {
    /// Resizable with bounds derived from `width` — **the default shape**
    /// (ag-grid parity — columns are resizable unless opted
    /// out): min = half the width (never under 24 px), max = 4× the width.
    /// Spell out [`Resizable`](Self::Resizable) for custom bounds;
    /// [`Fixed`](Self::Fixed) is the opt-out.
    #[must_use]
    pub fn resizable(width: f32) -> Self {
        ColumnSizing::Resizable {
            default: width,
            min: (width * 0.5).max(24.0).min(width),
            max: (width * 4.0).max(width),
        }
    }

    /// The width a fresh layout starts this column at.
    #[must_use]
    pub fn default_width(&self) -> f32 {
        match self {
            ColumnSizing::Fixed(w) => *w,
            ColumnSizing::Resizable { default, .. } => *default,
        }
    }

    /// Whether the column may be interactively resized.
    #[must_use]
    pub fn is_resizable(&self) -> bool {
        matches!(self, ColumnSizing::Resizable { .. })
    }

    /// Clamps a proposed width to this policy. Fixed columns ignore the proposal.
    #[must_use]
    pub fn clamp_width(&self, proposed: f32) -> f32 {
        match self {
            ColumnSizing::Fixed(w) => *w,
            ColumnSizing::Resizable { min, max, .. } => proposed.clamp(*min, *max),
        }
    }
}

/// A bare width means "resizable" (see [`ColumnSizing::resizable`]) — the
/// v30-parity default that lets column constructors take just a number.
impl From<f32> for ColumnSizing {
    fn from(width: f32) -> Self {
        ColumnSizing::resizable(width)
    }
}

/// The render seam — a trait the integrator satisfies.
///
/// The core defines it and never inspects [`Element`](Self::Element). All columns
/// in one table share the same `Element` and [`Row`](Self::Row), so
/// `Vec<Box<dyn Column<Element = E, Row = R>>>` stays object-safe. The integrator
/// implements it (e.g. `type Element = iced::Element<'static, Msg, ..>`) and
/// calls [`header`](Self::header)/[`cell`](Self::cell) in its `view`; the core
/// only supplies positions via [`ColumnLayout`].
///
/// Cell input is the whole `Row` (the per-row escape hatch); provided renderers
/// read just the field they need, which is the per-value default in practice.
/// `editor`/`footer` are designed-around but unbuilt.
pub trait Column {
    /// Opaque element type — the core never constructs or inspects it.
    type Element;
    /// The row this column reads from.
    type Row;

    /// This column's stable identity.
    fn id(&self) -> ColumnId;
    /// This column's sizing policy.
    fn sizing(&self) -> ColumnSizing;
    /// Whether clicking this column's header sorts by it. The column only
    /// *declares* sortability (a view property); the store *executes* the sort
    /// (the spec/execution straddle).
    ///
    /// **Defaults to `true`** (ag-grid parity — columns are
    /// sortable unless opted out). Opt out where sorting makes no sense —
    /// an append-only trace opts out everywhere — arrival order IS the
    /// meaning there. Caveat the default inherits: a sort only *does* anything if
    /// the store resolves this column's field (e.g. `VecStore::with_fields`);
    /// a sortable column with an unresolvable field shows a dead sort arrow.
    fn sortable(&self) -> bool {
        true
    }
    /// The header element (label + sort indicator + resize-handle area).
    fn header(&self) -> Self::Element;
    /// The cell element for `row`.
    fn cell(&self, row: &Self::Row) -> Self::Element;
    /// The footer element — aggregates/totals. `None` (the default)
    /// renders no footer cell; a
    /// grid shows its footer strip only when at least one of its columns
    /// provides one (checked once, at grid construction). The *aggregation*
    /// is integrator-side: footer closures capture whatever app state holds
    /// the totals, and re-render whenever the view does.
    fn footer(&self) -> Option<Self::Element> {
        None
    }
    /// A measurement-friendly rendering of the cell, for auto-size: an
    /// element whose natural (shrink) width IS the content width. The
    /// default measures [`cell`](Self::cell) itself — right wherever the
    /// cell is already content-sized. Override in two situations: the cell
    /// wraps its content in a fill-the-cell container (right/center
    /// alignment does — return the bare content instead), or the cell has
    /// **no** natural width at all (bars, sparklines — return `None`, and
    /// auto-size leaves the column's width alone).
    fn measure(&self, row: &Self::Row) -> Option<Self::Element> {
        Some(self.cell(row))
    }
    /// The footer's measurement-friendly rendering — auto-size fits the
    /// footer too (a total is often wider than any cell). The default
    /// measures [`footer`](Self::footer) as rendered; `None` simply means
    /// there is nothing to measure (no footer, or a fill-width footer like
    /// a bar's, which should not constrain the fit). Same override rule as
    /// [`measure`](Self::measure): alignment wrappers have no natural
    /// width — return the bare content.
    fn measure_footer(&self) -> Option<Self::Element> {
        self.footer()
    }
}

/// Shareable column-layout state: widths, order, and horizontal
/// scroll offset — a coherent value that can be handed to and shared across grid
/// instances to keep them column-aligned. A grid holds one; sharing one across
/// grids (pinning, split views) is integrator-side and not built here.
#[derive(Debug, Clone, PartialEq)]
pub struct ColumnLayout {
    cols: Vec<ColState>,
    h_scroll: f32,
}

#[derive(Debug, Clone, Copy, PartialEq)]
struct ColState {
    id: ColumnId,
    sizing: ColumnSizing,
    /// Committed width.
    width: f32,
    /// In-progress resize delta from the drag origin (0 when idle).
    resize_offset: f32,
}

/// One column's on-screen placement for rendering.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Placement {
    /// The column's stable id.
    pub id: ColumnId,
    /// The column's positional index.
    pub index: usize,
    /// Left edge in content space (before horizontal scroll), logical px.
    pub x: f32,
    /// Effective width (clamped, including any in-progress resize), logical px.
    pub width: f32,
}

impl ColumnLayout {
    /// Builds a layout from ordered `(id, sizing)` pairs, each starting at its
    /// policy's default width.
    pub fn new(columns: impl IntoIterator<Item = (ColumnId, ColumnSizing)>) -> Self {
        let cols = columns
            .into_iter()
            .map(|(id, sizing)| ColState {
                id,
                sizing,
                width: sizing.default_width(),
                resize_offset: 0.0,
            })
            .collect();
        Self {
            cols,
            h_scroll: 0.0,
        }
    }

    /// Number of columns.
    #[must_use]
    pub fn len(&self) -> usize {
        self.cols.len()
    }

    /// Whether there are no columns.
    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.cols.is_empty()
    }

    /// The id of the column at `index`, if any.
    #[must_use]
    pub fn id(&self, index: usize) -> Option<ColumnId> {
        self.cols.get(index).map(|c| c.id)
    }

    /// The positional index of the column with `id`, if present.
    #[must_use]
    pub fn index_of(&self, id: ColumnId) -> Option<usize> {
        self.cols.iter().position(|c| c.id == id)
    }

    /// The effective (clamped) width of the column at `index`, including any
    /// in-progress resize. `0.0` if out of range.
    #[must_use]
    pub fn width(&self, index: usize) -> f32 {
        self.cols
            .get(index)
            .map_or(0.0, |c| c.sizing.clamp_width(c.width + c.resize_offset))
    }

    /// The total width of all columns, logical px.
    #[must_use]
    pub fn total_width(&self) -> f32 {
        (0..self.cols.len()).map(|i| self.width(i)).sum()
    }

    /// The horizontal scroll offset (px).
    #[must_use]
    pub fn h_scroll(&self) -> f32 {
        self.h_scroll
    }

    /// Sets the horizontal scroll offset (clamped to `>= 0`). This is the value
    /// the header and body share to stay aligned.
    pub fn set_h_scroll(&mut self, x: f32) {
        self.h_scroll = if x.is_finite() { x.max(0.0) } else { 0.0 };
    }

    /// Previews a resize on the column at `index`: records the in-progress delta
    /// from the drag origin (px). No-op for a fixed column or a bad index.
    pub fn preview_resize(&mut self, index: usize, delta: f32) {
        if let Some(c) = self.cols.get_mut(index) {
            if c.sizing.is_resizable() {
                c.resize_offset = if delta.is_finite() { delta } else { 0.0 };
            }
        }
    }

    /// Commits the in-progress resize on the column at `index` into its width and
    /// clears the preview.
    pub fn commit_resize(&mut self, index: usize) {
        if let Some(c) = self.cols.get_mut(index) {
            c.width = c.sizing.clamp_width(c.width + c.resize_offset);
            c.resize_offset = 0.0;
        }
    }

    /// Sets the column at `index` to `width` directly, clamped by its sizing
    /// policy (a fixed column keeps its fixed width), clearing any in-progress
    /// resize. The programmatic width primitive: auto-size (fit-to-content)
    /// and persisted-layout restore both land here — the *measurement* and the
    /// *persistence* are integrator-side; the state transition is this.
    pub fn set_width(&mut self, index: usize, width: f32) {
        if let Some(c) = self.cols.get_mut(index) {
            c.width = c.sizing.clamp_width(if width.is_finite() { width } else { c.width });
            c.resize_offset = 0.0;
        }
    }

    /// Every column's [`Placement`] in order, with cumulative `x` (before
    /// horizontal scroll). Single pass.
    #[must_use]
    pub fn placements(&self) -> Vec<Placement> {
        let mut x = 0.0;
        let mut out = Vec::with_capacity(self.cols.len());
        for (index, c) in self.cols.iter().enumerate() {
            let width = c.sizing.clamp_width(c.width + c.resize_offset);
            out.push(Placement {
                id: c.id,
                index,
                x,
                width,
            });
            x += width;
        }
        out
    }
}

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

    fn layout() -> ColumnLayout {
        ColumnLayout::new([
            (ColumnId(10), ColumnSizing::Fixed(50.0)),
            (ColumnId(20), ColumnSizing::Resizable { default: 100.0, min: 40.0, max: 200.0 }),
            (ColumnId(30), ColumnSizing::Resizable { default: 80.0, min: 30.0, max: 300.0 }),
        ])
    }

    #[test]
    fn starts_at_default_widths() {
        let l = layout();
        assert_eq!(l.len(), 3);
        assert_eq!(l.width(0), 50.0);
        assert_eq!(l.width(1), 100.0);
        assert_eq!(l.total_width(), 230.0);
        assert_eq!(l.id(1), Some(ColumnId(20)));
        assert_eq!(l.index_of(ColumnId(30)), Some(2));
    }

    #[test]
    fn placements_are_cumulative() {
        let ps = layout().placements();
        assert_eq!(ps[0], Placement { id: ColumnId(10), index: 0, x: 0.0, width: 50.0 });
        assert_eq!(ps[1], Placement { id: ColumnId(20), index: 1, x: 50.0, width: 100.0 });
        assert_eq!(ps[2], Placement { id: ColumnId(30), index: 2, x: 150.0, width: 80.0 });
    }

    #[test]
    fn resize_previews_then_commits_clamped() {
        let mut l = layout();
        // Widen column 1 by 50 -> 150 (within [40,200]).
        l.preview_resize(1, 50.0);
        assert_eq!(l.width(1), 150.0);
        // Preview beyond max is clamped in the effective width...
        l.preview_resize(1, 500.0);
        assert_eq!(l.width(1), 200.0);
        // ...and commit folds the clamped width in, clearing the preview.
        l.commit_resize(1);
        assert_eq!(l.width(1), 200.0);
        l.preview_resize(1, 0.0);
        assert_eq!(l.width(1), 200.0);
        // Shrink below min clamps to min.
        l.preview_resize(1, -500.0);
        assert_eq!(l.width(1), 40.0);
    }

    /// The programmatic width primitive (auto-size and persisted-layout
    /// restore both land through it).
    #[test]
    fn set_width_clamps_by_policy_and_clears_preview() {
        let mut l = layout();
        // Resizable column: clamped into [40, 200].
        l.set_width(1, 150.0);
        assert_eq!(l.width(1), 150.0);
        l.set_width(1, 500.0);
        assert_eq!(l.width(1), 200.0);
        l.set_width(1, 1.0);
        assert_eq!(l.width(1), 40.0);
        // An in-progress preview is cleared by a direct set.
        l.preview_resize(1, 60.0);
        l.set_width(1, 100.0);
        assert_eq!(l.width(1), 100.0);
        // Fixed columns keep their width; junk input is ignored; bad index safe.
        l.set_width(0, 400.0);
        assert_eq!(l.width(0), 50.0);
        l.set_width(1, f32::NAN);
        assert_eq!(l.width(1), 100.0);
        l.set_width(99, 100.0); // no panic
    }

    #[test]
    fn fixed_columns_do_not_resize() {
        let mut l = layout();
        l.preview_resize(0, 100.0);
        assert_eq!(l.width(0), 50.0);
        l.commit_resize(0);
        assert_eq!(l.width(0), 50.0);
    }

    #[test]
    fn h_scroll_clamps_negative_and_nonfinite() {
        let mut l = layout();
        l.set_h_scroll(30.0);
        assert_eq!(l.h_scroll(), 30.0);
        l.set_h_scroll(-5.0);
        assert_eq!(l.h_scroll(), 0.0);
        l.set_h_scroll(f32::NAN);
        assert_eq!(l.h_scroll(), 0.0);
    }

    /// The v30-parity default shape: a bare width is resizable with derived
    /// bounds (min = half, floored at 24 px and never above the width;
    /// max = 4×, never below the width).
    #[test]
    fn bare_width_is_resizable_with_derived_bounds() {
        assert_eq!(
            ColumnSizing::from(100.0),
            ColumnSizing::Resizable { default: 100.0, min: 50.0, max: 400.0 }
        );
        // Small widths: min floors at 24 but never exceeds the width itself.
        assert_eq!(
            ColumnSizing::resizable(30.0),
            ColumnSizing::Resizable { default: 30.0, min: 24.0, max: 120.0 }
        );
        assert_eq!(
            ColumnSizing::resizable(16.0),
            ColumnSizing::Resizable { default: 16.0, min: 16.0, max: 64.0 }
        );
    }

    #[test]
    fn out_of_range_index_is_safe() {
        let mut l = layout();
        assert_eq!(l.width(99), 0.0);
        assert_eq!(l.id(99), None);
        l.preview_resize(99, 10.0); // no panic
        l.commit_resize(99); // no panic
    }
}
