//! Lightweight authored material values and schemas.
//!
//! This crate intentionally excludes Bevy's app, asset, rendering, scene, and physics layers so
//! headless tooling and the production UI gallery can share the exact authoring contracts without
//! compiling the game runtime.

use bevy_color::Color;
use bevy_math::{Vec2, Vec3};
use bevy_reflect::prelude::*;
use serde::{Deserialize, Serialize};

/// Reference to an imported content-browser asset or sub-asset.
#[derive(Reflect, Default, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[reflect(Default, Debug, PartialEq, Serialize, Deserialize)]
#[type_path = "shared::components"]
pub struct EditorAssetRef {
    /// Stable project asset registry UUID string.
    pub asset_id: String,
    /// Stable imported sub-asset ID inside the generated artifact.
    #[serde(default, alias = "mesh_label")]
    pub sub_asset_id: String,
    /// UI label cached for inspectors. The asset registry/artifact remains authoritative.
    #[serde(default)]
    pub label: String,
    /// Optional loadable project path cached for runtime hydration.
    #[serde(default)]
    pub source_path: Option<String>,
}

impl EditorAssetRef {
    pub fn new(
        asset_id: impl Into<String>,
        sub_asset_id: impl Into<String>,
        label: impl Into<String>,
    ) -> Self {
        Self {
            asset_id: asset_id.into(),
            sub_asset_id: sub_asset_id.into(),
            label: label.into(),
            source_path: None,
        }
    }

    pub fn with_source_path(mut self, source_path: impl Into<String>) -> Self {
        self.source_path = Some(source_path.into());
        self
    }

    pub fn is_resolved(&self) -> bool {
        !self.asset_id.trim().is_empty() && !self.sub_asset_id.trim().is_empty()
    }
}

/// How a material should be resolved at runtime.
#[derive(Reflect, Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[reflect(Default, Debug, PartialEq, Serialize, Deserialize)]
#[type_path = "shared::components"]
pub enum MaterialShaderKind {
    #[default]
    StandardLit,
    Unlit,
    Custom,
}

/// Reference to a built-in or custom shader schema.
#[derive(Reflect, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[reflect(Default, Debug, PartialEq, Serialize, Deserialize)]
#[type_path = "shared::components"]
pub struct ShaderRefDesc {
    pub kind: MaterialShaderKind,
    #[serde(default)]
    pub schema_path: Option<String>,
    #[serde(default)]
    pub shader_path: Option<String>,
}

impl Default for ShaderRefDesc {
    fn default() -> Self {
        Self {
            kind: MaterialShaderKind::StandardLit,
            schema_path: None,
            shader_path: None,
        }
    }
}

/// Serializable color description used by authored material documents.
#[derive(Reflect, Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[reflect(Default, Debug, Serialize, Deserialize)]
#[type_path = "shared::components"]
pub struct ColorDesc {
    pub r: f32,
    pub g: f32,
    pub b: f32,
    pub a: f32,
}

impl ColorDesc {
    pub const fn srgb(r: f32, g: f32, b: f32) -> Self {
        Self { r, g, b, a: 1.0 }
    }

    pub fn to_color(self) -> Color {
        Color::srgba(self.r, self.g, self.b, self.a)
    }
}

impl Default for ColorDesc {
    fn default() -> Self {
        Self::srgb(0.8, 0.8, 0.8)
    }
}

#[derive(Reflect, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[reflect(Default, Debug, PartialEq, Serialize, Deserialize)]
#[type_path = "shared::components"]
pub enum MaterialParameterValue {
    Bool(bool),
    Float(f32),
    Vec2(Vec2),
    Vec3(Vec3),
    Color(ColorDesc),
    Enum(String),
}

impl Default for MaterialParameterValue {
    fn default() -> Self {
        Self::Float(0.0)
    }
}

#[derive(Reflect, Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[reflect(Default, Debug, PartialEq, Serialize, Deserialize)]
#[type_path = "shared::components"]
pub struct MaterialParameter {
    pub name: String,
    pub value: MaterialParameterValue,
}

