use super::*;

pub(super) fn resolve_material_desc(
    reference: &MaterialRef,
    path: &str,
    live_documents: &LiveMaterialDocumentOverlay,
) -> Result<MaterialDesc, String> {
    let (asset, instance) = match live_documents.get(reference) {
        Some(LiveMaterialDocumentEntry {
            document: LiveMaterialDocument::Material(asset),
            ..
        }) => (asset.clone(), None),
        Some(LiveMaterialDocumentEntry {
            document: LiveMaterialDocument::Instance(instance),
            ..
        }) => (
            live_base_material(&instance.base, live_documents)?,
            Some(instance.clone()),
        ),
        None => return shared::load_resolved_material_from_path(path).map(|(desc, _)| desc),
    };
    let mut resolved = MaterialDesc {
        shader: asset.shader.clone(),
        ..Default::default()
    };
    asset.inputs.apply_to_material_desc(&mut resolved);
    if let Some(instance) = instance.as_ref() {
        instance.apply_to(&mut resolved);
    }
    Ok(resolved)
}

pub(super) fn material_live_revision(
    reference: &MaterialRef,
    live_documents: &LiveMaterialDocumentOverlay,
) -> Option<u64> {
    let entry = live_documents.get(reference)?;
    let base_revision = match &entry.document {
        LiveMaterialDocument::Material(_) => None,
        LiveMaterialDocument::Instance(instance) => live_documents
            .get(&instance.base)
            .map(|base| base.revision.rotate_left(1)),
    };
    Some(base_revision.map_or(entry.revision, |base| entry.revision ^ base))
}

