use super::*;

#[allow(clippy::too_many_arguments, clippy::type_complexity)]
pub(super) fn apply_material_property_blocks(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    owners: Query<&MaterialPropertyBlocks>,
    bindings: Query<(
        Entity,
        &HydratedMaterialSlotBinding,
        Option<&MeshMaterial3d<StandardMaterial>>,
        Option<&MeshMaterial3d<SurfaceMaterial>>,
    )>,
    mut standard_materials: ResMut<Assets<StandardMaterial>>,
    mut surface_materials: ResMut<Assets<SurfaceMaterial>>,
    surface_cache: Res<SurfaceMaterialCache>,
    mut cache: ResMut<MaterialPropertyBlockCache>,
    mut diagnostics: ResMut<SurfaceDiagnostics>,
) {
    let mut active = HashSet::new();
    for (entity, binding, standard_handle, surface_handle) in &bindings {
        let Ok(blocks) = owners.get(binding.owner) else {
            continue;
        };
        let Some(block) = blocks
            .slots
            .iter()
            .find(|block| block.slot_id == binding.slot_id)
        else {
            continue;
        };
        let revision = property_block_revision(
            block,
            standard_handle,
            surface_handle,
            surface_cache.revision_for_handles(standard_handle, surface_handle),
        );
        let key = (binding.owner, binding.slot_id.0.clone());
        active.insert(key.clone());
        let cached = cache
            .entries
            .get(&key)
            .filter(|entry| entry.revision == revision);
        let handle = if let Some(entry) = cached {
            match &entry.handle {
                PropertyBlockHandle::Standard(handle) => {
                    PropertyBlockHandle::Standard(handle.clone())
                }
                PropertyBlockHandle::Surface(handle) => {
                    PropertyBlockHandle::Surface(handle.clone())
                }
            }
        } else if let Some(source) = surface_handle
            .and_then(|handle| surface_materials.get(&handle.0))
            .cloned()
        {
            let mut material = source;
            let application =
                apply_standard_property_block(&mut material.base, block, &asset_server, true)
                    .and_then(|()| {
                        binding
                            .selection
                            .project_reference()
                            .map_or(Ok(()), |reference| {
                                apply_surface_property_block(
                                    &mut material,
                                    reference,
                                    block,
                                    &asset_server,
                                )
                            })
                    });
            if let Err(error) = application {
                record_diagnostic(
                    &mut diagnostics.0,
                    format!(
                        "material property block for {} was ignored: {error}",
                        binding.slot_id.0
                    ),
                );
                continue;
            }
            let handle = cache
                .entries
                .get(&key)
                .and_then(|entry| match &entry.handle {
                    PropertyBlockHandle::Surface(handle) => Some(handle.clone()),
                    PropertyBlockHandle::Standard(_) => None,
                });
            if let Some(handle) = handle {
                if let Some(mut existing) = surface_materials.get_mut(&handle) {
                    *existing = material;
                }
                PropertyBlockHandle::Surface(handle)
            } else {
                PropertyBlockHandle::Surface(surface_materials.add(material))
            }
        } else if let Some(source) = standard_handle
            .and_then(|handle| standard_materials.get(&handle.0))
            .cloned()
        {
            let mut material = source;
            if let Err(error) =
                apply_standard_property_block(&mut material, block, &asset_server, false)
            {
                record_diagnostic(
                    &mut diagnostics.0,
                    format!(
                        "material property block for {} was ignored: {error}",
                        binding.slot_id.0
                    ),
                );
                continue;
            }
            let handle = cache
                .entries
                .get(&key)
                .and_then(|entry| match &entry.handle {
                    PropertyBlockHandle::Standard(handle) => Some(handle.clone()),
                    PropertyBlockHandle::Surface(_) => None,
                });
            if let Some(handle) = handle {
                if let Some(mut existing) = standard_materials.get_mut(&handle) {
                    *existing = material;
                }
                PropertyBlockHandle::Standard(handle)
            } else {
                PropertyBlockHandle::Standard(standard_materials.add(material))
            }
        } else {
            continue;
        };
        match &handle {
            PropertyBlockHandle::Standard(handle) => {
                commands
                    .entity(entity)
                    .try_remove::<MeshMaterial3d<SurfaceMaterial>>()
                    .try_insert(MeshMaterial3d(handle.clone()));
            }
            PropertyBlockHandle::Surface(handle) => {
                commands
                    .entity(entity)
                    .try_remove::<MeshMaterial3d<StandardMaterial>>()
                    .try_insert(MeshMaterial3d(handle.clone()));
            }
        }
        cache
            .entries
            .insert(key, PropertyBlockCacheEntry { revision, handle });
    }
    cache.entries.retain(|key, entry| {
        if active.contains(key) {
            return true;
        }
        match &entry.handle {
            PropertyBlockHandle::Standard(handle) => {
                standard_materials.remove(handle.id());
            }
            PropertyBlockHandle::Surface(handle) => {
                surface_materials.remove(handle.id());
            }
        }
        false
    });
}

