//! Texture/channel controls shared by Material and Material Instance input tables.

use super::*;

use crate::design_system;
use crate::design_system::typography::TypeRole;
use egui_phosphor_icons::icons;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TextureFieldAdornment {
    TypedThumbnail,
    BrokenReference,
    Empty,
}

fn texture_field_adornment(
    presentation: Option<MaterialThumbnailPresentation>,
    current_present: bool,
) -> TextureFieldAdornment {
    if presentation.is_some() {
        TextureFieldAdornment::TypedThumbnail
    } else if current_present {
        TextureFieldAdornment::BrokenReference
    } else {
        TextureFieldAdornment::Empty
    }
}

pub(super) fn material_property_texture_binding(
    textures: &[MaterialTextureBinding],
    property: &MaterialInputDesc,
) -> MaterialTextureBinding {
    textures
        .iter()
        .find(|texture| texture.name == property.name)
        .cloned()
        .unwrap_or_else(|| MaterialTextureBinding {
            name: property.name.clone(),
            texture: None,
            channel: property
                .texture
                .as_ref()
                .map_or(TextureChannel::Rgba, |desc| desc.default_channel),
        })
}

pub(super) fn upsert_material_texture_binding(
    textures: &mut Vec<MaterialTextureBinding>,
    binding: MaterialTextureBinding,
) {
    if let Some(existing) = textures
        .iter_mut()
        .find(|texture| texture.name == binding.name)
    {
        *existing = binding;
    } else {
        textures.push(binding);
    }
}

pub(super) fn material_texture_channel_ui(
    ui: &mut egui::Ui,
    property: &MaterialInputDesc,
    binding: &mut MaterialTextureBinding,
) {
    let allow_override = property
        .texture
        .as_ref()
        .is_some_and(|texture| texture.allow_channel_override);
    if !allow_override {
        return;
    }
    egui::ComboBox::from_id_salt(("texture_channel", &property.name))
        .selected_text(texture_channel_label(binding.channel))
        .width(48.0)
        .show_ui(ui, |ui| {
            for channel in [
                TextureChannel::R,
                TextureChannel::G,
                TextureChannel::B,
                TextureChannel::A,
            ] {
                ui.selectable_value(
                    &mut binding.channel,
                    channel,
                    texture_channel_label(channel),
                );
            }
        });
}

pub(super) fn texture_channel_label(channel: TextureChannel) -> &'static str {
    match channel {
        TextureChannel::R => "R",
        TextureChannel::G => "G",
        TextureChannel::B => "B",
        TextureChannel::A => "A",
        TextureChannel::Rgb => "RGB",
        TextureChannel::Rgba => "RGBA",
    }
}

pub fn ensure_material_texture_binding<'a>(
    textures: &'a mut Vec<MaterialTextureBinding>,
    name: &str,
) -> &'a mut MaterialTextureBinding {
    let index = textures
        .iter()
        .position(|texture| texture.name == name)
        .unwrap_or_else(|| {
            textures.push(MaterialTextureBinding {
                name: name.to_string(),
                texture: None,
                channel: TextureChannel::Rgba,
            });
            textures.len() - 1
        });
    &mut textures[index]
}

pub(super) fn texture_binding_ui(
    ui: &mut egui::Ui,
    binding: &mut MaterialTextureBinding,
    texture_assets: &[MaterialTextureCandidate],
    drop_candidate: Option<&MaterialTextureCandidate>,
    inline_actions: bool,
) -> TextureSlotResponse {
    let response = texture_slot_ui(
        ui,
        format!("binding_{}", binding.name),
        TextureSlotSelection::Reference(binding.texture.as_ref()),
        "(none)",
        texture_assets,
        drop_candidate,
        inline_actions,
    );
    if response.clear {
        binding.texture = None;
    } else if let Some(candidate) = response.selected.as_ref() {
        binding.texture = Some(candidate.reference.clone());
    }
    response
}

