use editor_ui::materials::{
    MaterialHealth, MaterialLayerBadge, MaterialLocateTarget, MaterialPickerCandidate,
    MaterialSlotPanelViewModel, MaterialTextureCandidate, MaterialThumbnailPresentation,
};
use material_schema::{
    standard_lit_input_schema, ColorDesc, EditorAssetRef, MaterialInputSet, MaterialParameter,
    MaterialParameterValue, MaterialRenderState, MaterialShaderKind, MaterialTextureBinding,
    TextureChannel,
};
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FixtureId {
    Reference,
    Primitive,
    StaticMesh,
    SkinnedMesh,
    Inherited,
    Imported,
    Dirty,
    Processing,
    Failed,
    ReadOnly,
    BuiltIn,
}

impl FixtureId {
    pub const ALL: [Self; 11] = [
        Self::Reference,
        Self::Primitive,
        Self::StaticMesh,
        Self::SkinnedMesh,
        Self::Inherited,
        Self::Imported,
        Self::Dirty,
        Self::Processing,
        Self::Failed,
        Self::ReadOnly,
        Self::BuiltIn,
    ];

    pub const fn label(self) -> &'static str {
        match self {
            Self::Reference => "Penpot Layout",
            Self::Primitive => "Primitive",
            Self::StaticMesh => "Static Mesh",
            Self::SkinnedMesh => "Skinned Mesh",
            Self::Inherited => "Inherited",
            Self::Imported => "Imported Source",
            Self::Dirty => "Dirty",
            Self::Processing => "Processing",
            Self::Failed => "Failed",
            Self::ReadOnly => "Read only",
            Self::BuiltIn => "Built-in DefaultGrid",
        }
    }

    pub const fn is_renderer(self) -> bool {
        matches!(self, Self::StaticMesh | Self::SkinnedMesh)
    }
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct FixtureOverrides {
    pub material_label: Option<String>,
    pub material_path: Option<String>,
    pub metallic: Option<f32>,
    pub roughness: Option<f32>,
    pub occlusion: Option<f32>,
    pub normal_strength: Option<f32>,
    pub emissive_intensity: Option<f32>,
    pub uv_tiling: Option<[f32; 2]>,
}

pub struct FixtureModel {
    pub slot: MaterialSlotPanelViewModel,
    pub render_state: MaterialRenderState,
    pub inputs: MaterialInputSet,
    pub renderer_slots: Vec<RendererMaterialFixture>,
    pub textures: Vec<MaterialTextureCandidate>,
}

pub struct RendererMaterialFixture {
    pub slot: MaterialSlotPanelViewModel,
    pub render_state: MaterialRenderState,
    pub inputs: MaterialInputSet,
}

impl FixtureModel {
    pub fn new(
        id: FixtureId,
        overrides: &FixtureOverrides,
        material_thumbnail: MaterialThumbnailPresentation,
        texture_thumbnails: [MaterialThumbnailPresentation; 3],
    ) -> Self {
        let material_ref = EditorAssetRef::new(
            "c07a5b05-f27d-46c6-8b8c-9de9cdd1472c",
            "material:source",
            "M_Wood_Planks",
        )
        .with_source_path("materials/wood/M_Wood_Planks.mat");
        let label = overrides
            .material_label
            .clone()
            .unwrap_or_else(|| fixture_material_label(id).to_owned());
        let path = overrides
            .material_path
            .clone()
            .unwrap_or_else(|| fixture_material_path(id).to_owned());
        let (health, inherited, can_edit_shader, can_clear) = fixture_state(id);
        let slot = MaterialSlotPanelViewModel {
            slot_id: fixture_slot_id(id).to_owned(),
            label,
            path: Some(path),
            shader: "Standard Lit".to_owned(),
            shader_kind: Some(MaterialShaderKind::StandardLit),
            can_edit_shader,
            thumbnail: material_thumbnail,
            inherited,
            health,
            assigned: (!matches!(id, FixtureId::Inherited | FixtureId::BuiltIn))
                .then_some(material_ref.clone()),
            effective: Some(material_ref),
            candidates: material_candidates(material_thumbnail),
            drop_candidate: None,
            invalid_drop_reason: None,
            can_locate: !matches!(id, FixtureId::BuiltIn),
            can_clear,
            can_extract: matches!(id, FixtureId::Imported),
            can_create_instance: !matches!(id, FixtureId::Imported | FixtureId::BuiltIn),
            parameters_initially_open: true,
            expanded_header_when_parameters_closed: false,
        };
        let textures = texture_candidates(texture_thumbnails);
        let inputs = fixture_inputs(id, overrides, &textures);
        let renderer_slots = renderer_material_fixtures(&slot, &inputs);
        Self {
            slot,
            render_state: MaterialRenderState::default(),
            inputs,
            renderer_slots,
            textures,
        }
    }
}