pub(super) fn property_block_revision(
    block: &MaterialPropertyBlock,
    standard: Option<&MeshMaterial3d<StandardMaterial>>,
    surface: Option<&MeshMaterial3d<SurfaceMaterial>>,
    base_revision: u64,
) -> u64 {
    let mut hasher = DefaultHasher::new();
    format!("{block:?}").hash(&mut hasher);
    standard.map(|handle| handle.0.id()).hash(&mut hasher);
    surface.map(|handle| handle.0.id()).hash(&mut hasher);
    base_revision.hash(&mut hasher);
    hasher.finish()
}

pub(super) fn apply_standard_property_block(
    material: &mut StandardMaterial,
    block: &MaterialPropertyBlock,
    asset_server: &AssetServer,
    allow_custom: bool,
) -> Result<(), String> {
    validate_standard_property_block(block, allow_custom)?;
    for parameter in &block.parameters {
        match (
            parameter.name.to_ascii_lowercase().as_str(),
            &parameter.value,
        ) {
            ("base_color" | "albedo", MaterialParameterValue::Color(value)) => {
                material.base_color = value.to_color()
            }
            ("metallic", MaterialParameterValue::Float(value)) => material.metallic = *value,
            ("roughness", MaterialParameterValue::Float(value)) => {
                material.perceptual_roughness = *value
            }
            ("emissive", MaterialParameterValue::Color(value)) => {
                material.emissive = value.to_color().into()
            }
            _ => continue,
        }
    }
    for texture in &block.textures {
        let handle = texture
            .texture
            .as_ref()
            .and_then(|reference| reference.source_path.as_deref())
            .map(asset_server_path)
            .map(|path| asset_server.load(path));
        match texture.name.to_ascii_lowercase().as_str() {
            "base_color" | "albedo" => material.base_color_texture = handle,
            "normal" | "normal_map" => material.normal_map_texture = handle,
            "metallic_roughness" => material.metallic_roughness_texture = handle,
            "emissive" => material.emissive_texture = handle,
            _ => continue,
        }
    }
    Ok(())
}

pub(super) fn validate_standard_property_block(
    block: &MaterialPropertyBlock,
    allow_custom: bool,
) -> Result<(), String> {
    validate_standard_property_block_at(Path::new("."), block, allow_custom)
}

/// Validates a runtime block against the material path selected for editor promotion.
///
/// The editor passes the resolved project root so relative texture references are checked without
/// relying on process working-directory state. Custom properties are accepted only when the
/// selected material has an active Surface evaluator and its schema declares them.
pub fn validate_property_block_for_promotion(
    project_root: &Path,
    block: &MaterialPropertyBlock,
    schema: Option<&ShaderSchemaAsset>,
    custom_surface: bool,
) -> Result<(), String> {
    validate_standard_property_block_at(project_root, block, custom_surface)?;
    if custom_surface {
        validate_surface_property_block(schema, block)?;
    }
    Ok(())
}

