use super::uv_transform::{material_vec2_value, set_material_vec2_value, uv_transform_section};
use super::*;

use crate::ui::design_system;
use crate::ui::design_system::color_picker::{color_control, color_control_with_intensity};
use crate::ui::design_system::controls::{
    scalar_control, section_frame, section_header, section_header_with_summary, switch,
};
use crate::ui::design_system::property_grid::PropertyGridGeometry;
use crate::ui::design_system::typography::TypeRole;

pub(crate) fn material_input_schema_editor(
    ui: &mut egui::Ui,
    schema: &MaterialInputSchema,
    inputs: &mut MaterialInputSet,
    texture_assets: &[MaterialTextureCandidate],
    drop_candidate: Option<&MaterialTextureCandidate>,
) -> MaterialInputEditorResponse {
    material_input_schema_editor_with_inheritance(
        ui,
        schema,
        inputs,
        None,
        texture_assets,
        drop_candidate,
    )
}

pub(super) fn material_input_schema_editor_with_inheritance(
    ui: &mut egui::Ui,
    schema: &MaterialInputSchema,
    inputs: &mut MaterialInputSet,
    inherited: Option<&MaterialInputSet>,
    texture_assets: &[MaterialTextureCandidate],
    drop_candidate: Option<&MaterialTextureCandidate>,
) -> MaterialInputEditorResponse {
    let mut response = MaterialInputEditorResponse::default();
    let has_standard_packed_inputs = ["occlusion", "roughness", "metallic"]
        .iter()
        .all(|name| schema.inputs.iter().any(|input| input.name == *name));
    let mut groups = schema.groups.clone();
    if groups.is_empty() {
        groups.push(shared::MaterialInputGroupDesc {
            id: String::new(),
            display_name: "Properties".into(),
            order: 0,
            advanced: false,
        });
    }
    groups.sort_by_key(|group| group.order);
    for group in groups {
        let mut properties = schema
            .inputs
            .iter()
            .filter(|property| {
                property.group == group.id
                    && matches!(
                        property.presentation,
                        shared::MaterialInputPresentation::Row
                    )
            })
            .collect::<Vec<_>>();
        let companions = schema
            .inputs
            .iter()
            .filter(|property| property.group == group.id)
            .filter_map(|property| match &property.presentation {
                shared::MaterialInputPresentation::Companion { owner } => {
                    Some((owner.as_str(), property))
                }
                shared::MaterialInputPresentation::Row => None,
            })
            .collect::<Vec<_>>();
        properties.sort_by_key(|property| property.order);
        if properties.is_empty() {
            continue;
        }
        if group.id == "uv_transform" {
            material_uv_transform_ui(ui, inputs, inherited);
            ui.add_space(8.0);
            continue;
        }
        let open_id = ui.make_persistent_id(("material_input_group_open", &group.id));
        let mut open = ui
            .ctx()
            .data_mut(|data| data.get_persisted::<bool>(open_id))
            .unwrap_or(!group.advanced);
        section_frame(ui).show(ui, |ui| {
            if group.id == "surface_inputs" {
                let summary = format!("{} bindings", properties.len());
                section_header_with_summary(ui, "Inputs", Some(&summary), &mut open);
            } else {
                section_header(ui, &group.display_name, &mut open);
            }
            if open {
                let rows = material_input_rows(
                    ui,
                    &properties,
                    &companions,
                    inputs,
                    texture_assets,
                    drop_candidate,
                );
                response.accepted_drop |= rows.accepted_drop;
                response.locate = response.locate.take().or(rows.locate);
                response.browse_library |= rows.browse_library;
            }
        });
        ui.ctx()
            .data_mut(|data| data.insert_persisted(open_id, open));
        ui.add_space(8.0);
    }
    if has_standard_packed_inputs {
        super::advanced_preview::unsupported_advanced_preview(ui);
    }
    response
}

