use super::*;

pub(super) fn pack_schema_values(
    extension: &mut SurfaceExtension,
    schema: &ShaderSchemaAsset,
    inputs: &shared::MaterialInputSet,
    instance: Option<&MaterialInstanceAsset>,
    asset_server: &AssetServer,
    runtime_catalog: Option<&RuntimeContentCatalog>,
) -> Result<(), String> {
    let mut numeric_index = 0usize;
    let mut texture_index = 0usize;
    for property in &schema.schema.inputs {
        if property.texture.is_some()
            || matches!(property.property_type, ShaderPropertyType::Texture)
        {
            if texture_index >= SURFACE_TEXTURE_SLOTS {
                return Err(format!(
                    "surface schema exceeds {SURFACE_TEXTURE_SLOTS} textures"
                ));
            }
            let binding = instance
                .and_then(|instance| {
                    instance
                        .overrides
                        .textures
                        .iter()
                        .find(|value| value.name == property.name)
                })
                .or_else(|| {
                    inputs
                        .textures
                        .iter()
                        .find(|value| value.name == property.name)
                });
            let handle = binding
                .and_then(|binding| binding.texture.as_ref())
                .and_then(|reference| {
                    runtime_texture_spec(
                        runtime_catalog,
                        reference,
                        property.texture.as_ref().is_some_and(|texture| {
                            texture.semantic == shared::TextureSemantic::Color
                        }),
                    )
                })
                .map(|spec| shared::load_runtime_texture(asset_server, &spec));
            extension.set_texture(texture_index, handle);
            texture_index += 1;
        }
        if matches!(property.property_type, ShaderPropertyType::Texture) {
            continue;
        }
        if numeric_index >= SURFACE_PARAMETER_LANES {
            return Err(format!(
                "surface schema exceeds {SURFACE_PARAMETER_LANES} parameter lanes"
            ));
        }
        let value = instance
            .and_then(|instance| {
                instance
                    .overrides
                    .values
                    .iter()
                    .find(|value| value.name == property.name)
            })
            .or_else(|| {
                inputs
                    .values
                    .iter()
                    .find(|value| value.name == property.name)
            })
            .map(|value| &value.value)
            .or(property.default_value.as_ref());
        extension.uniform.params[numeric_index] = value
            .map(|value| parameter_lane(value, &property.property_type))
            .transpose()?
            .unwrap_or(Vec4::ZERO);
        numeric_index += 1;
    }
    Ok(())
}

pub(super) fn load_runtime_content_catalog() -> Option<RuntimeContentCatalog> {
    let source = fs::read_to_string("assets/content.catalog.ron").ok()?;
    ron::from_str(&source).ok()
}

pub(super) fn runtime_texture_path(
    catalog: Option<&RuntimeContentCatalog>,
    reference: &shared::EditorAssetRef,
) -> Option<String> {
    catalog
        .and_then(|catalog| {
            catalog
                .records
                .iter()
                .find(|record| record.id.as_string() == reference.asset_id)
        })
        .and_then(|record| record.texture.as_ref())
        .and_then(|runtime| runtime.processed_path.clone())
        .or_else(|| reference.source_path.clone())
}

pub(super) fn runtime_texture_spec(
    catalog: Option<&RuntimeContentCatalog>,
    reference: &shared::EditorAssetRef,
    fallback_srgb: bool,
) -> Option<shared::RuntimeTextureLoadSpec> {
    catalog
        .and_then(|catalog| catalog.texture_load_spec(reference))
        .or_else(|| {
            reference
                .source_path
                .as_ref()
                .map(|path| shared::RuntimeTextureLoadSpec {
                    path: path.clone(),
                    is_srgb: fallback_srgb,
                    filter: shared::TextureFilter::Linear,
                    wrap: shared::TextureWrap::Repeat,
                    anisotropy: 8,
                })
        })
}