pub(super) fn validate_standard_property_block_at(
    project_root: &Path,
    block: &MaterialPropertyBlock,
    allow_custom: bool,
) -> Result<(), String> {
    let mut parameter_names = HashSet::new();
    for parameter in &block.parameters {
        if !parameter_names.insert(parameter.name.as_str()) {
            return Err(format!(
                "parameter `{}` is assigned more than once",
                parameter.name
            ));
        }
        let name = parameter.name.to_ascii_lowercase();
        let valid = match (name.as_str(), &parameter.value) {
            ("base_color" | "albedo" | "emissive", MaterialParameterValue::Color(value)) => {
                [value.r, value.g, value.b, value.a]
                    .into_iter()
                    .all(f32::is_finite)
            }
            ("metallic" | "roughness", MaterialParameterValue::Float(value)) => {
                value.is_finite() && (0.0..=1.0).contains(value)
            }
            ("base_color" | "albedo" | "emissive" | "metallic" | "roughness", _) => false,
            _ if allow_custom => true,
            _ => {
                return Err(format!(
                    "parameter `{}` is not supported by Standard materials",
                    parameter.name
                ));
            }
        };
        if !valid {
            return Err(format!(
                "parameter `{}` has an invalid type or value",
                parameter.name
            ));
        }
    }

    let mut texture_names = HashSet::new();
    for texture in &block.textures {
        if !texture_names.insert(texture.name.as_str()) {
            return Err(format!(
                "texture `{}` is assigned more than once",
                texture.name
            ));
        }
        let name = texture.name.to_ascii_lowercase();
        if !matches!(
            name.as_str(),
            "base_color" | "albedo" | "normal" | "normal_map" | "metallic_roughness" | "emissive"
        ) && !allow_custom
        {
            return Err(format!(
                "texture `{}` is not supported by Standard materials",
                texture.name
            ));
        }
        if let Some(reference) = texture.texture.as_ref() {
            let path = reference
                .source_path
                .as_deref()
                .ok_or_else(|| format!("texture `{}` has no loadable source path", texture.name))?;
            let path = Path::new(path);
            let resolved = if path.is_absolute() {
                path.to_path_buf()
            } else {
                project_root.join(path)
            };
            if !resolved.is_file() {
                return Err(format!(
                    "texture `{}` is missing at {}",
                    texture.name,
                    path.display()
                ));
            }
        }
    }
    Ok(())
}

pub(super) fn apply_surface_property_block(
    material: &mut SurfaceMaterial,
    reference: &MaterialRef,
    block: &MaterialPropertyBlock,
    asset_server: &AssetServer,
) -> Result<(), String> {
    let path = reference
        .0
        .source_path
        .as_deref()
        .ok_or_else(|| "base material has no loadable path".to_string())?;
    let (mut asset, mut instance) = match MaterialAsset::load_from_path(path) {
        Ok(asset) => (asset, None),
        Err(_) => {
            let instance = MaterialInstanceAsset::load_from_path(path)?;
            let base_path = instance
                .base
                .0
                .source_path
                .as_deref()
                .ok_or_else(|| "material instance base has no loadable path".to_string())?;
            (MaterialAsset::load_from_path(base_path)?, Some(instance))
        }
    };
    let schema_path = asset
        .shader_ref
        .as_ref()
        .and_then(|reference| reference.source_path.as_deref())
        .or(asset.shader.schema_path.as_deref());
    let schema = schema_path
        .map(ShaderSchemaAsset::load_from_path)
        .transpose()?;
    validate_surface_property_block(schema.as_ref(), block)?;
    merge_surface_property_block(&mut asset, &mut instance, block);
    if let Some(schema) = schema.as_ref() {
        let runtime_catalog = load_runtime_content_catalog();
        pack_schema_values(
            &mut material.extension,
            schema,
            &asset.inputs,
            instance.as_ref(),
            asset_server,
            runtime_catalog.as_ref(),
        )?;
    }
    let mut resolved = MaterialDesc::default();
    asset.inputs.apply_to_material_desc(&mut resolved);
    if let Some(instance) = instance.as_ref() {
        instance.apply_to(&mut resolved);
    }
    material.extension.uniform.uv_transforms.fill(Vec4::new(
        resolved.uv_tiling.x,
        resolved.uv_tiling.y,
        resolved.uv_offset.x,
        resolved.uv_offset.y,
    ));
    Ok(())
}