/// Channels sampled from a texture binding.
#[derive(Reflect, Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[reflect(Default, Debug, PartialEq, Serialize, Deserialize)]
#[type_path = "shared::components"]
pub enum TextureChannel {
    R,
    G,
    B,
    A,
    Rgb,
    #[default]
    Rgba,
}

#[derive(Reflect, Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[reflect(Default, Debug, PartialEq, Serialize, Deserialize)]
#[type_path = "shared::components"]
pub struct MaterialTextureBinding {
    pub name: String,
    pub texture: Option<EditorAssetRef>,
    #[serde(default)]
    pub channel: TextureChannel,
}

#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
pub enum MaterialAlphaMode {
    #[default]
    Opaque,
    Cutout,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
pub struct MaterialRenderState {
    #[serde(default)]
    pub alpha_mode: MaterialAlphaMode,
    #[serde(default = "default_alpha_cutoff")]
    pub alpha_cutoff: f32,
    #[serde(default)]
    pub double_sided: bool,
}

const fn default_alpha_cutoff() -> f32 {
    0.5
}

impl Default for MaterialRenderState {
    fn default() -> Self {
        Self {
            alpha_mode: MaterialAlphaMode::Opaque,
            alpha_cutoff: default_alpha_cutoff(),
            double_sided: false,
        }
    }
}

/// Canonical values and texture bindings consumed by built-in and custom material schemas.
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct MaterialInputSet {
    #[serde(default)]
    pub values: Vec<MaterialParameter>,
    #[serde(default)]
    pub textures: Vec<MaterialTextureBinding>,
}

impl MaterialInputSet {
    pub fn from_material_desc<T: MaterialInputSource>(source: &T) -> Self {
        source.to_material_inputs()
    }

    pub fn apply_to_material_desc<T: MaterialInputTarget>(&self, target: &mut T) {
        target.apply_material_inputs(self);
    }

    pub fn texture(&self, name: &str) -> Option<&MaterialTextureBinding> {
        self.textures.iter().find(|binding| binding.name == name)
    }