pub(super) fn texture_slot_ui(
    ui: &mut egui::Ui,
    slot_id: impl std::hash::Hash,
    current: TextureSlotSelection<'_>,
    empty_label: &str,
    candidates: &[MaterialTextureCandidate],
    drop_candidate: Option<&MaterialTextureCandidate>,
    inline_actions: bool,
) -> TextureSlotResponse {
    let palette = design_system::palette(ui);
    let mut result = TextureSlotResponse::default();
    ui.push_id(slot_id, |ui| {
        let width = ui.available_width().max(1.0);
        let (outer, _) = ui.allocate_exact_size(egui::vec2(width, 22.0), egui::Sense::hover());
        let group = outer;
        let field_width = if inline_actions {
            (group.width() - 60.0).max(1.0)
        } else {
            (group.width() - 30.0).max(1.0)
        };
        let field_rect = egui::Rect::from_min_size(group.min, egui::vec2(field_width, 22.0));
        let locate_rect = egui::Rect::from_min_size(
            group.min + egui::vec2(field_width + 6.0, 0.0),
            egui::vec2(24.0, 22.0),
        );
        let clear_rect = inline_actions.then(|| {
            egui::Rect::from_min_size(
                group.min + egui::vec2(field_width + 36.0, 0.0),
                egui::vec2(24.0, 22.0),
            )
        });
        let selected_candidate = candidates.iter().find(|candidate| match current {
            TextureSlotSelection::Reference(reference) => reference.is_some_and(|reference| {
                reference.asset_id == candidate.reference.asset_id
                    && reference.sub_asset_id == candidate.reference.sub_asset_id
            }),
        });
        let current_present = match current {
            TextureSlotSelection::Reference(reference) => reference.is_some(),
        };
        let display_label = selected_candidate
            .map(|candidate| candidate.label.as_str())
            .or_else(|| match current {
                TextureSlotSelection::Reference(reference) => {
                    reference.map(|reference| reference.label.as_str())
                }
            })
            .unwrap_or(empty_label);
        let presentation = selected_candidate.map(|candidate| candidate.thumbnail);
        let adornment = texture_field_adornment(presentation, current_present);
        let label_color = if current_present {
            palette.text_primary
        } else {
            palette.text_muted
        };
        ui.painter().rect(
            field_rect,
            4.0,
            palette.control,
            egui::Stroke::new(1.0_f32, palette.border),
            egui::StrokeKind::Inside,
        );
        let field = ui.interact(
            field_rect,
            ui.make_persistent_id("texture_field"),
            egui::Sense::click(),
        );
        let image_rect = egui::Rect::from_center_size(
            egui::pos2(field_rect.left() + 14.0, field_rect.center().y),
            egui::vec2(16.0, 16.0),
        );
        match selected_candidate.map(|candidate| candidate.thumbnail) {
            Some(MaterialThumbnailPresentation::Ready(texture)) => {
                ui.painter().image(
                    texture,
                    image_rect,
                    egui::Rect::from_min_max(egui::Pos2::ZERO, egui::pos2(1.0, 1.0)),
                    egui::Color32::WHITE,
                );
            }
            Some(MaterialThumbnailPresentation::Pending) => {
                paint_field_icon(ui, image_rect, icons::CIRCLE_NOTCH, palette.text_muted);
            }
            Some(MaterialThumbnailPresentation::Failed) => {
                paint_field_icon(ui, image_rect, icons::WARNING, palette.error);
            }
            Some(MaterialThumbnailPresentation::Empty) => {
                paint_field_icon(ui, image_rect, icons::IMAGE, palette.text_muted);
            }
            None => paint_field_icon(
                ui,
                image_rect,
                if adornment == TextureFieldAdornment::BrokenReference {
                    icons::WARNING
                } else {
                    icons::IMAGE
                },
                if adornment == TextureFieldAdornment::BrokenReference {
                    palette.error
                } else {
                    palette.text_muted
                },
            ),
        }
        let text_rect = egui::Rect::from_min_max(
            egui::pos2(field_rect.left() + 27.0, field_rect.top()),
            egui::pos2(field_rect.right() - 6.0, field_rect.bottom()),
        );
        // `Label::truncate` centers its galley when forced into the full fixed field rectangle.
        // Paint the truncated galley at the Penpot text origin instead: the typed thumbnail and
        // the asset name then form one stable left-aligned identity regardless of field width.
        let galley =
            egui::WidgetText::from(TypeRole::AssetLabel.text(display_label).color(label_color))
                .into_galley(
                    ui,
                    Some(egui::TextWrapMode::Truncate),
                    text_rect.width(),
                    egui::FontSelection::Default,
                );
        let text_pos = egui::pos2(
            text_rect.left(),
            text_rect.center().y - galley.size().y * 0.5,
        );
        let text_painter = ui
            .painter()
            .with_clip_rect(text_rect.intersect(ui.clip_rect()));
        text_painter.galley(text_pos, galley, label_color);
        field.clone().on_hover_text(display_label);
        let drop_hovered = drop_candidate.is_some() && field.hovered();
        if drop_hovered {
            ui.painter().rect_stroke(
                field_rect,
                4.0,
                egui::Stroke::new(2.0_f32, palette.accent),
                egui::StrokeKind::Inside,
            );
        }
        super::pickers::texture_picker_popup(
            ui,
            field.id.with("texture_picker"),
            &field,
            candidates,
            selected_candidate,
            &mut result,
        );
        if let Some(clear_rect) = clear_rect {
            let locate = ui.put(
                locate_rect,
                egui::Button::new(icon_text(icons::CROSSHAIR, 14.0))
                    .min_size(locate_rect.size())
                    .fill(palette.control)
                    .stroke(egui::Stroke::new(1.0_f32, palette.border)),
            );
            if selected_candidate.is_some() && locate.clicked() {
                result.locate = selected_candidate.map(|candidate| candidate.locate.clone());
            }
            locate.on_hover_text(if selected_candidate.is_some() {
                "Locate texture in Content Browser"
            } else {
                "No texture to locate"
            });
            let clear = ui.put(
                clear_rect,
                egui::Button::new(icon_text(icons::X, 14.0))
                    .min_size(clear_rect.size())
                    .fill(palette.control)
                    .stroke(egui::Stroke::new(1.0_f32, palette.border)),
            );
            if current_present && clear.clicked() {
                result.clear = true;
            }
            clear.on_hover_text(if current_present {
                "Clear texture"
            } else {
                "No texture to clear"
            });
        } else {
            transient_texture_actions(
                ui,
                locate_rect,
                selected_candidate,
                current_present,
                &mut result,
            );
        }
        if drop_hovered && ui.input(|input| input.pointer.any_released()) {
            if let Some(candidate) = drop_candidate {
                result.selected = Some(candidate.clone());
                result.accepted_drop = true;
            }
        }
    });
    result
}