pub(super) fn merge_surface_property_block(
    asset: &mut MaterialAsset,
    instance: &mut Option<MaterialInstanceAsset>,
    block: &MaterialPropertyBlock,
) {
    if let Some(instance) = instance.as_mut() {
        instance.merge_overrides(block.parameters.clone(), block.textures.clone());
    } else {
        merge_material_input_overrides(&mut asset.inputs, block);
    }
}

pub(super) fn merge_material_input_overrides(
    inputs: &mut shared::MaterialInputSet,
    block: &MaterialPropertyBlock,
) {
    for parameter in &block.parameters {
        if let Some(existing) = inputs
            .values
            .iter_mut()
            .find(|value| value.name == parameter.name)
        {
            *existing = parameter.clone();
        } else {
            inputs.values.push(parameter.clone());
        }
    }
    for texture in &block.textures {
        if let Some(existing) = inputs
            .textures
            .iter_mut()
            .find(|value| value.name == texture.name)
        {
            *existing = texture.clone();
        } else {
            inputs.textures.push(texture.clone());
        }
    }
}

pub(super) fn validate_surface_property_block(
    schema: Option<&ShaderSchemaAsset>,
    block: &MaterialPropertyBlock,
) -> Result<(), String> {
    for parameter in &block.parameters {
        if matches!(
            parameter.name.to_ascii_lowercase().as_str(),
            "base_color" | "albedo" | "emissive" | "metallic" | "roughness"
        ) {
            continue;
        }
        let property = schema
            .and_then(|schema| {
                schema
                    .schema
                    .inputs
                    .iter()
                    .find(|property| property.name == parameter.name)
            })
            .ok_or_else(|| {
                format!(
                    "parameter `{}` is not declared by the Surface schema",
                    parameter.name
                )
            })?;
        if matches!(property.property_type, ShaderPropertyType::Texture) {
            return Err(format!(
                "parameter `{}` targets a texture property",
                parameter.name
            ));
        }
        parameter_lane(&parameter.value, &property.property_type)?;
        if !parameter_value_is_finite(&parameter.value) {
            return Err(format!(
                "parameter `{}` contains a non-finite value",
                parameter.name
            ));
        }
        if let (MaterialParameterValue::Float(value), ShaderPropertyType::Float { min, max }) =
            (&parameter.value, &property.property_type)
        {
            if !value.is_finite()
                || min.is_some_and(|min| *value < min)
                || max.is_some_and(|max| *value > max)
            {
                return Err(format!(
                    "parameter `{}` is outside its declared finite range",
                    parameter.name
                ));
            }
        }
    }
    for texture in &block.textures {
        if matches!(
            texture.name.to_ascii_lowercase().as_str(),
            "base_color" | "albedo" | "normal" | "normal_map" | "metallic_roughness" | "emissive"
        ) {
            continue;
        }
        let property = schema
            .and_then(|schema| {
                schema
                    .schema
                    .inputs
                    .iter()
                    .find(|property| property.name == texture.name)
            })
            .ok_or_else(|| {
                format!(
                    "texture `{}` is not declared by the Surface schema",
                    texture.name
                )
            })?;
        if !matches!(property.property_type, ShaderPropertyType::Texture) {
            return Err(format!(
                "texture `{}` targets a non-texture property",
                texture.name
            ));
        }
    }
    Ok(())
}

pub(super) fn parameter_value_is_finite(value: &MaterialParameterValue) -> bool {
    match value {
        MaterialParameterValue::Bool(_) | MaterialParameterValue::Enum(_) => true,
        MaterialParameterValue::Float(value) => value.is_finite(),
        MaterialParameterValue::Vec2(value) => value.is_finite(),
        MaterialParameterValue::Vec3(value) => value.is_finite(),
        MaterialParameterValue::Color(value) => [value.r, value.g, value.b, value.a]
            .into_iter()
            .all(f32::is_finite),
    }
}
