//! Action-returning Penpot material-slot shell shared by primitive and mesh inspectors.

use bevy_egui::egui;
use egui_phosphor_icons::icons;
use shared::{EditorAssetRef, MaterialShaderKind};

use super::layout::MaterialPanelGeometry;
use super::MaterialThumbnailPresentation;
use crate::ui::design_system::controls::status;
use crate::ui::design_system::typography::TypeRole;
use crate::ui::design_system::{self, DesignPalette, HEADER_HEIGHT};

mod header_actions;

use header_actions::{expanded_status_actions, overflow_menu, SLOT_STATUS_ACTION_WIDTH};

const SLOT_ACCENT_X: f32 = 8.0;
const SLOT_CARET_CENTER_X: f32 = 23.0;
const SLOT_PREVIEW_X: f32 = 35.0;
const SHADER_CONTROL_WIDTH: f32 = 220.0;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum MaterialLayerBadge {
    ModelSource,
    ModelDefault,
    ProjectDefault,
    DefaultGrid,
}

impl MaterialLayerBadge {
    pub(crate) const fn label(self) -> &'static str {
        match self {
            Self::ModelSource => "Model Source",
            Self::ModelDefault => "Model Default",
            Self::ProjectDefault => "Project Default",
            Self::DefaultGrid => "Default Grid",
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum MaterialHealth {
    Healthy,
    Dirty,
    Processing,
    Broken,
    ReadOnly,
}

impl MaterialHealth {
    fn presentation(self, palette: DesignPalette) -> (egui::Color32, &'static str, &'static str) {
        match self {
            Self::Healthy => (
                palette.healthy,
                "Compiled",
                "Material is resolved and up to date",
            ),
            Self::Dirty => (
                palette.warning,
                "Unsaved",
                "Material has unsaved in-memory changes",
            ),
            Self::Processing => (
                palette.accent,
                "Processing",
                "Derived material data is processing",
            ),
            Self::Broken => (
                palette.error,
                "Failed",
                "Material reference or processing failed",
            ),
            Self::ReadOnly => (
                palette.text_muted,
                "Read only",
                "This material cannot be edited in place",
            ),
        }
    }

    fn icon(self) -> egui_phosphor_icons::Icon {
        match self {
            Self::Healthy => icons::CHECK_CIRCLE,
            Self::Dirty => icons::PENCIL_SIMPLE,
            Self::Processing => icons::CIRCLE_NOTCH,
            Self::Broken => icons::WARNING_CIRCLE,
            Self::ReadOnly => icons::LOCK_SIMPLE,
        }
    }
}

#[derive(Debug, Clone)]
pub(crate) struct MaterialPickerCandidate {
    pub reference: EditorAssetRef,
    pub label: String,
    pub detail: String,
    pub thumbnail: MaterialThumbnailPresentation,
}

#[derive(Debug, Clone)]
pub(crate) struct MaterialSlotPanelViewModel {
    pub slot_id: String,
    pub label: String,
    pub path: Option<String>,
    pub shader: String,
    pub shader_kind: Option<MaterialShaderKind>,
    pub can_edit_shader: bool,
    pub thumbnail: MaterialThumbnailPresentation,
    pub inherited: Option<MaterialLayerBadge>,
    pub health: MaterialHealth,
    pub assigned: Option<EditorAssetRef>,
    pub effective: Option<EditorAssetRef>,
    pub candidates: Vec<MaterialPickerCandidate>,
    pub drop_candidate: Option<MaterialPickerCandidate>,
    pub invalid_drop_reason: Option<String>,
    pub can_locate: bool,
    pub can_clear: bool,
    pub can_extract: bool,
    pub can_create_instance: bool,
}

#[derive(Debug, Clone, PartialEq)]
pub(crate) enum MaterialSlotAction {
    Assign {
        reference: EditorAssetRef,
        from_drop: bool,
    },
    BrowseLibrary,
    Locate,
    Clear,
    ExtractEditable,
    CreateInstance,
    SetShader(MaterialShaderKind),
}

#[derive(Debug, Clone, PartialEq)]
pub(crate) struct MaterialSlotPanelAction {
    pub slot_id: String,
    pub action: MaterialSlotAction,
}

#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct MaterialsSectionViewModel {
    pub slot_ids: Vec<String>,
}

pub(crate) struct MaterialSlotPanelResponse {
    pub actions: Vec<MaterialSlotPanelAction>,
}

pub(crate) fn materials_section<R>(
    ui: &mut egui::Ui,
    _view: &MaterialsSectionViewModel,
    add_slots: impl FnOnce(&mut egui::Ui) -> R,
) -> R {
    let start = ui.next_widget_position();
    let expanded = actor_materials_rect(ui.max_rect(), start.y);
    let mut child = ui.new_child(
        egui::UiBuilder::new()
            .max_rect(expanded)
            .layout(egui::Layout::top_down(egui::Align::Min)),
    );
    child.set_clip_rect(ui.clip_rect());
    let result = design_system::scope(&mut child, |ui| {
        let palette = design_system::palette(ui);
        ui.allocate_ui_with_layout(
            egui::vec2(
                ui.available_width().max(1.0),
                design_system::MATERIALS_HEADING_HEIGHT,
            ),
            egui::Layout::left_to_right(egui::Align::Center),
            |ui| {
                ui.label(
                    TypeRole::Title
                        .text("Materials")
                        .color(palette.text_primary),
                );
            },
        );
        ui.add_space(8.0);
        add_slots(ui)
    });
    // `min_rect` is not a safe content-height signal for a child hosted by a ScrollArea: nested
    // frames may temporarily inherit the parent's virtual extent even though their layout cursor
    // has only advanced through finite authored rows. Advancing from that rect creates a phantom
    // tail after multi-slot renderers and lets the Inspector scrollbar enter an empty region.
    // The cursor is the authoritative end of this top-down section.
    let used_bottom = materials_used_bottom(child.next_widget_position().y, start.y);
    ui.advance_cursor_after_rect(egui::Rect::from_min_max(
        egui::pos2(ui.max_rect().left(), start.y),
        egui::pos2(ui.max_rect().right(), used_bottom),
    ));
    result
}

fn actor_materials_rect(body: egui::Rect, start_y: f32) -> egui::Rect {
    // A ScrollArea content Ui has an effectively unbounded max-rect in its scrolling direction.
    // Carrying that bottom edge into the child gives the Materials section a phantom multi-screen
    // height even though its painted contents are finite. Seed only the exported width here; egui
    // grows the zero-height child as headings and slots are actually allocated.
    egui::Rect::from_min_size(
        egui::pos2(body.left(), start_y),
        egui::vec2(body.width(), 0.0),
    )
}

fn materials_used_bottom(cursor_y: f32, start_y: f32) -> f32 {
    if cursor_y.is_finite() {
        cursor_y.max(start_y)
    } else {
        start_y
    }
}

pub(crate) fn material_slot_panel(
    ui: &mut egui::Ui,
    id: impl std::hash::Hash,
    view: &MaterialSlotPanelViewModel,
    content_ui: impl FnOnce(&mut egui::Ui),
    diagnostics_ui: impl FnOnce(&mut egui::Ui),
) -> MaterialSlotPanelResponse {
    design_system::scope(ui, |ui| {
        let palette = design_system::palette(ui);
        let id = ui.make_persistent_id(id);
        let open_id = id.with("parameters_open");
        let mut parameters_open = ui
            .ctx()
            .data_mut(|data| data.get_persisted::<bool>(open_id))
            .unwrap_or(true);
        let mut slot_actions = Vec::new();

        egui::Frame::new()
            .fill(palette.panel)
            .stroke(egui::Stroke::new(1.0_f32, palette.border))
            .corner_radius(egui::CornerRadius::same(8))
            .show(ui, |ui| {
                slot_header(
                    ui,
                    id,
                    view,
                    &mut parameters_open,
                    &mut slot_actions,
                    diagnostics_ui,
                );
                if parameters_open {
                    parameter_body(ui, content_ui);
                }
                ui.ctx()
                    .data_mut(|data| data.insert_persisted(open_id, parameters_open));
            });
        MaterialSlotPanelResponse {
            actions: slot_actions
                .into_iter()
                .map(|action| MaterialSlotPanelAction {
                    slot_id: view.slot_id.clone(),
                    action,
                })
                .collect(),
        }
    })
}

fn slot_header(
    ui: &mut egui::Ui,
    id: egui::Id,
    view: &MaterialSlotPanelViewModel,
    open: &mut bool,
    actions: &mut Vec<MaterialSlotAction>,
    diagnostics_ui: impl FnOnce(&mut egui::Ui),
) {
    let palette = design_system::palette(ui);
    let height = if *open {
        HEADER_HEIGHT
    } else {
        MaterialPanelGeometry::REFERENCE.collapsed_header
    };
    let width = ui.available_width().max(1.0);
    let (rect, response) = ui.allocate_exact_size(egui::vec2(width, height), egui::Sense::click());
    ui.painter().rect_filled(rect, 7.0, palette.recessed);
    ui.painter().rect_filled(
        egui::Rect::from_min_size(
            rect.min + egui::vec2(SLOT_ACCENT_X, 8.0),
            egui::vec2(3.0, height - 16.0),
        ),
        2.0,
        palette.accent,
    );
    let caret_rect = egui::Rect::from_center_size(
        egui::pos2(rect.left() + SLOT_CARET_CENTER_X, rect.center().y),
        egui::vec2(18.0, 28.0),
    );
    let caret_icon = if *open {
        icons::CARET_DOWN
    } else {
        icons::CARET_RIGHT
    };
    let caret = ui.put(
        caret_rect,
        egui::Button::new(
            egui::RichText::new(caret_icon.as_str()).font(egui::FontId::new(
                12.0,
                egui::FontFamily::Name("phosphor-bold".into()),
            )),
        )
        .frame(false),
    );
    if caret.clicked() {
        *open = !*open;
    }
    let preview_size = if *open { 64.0 } else { 32.0 };
    let preview_rect = egui::Rect::from_min_size(
        egui::pos2(
            rect.left() + SLOT_PREVIEW_X,
            rect.center().y - preview_size * 0.5,
        ),
        egui::vec2(preview_size, preview_size),
    );
    let preview = material_thumbnail_at(ui, view, preview_rect);
    let action_width = SLOT_STATUS_ACTION_WIDTH;
    let identity_left = preview_rect.right() + 6.0;
    let actions_left = rect.right() - action_width - 8.0;
    let identity_right = (identity_left + design_system::MATERIAL_IDENTITY_WIDTH)
        .min(actions_left)
        .max(identity_left + 1.0);
    let identity_rect = egui::Rect::from_min_max(
        egui::pos2(identity_left, rect.top() + if *open { 8.0 } else { 7.0 }),
        egui::pos2(
            identity_right,
            rect.bottom() - if *open { 8.0 } else { 7.0 },
        ),
    );
    let identity = ui
        .scope_builder(egui::UiBuilder::new().max_rect(identity_rect), |ui| {
            ui.set_clip_rect(identity_rect.intersect(ui.clip_rect()));
            if *open {
                material_identity(ui, view, actions)
            } else {
                compact_material_identity(ui, view)
            }
        })
        .inner;
    let actions_rect = egui::Rect::from_min_size(
        egui::pos2(actions_left, rect.top() + if *open { 9.0 } else { 7.0 }),
        egui::vec2(action_width, if *open { 64.0 } else { height - 14.0 }),
    );
    if *open {
        expanded_status_actions(ui, actions_rect, view, actions, diagnostics_ui, palette);
    } else {
        ui.scope_builder(egui::UiBuilder::new().max_rect(actions_rect), |ui| {
            ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
                overflow_menu(ui, view, actions, diagnostics_ui);
                let (color, label, tooltip) = view.health.presentation(palette);
                ui.horizontal(|ui| status(ui, view.health.icon(), color, label, tooltip));
            });
        });
    }
    if response.clicked() {
        if let Some(pointer) = ui.input(|input| input.pointer.interact_pos()) {
            if !caret_rect.contains(pointer)
                && !preview_rect.contains(pointer)
                && !identity_rect.contains(pointer)
                && !actions_rect.contains(pointer)
            {
                *open = !*open;
            }
        }
    }
    super::pickers::material_picker_popup(
        ui,
        id.with("material_picker"),
        &preview.union(identity),
        view,
        actions,
    );
    if response.hovered()
        && view.drop_candidate.is_some()
        && ui.input(|input| input.pointer.any_released())
    {
        if let Some(candidate) = view.drop_candidate.as_ref() {
            actions.push(MaterialSlotAction::Assign {
                reference: candidate.reference.clone(),
                from_drop: true,
            });
        }
    }
    if response.hovered() {
        if let Some(reason) = view.invalid_drop_reason.as_deref() {
            response.on_hover_text(reason);
        }
    }
}

