//! Numeric Penpot v2.3.1 material-panel geometry shared by rendering and tests.

use egui;

use crate::design_system::{
    ADVANCED_COLLAPSED_HEIGHT, ADVANCED_EXPANDED_HEIGHT, COLLAPSED_HEADER_HEIGHT, HEADER_HEIGHT,
    INPUT_SECTION_HEADER_HEIGHT, MATERIAL_IDENTITY_WIDTH, MATERIAL_PREVIEW_SIZE, OUTER_PADDING,
    PARAMETER_ROW_HEIGHT, SECTION_GAP, SURFACE_SECTION_HEIGHT, UV_SECTION_HEIGHT,
};

/// Smallest canvas that can still contain the exported compact Surface controls.
///
/// The Penpot board owns a 372 px parameter section. In the actor Inspector, component-card
/// chrome can temporarily reduce that canvas by a few pixels even while the Inspector itself is
/// at its supported 420 px floor. The fixed compact controls only require 362 px, so keep the
/// intended two-line layout until that real containment limit and reserve `Transient` for actual
/// sub-minimum docking frames.
pub(super) const COMPACT_SECTION_WIDTH: f32 = 357.0;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum ResponsiveMaterialLayout {
    Reference,
    Compact,
    Transient,
}

impl ResponsiveMaterialLayout {
    pub(super) fn for_width(width: f32) -> Self {
        if width + f32::EPSILON >= crate::design_system::property_grid::WIDE_SECTION_WIDTH {
            Self::Reference
        } else if width + f32::EPSILON >= COMPACT_SECTION_WIDTH {
            Self::Compact
        } else {
            Self::Transient
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub(super) struct SurfaceSectionGeometry {
    pub height: f32,
    pub title: egui::Rect,
    pub blend: egui::Rect,
    pub packed: egui::Rect,
    pub double_sided: egui::Rect,
}

impl SurfaceSectionGeometry {
    pub(super) fn for_rect(rect: egui::Rect) -> Self {
        let local = |x: f32, y: f32, width: f32, height: f32| {
            let x = x.clamp(0.0, rect.width());
            let y = y.clamp(0.0, rect.height());
            egui::Rect::from_min_size(
                rect.min + egui::vec2(x, y),
                egui::vec2(
                    width.max(0.0).min(rect.width() - x),
                    height.max(0.0).min(rect.height() - y),
                ),
            )
        };
        match ResponsiveMaterialLayout::for_width(rect.width()) {
            ResponsiveMaterialLayout::Reference => Self {
                height: SURFACE_SECTION_HEIGHT,
                title: local(10.0, 10.0, 48.0, 26.0),
                blend: local(66.0, 10.0, 250.0, 26.0),
                packed: local(324.0, 11.0, 128.0, 24.0),
                double_sided: local(460.0, 12.0, 99.0, 22.0),
            },
            ResponsiveMaterialLayout::Compact => Self {
                height: crate::design_system::COMPACT_SURFACE_SECTION_HEIGHT,
                title: local(10.0, 10.0, 48.0, 26.0),
                blend: local(66.0, 10.0, 50.0, 26.0),
                packed: local(124.0, 11.0, 128.0, 24.0),
                double_sided: local(260.0, 12.0, 99.0, 22.0),
            },
            ResponsiveMaterialLayout::Transient => Self {
                height: 100.0,
                title: local(10.0, 10.0, 48.0, 26.0),
                blend: local(66.0, 10.0, (rect.width() - 76.0).clamp(1.0, 250.0), 26.0),
                packed: local(10.0, 42.0, (rect.width() - 20.0).clamp(0.0, 128.0), 24.0),
                double_sided: local(146.0, 43.0, (rect.width() - 156.0).clamp(0.0, 99.0), 22.0),
            },
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub(super) struct UvGroupGeometry {
    pub label: egui::Rect,
    pub x: egui::Rect,
    pub y: egui::Rect,
    pub reset: Option<egui::Rect>,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub(super) struct UvSectionGeometry {
    pub height: f32,
    pub title: egui::Rect,
    pub offset: UvGroupGeometry,
    pub tiling: UvGroupGeometry,
}

impl UvSectionGeometry {
    pub(super) fn for_rect(rect: egui::Rect) -> Self {
        let mode = ResponsiveMaterialLayout::for_width(rect.width());
        let title = egui::Rect::from_min_size(
            rect.min + egui::vec2(12.0, 7.0),
            egui::vec2(20.0_f32.min(rect.width()), 14.0_f32.min(rect.height())),
        );
        if mode == ResponsiveMaterialLayout::Transient {
            let group_width = (rect.width() - 20.0).max(1.0);
            return Self {
                height: 100.0,
                title,
                offset: uv_group(rect.min + egui::vec2(42.0, 7.0), group_width),
                tiling: uv_group(rect.min + egui::vec2(42.0, 47.0), group_width),
            };
        }
        if mode == ResponsiveMaterialLayout::Reference {
            Self {
                height: UV_SECTION_HEIGHT,
                title,
                offset: uv_group(rect.min + egui::vec2(104.667, 7.0), 165.0),
                tiling: uv_group(rect.min + egui::vec2(332.333, 7.0), 164.0),
            }
        } else {
            Self {
                height: crate::design_system::COMPACT_UV_SECTION_HEIGHT,
                title,
                offset: uv_group(rect.min + egui::vec2(118.0, 7.0), 165.0),
                tiling: uv_group(rect.min + egui::vec2(118.5, 47.0), 164.0),
            }
        }
    }
}

fn uv_group(origin: egui::Pos2, group_width: f32) -> UvGroupGeometry {
    let local = |x: f32, y: f32, control_width: f32, height: f32| {
        let x = x.clamp(0.0, group_width.max(0.0));
        egui::Rect::from_min_size(
            origin + egui::vec2(x, y),
            egui::vec2(control_width.max(0.0).min(group_width - x), height),
        )
    };
    let reset = (group_width >= 164.0).then(|| local(group_width - 24.0, 8.0, 24.0, 24.0));
    UvGroupGeometry {
        label: local(0.0, 10.0, 25.0, 20.0),
        x: local(30.0, 8.0, 50.0, 24.0),
        y: local(86.0, 8.0, 50.0, 24.0),
        reset,
    }
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub(super) struct MaterialPanelGeometry {
    pub header: f32,
    pub collapsed_header: f32,
    pub asset_zone: f32,
    pub surface: f32,
    pub input_header: f32,
    pub input_row: f32,
    pub uv: f32,
    pub advanced_collapsed: f32,
    pub advanced_expanded: f32,
    pub preview: f32,
    pub identity: f32,
    pub outer_padding: f32,
    pub section_gap: f32,
}

impl MaterialPanelGeometry {
    pub(super) const REFERENCE: Self = Self {
        header: HEADER_HEIGHT,
        collapsed_header: COLLAPSED_HEADER_HEIGHT,
        asset_zone: HEADER_HEIGHT,
        surface: SURFACE_SECTION_HEIGHT,
        input_header: INPUT_SECTION_HEADER_HEIGHT,
        input_row: PARAMETER_ROW_HEIGHT,
        uv: UV_SECTION_HEIGHT,
        advanced_collapsed: ADVANCED_COLLAPSED_HEIGHT,
        advanced_expanded: ADVANCED_EXPANDED_HEIGHT,
        preview: MATERIAL_PREVIEW_SIZE,
        identity: MATERIAL_IDENTITY_WIDTH,
        outer_padding: OUTER_PADDING,
        section_gap: SECTION_GAP,
    };

    #[cfg(test)]
    pub(super) const fn slot_width(inspector_width: f32) -> f32 {
        inspector_width - OUTER_PADDING * 2.0
    }

    #[cfg(test)]
    pub(super) const fn parameter_width(slot_width: f32) -> f32 {
        slot_width - 27.0
    }

    #[cfg(test)]
    pub(super) const fn inputs_height(compact: bool, primary_rows: usize) -> f32 {
        INPUT_SECTION_HEADER_HEIGHT
            + primary_rows as f32
                * if compact {
                    crate::design_system::COMPACT_PARAMETER_ROW_HEIGHT
                } else {
                    PARAMETER_ROW_HEIGHT
                }
    }
}

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

    #[test]
    fn final_penpot_reference_geometry_is_exact() {
        let geometry = MaterialPanelGeometry::REFERENCE;
        assert_eq!(geometry.header, 82.0);
        assert_eq!(geometry.collapsed_header, 52.0);
        assert_eq!(geometry.asset_zone, 82.0);
        assert_eq!(geometry.surface, 46.0);
        assert_eq!(geometry.input_header, 24.0);
        assert_eq!(geometry.input_row, 32.0);
        assert_eq!(geometry.uv, 54.0);
        assert_eq!(geometry.advanced_collapsed, 30.0);
        assert_eq!(geometry.advanced_expanded, 178.0);
        assert_eq!(geometry.preview, 64.0);
        assert_eq!(geometry.identity, 229.5);
        assert_eq!(620.0 - OUTER_PADDING * 2.0, 596.0);
        assert_eq!(420.0 - OUTER_PADDING * 2.0, 396.0);
        assert_eq!(MaterialPanelGeometry::slot_width(620.0), 596.0);
        assert_eq!(MaterialPanelGeometry::slot_width(420.0), 396.0);
        assert_eq!(MaterialPanelGeometry::parameter_width(596.0), 569.0);
        assert_eq!(MaterialPanelGeometry::parameter_width(396.0), 369.0);
        assert_eq!(MaterialPanelGeometry::inputs_height(false, 6), 216.0);
        assert_eq!(MaterialPanelGeometry::inputs_height(true, 6), 372.0);
    }

    #[test]
    fn surface_geometry_matches_both_exported_references() {
        let wide = SurfaceSectionGeometry::for_rect(egui::Rect::from_min_size(
            egui::Pos2::ZERO,
            egui::vec2(569.0, 46.0),
        ));
        assert_eq!(
            wide.title,
            egui::Rect::from_min_size(egui::pos2(10.0, 10.0), egui::vec2(48.0, 26.0))
        );
        assert_eq!(
            wide.blend,
            egui::Rect::from_min_size(egui::pos2(66.0, 10.0), egui::vec2(250.0, 26.0))
        );
        assert_eq!(
            wide.packed,
            egui::Rect::from_min_size(egui::pos2(324.0, 11.0), egui::vec2(128.0, 24.0))
        );
        assert_eq!(
            wide.double_sided,
            egui::Rect::from_min_size(egui::pos2(460.0, 12.0), egui::vec2(99.0, 22.0))
        );

        let narrow = SurfaceSectionGeometry::for_rect(egui::Rect::from_min_size(
            egui::Pos2::ZERO,
            egui::vec2(369.0, 46.0),
        ));
        assert_eq!(
            narrow.blend,
            egui::Rect::from_min_size(egui::pos2(66.0, 10.0), egui::vec2(50.0, 26.0))
        );
        assert_eq!(
            narrow.packed,
            egui::Rect::from_min_size(egui::pos2(124.0, 11.0), egui::vec2(128.0, 24.0))
        );
        assert_eq!(
            narrow.double_sided,
            egui::Rect::from_min_size(egui::pos2(260.0, 12.0), egui::vec2(99.0, 22.0))
        );
    }

    #[test]
    fn uv_geometry_centers_title_and_keeps_independent_resets() {
        for (width, height) in [(569.0, 54.0), (369.0, 94.0)] {
            let section = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(width, height));
            let geometry = UvSectionGeometry::for_rect(section);
            assert_eq!(geometry.title.min, egui::pos2(12.0, 7.0));
            assert_eq!(geometry.title.height(), 14.0);
            assert_eq!(geometry.offset.x.size(), egui::vec2(50.0, 24.0));
            assert_eq!(geometry.offset.y.size(), egui::vec2(50.0, 24.0));
            assert!(geometry.offset.reset.is_some());
            assert!(geometry.tiling.reset.is_some());
            for rect in [
                geometry.title,
                geometry.offset.label,
                geometry.offset.x,
                geometry.offset.y,
                geometry.offset.reset.expect("reference reset"),
                geometry.tiling.label,
                geometry.tiling.x,
                geometry.tiling.y,
                geometry.tiling.reset.expect("reference reset"),
            ] {
                assert!(section.contains_rect(rect));
                assert!(rect.is_finite());
            }
        }
    }

    #[test]
    fn transient_geometry_stays_finite_and_bounded() {
        let section = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(300.0, 100.0));
        let surface = SurfaceSectionGeometry::for_rect(section);
        let uv = UvSectionGeometry::for_rect(section);
        for rect in [
            surface.title,
            surface.blend,
            surface.packed,
            surface.double_sided,
        ] {
            assert!(rect.is_finite());
            assert!(section.contains_rect(rect));
        }
        for rect in [
            uv.title,
            uv.offset.label,
            uv.offset.x,
            uv.offset.y,
            uv.tiling.label,
            uv.tiling.x,
            uv.tiling.y,
        ] {
            assert!(rect.is_finite());
            assert!(section.contains_rect(rect));
        }
    }

    #[test]
    fn inspector_chrome_reduced_floor_keeps_the_penpot_compact_surface() {
        let section = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(363.0, 46.0));
        let surface = SurfaceSectionGeometry::for_rect(section);
        assert_eq!(surface.height, 46.0);
        assert_eq!(surface.packed.min, egui::pos2(124.0, 11.0));
        assert_eq!(surface.packed.max, egui::pos2(252.0, 35.0));
        assert_eq!(surface.double_sided.min, egui::pos2(260.0, 12.0));
    }
}