fn renderer_material_fixtures(
    primary: &MaterialSlotPanelViewModel,
    inputs: &MaterialInputSet,
) -> Vec<RendererMaterialFixture> {
    let mut painted = primary.clone();
    painted.slot_id = "slot:draw:scene0:node2:mesh2:primitive1".to_owned();
    painted.label = "M_Painted_Metal".to_owned();
    painted.path = Some("assets/materials/office/M_Painted_Metal.mat".to_owned());
    painted.assigned = painted.assigned.as_ref().map(|_| {
        EditorAssetRef::new(
            "gallery-painted-metal",
            "material:source",
            "M_Painted_Metal",
        )
        .with_source_path("assets/materials/office/M_Painted_Metal.mat")
    });
    painted.effective = painted.assigned.clone();
    painted.parameters_initially_open = false;

    let mut empty = primary.clone();
    empty.slot_id = "slot:draw:scene0:node2:mesh2:primitive2".to_owned();
    empty.label = "No material assigned".to_owned();
    empty.path = None;
    empty.shader.clear();
    empty.shader_kind = None;
    empty.can_edit_shader = false;
    empty.thumbnail = MaterialThumbnailPresentation::Empty;
    empty.health = MaterialHealth::Healthy;
    empty.assigned = None;
    empty.effective = None;
    empty.can_locate = false;
    empty.can_clear = false;
    empty.can_create_instance = false;
    empty.parameters_initially_open = false;

    [primary.clone(), painted, empty]
        .into_iter()
        .map(|slot| RendererMaterialFixture {
            slot,
            render_state: MaterialRenderState::default(),
            inputs: inputs.clone(),
        })
        .collect()
}

fn fixture_state(id: FixtureId) -> (MaterialHealth, Option<MaterialLayerBadge>, bool, bool) {
    match id {
        FixtureId::Inherited => (
            MaterialHealth::Healthy,
            Some(MaterialLayerBadge::ModelDefault),
            false,
            false,
        ),
        FixtureId::Imported => (
            MaterialHealth::ReadOnly,
            Some(MaterialLayerBadge::ModelSource),
            false,
            false,
        ),
        FixtureId::Dirty => (MaterialHealth::Dirty, None, true, true),
        FixtureId::Processing => (MaterialHealth::Processing, None, true, true),
        FixtureId::Failed => (MaterialHealth::Broken, None, false, true),
        FixtureId::ReadOnly => (MaterialHealth::ReadOnly, None, false, false),
        FixtureId::BuiltIn => (
            MaterialHealth::ReadOnly,
            Some(MaterialLayerBadge::DefaultGrid),
            false,
            false,
        ),
        FixtureId::Reference
        | FixtureId::Primitive
        | FixtureId::StaticMesh
        | FixtureId::SkinnedMesh => (MaterialHealth::Healthy, None, true, true),
    }
}

fn fixture_slot_id(id: FixtureId) -> &'static str {
    match id {
        FixtureId::Reference => "slot:gallery:penpot-reference",
        FixtureId::Primitive => "slot:primitive:surface",
        FixtureId::StaticMesh => "slot:draw:scene0:node2:mesh2:primitive0",
        FixtureId::SkinnedMesh => "slot:skin:scene0:node1:mesh0:primitive0",
        _ => "slot:gallery:surface",
    }
}

fn fixture_material_label(id: FixtureId) -> &'static str {
    match id {
        FixtureId::Imported => "metal_office_desk",
        FixtureId::BuiltIn => "Default Grid — Engine Built-in",
        FixtureId::Failed => "Missing explicit material",
        _ => "M_Wood_Planks",
    }
}

fn fixture_material_path(id: FixtureId) -> &'static str {
    match id {
        FixtureId::Imported => "assets/Furniture/Office/metal_office_desk_2k.gltf#material:0",
        FixtureId::BuiltIn => "builtin://materials/default-grid",
        FixtureId::Failed => "assets/materials/missing.material.ron",
        _ => "materials/wood/M_Wood_Planks.mat",
    }
}

fn material_candidates(thumbnail: MaterialThumbnailPresentation) -> Vec<MaterialPickerCandidate> {
    ["M_Wood_Planks", "M_Painted_Metal", "M_Concrete"]
        .into_iter()
        .enumerate()
        .map(|(index, label)| MaterialPickerCandidate {
            reference: EditorAssetRef::new(
                format!("gallery-material-{index}"),
                "material:source",
                label,
            )
            .with_source_path(format!("assets/materials/{label}.material.ron")),
            label: label.to_owned(),
            detail: format!("assets/materials/{label}.material.ron"),
            thumbnail,
        })
        .collect()
}

fn texture_candidates(
    thumbnails: [MaterialThumbnailPresentation; 3],
) -> Vec<MaterialTextureCandidate> {
    [
        ("T_Wood_ORM", "materials/textures/T_Wood_ORM.png"),
        ("T_Wood_ORM", "materials/textures/T_Wood_ORM.png"),
        ("T_Wood_ORM", "materials/textures/T_Wood_ORM.png"),
    ]
    .into_iter()
    .enumerate()
    .map(|(index, (label, path))| MaterialTextureCandidate {
        label: label.to_owned(),
        detail: path.to_owned(),
        reference: EditorAssetRef::new(format!("gallery-texture-{index}"), "texture:source", label)
            .with_source_path(path),
        locate: MaterialLocateTarget::File(path.to_owned()),
        thumbnail: thumbnails[index],
    })
    .collect()
}

