//! Layout style primitives — sizing, padding, margin, alignment, and anchors.
//!
//! Every Stage-8 widget carries a [`LayoutStyle`] that tells the layout
//! algorithm how to size and position it inside its parent's content rect.
//! The primitives here are deliberately small and orthogonal so they compose
//! into the three layout modes (stack, grid, anchor) without each mode
//! introducing its own bespoke parameters.
//!
//! All linear measurements (`Sizing::Fixed`, [`Insets`] fields, anchor
//! offsets, stack/grid gaps) are in **logical pixels**. The layout function
//! takes a separate `scale` factor (typically the window's DPI scale) and
//! multiplies these values at resolve time, so one widget tree lays out
//! sensibly on a 1× laptop and a 2× HiDPI monitor without per-widget rewrites.

use glam::Vec2;
use serde::{Deserialize, Serialize};

/// How a widget asks to be sized along one axis.
///
/// Sizing interacts with the parent's layout mode:
///
/// - In a stack, the **main axis** sums all `Fixed` and `FitContent` sizes,
///   then divides leftover space among `Grow` siblings by weight. The
///   **cross axis** sizes each child independently (`Grow` fills the parent's
///   cross extent; the other variants behave like the main axis).
/// - In a grid, every child fills its cell, but `Fixed`/`FitContent` cap the
///   child's drawn size and let [`LayoutStyle::align_horizontal`] /
///   [`LayoutStyle::align_vertical`] position the smaller rect inside the
///   cell.
/// - In an anchor parent, child sizing is **ignored** along axes the anchor
///   actually constrains; the anchor + offsets fully determine the child's
///   rect.
#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
pub enum Sizing {
    /// A fixed size in logical pixels. Multiplied by the layout scale factor.
    Fixed(f32),
    /// Take a share of the parent's leftover space, weighted by `f32`.
    ///
    /// Two siblings with `Grow(1.0)` split leftover space evenly; `Grow(2.0)`
    /// next to `Grow(1.0)` takes 2/3 of it. A non-positive weight contributes
    /// nothing and the child collapses to zero on that axis.
    Grow(f32),
    /// Size to fit the widget's own content — the intrinsic size for leaves,
    /// the recursive content extent for containers.
    #[default]
    FitContent,
}

/// Per-side spacing in logical pixels — used for both padding (inside) and
/// margin (outside).
///
/// Padding shrinks a widget's `content_rect` (children draw inside it); margin
/// reserves space *around* the widget so siblings don't touch it. Both are
/// scaled by the layout scale factor at resolve time.
#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
pub struct Insets {
    pub left: f32,
    pub right: f32,
    pub top: f32,
    pub bottom: f32,
}

impl Insets {
    pub const ZERO: Self = Self {
        left: 0.0,
        right: 0.0,
        top: 0.0,
        bottom: 0.0,
    };

    /// Same value on every side.
    pub const fn all(v: f32) -> Self {
        Self {
            left: v,
            right: v,
            top: v,
            bottom: v,
        }
    }

    /// Symmetric: one value for left+right, another for top+bottom.
    pub const fn symmetric(horizontal: f32, vertical: f32) -> Self {
        Self {
            left: horizontal,
            right: horizontal,
            top: vertical,
            bottom: vertical,
        }
    }

    /// Combined horizontal extent (`left + right`).
    #[inline]
    pub fn horizontal(&self) -> f32 {
        self.left + self.right
    }

    /// Combined vertical extent (`top + bottom`).
    #[inline]
    pub fn vertical(&self) -> f32 {
        self.top + self.bottom
    }

    /// Component-wise scale (used internally by the layout algorithm to apply
    /// the DPI factor; exposed for tests that want to verify the scaling).
    #[inline]
    pub fn scaled(&self, scale: f32) -> Self {
        Self {
            left: self.left * scale,
            right: self.right * scale,
            top: self.top * scale,
            bottom: self.bottom * scale,
        }
    }
}

/// Alignment along one axis when a widget is smaller than its slot.
///
/// In a row stack, `align_vertical` decides whether a short child docks to the
/// top, middle, or bottom of the row's content rect. The stack's own
/// [`Stack::main_align`](super::widget::Stack::main_align) does the analogous
/// thing along the **main** axis when all children are sized but don't sum to
/// the full main extent.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum Align {
    /// Top / left edge.
    #[default]
    Start,
    /// Centered in the available space.
    Center,
    /// Bottom / right edge.
    End,
}

