//! Sparse Material Instance authoring through the same Penpot input renderer as Materials.

use super::*;

pub(super) fn material_instance_input_schema_editor(
    ui: &mut egui::Ui,
    schema: &MaterialInputSchema,
    base: &MaterialInputSet,
    overrides: &mut MaterialInputSet,
    texture_assets: &[MaterialTextureCandidate],
    drop_candidate: Option<&MaterialTextureCandidate>,
) -> MaterialInputEditorResponse {
    let normalized_base = normalized_inputs(schema, base);
    let mut effective = normalized_base.clone();
    overlay_inputs(&mut effective, overrides);

    let response = material_input_schema_editor_with_inheritance(
        ui,
        schema,
        &mut effective,
        Some(&normalized_base),
        texture_assets,
        drop_candidate,
    );
    *overrides = sparse_difference(&normalized_base, &effective);
    response
}

fn normalized_inputs(schema: &MaterialInputSchema, source: &MaterialInputSet) -> MaterialInputSet {
    let mut normalized = source.clone();
    for property in &schema.inputs {
        if !matches!(property.property_type, ShaderPropertyType::Texture)
            && !normalized
                .values
                .iter()
                .any(|value| value.name == property.name)
        {
            normalized.values.push(MaterialParameter {
                name: property.name.clone(),
                value: property
                    .default_value
                    .clone()
                    .unwrap_or_else(|| default_value_for_shader_property(&property.property_type)),
            });
        }
        if let Some(texture) = property.texture.as_ref() {
            if !normalized
                .textures
                .iter()
                .any(|binding| binding.name == property.name)
            {
                normalized.textures.push(MaterialTextureBinding {
                    name: property.name.clone(),
                    texture: None,
                    channel: texture.default_channel,
                });
            }
        }
    }
    normalized
}

fn overlay_inputs(target: &mut MaterialInputSet, overlay: &MaterialInputSet) {
    for value in &overlay.values {
        upsert_value(&mut target.values, value.clone());
    }
    for texture in &overlay.textures {
        upsert_texture(&mut target.textures, texture.clone());
    }
}

fn sparse_difference(base: &MaterialInputSet, effective: &MaterialInputSet) -> MaterialInputSet {
    MaterialInputSet {
        values: effective
            .values
            .iter()
            .filter(|value| base.values.iter().find(|base| base.name == value.name) != Some(*value))
            .cloned()
            .collect(),
        textures: effective
            .textures
            .iter()
            .filter(|texture| {
                base.textures.iter().find(|base| base.name == texture.name) != Some(*texture)
            })
            .cloned()
            .collect(),
    }
}

fn upsert_value(values: &mut Vec<MaterialParameter>, value: MaterialParameter) {
    if let Some(current) = values.iter_mut().find(|current| current.name == value.name) {
        *current = value;
    } else {
        values.push(value);
    }
}

fn upsert_texture(textures: &mut Vec<MaterialTextureBinding>, texture: MaterialTextureBinding) {
    if let Some(current) = textures
        .iter_mut()
        .find(|current| current.name == texture.name)
    {
        *current = texture;
    } else {
        textures.push(texture);
    }
}

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

    #[test]
    fn instance_editor_retains_only_values_different_from_base() {
        let schema = standard_lit_input_schema();
        let base = normalized_inputs(&schema, &MaterialInputSet::default());
        let mut effective = base.clone();
        let roughness = effective
            .values
            .iter_mut()
            .find(|value| value.name == "roughness")
            .expect("roughness default");
        roughness.value = MaterialParameterValue::Float(0.2);

        let overrides = sparse_difference(&base, &effective);

        assert_eq!(overrides.values.len(), 1);
        assert_eq!(overrides.values[0].name, "roughness");
        assert!(overrides.textures.is_empty());
    }

    #[test]
    fn instance_editor_clearing_back_to_base_removes_override() {
        let schema = standard_lit_input_schema();
        let base = normalized_inputs(&schema, &MaterialInputSet::default());
        let overrides = sparse_difference(&base, &base);
        assert!(overrides.values.is_empty());
        assert!(overrides.textures.is_empty());
    }

    #[test]
    fn instance_overlay_preserves_unknown_custom_schema_values() {
        let mut effective = MaterialInputSet::default();
        let overrides = MaterialInputSet {
            values: vec![MaterialParameter {
                name: "graph_custom".into(),
                value: MaterialParameterValue::Float(3.0),
            }],
            textures: Vec::new(),
        };
        overlay_inputs(&mut effective, &overrides);
        assert_eq!(
            sparse_difference(&MaterialInputSet::default(), &effective),
            overrides
        );
    }
}