fn transient_texture_actions(
    ui: &mut egui::Ui,
    rect: egui::Rect,
    selected_candidate: Option<&MaterialTextureCandidate>,
    current_present: bool,
    result: &mut TextureSlotResponse,
) {
    ui.scope_builder(egui::UiBuilder::new().max_rect(rect), |ui| {
        ui.set_clip_rect(rect.intersect(ui.clip_rect()));
        ui.menu_button(icon_text(icons::DOTS_THREE, 14.0), |ui| {
            if ui
                .add_enabled(selected_candidate.is_some(), egui::Button::new("Locate"))
                .clicked()
            {
                result.locate = selected_candidate.map(|candidate| candidate.locate.clone());
                ui.close();
            }
            if ui
                .add_enabled(current_present, egui::Button::new("Clear"))
                .clicked()
            {
                result.clear = true;
                ui.close();
            }
        })
        .response
        .on_hover_text("Texture actions");
    });
}

fn paint_field_icon(
    ui: &egui::Ui,
    rect: egui::Rect,
    icon: egui_phosphor_icons::Icon,
    color: egui::Color32,
) {
    ui.painter().text(
        rect.center(),
        egui::Align2::CENTER_CENTER,
        icon.as_str(),
        egui::FontId::new(12.0, egui::FontFamily::Name("phosphor-bold".into())),
        color,
    );
}

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

    #[test]
    fn ready_texture_uses_its_thumbnail_without_a_redundant_asset_glyph() {
        assert_eq!(
            texture_field_adornment(
                Some(MaterialThumbnailPresentation::Ready(
                    egui::TextureId::Managed(7)
                )),
                true,
            ),
            TextureFieldAdornment::TypedThumbnail
        );
    }

    #[test]
    fn unresolved_and_empty_texture_fields_keep_distinct_fallbacks() {
        assert_eq!(
            texture_field_adornment(None, true),
            TextureFieldAdornment::BrokenReference
        );
        assert_eq!(
            texture_field_adornment(None, false),
            TextureFieldAdornment::Empty
        );
    }
}