#[allow(clippy::too_many_arguments, clippy::type_complexity)]
pub(super) fn sync_surface_material_bindings(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    mut shaders: ResMut<Assets<Shader>>,
    mut standard_materials: ResMut<Assets<StandardMaterial>>,
    mut surface_materials: ResMut<Assets<SurfaceMaterial>>,
    mut cache: ResMut<SurfaceMaterialCache>,
    live_documents: Res<LiveMaterialDocumentOverlay>,
    mut default_grid: ResMut<DefaultGridMaterialHandles>,
    content_defaults: Res<shared::ProjectContentDefaults>,
    mut diagnostics: ResMut<SurfaceDiagnostics>,
    mut evaluator_registry: ResMut<SurfaceEvaluatorRegistry>,
    bindings: Query<(
        Entity,
        &HydratedMaterialSlotBinding,
        Option<&MeshMaterial3d<StandardMaterial>>,
        Option<&MeshMaterial3d<SurfaceMaterial>>,
    )>,
) {
    let default_grid_handle = ensure_default_grid_material(
        &mut shaders,
        &mut standard_materials,
        &mut surface_materials,
        &mut default_grid,
        &mut evaluator_registry,
    );
    for (entity, binding, standard_handle, surface_handle) in &bindings {
        if matches!(
            binding.selection,
            HydratedMaterialSelection::ImportedSource { .. }
        ) {
            continue;
        }
        let reference = binding
            .selection
            .project_reference()
            .or(content_defaults.default_material.as_ref());
        let Some(reference) = reference else {
            queue_default_grid_binding(&mut commands, entity, default_grid_handle.clone());
            continue;
        };
        let Some(path) = reference.0.source_path.as_deref() else {
            record_diagnostic(
                &mut diagnostics.0,
                format!(
                    "material {} has no loadable source path; using {DEFAULT_GRID_LABEL}",
                    reference.0.label
                ),
            );
            queue_default_grid_binding(&mut commands, entity, default_grid_handle.clone());
            continue;
        };
        let live_revision = material_live_revision(reference, &live_documents).or_else(|| {
            cache
                .instance_bases
                .get(reference)
                .and_then(|base| live_documents.get(base))
                .map(|entry| entry.revision.rotate_left(1))
        });
        let revision = live_revision
            .map(|revision| revision | (1_u64 << 63))
            .unwrap_or_else(|| cache.disk_generation());
        if cache.failed_revisions.get(reference) == Some(&revision) {
            queue_default_grid_binding(&mut commands, entity, default_grid_handle.clone());
            continue;
        }
        if cache.standard_only.get(reference) == Some(&revision) {
            if let Some(handle) = cache.standard_handles.get(reference).cloned() {
                if standard_handle.is_none_or(|current| current.0 != handle)
                    || surface_handle.is_some()
                {
                    commands
                        .entity(entity)
                        .try_remove::<MeshMaterial3d<SurfaceMaterial>>()
                        .try_insert(MeshMaterial3d(handle));
                }
            }
            continue;
        }
        let stale = cache.revisions.get(reference) != Some(&revision);
        let handle = if !stale {
            cache.handles.get(reference).cloned()
        } else {
            None
        };
        let handle = match handle {
            Some(handle) => handle,
            None => match build_surface_material(
                reference,
                path,
                &live_documents,
                &asset_server,
                &mut shaders,
                &mut evaluator_registry,
            ) {
                Ok((BuiltRendererMaterial::Surface(material), base_reference)) => {
                    let material = *material;
                    if let Some(base_reference) = base_reference {
                        cache
                            .instance_bases
                            .insert(reference.clone(), base_reference);
                    } else {
                        cache.instance_bases.remove(reference);
                    }
                    cache.standard_only.remove(reference);
                    cache.standard_handles.remove(reference);
                    cache.failed_revisions.remove(reference);
                    if let Some(existing) = cache.handles.get(reference).cloned() {
                        if let Some(mut slot) = surface_materials.get_mut(&existing) {
                            *slot = material;
                        }
                        existing
                    } else {
                        let handle = surface_materials.add(material);
                        cache.handles.insert(reference.clone(), handle.clone());
                        handle
                    }
                }
                Ok((BuiltRendererMaterial::Standard(material), base_reference)) => {
                    let material = *material;
                    if let Some(base_reference) = base_reference {
                        cache
                            .instance_bases
                            .insert(reference.clone(), base_reference);
                    } else {
                        cache.instance_bases.remove(reference);
                    }
                    // Plain Material assets stay on Bevy's StandardMaterial path, while still
                    // sharing one live-updated handle across every renderer slot.
                    // Never adopt the renderer's current handle here: hydration starts every
                    // surface on one singleton emergency handle, so adopting it would make
                    // unrelated Materials overwrite each other. Cache one handle per reference.
                    let handle = cache
                        .standard_handles
                        .get(reference)
                        .cloned()
                        .unwrap_or_else(|| standard_materials.add(material.clone()));
                    if let Some(mut slot) = standard_materials.get_mut(&handle) {
                        *slot = material;
                    }
                    cache.handles.remove(reference);
                    cache.revisions.remove(reference);
                    cache
                        .standard_handles
                        .insert(reference.clone(), handle.clone());
                    cache.standard_only.insert(reference.clone(), revision);
                    cache.failed_revisions.remove(reference);
                    evaluator_registry
                        .evaluators
                        .remove(&stable_shader_id(reference));
                    if standard_handle.is_none_or(|current| current.0 != handle)
                        || surface_handle.is_some()
                    {
                        commands
                            .entity(entity)
                            .try_remove::<MeshMaterial3d<SurfaceMaterial>>()
                            .try_insert(MeshMaterial3d(handle));
                    }
                    continue;
                }
                Err(error) => {
                    cache.failed_revisions.insert(reference.clone(), revision);
                    record_diagnostic(
                        &mut diagnostics.0,
                        format!(
                            "material {} could not build Surface ABI: {error}; using {DEFAULT_GRID_LABEL}",
                            reference.0.label
                        ),
                    );
                    queue_default_grid_binding(&mut commands, entity, default_grid_handle.clone());
                    continue;
                }
            },
        };
        cache.revisions.insert(reference.clone(), revision);
        if surface_handle.is_none_or(|current| current.0 != handle) {
            commands
                .entity(entity)
                .try_remove::<MeshMaterial3d<StandardMaterial>>()
                .try_insert(MeshMaterial3d(handle));
        }
    }
}

