use super::*;
use bevy::ecs::system::SystemParam;

#[derive(SystemParam)]
pub(super) struct TerrainMaterialResources<'w> {
    asset_server: Res<'w, AssetServer>,
    materials: ResMut<'w, Assets<TerrainLayerMaterial>>,
    cache: ResMut<'w, TerrainLayerMaterialCache>,
    surface_cache: Res<'w, SurfaceMaterialCache>,
    live_documents: Res<'w, LiveMaterialDocumentOverlay>,
    diagnostics: ResMut<'w, SurfaceDiagnostics>,
}

pub(super) fn sync_terrain_layer_material_bindings(
    mut commands: Commands,
    resources: TerrainMaterialResources,
    bindings: TerrainMaterialBindings,
) {
    let TerrainMaterialResources {
        asset_server,
        mut materials,
        mut cache,
        surface_cache,
        live_documents,
        mut diagnostics,
    } = resources;
    for (entity, binding) in &bindings {
        if binding.layers.is_empty() {
            continue;
        }
        let revision = terrain_layer_revision(
            &binding.layers,
            &live_documents,
            surface_cache.disk_generation(),
        );
        let cached = cache
            .0
            .get(&binding.owner)
            .filter(|entry| entry.layers == binding.layers && entry.revision == revision)
            .map(|entry| entry.handle.clone());
        let handle = cached.unwrap_or_else(|| {
            let material = build_terrain_layer_material(
                &asset_server,
                &binding.layers,
                &live_documents,
                &mut diagnostics.0,
            );
            let handle = cache
                .0
                .get(&binding.owner)
                .map(|entry| entry.handle.clone())
                .filter(|handle| {
                    materials
                        .get_mut(handle)
                        .map(|mut slot| {
                            *slot = material.clone();
                        })
                        .is_some()
                })
                .unwrap_or_else(|| materials.add(material));
            cache.0.insert(
                binding.owner,
                TerrainLayerCacheEntry {
                    layers: binding.layers.clone(),
                    handle: handle.clone(),
                    revision,
                },
            );
            handle
        });
        queue_terrain_material_binding(&mut commands, entity, handle);
    }
}

pub(super) fn queue_terrain_material_binding(
    commands: &mut Commands,
    entity: Entity,
    handle: Handle<TerrainLayerMaterial>,
) {
    // Generated render entities can be removed by a scene switch after this system's query
    // but before deferred commands apply. That lifecycle race is expected and must not abort
    // the editor while opening another scene.
    commands
        .entity(entity)
        .try_remove::<MeshMaterial3d<StandardMaterial>>()
        .try_insert(MeshMaterial3d(handle));
}

pub(super) fn terrain_layer_revision(
    layers: &[TerrainMaterialLayer],
    live_documents: &LiveMaterialDocumentOverlay,
    disk_generation: u64,
) -> u64 {
    let mut hash = 0xcbf29ce484222325u64;
    for layer in layers {
        for byte in layer.uv_scale.to_bits().to_le_bytes() {
            hash ^= u64::from(byte);
            hash = hash.wrapping_mul(0x100000001b3);
        }
        let Some(reference) = layer.material.as_ref() else {
            hash ^= u64::MAX;
            continue;
        };
        for byte in reference.asset_id.bytes() {
            hash ^= u64::from(byte);
            hash = hash.wrapping_mul(0x100000001b3);
        }
        let revision = material_live_revision(&MaterialRef(reference.clone()), live_documents)
            .unwrap_or(disk_generation);
        for byte in revision.to_le_bytes() {
            hash ^= u64::from(byte);
            hash = hash.wrapping_mul(0x100000001b3);
        }
    }
    hash
}

pub(super) fn build_terrain_layer_material(
    asset_server: &AssetServer,
    layers: &[TerrainMaterialLayer],
    live_documents: &LiveMaterialDocumentOverlay,
    diagnostics: &mut Vec<String>,
) -> TerrainLayerMaterial {
    let mut extension = TerrainLayerExtension::default();
    for (index, layer) in layers.iter().take(TERRAIN_MATERIAL_LAYER_LIMIT).enumerate() {
        extension.uniform.uv_scales[index] = layer.uv_scale;
        let desc = resolve_terrain_layer(layer, index, live_documents, diagnostics);
        let color = desc.base_color.to_color().to_linear();
        extension.uniform.base_colors[index] =
            Vec4::new(color.red, color.green, color.blue, color.alpha);
        extension.uniform.properties[index] = Vec4::new(
            desc.metallic,
            desc.roughness,
            f32::from(desc.normal_map_texture.is_some()),
            1.0,
        );
        extension.uniform.base_texture_enabled[index] =
            f32::from(desc.base_color_texture.is_some());
        let base_color = desc
            .base_color_texture
            .as_deref()
            .map(|path| asset_server.load(asset_server_path(path)));
        let normal = desc
            .normal_map_texture
            .as_deref()
            .map(|path| asset_server.load(asset_server_path(path)));
        match index {
            0 => {
                extension.base_color0 = base_color;
                extension.normal0 = normal;
            }
            1 => {
                extension.base_color1 = base_color;
                extension.normal1 = normal;
            }
            2 => {
                extension.base_color2 = base_color;
                extension.normal2 = normal;
            }
            3 => {
                extension.base_color3 = base_color;
                extension.normal3 = normal;
            }
            _ => {}
        }
    }
    TerrainLayerMaterial {
        base: StandardMaterial {
            base_color: Color::WHITE,
            perceptual_roughness: 1.0,
            // Terrain is intentionally absent from the Solari acceleration structure until its
            // ray-tracing evaluator exists, so keep the layer blend in the raster forward pass.
            opaque_render_method: OpaqueRendererMethod::Forward,
            ..default()
        },
        extension,
    }
}

pub(super) fn resolve_terrain_layer(
    layer: &TerrainMaterialLayer,
    index: usize,
    live_documents: &LiveMaterialDocumentOverlay,
    diagnostics: &mut Vec<String>,
) -> MaterialDesc {
    let Some(reference) = layer.material.as_ref() else {
        record_diagnostic(
            diagnostics,
            format!(
                "terrain material layer {} is unassigned; using visible fallback",
                index + 1
            ),
        );
        return terrain_layer_fallback();
    };
    let Some(path) = reference.source_path.as_deref() else {
        record_diagnostic(
            diagnostics,
            format!(
                "terrain material layer {} ({}) has no loadable source path; using visible fallback",
                index + 1,
                reference.label
            ),
        );
        return terrain_layer_fallback();
    };
    match resolve_material_desc(&MaterialRef(reference.clone()), path, live_documents) {
        Ok(desc) => desc,
        Err(error) => {
            record_diagnostic(
                diagnostics,
                format!(
                    "terrain material layer {} ({}) could not resolve: {error}; using visible fallback",
                    index + 1,
                    reference.label
                ),
            );
            terrain_layer_fallback()
        }
    }
}

pub(super) fn terrain_layer_fallback() -> MaterialDesc {
    MaterialDesc {
        base_color: shared::ColorDesc::srgb(0.24, 0.29, 0.25),
        roughness: 0.92,
        ..Default::default()
    }
}