/// How a child positions itself inside an [`AnchorGroup`](super::widget::AnchorGroup)
/// parent.
///
/// Anchors are two normalized points in `[0, 1]²` (the **anchor rectangle**)
/// plus per-corner offsets in logical pixels. The child's resulting rect is:
///
/// ```text
/// rect.min = parent.content.min + parent.content.size * anchor.min + offset_min * scale
/// rect.max = parent.content.min + parent.content.size * anchor.max + offset_max * scale
/// ```
///
/// This is the standard Unity / Godot anchor formulation: pick two anchor
/// corners (a single point for "follow that corner", a full rectangle for
/// "dock to this edge / fill"), then nudge with offsets. The default is
/// [`Anchor::FILL`].
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Anchor {
    pub min: Vec2,
    pub max: Vec2,
    pub offset_min: Vec2,
    pub offset_max: Vec2,
}

impl Anchor {
    /// Fill the parent's content rect exactly. The default for new widgets.
    pub const FILL: Self = Self {
        min: Vec2::ZERO,
        max: Vec2::ONE,
        offset_min: Vec2::ZERO,
        offset_max: Vec2::ZERO,
    };

    /// Pin to the top-left corner with `offset_max` controlling the child's
    /// size (which is otherwise zero because `min == max`).
    pub const TOP_LEFT: Self = Self {
        min: Vec2::ZERO,
        max: Vec2::ZERO,
        offset_min: Vec2::ZERO,
        offset_max: Vec2::ZERO,
    };

    /// Pin to the top-right corner.
    pub const TOP_RIGHT: Self = Self {
        min: Vec2::new(1.0, 0.0),
        max: Vec2::new(1.0, 0.0),
        offset_min: Vec2::ZERO,
        offset_max: Vec2::ZERO,
    };

    /// Pin to the bottom-left corner.
    pub const BOTTOM_LEFT: Self = Self {
        min: Vec2::new(0.0, 1.0),
        max: Vec2::new(0.0, 1.0),
        offset_min: Vec2::ZERO,
        offset_max: Vec2::ZERO,
    };

    /// Pin to the bottom-right corner.
    pub const BOTTOM_RIGHT: Self = Self {
        min: Vec2::ONE,
        max: Vec2::ONE,
        offset_min: Vec2::ZERO,
        offset_max: Vec2::ZERO,
    };

    /// Dock to the top edge — full width, child height controlled by
    /// `offset_max.y`.
    pub const TOP: Self = Self {
        min: Vec2::ZERO,
        max: Vec2::new(1.0, 0.0),
        offset_min: Vec2::ZERO,
        offset_max: Vec2::ZERO,
    };

    /// Dock to the bottom edge — full width, child height controlled by
    /// `offset_min.y` (negative pushes the top edge upward).
    pub const BOTTOM: Self = Self {
        min: Vec2::new(0.0, 1.0),
        max: Vec2::ONE,
        offset_min: Vec2::ZERO,
        offset_max: Vec2::ZERO,
    };

    /// Dock to the left edge — full height, child width via `offset_max.x`.
    pub const LEFT: Self = Self {
        min: Vec2::ZERO,
        max: Vec2::new(0.0, 1.0),
        offset_min: Vec2::ZERO,
        offset_max: Vec2::ZERO,
    };

    /// Dock to the right edge — full height, child width via `offset_min.x`
    /// (negative widens the child leftward).
    pub const RIGHT: Self = Self {
        min: Vec2::new(1.0, 0.0),
        max: Vec2::ONE,
        offset_min: Vec2::ZERO,
        offset_max: Vec2::ZERO,
    };

    /// Construct an anchor with explicit corner pair (offsets zero).
    pub const fn between(min: Vec2, max: Vec2) -> Self {
        Self {
            min,
            max,
            offset_min: Vec2::ZERO,
            offset_max: Vec2::ZERO,
        }
    }

    /// Add fixed offsets in logical pixels to the resolved corners.
    pub const fn with_offsets(mut self, offset_min: Vec2, offset_max: Vec2) -> Self {
        self.offset_min = offset_min;
        self.offset_max = offset_max;
        self
    }
}

impl Default for Anchor {
    fn default() -> Self {
        Self::FILL
    }
}