fn compact_material_identity(
    ui: &mut egui::Ui,
    view: &MaterialSlotPanelViewModel,
) -> egui::Response {
    let palette = design_system::palette(ui);
    let name = ui
        .add(
            egui::Label::new(
                TypeRole::Section
                    .text(&view.label)
                    .color(palette.text_primary),
            )
            .truncate()
            .sense(egui::Sense::click()),
        )
        .on_hover_text(&view.label);
    let detail_text = view
        .inherited
        .map(MaterialLayerBadge::label)
        .unwrap_or(view.shader.as_str());
    let detail = ui
        .add(
            egui::Label::new(TypeRole::Small.text(detail_text).color(palette.text_muted))
                .truncate()
                .sense(egui::Sense::click()),
        )
        .on_hover_text(detail_text);
    name.union(detail)
}

fn parameter_body(ui: &mut egui::Ui, content_ui: impl FnOnce(&mut egui::Ui)) {
    let palette = design_system::palette(ui);
    egui::Frame::new()
        .fill(palette.panel)
        .inner_margin(egui::Margin {
            left: 19,
            right: 8,
            top: 8,
            bottom: 8,
        })
        .show(ui, |ui| {
            ui.label(TypeRole::Caption.text("PARAMETERS").color(palette.accent));
            let spine_x = ui.min_rect().left() - 7.0;
            let spine_top = ui.cursor().top();
            content_ui(ui);
            let spine_bottom = ui.min_rect().bottom().max(spine_top);
            ui.painter().vline(
                spine_x,
                spine_top..=spine_bottom,
                egui::Stroke::new(1.0_f32, palette.accent),
            );
        });
}