fn material_uv_transform_ui(
    ui: &mut egui::Ui,
    inputs: &mut MaterialInputSet,
    inherited: Option<&MaterialInputSet>,
) {
    let mut offset = material_vec2_value(inputs, "uv_offset", Vec2::ZERO);
    let mut tiling = material_vec2_value(inputs, "uv_tiling", Vec2::ONE);
    let reset_offset = inherited.map_or(Vec2::ZERO, |inputs| {
        material_vec2_value(inputs, "uv_offset", Vec2::ZERO)
    });
    let reset_tiling = inherited.map_or(Vec2::ONE, |inputs| {
        material_vec2_value(inputs, "uv_tiling", Vec2::ONE)
    });
    let original = (offset, tiling);
    uv_transform_section(ui, &mut offset, &mut tiling, reset_offset, reset_tiling);
    if (offset, tiling) != original {
        set_material_vec2_value(inputs, "uv_offset", offset);
        set_material_vec2_value(inputs, "uv_tiling", tiling);
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum PackedMapMode {
    Arm,
    Separate,
}

pub(super) fn packed_map_mode(inputs: &MaterialInputSet) -> PackedMapMode {
    let channels = [
        ("occlusion", TextureChannel::R),
        ("roughness", TextureChannel::G),
        ("metallic", TextureChannel::B),
    ];
    if channels.iter().all(|(name, channel)| {
        inputs
            .texture(name)
            .is_some_and(|binding| binding.channel == *channel)
    }) {
        PackedMapMode::Arm
    } else {
        PackedMapMode::Separate
    }
}

pub(super) fn apply_arm_texture_preset(inputs: &mut MaterialInputSet) {
    let shared_texture = ["occlusion", "roughness", "metallic"]
        .iter()
        .find_map(|name| {
            inputs
                .texture(name)
                .and_then(|binding| binding.texture.clone())
        });
    for (name, channel) in [
        ("occlusion", TextureChannel::R),
        ("roughness", TextureChannel::G),
        ("metallic", TextureChannel::B),
    ] {
        let binding = ensure_material_texture_binding(&mut inputs.textures, name);
        binding.channel = channel;
        if let Some(texture) = shared_texture.clone() {
            binding.texture = Some(texture);
        }
    }
}

pub(super) fn apply_separate_texture_preset(inputs: &mut MaterialInputSet) {
    for name in ["occlusion", "roughness", "metallic"] {
        ensure_material_texture_binding(&mut inputs.textures, name).channel = TextureChannel::R;
    }
}

pub(super) fn material_input_rows(
    ui: &mut egui::Ui,
    properties: &[&MaterialInputDesc],
    companions: &[(&str, &MaterialInputDesc)],
    inputs: &mut MaterialInputSet,
    texture_assets: &[MaterialTextureCandidate],
    drop_candidate: Option<&MaterialTextureCandidate>,
) -> MaterialInputEditorResponse {
    let palette = design_system::palette(ui);
    let mut response = MaterialInputEditorResponse::default();
    let geometry = PropertyGridGeometry::for_width(ui.available_width());
    // Property rows are a contiguous Penpot table. Default egui item spacing otherwise adds four
    // pixels between every nominally 31 px row and breaks the reference geometry.
    ui.spacing_mut().item_spacing.y = 0.0;
    for (row, property) in properties.iter().enumerate() {
        let companion = companions
            .iter()
            .find_map(|(owner, companion)| (*owner == property.name).then_some(*companion));
        ui.push_id(("material_input_row", &property.name), |ui| {
            let width = ui.available_width().max(1.0);
            let (row_rect, _) = ui
                .allocate_exact_size(egui::vec2(width, geometry.row_height), egui::Sense::hover());
            ui.painter().rect_filled(
                row_rect,
                0.0,
                if row.is_multiple_of(2) {
                    palette.row_even
                } else {
                    palette.row_odd
                },
            );
            let rects = geometry.row_rects(row_rect);
            rect_ui(ui, rects.label, |ui| material_input_label(ui, property));
            let is_color = matches!(property.property_type, ShaderPropertyType::Color);
            rect_ui(
                ui,
                if is_color {
                    rects.color_value
                } else {
                    rects.value
                },
                |ui| material_input_value_ui(ui, property, companion, inputs),
            );
            if !is_color {
                rect_ui(ui, rects.channel, |ui| {
                    material_input_channel_ui(ui, property, inputs)
                });
            }
            if material_input_uses_texture(property) {
                let texture = rect_ui(ui, rects.texture_group, |ui| {
                    material_input_texture_ui(
                        ui,
                        property,
                        inputs,
                        texture_assets,
                        drop_candidate,
                        rects.inline_texture_actions,
                    )
                });
                response.accepted_drop |= texture.accepted_drop;
                response.locate = response.locate.take().or(texture.locate);
                response.browse_library |= texture.browse_library;
            }
        });
    }
    response
}

fn rect_ui<R>(
    ui: &mut egui::Ui,
    rect: egui::Rect,
    add_contents: impl FnOnce(&mut egui::Ui) -> R,
) -> R {
    ui.scope_builder(egui::UiBuilder::new().max_rect(rect), |ui| {
        ui.set_clip_rect(rect.intersect(ui.clip_rect()));
        add_contents(ui)
    })
    .inner
}

pub(super) fn material_input_label(ui: &mut egui::Ui, property: &MaterialInputDesc) {
    let palette = design_system::palette(ui);
    ui.add(
        egui::Label::new(
            TypeRole::Body
                .text(&property.display_name)
                .color(palette.text_secondary),
        )
        .truncate(),
    )
    .on_hover_text(&property.tooltip);
}

pub(super) fn material_input_uses_texture(property: &MaterialInputDesc) -> bool {
    property.texture.is_some() || matches!(property.property_type, ShaderPropertyType::Texture)
}

pub(super) fn material_input_value_ui(
    ui: &mut egui::Ui,
    property: &MaterialInputDesc,
    companion: Option<&MaterialInputDesc>,
    inputs: &mut MaterialInputSet,
) {
    let MaterialInputSet { values, .. } = inputs;
    ui.horizontal(|ui| {
        if matches!(property.property_type, ShaderPropertyType::Texture) {
            ui.label(egui::RichText::new("Texture").small().color(TEXT_DIM));
        } else {
            let default = property
                .default_value
                .clone()
                .unwrap_or_else(|| default_value_for_shader_property(&property.property_type));
            let mut value = values
                .iter()
                .find(|value| value.name == property.name)
                .map(|parameter| parameter.value.clone())
                .unwrap_or(default);
            if !parameter_value_matches_property(&value, &property.property_type) {
                value = default_value_for_shader_property(&property.property_type);
            }
            let original = value.clone();
            if matches!(property.property_type, ShaderPropertyType::Color)
                && companion.is_some_and(|input| input.name == "emissive_intensity")
            {
                let companion = companion.expect("checked companion");
                let companion_default = companion
                    .default_value
                    .clone()
                    .unwrap_or(MaterialParameterValue::Float(0.0));
                let mut companion_value = values
                    .iter()
                    .find(|value| value.name == companion.name)
                    .map(|parameter| parameter.value.clone())
                    .unwrap_or(companion_default);
                if !parameter_value_matches_property(&companion_value, &companion.property_type) {
                    companion_value = default_value_for_shader_property(&companion.property_type);
                }
                let original_companion = companion_value.clone();
                if let (
                    MaterialParameterValue::Color(color),
                    MaterialParameterValue::Float(intensity),
                ) = (&mut value, &mut companion_value)
                {
                    let mut rgba = [color.r, color.g, color.b, color.a];
                    let picker = color_control_with_intensity(
                        ui,
                        "material_emissive_color",
                        &property.display_name,
                        &mut rgba,
                        intensity,
                    );
                    if picker.changed {
                        *color = ColorDesc {
                            r: rgba[0],
                            g: rgba[1],
                            b: rgba[2],
                            a: rgba[3],
                        };
                    }
                }
                if value != original {
                    upsert_material_parameter(values, &property.name, value);
                }
                if companion_value != original_companion {
                    upsert_material_parameter(values, &companion.name, companion_value);
                }
            } else {
                material_parameter_control_ui(
                    ui,
                    &property.display_name,
                    &property.property_type,
                    &mut value,
                );
                if value != original {
                    upsert_material_parameter(values, &property.name, value);
                }
            }
        }
    });
}

fn upsert_material_parameter(
    values: &mut Vec<MaterialParameter>,
    name: &str,
    value: MaterialParameterValue,
) {
    if let Some(parameter) = values.iter_mut().find(|parameter| parameter.name == name) {
        parameter.value = value;
    } else {
        values.push(MaterialParameter {
            name: name.to_string(),
            value,
        });
    }
}

fn material_input_channel_ui(
    ui: &mut egui::Ui,
    property: &MaterialInputDesc,
    inputs: &mut MaterialInputSet,
) {
    let palette = design_system::palette(ui);
    if property
        .texture
        .as_ref()
        .is_some_and(|texture| texture.allow_channel_override)
    {
        let original = material_property_texture_binding(&inputs.textures, property);
        let mut binding = original.clone();
        material_texture_channel_ui(ui, property, &mut binding);
        if binding != original {
            upsert_material_texture_binding(&mut inputs.textures, binding);
        }
    } else {
        let channel = property.texture.as_ref().map(|texture| {
            if matches!(texture.semantic, shared::TextureSemantic::Color) {
                "RGB"
            } else {
                texture_channel_label(texture.default_channel)
            }
        });
        if let Some(channel) = channel {
            readonly_channel(ui, channel);
        } else {
            ui.label(TypeRole::Body.text("—").color(palette.text_muted));
        }
    }
}

pub(super) fn material_input_texture_ui(
    ui: &mut egui::Ui,
    property: &MaterialInputDesc,
    inputs: &mut MaterialInputSet,
    texture_assets: &[MaterialTextureCandidate],
    drop_candidate: Option<&MaterialTextureCandidate>,
    inline_actions: bool,
) -> TextureSlotResponse {
    if !material_input_uses_texture(property) {
        return TextureSlotResponse::default();
    }
    let original = material_property_texture_binding(&inputs.textures, property);
    let mut binding = original.clone();
    let response = texture_binding_ui(
        ui,
        &mut binding,
        texture_assets,
        drop_candidate,
        inline_actions,
    );
    if binding != original {
        upsert_material_texture_binding(&mut inputs.textures, binding);
    }
    response
}

pub(super) fn material_parameter_control_ui(
    ui: &mut egui::Ui,
    label: &str,
    property_type: &ShaderPropertyType,
    value: &mut MaterialParameterValue,
) {
    if !parameter_value_matches_property(value, property_type) {
        *value = default_value_for_shader_property(property_type);
    }
    match value {
        MaterialParameterValue::Bool(value) => {
            switch(ui, value);
        }
        MaterialParameterValue::Float(value) => {
            let (min, max) = match property_type {
                ShaderPropertyType::Float { min, max } => (*min, *max),
                _ => (None, None),
            };
            match (min, max) {
                (Some(min), Some(max)) => {
                    scalar_control(ui, label, value, min..=max, usize::from(max <= 1_000.0) * 2);
                }
                _ => {
                    ui.add(egui::DragValue::new(value).speed(0.01));
                }
            }
        }
        MaterialParameterValue::Vec2(value) => {
            ui.add(egui::DragValue::new(&mut value.x).speed(0.01).prefix("X "));
            ui.add(egui::DragValue::new(&mut value.y).speed(0.01).prefix("Y "));
        }
        MaterialParameterValue::Vec3(value) => {
            ui.add(egui::DragValue::new(&mut value.x).speed(0.01).prefix("X "));
            ui.add(egui::DragValue::new(&mut value.y).speed(0.01).prefix("Y "));
            ui.add(egui::DragValue::new(&mut value.z).speed(0.01).prefix("Z "));
        }
        MaterialParameterValue::Color(value) => {
            let mut rgba = [value.r, value.g, value.b, value.a];
            let response = color_control(ui, "material_color", label, &mut rgba);
            if response.changed {
                *value = ColorDesc {
                    r: rgba[0],
                    g: rgba[1],
                    b: rgba[2],
                    a: rgba[3],
                };
            }
        }
        MaterialParameterValue::Enum(value) => {
            let options = match property_type {
                ShaderPropertyType::Enum { options } => options.as_slice(),
                _ => &[],
            };
            egui::ComboBox::from_id_salt(("material_enum", value.as_str()))
                .selected_text(value.as_str())
                .show_ui(ui, |ui| {
                    for option in options {
                        ui.selectable_value(value, option.clone(), option);
                    }
                });
        }
    }
}

fn readonly_channel(ui: &mut egui::Ui, channel: &str) {
    let palette = design_system::palette(ui);
    let (rect, response) = ui.allocate_exact_size(egui::vec2(48.0, 22.0), egui::Sense::hover());
    ui.painter().rect(
        rect,
        4.0,
        palette.control,
        egui::Stroke::new(1.0_f32, palette.border),
        egui::StrokeKind::Inside,
    );
    ui.painter().text(
        rect.left_center() + egui::vec2(9.0, 0.0),
        egui::Align2::LEFT_CENTER,
        channel,
        TypeRole::Body.font(),
        palette.text_secondary,
    );
    ui.painter().text(
        rect.right_center() - egui::vec2(8.0, 0.0),
        egui::Align2::CENTER_CENTER,
        egui_phosphor_icons::icons::CARET_DOWN.as_str(),
        egui::FontId::new(9.0, egui::FontFamily::Name("phosphor-bold".into())),
        palette.text_muted,
    );
    response.on_hover_text("Channel is fixed by this material input");
}

pub(super) fn default_value_for_shader_property(
    property_type: &ShaderPropertyType,
) -> MaterialParameterValue {
    match property_type {
        ShaderPropertyType::Bool => MaterialParameterValue::Bool(false),
        ShaderPropertyType::Float { .. } => MaterialParameterValue::Float(0.0),
        ShaderPropertyType::Vec2 => MaterialParameterValue::Vec2(Vec2::ZERO),
        ShaderPropertyType::Vec3 => MaterialParameterValue::Vec3(Vec3::ZERO),
        ShaderPropertyType::Color => MaterialParameterValue::Color(ColorDesc::default()),
        ShaderPropertyType::Enum { options } => {
            MaterialParameterValue::Enum(options.first().cloned().unwrap_or_default())
        }
        ShaderPropertyType::Texture => MaterialParameterValue::Float(0.0),
    }
}

pub(super) fn parameter_value_matches_property(
    value: &MaterialParameterValue,
    property_type: &ShaderPropertyType,
) -> bool {
    matches!(
        (value, property_type),
        (MaterialParameterValue::Bool(_), ShaderPropertyType::Bool)
            | (
                MaterialParameterValue::Float(_),
                ShaderPropertyType::Float { .. }
            )
            | (MaterialParameterValue::Vec2(_), ShaderPropertyType::Vec2)
            | (MaterialParameterValue::Vec3(_), ShaderPropertyType::Vec3)
            | (MaterialParameterValue::Color(_), ShaderPropertyType::Color)
            | (
                MaterialParameterValue::Enum(_),
                ShaderPropertyType::Enum { .. }
            )
    )
}

pub(super) fn compact_text_edit(ui: &mut egui::Ui, label: &str, value: &mut String) {
    ui.horizontal(|ui| {
        ui.add_sized([92.0, 20.0], egui::Label::new(label));
        ui.add_sized(
            [fit_width(ui, 100.0, f32::INFINITY), 22.0],
            egui::TextEdit::singleline(value),
        );
    });
}

pub(super) fn shader_kind_picker(ui: &mut egui::Ui, kind: &mut MaterialShaderKind) {
    ui.horizontal(|ui| {
        ui.add_sized([92.0, 20.0], egui::Label::new("Shader"));
        shader_kind_combo(ui, kind, "asset_material_shader_kind");
    });
}

pub(super) fn shader_kind_combo(
    ui: &mut egui::Ui,
    kind: &mut MaterialShaderKind,
    salt: impl std::hash::Hash,
) {
    egui::ComboBox::from_id_salt(salt)
        .selected_text(match kind {
            MaterialShaderKind::StandardLit => "Standard Lit",
            MaterialShaderKind::Unlit => "Unlit",
            MaterialShaderKind::Custom => "Custom Surface",
        })
        .width(150.0_f32.min(ui.available_width().max(1.0)))
        .show_ui(ui, |ui| {
            ui.selectable_value(kind, MaterialShaderKind::StandardLit, "Standard Lit");
            ui.selectable_value(kind, MaterialShaderKind::Unlit, "Unlit");
            ui.selectable_value(kind, MaterialShaderKind::Custom, "Custom Surface");
        });
}

pub(super) fn optional_path_edit(ui: &mut egui::Ui, label: &str, value: &mut Option<String>) {
    ui.horizontal(|ui| {
        ui.add_sized([92.0, 20.0], egui::Label::new(label));
        let mut text = value.clone().unwrap_or_default();
        if ui
            .add_sized(
                [fit_width(ui, 80.0, f32::INFINITY), 22.0],
                egui::TextEdit::singleline(&mut text),
            )
            .changed()
        {
            *value = if text.trim().is_empty() {
                None
            } else {
                Some(text)
            };
        }
        if ui.button("Clear").clicked() {
            *value = None;
        }
    });
}

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

    #[test]
    fn rendering_missing_standard_defaults_does_not_author_or_dirty_inputs() {
        let context = egui::Context::default();
        context.set_fonts(crate::ui::fonts::editor_font_definitions());
        let mut inputs = MaterialInputSet::default();
        let original = inputs.clone();
        let schema = shared::standard_lit_input_schema();
        let _ = context.run_ui(
            egui::RawInput {
                screen_rect: Some(egui::Rect::from_min_size(
                    egui::Pos2::ZERO,
                    egui::vec2(700.0, 900.0),
                )),
                ..Default::default()
            },
            |ui| {
                material_input_schema_editor(ui, &schema, &mut inputs, &[], None);
            },
        );
        assert_eq!(inputs, original);
    }
}