/// Combined style controlling how a widget sizes, spaces, and aligns itself
/// inside its parent's slot.
///
/// `LayoutStyle` is deliberately one flat struct (rather than per-axis or
/// per-mode sub-structs) because every widget needs the same fields and most
/// of them are zero by default. Tests and authors can write
/// `LayoutStyle::default()` and only set the fields they care about.
#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
pub struct LayoutStyle {
    /// Horizontal sizing rule.
    pub width: Sizing,
    /// Vertical sizing rule.
    pub height: Sizing,
    /// Space *inside* this widget's rect, before children are arranged.
    pub padding: Insets,
    /// Space *outside* this widget's rect, reserved in the parent's layout
    /// before computing leftover space.
    pub margin: Insets,
    /// Horizontal alignment when this widget's resolved width is smaller than
    /// the slot the parent gave it.
    pub align_horizontal: Align,
    /// Vertical alignment when this widget's resolved height is smaller than
    /// the slot the parent gave it.
    pub align_vertical: Align,
    /// Anchor — only consulted when this widget's parent is an
    /// [`AnchorGroup`](super::widget::AnchorGroup); ignored otherwise.
    pub anchor: Anchor,
}

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

    #[test]
    fn default_layout_style_is_fit_content_fill_anchor() {
        let s = LayoutStyle::default();
        assert_eq!(s.width, Sizing::FitContent);
        assert_eq!(s.height, Sizing::FitContent);
        assert_eq!(s.padding, Insets::ZERO);
        assert_eq!(s.margin, Insets::ZERO);
        assert_eq!(s.align_horizontal, Align::Start);
        assert_eq!(s.align_vertical, Align::Start);
        assert_eq!(s.anchor, Anchor::FILL);
    }

    #[test]
    fn insets_helpers_are_correct() {
        let i = Insets::all(4.0);
        assert_eq!(i.left, 4.0);
        assert_eq!(i.right, 4.0);
        assert_eq!(i.top, 4.0);
        assert_eq!(i.bottom, 4.0);
        assert_eq!(i.horizontal(), 8.0);
        assert_eq!(i.vertical(), 8.0);

        let s = Insets::symmetric(2.0, 6.0);
        assert_eq!(s.horizontal(), 4.0);
        assert_eq!(s.vertical(), 12.0);

        let scaled = i.scaled(2.0);
        assert_eq!(scaled, Insets::all(8.0));
    }

    #[test]
    fn anchor_constants_match_doc_corners() {
        // FILL spans the whole parent.
        assert_eq!(Anchor::FILL.min, Vec2::ZERO);
        assert_eq!(Anchor::FILL.max, Vec2::ONE);
        // Each corner pin collapses to a point.
        assert_eq!(Anchor::TOP_LEFT.min, Anchor::TOP_LEFT.max);
        assert_eq!(Anchor::TOP_RIGHT.min, Vec2::new(1.0, 0.0));
        assert_eq!(Anchor::BOTTOM_LEFT.max, Vec2::new(0.0, 1.0));
        assert_eq!(Anchor::BOTTOM_RIGHT.min, Vec2::ONE);
        // Edge docks span one full axis.
        assert_eq!(Anchor::TOP.min, Vec2::ZERO);
        assert_eq!(Anchor::TOP.max, Vec2::new(1.0, 0.0));
        assert_eq!(Anchor::BOTTOM.min, Vec2::new(0.0, 1.0));
        assert_eq!(Anchor::LEFT.max, Vec2::new(0.0, 1.0));
        assert_eq!(Anchor::RIGHT.min, Vec2::new(1.0, 0.0));
    }

    #[test]
    fn layout_style_round_trips_through_ron() {
        let s = LayoutStyle {
            width: Sizing::Grow(2.0),
            height: Sizing::Fixed(48.0),
            padding: Insets::all(8.0),
            margin: Insets::symmetric(4.0, 2.0),
            align_horizontal: Align::Center,
            align_vertical: Align::End,
            anchor: Anchor::TOP_RIGHT.with_offsets(Vec2::new(-100.0, 0.0), Vec2::ZERO),
        };
        let text = ron::ser::to_string_pretty(&s, ron::ser::PrettyConfig::default()).unwrap();
        let decoded: LayoutStyle = ron::de::from_str(&text).unwrap();
        assert_eq!(s, decoded);
    }
}