pub(super) fn queue_default_grid_binding(
    commands: &mut Commands,
    entity: Entity,
    handle: Handle<SurfaceMaterial>,
) {
    commands
        .entity(entity)
        .try_remove::<MeshMaterial3d<StandardMaterial>>()
        .try_insert(MeshMaterial3d(handle));
}

pub(super) fn ensure_default_grid_material(
    shaders: &mut Assets<Shader>,
    standard_materials: &mut Assets<StandardMaterial>,
    surface_materials: &mut Assets<SurfaceMaterial>,
    handles: &mut DefaultGridMaterialHandles,
    evaluator_registry: &mut SurfaceEvaluatorRegistry,
) -> Handle<SurfaceMaterial> {
    if let Some(handle) = handles.surface.as_ref() {
        return handle.clone();
    }
    validate_surface_evaluator(DEFAULT_GRID_EVALUATOR)
        .expect("embedded DefaultGrid evaluator must remain valid");
    let shader_id = AssetId::<Shader>::Uuid {
        uuid: DEFAULT_GRID_SHADER_UUID,
    };
    shaders
        .insert(
            shader_id,
            Shader::from_wgsl(
                compose_surface_shader(DEFAULT_GRID_EVALUATOR),
                "builtin://default-grid.wgsl",
            ),
        )
        .expect("reserved DefaultGrid shader ID must be replaceable");
    evaluator_registry.evaluators.insert(
        DEFAULT_GRID_SHADER_ID,
        SurfaceEvaluatorRecord {
            shader_id: DEFAULT_GRID_SHADER_ID,
            evaluator_source: DEFAULT_GRID_EVALUATOR.to_string(),
            revision: evaluator_revision(DEFAULT_GRID_EVALUATOR),
            alpha_mode: shared::MaterialAlphaMode::Opaque,
        },
    );
    let emergency = standard_materials.add(StandardMaterial {
        base_color: Color::srgb(0.46, 0.46, 0.46),
        perceptual_roughness: 0.86,
        cull_mode: None,
        ..default()
    });
    let material = SurfaceMaterial {
        base: StandardMaterial {
            base_color: Color::WHITE,
            perceptual_roughness: 0.86,
            cull_mode: None,
            ..default()
        },
        extension: SurfaceExtension {
            uniform: SurfaceUniform {
                shader_id: DEFAULT_GRID_SHADER_ID,
                ..default()
            },
            shader: Handle::Uuid(DEFAULT_GRID_SHADER_UUID, Default::default()),
            ..default()
        },
    };
    let handle = surface_materials.add(material);
    handles.emergency_standard = Some(emergency);
    handles.surface = Some(handle.clone());
    handle
}