fn material_identity(
    ui: &mut egui::Ui,
    view: &MaterialSlotPanelViewModel,
    actions: &mut Vec<MaterialSlotAction>,
) -> egui::Response {
    let palette = design_system::palette(ui);
    let name = ui
        .add(
            egui::Label::new(
                TypeRole::Section
                    .text(&view.label)
                    .color(palette.text_primary),
            )
            .truncate()
            .sense(egui::Sense::click()),
        )
        .on_hover_text(&view.label);
    let path = view.path.as_deref().unwrap_or("Engine built-in material");
    let identity = view.inherited.map_or_else(
        || path.to_string(),
        |layer| format!("{path} · {}", layer.label()),
    );
    let detail = ui
        .add(
            egui::Label::new(TypeRole::Small.text(&identity).color(palette.text_muted))
                .truncate()
                .sense(egui::Sense::click()),
        )
        .on_hover_text(&identity);
    if view.can_edit_shader {
        if let Some(mut kind) = view.shader_kind {
            egui::ComboBox::from_id_salt("material_identity_shader_kind")
                .selected_text(&view.shader)
                .width(SHADER_CONTROL_WIDTH.min(ui.available_width().max(1.0)))
                .show_ui(ui, |ui| {
                    ui.selectable_value(&mut kind, MaterialShaderKind::StandardLit, "Standard Lit");
                    ui.selectable_value(&mut kind, MaterialShaderKind::Unlit, "Unlit");
                    ui.selectable_value(&mut kind, MaterialShaderKind::Custom, "Custom Surface");
                });
            if Some(kind) != view.shader_kind {
                actions.push(MaterialSlotAction::SetShader(kind));
            }
        }
    } else {
        ui.add_sized(
            [
                SHADER_CONTROL_WIDTH.min(ui.available_width().max(1.0)),
                22.0,
            ],
            egui::Label::new(
                TypeRole::Body
                    .text(&view.shader)
                    .color(palette.text_secondary),
            ),
        );
    }
    name.union(detail)
}