    pub fn validate_unique(&self) -> Result<(), String> {
        let mut names = std::collections::HashSet::new();
        for value in &self.values {
            if !names.insert(("value", value.name.as_str())) {
                return Err(format!(
                    "material value `{}` is assigned more than once",
                    value.name
                ));
            }
        }
        for texture in &self.textures {
            if !names.insert(("texture", texture.name.as_str())) {
                return Err(format!(
                    "material texture `{}` is assigned more than once",
                    texture.name
                ));
            }
        }
        Ok(())
    }
}

/// Runtime-owned descriptors implement this bridge without making the schema crate depend on the
/// renderer or scene representation.
pub trait MaterialInputSource {
    fn to_material_inputs(&self) -> MaterialInputSet;
}

/// Runtime-owned descriptors implement this bridge without making lightweight authoring tools
/// depend on those descriptors.
pub trait MaterialInputTarget {
    fn apply_material_inputs(&mut self, inputs: &MaterialInputSet);
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ShaderPropertyType {
    Bool,
    Float { min: Option<f32>, max: Option<f32> },
    Vec2,
    Vec3,
    Color,
    Enum { options: Vec<String> },
    Texture,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct MaterialInputGroupDesc {
    pub id: String,
    pub display_name: String,
    #[serde(default)]
    pub order: i32,
    #[serde(default)]
    pub advanced: bool,
}

#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
pub enum TextureSemantic {
    #[default]
    Auto,
    Color,
    Normal,
    Scalar,
    Mask,
    Hdr,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct MaterialTextureInputDesc {
    #[serde(default)]
    pub semantic: TextureSemantic,
    #[serde(default)]
    pub default_channel: TextureChannel,
    #[serde(default)]
    pub allow_channel_override: bool,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub enum MaterialInputPresentation {
    #[default]
    Row,
    Companion {
        owner: String,
    },
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct MaterialInputDesc {
    pub name: String,
    pub display_name: String,
    #[serde(default)]
    pub group: String,
    #[serde(default)]
    pub order: i32,
    pub property_type: ShaderPropertyType,
    #[serde(default)]
    pub default_value: Option<MaterialParameterValue>,
    #[serde(default)]
    pub texture: Option<MaterialTextureInputDesc>,
    #[serde(default)]
    pub tooltip: String,
    #[serde(default)]
    pub advanced: bool,
    #[serde(default)]
    pub presentation: MaterialInputPresentation,
}

impl Default for MaterialInputDesc {
    fn default() -> Self {
        Self {
            name: String::new(),
            display_name: String::new(),
            group: String::new(),
            order: 0,
            property_type: ShaderPropertyType::Float {
                min: None,
                max: None,
            },
            default_value: None,
            texture: None,
            tooltip: String::new(),
            advanced: false,
            presentation: MaterialInputPresentation::Row,
        }
    }
}

/// Compatibility source alias for code that has not yet adopted material-input terminology.
pub type ShaderPropertyDesc = MaterialInputDesc;

#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct MaterialInputSchema {
    #[serde(default)]
    pub groups: Vec<MaterialInputGroupDesc>,
    #[serde(default)]
    pub inputs: Vec<MaterialInputDesc>,
}

/// Engine-owned Standard Lit authoring schema. Custom Surface schemas use the same shape.
pub fn standard_lit_input_schema() -> MaterialInputSchema {
    let groups = vec![
        MaterialInputGroupDesc {
            id: "surface_inputs".into(),
            display_name: "Surface Inputs".into(),
            order: 0,
            advanced: false,
        },
        MaterialInputGroupDesc {
            id: "advanced_inputs".into(),
            display_name: "Advanced Inputs".into(),
            order: 100,
            advanced: true,
        },
        MaterialInputGroupDesc {
            id: "uv_transform".into(),
            display_name: "UV Transform".into(),
            order: 90,
            advanced: false,
        },
    ];
    let scalar_texture = |channel| MaterialTextureInputDesc {
        semantic: TextureSemantic::Scalar,
        default_channel: channel,
        allow_channel_override: true,
    };
    let inputs = vec![
        MaterialInputDesc {
            name: "base_color".into(),
            display_name: "Base Color".into(),
            group: "surface_inputs".into(),
            order: 0,
            property_type: ShaderPropertyType::Color,
            default_value: Some(MaterialParameterValue::Color(ColorDesc::default())),
            texture: Some(MaterialTextureInputDesc {
                semantic: TextureSemantic::Color,
                default_channel: TextureChannel::Rgba,
                allow_channel_override: false,
            }),
            tooltip: "Color multiplier applied to the base-color texture.".into(),
            advanced: false,
            presentation: MaterialInputPresentation::Row,
        },
        MaterialInputDesc {
            name: "metallic".into(),
            display_name: "Metallic".into(),
            group: "surface_inputs".into(),
            order: 10,
            property_type: ShaderPropertyType::Float {
                min: Some(0.0),
                max: Some(1.0),
            },
            default_value: Some(MaterialParameterValue::Float(0.0)),
            texture: Some(scalar_texture(TextureChannel::B)),
            tooltip: "Scalar multiplier; ARM/ORM uses the blue channel.".into(),
            advanced: false,
            presentation: MaterialInputPresentation::Row,
        },
        MaterialInputDesc {
            name: "roughness".into(),
            display_name: "Roughness".into(),
            group: "surface_inputs".into(),
            order: 20,
            property_type: ShaderPropertyType::Float {
                min: Some(0.0),
                max: Some(1.0),
            },
            default_value: Some(MaterialParameterValue::Float(0.65)),
            texture: Some(scalar_texture(TextureChannel::G)),
            tooltip: "Scalar multiplier; ARM/ORM uses the green channel.".into(),
            advanced: false,
            presentation: MaterialInputPresentation::Row,
        },
        MaterialInputDesc {
            name: "occlusion".into(),
            display_name: "Occlusion".into(),
            group: "surface_inputs".into(),
            order: 30,
            property_type: ShaderPropertyType::Float {
                min: Some(0.0),
                max: Some(1.0),
            },
            default_value: Some(MaterialParameterValue::Float(1.0)),
            texture: Some(scalar_texture(TextureChannel::R)),
            tooltip: "Ambient-occlusion strength; ARM/ORM uses the red channel.".into(),
            advanced: false,
            presentation: MaterialInputPresentation::Row,
        },
        MaterialInputDesc {
            name: "normal".into(),
            display_name: "Normal".into(),
            group: "surface_inputs".into(),
            order: 40,
            property_type: ShaderPropertyType::Float {
                min: Some(0.0),
                max: Some(1.0),
            },
            default_value: Some(MaterialParameterValue::Float(0.85)),
            texture: Some(MaterialTextureInputDesc {
                semantic: TextureSemantic::Normal,
                default_channel: TextureChannel::Rgb,
                allow_channel_override: false,
            }),
            tooltip: String::new(),
            advanced: false,
            presentation: MaterialInputPresentation::Row,
        },
        MaterialInputDesc {
            name: "emissive_color".into(),
            display_name: "Emissive".into(),
            group: "surface_inputs".into(),
            order: 50,
            property_type: ShaderPropertyType::Color,
            default_value: Some(MaterialParameterValue::Color(ColorDesc::srgb(
                1.0, 1.0, 1.0,
            ))),
            texture: Some(MaterialTextureInputDesc {
                semantic: TextureSemantic::Color,
                default_channel: TextureChannel::Rgb,
                allow_channel_override: false,
            }),
            tooltip: "Emissive color multiplied by the texture and intensity.".into(),
            advanced: false,
            presentation: MaterialInputPresentation::Row,
        },
        MaterialInputDesc {
            name: "emissive_intensity".into(),
            display_name: "Emissive Intensity".into(),
            group: "surface_inputs".into(),
            order: 60,
            property_type: ShaderPropertyType::Float {
                min: Some(0.0),
                max: Some(50_000.0),
            },
            default_value: Some(MaterialParameterValue::Float(0.0)),
            texture: None,
            tooltip: "Emissive luminance multiplier in nits.".into(),
            advanced: false,
            presentation: MaterialInputPresentation::Companion {
                owner: "emissive_color".into(),
            },
        },
        MaterialInputDesc {
            name: "uv_offset".into(),
            display_name: "Offset".into(),
            group: "uv_transform".into(),
            order: 0,
            property_type: ShaderPropertyType::Vec2,
            default_value: Some(MaterialParameterValue::Vec2(Vec2::ZERO)),
            texture: None,
            tooltip: "Shared UV translation for every texture on this material.".into(),
            advanced: false,
            presentation: MaterialInputPresentation::Row,
        },
        MaterialInputDesc {
            name: "uv_tiling".into(),
            display_name: "Tiling".into(),
            group: "uv_transform".into(),
            order: 10,
            property_type: ShaderPropertyType::Vec2,
            default_value: Some(MaterialParameterValue::Vec2(Vec2::ONE)),
            texture: None,
            tooltip: "Shared UV scale for every texture on this material.".into(),
            advanced: false,
            presentation: MaterialInputPresentation::Row,
        },
    ];
    MaterialInputSchema { groups, inputs }
}

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

    #[test]
    fn standard_lit_has_six_primary_surface_rows() {
        let schema = standard_lit_input_schema();
        let surface = schema
            .inputs
            .iter()
            .filter(|input| input.group == "surface_inputs")
            .collect::<Vec<_>>();
        assert_eq!(surface.len(), 7);
        assert_eq!(
            surface
                .iter()
                .filter(|input| matches!(input.presentation, MaterialInputPresentation::Row))
                .count(),
            6
        );
    }
}