pub(super) fn packed_runtime_texture_spec(
    catalog: Option<&RuntimeContentCatalog>,
    inputs: &MaterialInputSet,
    instance: Option<&MaterialInstanceAsset>,
    material: &MaterialRef,
) -> Result<Option<shared::RuntimeTextureLoadSpec>, String> {
    let Some(catalog) = catalog else {
        return Ok(None);
    };
    let Some(path) = catalog
        .records
        .iter()
        .find(|record| record.id.as_string() == material.0.asset_id)
        .and_then(|record| record.material.as_ref())
        .and_then(|runtime| runtime.packed_arm_path.clone())
    else {
        return Ok(None);
    };
    let mut source_specs = ["occlusion", "roughness", "metallic"]
        .into_iter()
        .filter_map(|name| {
            instance
                .and_then(|instance| instance.overrides.texture(name))
                .or_else(|| inputs.texture(name))
                .and_then(|binding| binding.texture.as_ref())
                .and_then(|reference| catalog.texture_load_spec(reference))
        });
    let Some(first) = source_specs.next() else {
        return Ok(None);
    };
    if source_specs.any(|spec| {
        spec.filter != first.filter
            || spec.wrap != first.wrap
            || spec.anisotropy != first.anisotropy
    }) {
        return Err("packed AO/roughness/metallic textures must use matching filter, wrap, and anisotropy settings".into());
    }
    Ok(Some(shared::RuntimeTextureLoadSpec {
        path,
        is_srgb: false,
        filter: first.filter,
        wrap: first.wrap,
        anisotropy: first.anisotropy,
    }))
}

pub(super) fn apply_runtime_texture_paths(
    resolved: &mut MaterialDesc,
    inputs: &MaterialInputSet,
    instance: Option<&MaterialInstanceAsset>,
    material: &MaterialRef,
    catalog: Option<&RuntimeContentCatalog>,
) {
    let binding = |name: &str| {
        instance
            .and_then(|instance| instance.overrides.texture(name))
            .or_else(|| inputs.texture(name))
            .and_then(|binding| binding.texture.as_ref())
            .and_then(|reference| runtime_texture_path(catalog, reference))
    };
    resolved.base_color_texture = binding("base_color");
    resolved.normal_map_texture = binding("normal");
    resolved.emissive_texture = binding("emissive");
    let packed_arm = catalog
        .and_then(|catalog| {
            catalog
                .records
                .iter()
                .find(|record| record.id.as_string() == material.0.asset_id)
        })
        .and_then(|record| record.material.as_ref())
        .and_then(|runtime| runtime.packed_arm_path.clone());
    resolved.metallic_roughness_texture = packed_arm.clone();
    resolved.occlusion_texture = packed_arm;
}

pub(super) fn parameter_lane(
    value: &MaterialParameterValue,
    property_type: &ShaderPropertyType,
) -> Result<Vec4, String> {
    match (value, property_type) {
        (MaterialParameterValue::Bool(value), ShaderPropertyType::Bool) => {
            Ok(Vec4::new(u32::from(*value) as f32, 0.0, 0.0, 0.0))
        }
        (MaterialParameterValue::Float(value), ShaderPropertyType::Float { .. }) => {
            Ok(Vec4::new(*value, 0.0, 0.0, 0.0))
        }
        (MaterialParameterValue::Vec2(value), ShaderPropertyType::Vec2) => {
            Ok(Vec4::new(value.x, value.y, 0.0, 0.0))
        }
        (MaterialParameterValue::Vec3(value), ShaderPropertyType::Vec3) => Ok(value.extend(0.0)),
        (MaterialParameterValue::Color(value), ShaderPropertyType::Color) => {
            Ok(Vec4::new(value.r, value.g, value.b, value.a))
        }
        (MaterialParameterValue::Enum(value), ShaderPropertyType::Enum { options }) => {
            let index = options
                .iter()
                .position(|option| option == value)
                .ok_or_else(|| format!("enum value `{value}` is not declared by the schema"))?;
            Ok(Vec4::new(index as f32, 0.0, 0.0, 0.0))
        }
        _ => Err("material value does not match shader schema type".into()),
    }
}