pub(super) fn build_surface_material(
    reference: &MaterialRef,
    path: &str,
    live_documents: &LiveMaterialDocumentOverlay,
    asset_server: &AssetServer,
    shaders: &mut Assets<Shader>,
    evaluator_registry: &mut SurfaceEvaluatorRegistry,
) -> Result<(BuiltRendererMaterial, Option<MaterialRef>), String> {
    let (asset, instance, base_reference) = match live_documents.get(reference) {
        Some(LiveMaterialDocumentEntry {
            document: LiveMaterialDocument::Material(asset),
            ..
        }) => (asset.clone(), None, None),
        Some(LiveMaterialDocumentEntry {
            document: LiveMaterialDocument::Instance(instance),
            ..
        }) => {
            if instance.schema_version != MATERIAL_INSTANCE_SCHEMA_VERSION {
                return Err(format!(
                    "unsupported material-instance schema {}",
                    instance.schema_version
                ));
            }
            (
                live_base_material(&instance.base, live_documents)?,
                Some(instance.clone()),
                Some(instance.base.clone()),
            )
        }
        None => match MaterialAsset::load_from_path(path) {
            Ok(asset) => (asset, None, None),
            Err(_) => {
                let instance = MaterialInstanceAsset::load_from_path(path)?;
                if instance.schema_version != MATERIAL_INSTANCE_SCHEMA_VERSION {
                    return Err(format!(
                        "unsupported material-instance schema {}",
                        instance.schema_version
                    ));
                }
                let base_reference = instance.base.clone();
                (
                    live_base_material(&base_reference, live_documents)?,
                    Some(instance),
                    Some(base_reference),
                )
            }
        },
    };
    let mut resolved = shared::MaterialDesc {
        shader: asset.shader.clone(),
        ..Default::default()
    };
    asset.inputs.apply_to_material_desc(&mut resolved);
    if let Some(instance) = instance.as_ref() {
        instance.apply_to(&mut resolved);
    }
    let runtime_catalog = load_runtime_content_catalog();
    apply_runtime_texture_paths(
        &mut resolved,
        &asset.inputs,
        instance.as_ref(),
        reference,
        runtime_catalog.as_ref(),
    );
    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()?;
    let evaluator_source = schema
        .as_ref()
        .and_then(|schema| schema.wgsl_path.as_deref())
        .map(fs::read_to_string)
        .transpose()
        .map_err(|error| format!("could not read surface WGSL: {error}"))?;

    // Hydration deliberately attaches the singleton emergency StandardMaterial first so geometry
    // stays visible before this resolver runs. Its neutral values must never become the base of an
    // assigned project Material.
    let runtime_binding = |name: &str| {
        instance
            .as_ref()
            .and_then(|instance| instance.overrides.texture(name))
            .or_else(|| asset.inputs.texture(name))
    };
    let base_color_spec = runtime_binding("base_color")
        .and_then(|binding| binding.texture.as_ref())
        .and_then(|reference| runtime_texture_spec(runtime_catalog.as_ref(), reference, true));
    let normal_spec = runtime_binding("normal")
        .and_then(|binding| binding.texture.as_ref())
        .and_then(|reference| runtime_texture_spec(runtime_catalog.as_ref(), reference, false));
    let emissive_spec = runtime_binding("emissive")
        .and_then(|binding| binding.texture.as_ref())
        .and_then(|reference| runtime_texture_spec(runtime_catalog.as_ref(), reference, true));
    let packed_spec = packed_runtime_texture_spec(
        runtime_catalog.as_ref(),
        &asset.inputs,
        instance.as_ref(),
        reference,
    )?;
    let specs = [base_color_spec, normal_spec, emissive_spec, packed_spec]
        .into_iter()
        .flatten()
        .map(|spec| (spec.path.clone(), spec))
        .collect::<HashMap<_, _>>();
    let mut base =
        shared::material_from_desc_with_texture_loader(asset_server, &resolved, |path| {
            specs.get(path).map_or_else(
                || asset_server.load(asset_server_path(path)),
                |spec| shared::load_runtime_texture(asset_server, spec),
            )
        });
    base.alpha_mode = match asset.render_state.alpha_mode {
        shared::MaterialAlphaMode::Opaque => AlphaMode::Opaque,
        shared::MaterialAlphaMode::Cutout => AlphaMode::Mask(asset.render_state.alpha_cutoff),
    };
    base.cull_mode =
        (!asset.render_state.double_sided).then_some(bevy::render::render_resource::Face::Back);

    let Some(evaluator) = evaluator_source.as_deref() else {
        return Ok((
            BuiltRendererMaterial::Standard(Box::new(base)),
            base_reference,
        ));
    };
    validate_surface_evaluator(evaluator)?;
    let runtime_shader_id = stable_shader_id(reference);
    let evaluator_source_revision = evaluator_revision(evaluator);
    let shader_changed = evaluator_registry
        .evaluators
        .get(&runtime_shader_id)
        .is_none_or(|record| record.revision != evaluator_source_revision);
    evaluator_registry.evaluators.insert(
        runtime_shader_id,
        SurfaceEvaluatorRecord {
            shader_id: runtime_shader_id,
            evaluator_source: evaluator.to_string(),
            revision: evaluator_source_revision,
            alpha_mode: asset.render_state.alpha_mode,
        },
    );
    let virtual_path = format!("generated://surface/{}.wgsl", reference.0.asset_id);
    let shader_uuid = shader_uuid_for_material(reference);
    let shader_id = AssetId::<Shader>::Uuid { uuid: shader_uuid };
    if shader_changed || shaders.get(shader_id).is_none() {
        shaders
            .insert(
                shader_id,
                Shader::from_wgsl(compose_surface_shader(evaluator), virtual_path),
            )
            .map_err(|error| format!("could not replace generated surface shader: {error}"))?;
    }
    let shader = Handle::Uuid(shader_uuid, Default::default());

    let mut extension = SurfaceExtension {
        shader,
        ..default()
    };
    extension.uniform.alpha_cutoff = asset.render_state.alpha_cutoff;
    extension.uniform.shader_id = runtime_shader_id;
    if let Some(schema) = schema.as_ref() {
        pack_schema_values(
            &mut extension,
            schema,
            &asset.inputs,
            instance.as_ref(),
            asset_server,
            runtime_catalog.as_ref(),
        )?;
    }
    extension.uniform.uv_transforms.fill(Vec4::new(
        resolved.uv_tiling.x,
        resolved.uv_tiling.y,
        resolved.uv_offset.x,
        resolved.uv_offset.y,
    ));
    Ok((
        BuiltRendererMaterial::Surface(Box::new(ExtendedMaterial { base, extension })),
        base_reference,
    ))
}