fn fixture_inputs(
    id: FixtureId,
    overrides: &FixtureOverrides,
    textures: &[MaterialTextureCandidate],
) -> MaterialInputSet {
    let schema = standard_lit_input_schema();
    let mut set = MaterialInputSet::default();
    for input in &schema.inputs {
        if let Some(value) = input.default_value.clone() {
            set.values.push(MaterialParameter {
                name: input.name.clone(),
                value,
            });
        }
        if let Some(texture) = input.texture.as_ref() {
            set.textures.push(MaterialTextureBinding {
                name: input.name.clone(),
                texture: None,
                channel: texture.default_channel,
            });
        }
    }
    set_value(
        &mut set,
        "base_color",
        MaterialParameterValue::Color(ColorDesc::srgb(231.0 / 255.0, 212.0 / 255.0, 180.0 / 255.0)),
    );
    set_value(
        &mut set,
        "emissive_color",
        MaterialParameterValue::Color(ColorDesc::srgb(231.0 / 255.0, 212.0 / 255.0, 180.0 / 255.0)),
    );
    set_float(&mut set, "metallic", overrides.metallic.unwrap_or(0.72));
    set_float(&mut set, "roughness", overrides.roughness.unwrap_or(0.72));
    set_float(&mut set, "occlusion", overrides.occlusion.unwrap_or(0.72));
    set_float(
        &mut set,
        "normal",
        overrides.normal_strength.unwrap_or(0.72),
    );
    set_float(
        &mut set,
        "emissive_intensity",
        overrides.emissive_intensity.unwrap_or(0.0),
    );
    if let Some([x, y]) = overrides.uv_tiling {
        if let Some(parameter) = set
            .values
            .iter_mut()
            .find(|value| value.name == "uv_tiling")
        {
            if let MaterialParameterValue::Vec2(value) = &mut parameter.value {
                value.x = x;
                value.y = y;
            }
        }
    }
    bind(&mut set, "base_color", &textures[0], TextureChannel::Rgba);
    for (name, channel) in [
        ("occlusion", TextureChannel::R),
        ("roughness", TextureChannel::G),
        ("metallic", TextureChannel::B),
    ] {
        bind(&mut set, name, &textures[1], channel);
    }
    bind(&mut set, "normal", &textures[2], TextureChannel::Rgb);
    if id == FixtureId::Reference {
        // Penpot's sample binds the same mock texture to Emissive. Its all-G scalar labels conflict
        // with the simultaneously selected ORM mode; keep Blacksite's valid R/G/B contract and
        // document those three sample glyphs as an intentional handoff delta.
        bind(
            &mut set,
            "emissive_color",
            &textures[1],
            TextureChannel::Rgb,
        );
    }
    set
}

fn set_float(inputs: &mut MaterialInputSet, name: &str, value: f32) {
    set_value(inputs, name, MaterialParameterValue::Float(value));
}

fn set_value(inputs: &mut MaterialInputSet, name: &str, value: MaterialParameterValue) {
    if let Some(parameter) = inputs.values.iter_mut().find(|item| item.name == name) {
        parameter.value = value;
    }
}

fn bind(
    inputs: &mut MaterialInputSet,
    name: &str,
    candidate: &MaterialTextureCandidate,
    channel: TextureChannel,
) {
    if let Some(binding) = inputs.textures.iter_mut().find(|item| item.name == name) {
        binding.texture = Some(candidate.reference.clone());
        binding.channel = channel;
    }
}

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

    fn test_textures() -> Vec<MaterialTextureCandidate> {
        texture_candidates([MaterialThumbnailPresentation::Pending; 3])
    }

    #[test]
    fn reference_fixture_keeps_valid_orm_channels_and_adds_penpot_emissive_sample() {
        let textures = test_textures();
        let reference = fixture_inputs(
            FixtureId::Reference,
            &FixtureOverrides::default(),
            &textures,
        );
        assert_eq!(
            reference
                .texture("occlusion")
                .map(|binding| binding.channel),
            Some(TextureChannel::R)
        );
        assert_eq!(
            reference
                .texture("roughness")
                .map(|binding| binding.channel),
            Some(TextureChannel::G)
        );
        assert_eq!(
            reference.texture("metallic").map(|binding| binding.channel),
            Some(TextureChannel::B)
        );
        assert!(reference
            .texture("emissive_color")
            .and_then(|binding| binding.texture.as_ref())
            .is_some());

        let primitive = fixture_inputs(
            FixtureId::Primitive,
            &FixtureOverrides::default(),
            &textures,
        );
        assert_eq!(
            primitive
                .texture("occlusion")
                .map(|binding| binding.channel),
            Some(TextureChannel::R)
        );
        assert_eq!(
            primitive
                .texture("roughness")
                .map(|binding| binding.channel),
            Some(TextureChannel::G)
        );
        assert_eq!(
            primitive.texture("metallic").map(|binding| binding.channel),
            Some(TextureChannel::B)
        );
        assert!(primitive
            .texture("emissive_color")
            .and_then(|binding| binding.texture.as_ref())
            .is_none());
    }
}