fn material_thumbnail_at(
    ui: &mut egui::Ui,
    view: &MaterialSlotPanelViewModel,
    rect: egui::Rect,
) -> egui::Response {
    let palette = design_system::palette(ui);
    let response = ui.interact(
        rect,
        ui.make_persistent_id(("material_preview", &view.slot_id)),
        egui::Sense::click(),
    );
    let size = rect.width();
    let cell = size / 4.0;
    for row in 0..4 {
        for column in 0..4 {
            ui.painter().rect_filled(
                egui::Rect::from_min_size(
                    rect.min + egui::vec2(column as f32 * cell, row as f32 * cell),
                    egui::vec2(cell, cell),
                )
                .intersect(rect),
                0.0,
                if (row + column) % 2 == 0 {
                    palette.checker
                } else {
                    palette.recessed
                },
            );
        }
    }
    match view.thumbnail {
        MaterialThumbnailPresentation::Ready(texture) => {
            ui.painter().image(
                texture,
                rect.shrink(2.0),
                egui::Rect::from_min_max(egui::Pos2::ZERO, egui::pos2(1.0, 1.0)),
                egui::Color32::WHITE,
            );
        }
        MaterialThumbnailPresentation::Pending => {
            ui.painter().text(
                rect.center(),
                egui::Align2::CENTER_CENTER,
                icons::CIRCLE_NOTCH.as_str(),
                egui::FontId::new(22.0, egui::FontFamily::Name("phosphor-regular".into())),
                palette.text_secondary,
            );
        }
        MaterialThumbnailPresentation::Failed => {
            ui.painter().text(
                rect.center(),
                egui::Align2::CENTER_CENTER,
                icons::WARNING.as_str(),
                egui::FontId::new(22.0, egui::FontFamily::Name("phosphor-regular".into())),
                palette.error,
            );
        }
    }
    ui.painter().rect_stroke(
        rect,
        4.0,
        egui::Stroke::new(1.0_f32, palette.border),
        egui::StrokeKind::Inside,
    );
    response.on_hover_text(format!("Open {} material picker", view.label))
}

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

    fn view() -> MaterialSlotPanelViewModel {
        MaterialSlotPanelViewModel {
            slot_id: "slot:test".into(),
            label: "M_Wood_Planks".into(),
            path: Some("materials/wood/M_Wood_Planks.mat".into()),
            shader: "Standard Lit".into(),
            shader_kind: Some(MaterialShaderKind::StandardLit),
            can_edit_shader: true,
            thumbnail: MaterialThumbnailPresentation::Pending,
            inherited: None,
            health: MaterialHealth::Healthy,
            assigned: None,
            effective: None,
            candidates: Vec::new(),
            drop_candidate: None,
            invalid_drop_reason: None,
            can_locate: true,
            can_clear: true,
            can_extract: false,
            can_create_instance: true,
        }
    }

    #[test]
    fn slot_header_insets_match_the_current_penpot_export() {
        assert_eq!(SLOT_ACCENT_X, 8.0);
        assert_eq!(SLOT_CARET_CENTER_X, 23.0);
        assert_eq!(SLOT_PREVIEW_X, 35.0);
        assert_eq!(SLOT_PREVIEW_X - (SLOT_CARET_CENTER_X + 6.0), 6.0);
        assert_eq!(SLOT_STATUS_ACTION_WIDTH, 140.0);
        assert_eq!(SHADER_CONTROL_WIDTH, 220.0);
    }

    #[test]
    fn actor_component_body_preserves_penpot_slot_widths() {
        let wide_body = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(596.0, 700.0));
        let compact_body = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(396.0, 700.0));

        let wide = actor_materials_rect(wide_body, 32.0);
        let compact = actor_materials_rect(compact_body, 32.0);

        assert_eq!(wide.width(), 596.0);
        assert_eq!(compact.width(), 396.0);
        assert_eq!(wide.left(), 0.0);
        assert_eq!(compact.left(), 0.0);
        assert_eq!(wide.top(), 32.0);
        assert_eq!(compact.top(), 32.0);
        assert_eq!(wide.height(), 0.0);
        assert_eq!(compact.height(), 0.0);
    }

    #[test]
    fn materials_height_uses_the_finite_layout_cursor_not_a_virtual_scroll_extent() {
        assert_eq!(materials_used_bottom(948.0, 320.0), 948.0);
        assert_eq!(materials_used_bottom(240.0, 320.0), 320.0);
        assert_eq!(materials_used_bottom(f32::INFINITY, 320.0), 320.0);
    }

    #[test]
    fn penpot_direct_assignment_has_no_redundant_explicit_badge() {
        assert!(view().inherited.is_none());
    }

    #[test]
    fn penpot_assignment_action_carries_exact_stable_reference() {
        let reference = EditorAssetRef::new("asset-id", "material:source", "M_Wood");
        let action = MaterialSlotPanelAction {
            slot_id: "slot:test".into(),
            action: MaterialSlotAction::Assign {
                reference: reference.clone(),
                from_drop: false,
            },
        };
        assert_eq!(
            action,
            MaterialSlotPanelAction {
                slot_id: "slot:test".into(),
                action: MaterialSlotAction::Assign {
                    reference,
                    from_drop: false,
                },
            }
        );
    }

    #[test]
    fn penpot_supported_material_states_share_one_view_model() {
        for health in [
            MaterialHealth::Healthy,
            MaterialHealth::Dirty,
            MaterialHealth::Processing,
            MaterialHealth::Broken,
            MaterialHealth::ReadOnly,
        ] {
            let mut view = view();
            view.health = health;
            let palette = DesignPalette::from(crate::ui::theme::BLACKSITE_PALETTE);
            assert!(!view.health.presentation(palette).1.is_empty());
        }
    }

    #[test]
    fn penpot_material_statuses_use_semantic_phosphor_icons() {
        assert_eq!(MaterialHealth::Healthy.icon(), icons::CHECK_CIRCLE);
        assert_eq!(MaterialHealth::Dirty.icon(), icons::PENCIL_SIMPLE);
        assert_eq!(MaterialHealth::Processing.icon(), icons::CIRCLE_NOTCH);
        assert_eq!(MaterialHealth::Broken.icon(), icons::WARNING_CIRCLE);
        assert_eq!(MaterialHealth::ReadOnly.icon(), icons::LOCK_SIMPLE);
    }
}