pub(super) fn live_base_material(
    reference: &MaterialRef,
    live_documents: &LiveMaterialDocumentOverlay,
) -> Result<MaterialAsset, String> {
    match live_documents.get(reference) {
        Some(LiveMaterialDocumentEntry {
            document: LiveMaterialDocument::Material(asset),
            ..
        }) => Ok(asset.clone()),
        Some(LiveMaterialDocumentEntry {
            document: LiveMaterialDocument::Instance(_),
            ..
        }) => Err("nested Material Instances are not supported".to_string()),
        None => {
            let base_path = reference
                .0
                .source_path
                .as_deref()
                .ok_or_else(|| "material instance base has no loadable path".to_string())?;
            MaterialAsset::load_from_path(base_path)
        }
    }
}

pub(super) fn stable_shader_id(reference: &MaterialRef) -> u32 {
    let mut hash = 0x811c9dc5u32;
    for byte in reference.0.asset_id.bytes() {
        hash ^= u32::from(byte);
        hash = hash.wrapping_mul(0x01000193);
    }
    hash
}

pub(super) fn evaluator_revision(source: &str) -> u64 {
    let mut hash = 0xcbf29ce484222325u64;
    for byte in source.bytes() {
        hash ^= u64::from(byte);
        hash = hash.wrapping_mul(0x100000001b3);
    }
    hash
}

pub(super) fn shader_uuid_for_material(reference: &MaterialRef) -> uuid::Uuid {
    if let Ok(source) = uuid::Uuid::parse_str(&reference.0.asset_id) {
        let mixed = source.as_u128() ^ 0x7f8c_4e90_f9ab_4f28_9b44_47c1_b2d5_5ac1u128;
        return uuid::Uuid::from_u128(mixed);
    }
    let mut bytes = [0u8; 16];
    for (index, byte) in reference.0.asset_id.bytes().enumerate() {
        bytes[index % 16] = bytes[index % 16].wrapping_mul(31).wrapping_add(byte);
    }
    uuid::Uuid::from_bytes(bytes)
}
