use super::*;

/// Reuses the guarded Content Browser material editor inside an actor material-slot foldout.
pub(crate) fn inline_material_document_editor(
    world: &mut World,
    ui: &mut egui::Ui,
    path: &str,
) -> bool {
    let Some(asset) = world
        .get_resource::<EditorAssets>()
        .and_then(|assets| {
            assets
                .assets
                .iter()
                .find(|asset| asset.path.as_deref() == Some(path))
        })
        .cloned()
    else {
        ui.colored_label(
            crate::ui::theme::ERROR,
            "Material is missing from the content catalog",
        );
        return false;
    };
    let loaded_kind = world
        .resource::<crate::asset_documents::AuthoredAssetDocumentStore>()
        .snapshot_for_path(path)
        .map(|snapshot| snapshot.key.kind);
    let kind = loaded_kind.or_else(|| {
        if MaterialInstanceAsset::load_from_path(path).is_ok() {
            Some(crate::asset_documents::AuthoredAssetDocumentKind::MaterialInstance)
        } else if MaterialAsset::load_from_path(path).is_ok() {
            Some(crate::asset_documents::AuthoredAssetDocumentKind::Material)
        } else {
            None
        }
    });
    ui.push_id(("inline_material_document", path), |ui| match kind {
        Some(crate::asset_documents::AuthoredAssetDocumentKind::MaterialInstance) => {
            material_instance_asset_editor(world, ui, &asset, path, false);
        }
        Some(crate::asset_documents::AuthoredAssetDocumentKind::Material) => {
            material_asset_editor(world, ui, &asset, path, false);
        }
        _ => {
            ui.colored_label(crate::ui::theme::ERROR, "Material document is invalid");
        }
    });
    true
}

pub(crate) fn material_shader_label(world: &World, path: Option<&str>) -> String {
    let kind = material_shader_kind(world, path);
    match kind {
        Some(MaterialShaderKind::StandardLit) => "Standard Lit".into(),
        Some(MaterialShaderKind::Unlit) => "Unlit".into(),
        Some(MaterialShaderKind::Custom) => "Custom Surface".into(),
        None if path.is_none() => "Default Grid".into(),
        None => "Imported Source".into(),
    }
}

pub(crate) fn material_shader_kind(
    world: &World,
    path: Option<&str>,
) -> Option<MaterialShaderKind> {
    let path = path?;
    let documents = world.get_resource::<crate::asset_documents::AuthoredAssetDocumentStore>();
    let asset = documents
        .and_then(|store| store.snapshot_for_path(path))
        .and_then(|snapshot| documents.and_then(|store| store.material(&snapshot.key)))
        .cloned()
        .or_else(|| MaterialAsset::load_from_path(path).ok());
    asset.map(|asset| asset.shader.kind).or_else(|| {
        MaterialInstanceAsset::load_from_path(path)
            .ok()
            .and_then(|instance| instance.base.0.source_path)
            .and_then(|base| MaterialAsset::load_from_path(&base).ok())
            .map(|asset| asset.shader.kind)
    })
}

pub(crate) fn set_material_shader_kind(world: &mut World, path: &str, kind: MaterialShaderKind) {
    let Some(asset) = world
        .get_resource::<EditorAssets>()
        .and_then(|assets| {
            assets
                .assets
                .iter()
                .find(|asset| asset.path.as_deref() == Some(path))
        })
        .cloned()
    else {
        return;
    };
    let key = crate::asset_documents::ensure_material_document(world, path, &asset.label);
    let Some(mut draft) = world
        .resource::<crate::asset_documents::AuthoredAssetDocumentStore>()
        .material(&key)
        .cloned()
    else {
        return;
    };
    if draft.shader.kind == kind {
        return;
    }
    draft.shader.kind = kind;
    if draft.shader.kind != MaterialShaderKind::Custom {
        draft.shader_ref = None;
        draft.shader.schema_path = None;
        draft.shader.shader_path = None;
    }
    crate::asset_documents::update_material_document(world, &key, draft);
}

pub(crate) fn create_material_instance_from_base(
    world: &mut World,
    base_path: &str,
    base_label: &str,
) {
    let base_reference = world
        .get_resource_mut::<crate::asset_db::AssetRegistry>()
        .ok_or_else(|| "Asset registry is unavailable".to_string())
        .and_then(|mut registry| {
            ensure_asset_record(
                &mut registry,
                base_path.to_string(),
                base_label.to_string(),
                "Material",
            )
            .map(|record| {
                MaterialRef::new(
                    EditorAssetRef::new(
                        record.id.as_string(),
                        "material:source",
                        base_label.to_string(),
                    )
                    .with_source_path(base_path),
                )
            })
        });
    let base_reference = match base_reference {
        Ok(reference) => reference,
        Err(error) => {
            world.resource_mut::<SceneIo>().status = error;
            return;
        }
    };
    let base = Path::new(base_path);
    let parent = base
        .parent()
        .unwrap_or_else(|| Path::new("assets/materials"));
    let stem = base
        .file_stem()
        .and_then(|value| value.to_str())
        .unwrap_or("material");
    let mut index = 1usize;
    let path = loop {
        let suffix = if index == 1 {
            "instance".to_string()
        } else {
            format!("instance_{index}")
        };
        let candidate = parent.join(format!("{stem}_{suffix}.ron"));
        if !candidate.exists() {
            break candidate;
        }
        index += 1;
    };
    let instance = MaterialInstanceAsset {
        schema_version: shared::MATERIAL_INSTANCE_SCHEMA_VERSION,
        label: format!("{base_label} Instance"),
        base: base_reference,
        overrides: MaterialInputSet::default(),
    };
    let result = ron::ser::to_string_pretty(&instance, ron::ser::PrettyConfig::default())
        .map_err(|error| format!("could not serialize material instance: {error}"))
        .and_then(|text| {
            publish_authored_file(
                world,
                &path,
                text.as_bytes(),
                &FileSnapshot::missing(),
                FileWriteIntent::MaterialInstance,
            )
        });
    if let Err(error) = result {
        world.resource_mut::<SceneIo>().status = error;
        return;
    }
    let normalized = path.to_string_lossy().replace('\\', "/");
    let registry_error =
        if let Some(mut registry) = world.get_resource_mut::<crate::asset_db::AssetRegistry>() {
            ensure_asset_record(
                &mut registry,
                normalized.clone(),
                instance.label.clone(),
                "Material",
            )
            .map(|_| ())
            .and_then(|_| save_registry(&registry))
            .err()
        } else {
            Some("Asset registry is unavailable".to_string())
        };
    if let Some(error) = registry_error {
        world.resource_mut::<SceneIo>().status =
            format!("Created {normalized}, but registry update failed: {error}");
        return;
    }
    world.resource_mut::<EditorAssets>().refresh();
    world
        .resource_mut::<EditorAssets>()
        .select(AssetSelection::File(normalized.clone()));
    invalidate_on_catalog_refresh(world);
    world.resource_mut::<SceneIo>().status = format!("Created material instance {normalized}");
}

pub(super) fn shader_schema_assets(world: &World) -> Vec<(String, String, EditorAssetRef)> {
    let registry = world.get_resource::<crate::asset_db::AssetRegistry>();
    let mut schemas: Vec<(String, String, EditorAssetRef)> = world
        .resource::<EditorAssets>()
        .assets
        .iter()
        .filter(|asset| matches!(asset.kind, EditorAssetKind::ShaderSchema))
        .filter_map(|asset| {
            let path = asset.path.clone()?;
            let record = registry.and_then(|registry| find_asset_by_path(registry, &path))?;
            Some((
                asset.label.clone(),
                path.clone(),
                EditorAssetRef::new(record.id.as_string(), "shader:schema", asset.label.clone())
                    .with_source_path(path),
            ))
        })
        .collect();
    schemas.sort_by(|a, b| natural_cmp(&a.0, &b.0));
    schemas
}

pub(super) fn material_texture_candidates(world: &mut World) -> Vec<MaterialTextureCandidate> {
    let Some(registry) = world.get_resource::<crate::asset_db::AssetRegistry>() else {
        return Vec::new();
    };
    let assets = &world.resource::<EditorAssets>().assets;
    let thumbnails = world
        .get_resource::<crate::assets::AssetThumbnailCache>()
        .map(crate::assets::AssetThumbnailCache::snapshot);
    let mut textures = Vec::new();
    let mut seen = HashSet::new();

    for asset in assets
        .iter()
        .filter(|asset| matches!(asset.kind, EditorAssetKind::Texture))
    {
        let Some(path) = asset.path.as_deref() else {
            continue;
        };
        let Some(record) = find_asset_by_path(registry, path) else {
            continue;
        };
        let reference =
            EditorAssetRef::new(record.id.as_string(), "texture:source", asset.label.clone())
                .with_source_path(path);
        if !seen.insert((reference.asset_id.clone(), reference.sub_asset_id.clone())) {
            continue;
        }
        textures.push(MaterialTextureCandidate {
            label: asset.label.clone(),
            detail: path.to_string(),
            reference,
            locate: MaterialLocateTarget::File(path.to_string()),
            thumbnail: thumbnails.as_ref().map_or(
                MaterialThumbnailPresentation::Pending,
                |cache| {
                    if let Some(texture) = cache.texture_for(asset) {
                        MaterialThumbnailPresentation::Ready(texture)
                    } else if cache.is_failed(asset) {
                        MaterialThumbnailPresentation::Failed
                    } else {
                        MaterialThumbnailPresentation::Pending
                    }
                },
            ),
        });
    }

    for asset in assets
        .iter()
        .filter(|asset| matches!(asset.kind, EditorAssetKind::Model))
    {
        let Some(parent_path) = asset.path.as_deref() else {
            continue;
        };
        let Some(record) = find_asset_by_path(registry, parent_path) else {
            continue;
        };
        let Some(manifest) = record
            .model_import()
            .static_mesh_manifest_path
            .as_deref()
            .and_then(|path| load_static_mesh_manifest(path).ok())
        else {
            continue;
        };
        for source_path in manifest
            .source
            .dependencies
            .iter()
            .filter(|path| is_material_texture_path(path))
        {
            let label = Path::new(source_path)
                .file_stem()
                .and_then(|stem| stem.to_str())
                .unwrap_or(source_path)
                .to_string();
            let sub_asset_id = format!("texture:{}", stable_material_subasset_slug(source_path));
            let reference =
                EditorAssetRef::new(record.id.as_string(), sub_asset_id.clone(), label.clone())
                    .with_source_path(source_path);
            if !seen.insert((reference.asset_id.clone(), reference.sub_asset_id.clone())) {
                continue;
            }
            textures.push(MaterialTextureCandidate {
                label: label.clone(),
                detail: format!("{} | {source_path}", asset.label),
                reference,
                locate: MaterialLocateTarget::SubAsset {
                    parent_path: parent_path.to_string(),
                    sub_asset_id,
                    label,
                    source_path: Some(source_path.clone()),
                },
                thumbnail: MaterialThumbnailPresentation::Pending,
            });
        }
    }

    textures.sort_by(|left, right| {
        natural_cmp(&left.label, &right.label)
            .then_with(|| natural_cmp(&left.detail, &right.detail))
    });
    textures
}

/// Requests thumbnails only for textures currently visible through authored material inputs.
///
/// Candidate enumeration must remain metadata-only. Eagerly prefetching the full project texture
/// catalog pins every source image in `EguiUserTextures`; a handful of Poly Haven 2K/4K maps can
/// otherwise exhaust VRAM before Solari allocates its view reservoirs.
pub(super) fn prefetch_bound_material_textures<'a>(
    world: &mut World,
    input_sets: impl IntoIterator<Item = &'a MaterialInputSet>,
) {
    let references = input_sets
        .into_iter()
        .flat_map(|inputs| inputs.textures.iter())
        .filter_map(|binding| binding.texture.as_ref())
        .map(|reference| (reference.asset_id.clone(), reference.source_path.clone()))
        .collect::<HashSet<_>>();
    if references.is_empty() {
        return;
    }

    let registry = world.get_resource::<crate::asset_db::AssetRegistry>();
    let assets = world
        .resource::<EditorAssets>()
        .assets
        .iter()
        .filter(|asset| matches!(asset.kind, EditorAssetKind::Texture))
        .filter(|asset| {
            let path = asset.path.as_deref();
            let asset_id = path
                .and_then(|path| registry.and_then(|registry| find_asset_by_path(registry, path)))
                .map(|record| record.id.as_string());
            references.iter().any(|(reference_id, reference_path)| {
                asset_id.as_deref() == Some(reference_id.as_str())
                    || path.is_some_and(|path| reference_path.as_deref() == Some(path))
            })
        })
        .cloned()
        .collect::<Vec<_>>();
    prefetch_asset_thumbnails(world, &assets);
}

fn is_material_texture_path(path: &str) -> bool {
    Path::new(path)
        .extension()
        .and_then(|extension| extension.to_str())
        .is_some_and(|extension| {
            matches!(
                extension.to_ascii_lowercase().as_str(),
                "png" | "jpg" | "jpeg" | "webp" | "ktx2"
            )
        })
}

fn stable_material_subasset_slug(label: &str) -> String {
    let mut slug = String::new();
    for ch in label.chars() {
        if ch.is_ascii_alphanumeric() {
            slug.push(ch.to_ascii_lowercase());
        } else if !slug.ends_with('_') {
            slug.push('_');
        }
    }
    slug.trim_matches('_').to_string()
}

pub(super) fn material_shader_settings(
    ui: &mut egui::Ui,
    asset: &mut MaterialAsset,
    shader_schemas: &[(String, String, EditorAssetRef)],
    show_kind: bool,
) -> MaterialInputSchema {
    if show_kind {
        shader_kind_picker(ui, &mut asset.shader.kind);
    }
    if matches!(asset.shader.kind, MaterialShaderKind::Custom) {
        let mut selected_schema = asset.shader.schema_path.clone();
        ui.horizontal(|ui| {
            ui.add_sized([92.0, 20.0], egui::Label::new("Schema"));
            egui::ComboBox::from_id_salt("asset_material_shader_schema")
                .selected_text(
                    selected_schema
                        .as_deref()
                        .and_then(|path| {
                            shader_schemas
                                .iter()
                                .find(|(_, schema_path, _)| schema_path == path)
                                .map(|(label, _, _)| label.as_str())
                        })
                        .unwrap_or("(none)"),
                )
                .show_ui(ui, |ui| {
                    if ui
                        .selectable_label(selected_schema.is_none(), "(none)")
                        .clicked()
                    {
                        selected_schema = None;
                    }
                    for (label, path, _) in shader_schemas {
                        if ui
                            .selectable_label(selected_schema.as_deref() == Some(path), label)
                            .clicked()
                        {
                            selected_schema = Some(path.clone());
                        }
                    }
                });
        });
        if selected_schema != asset.shader.schema_path {
            asset.shader.schema_path = selected_schema.clone();
            if let Some(path) = selected_schema.as_deref() {
                asset.shader_ref = shader_schemas
                    .iter()
                    .find(|(_, schema_path, _)| schema_path == path)
                    .map(|(_, _, reference)| reference.clone());
                if let Err(error) = apply_shader_schema(asset, path) {
                    ui.colored_label(egui::Color32::YELLOW, error);
                }
            } else {
                asset.shader_ref = None;
            }
        }
        optional_path_edit(ui, "WGSL shader", &mut asset.shader.shader_path);
        if let Some(path) = asset.shader.schema_path.clone() {
            ui.horizontal(|ui| {
                if ui.button("Reload Schema").clicked() {
                    if let Err(error) = apply_shader_schema(asset, &path) {
                        ui.colored_label(egui::Color32::YELLOW, error);
                    }
                }
                ui.small(egui::RichText::new(path).color(TEXT_DIM));
            });
        }
    } else {
        asset.shader_ref = None;
        asset.shader.schema_path = None;
        asset.shader.shader_path = None;
    }

    if matches!(asset.shader.kind, MaterialShaderKind::Custom) {
        asset
            .shader
            .schema_path
            .as_deref()
            .and_then(|path| ShaderSchemaAsset::load_from_path(path).ok())
            .map(|asset| asset.schema)
            .unwrap_or_default()
    } else {
        standard_lit_input_schema()
    }
}

pub(super) fn apply_shader_schema(asset: &mut MaterialAsset, path: &str) -> Result<(), String> {
    let schema = ShaderSchemaAsset::load_from_path(path)?;
    asset.shader.kind = schema.kind;
    asset.shader.schema_path = Some(path.to_string());
    if asset.shader.shader_path.is_none() {
        asset.shader.shader_path = schema.wgsl_path.clone();
    }

    for property in &schema.schema.inputs {
        if property.texture.is_some()
            || matches!(property.property_type, ShaderPropertyType::Texture)
        {
            let binding =
                ensure_material_texture_binding(&mut asset.inputs.textures, &property.name);
            if let Some(texture) = property.texture.as_ref() {
                binding.channel = texture.default_channel;
            }
        }
        if matches!(property.property_type, ShaderPropertyType::Texture) {
            continue;
        }
        let default = property
            .default_value
            .clone()
            .unwrap_or_else(|| default_value_for_shader_property(&property.property_type));
        match asset
            .inputs
            .values
            .iter_mut()
            .find(|parameter| parameter.name == property.name)
        {
            Some(parameter) => {
                if !parameter_value_matches_property(&parameter.value, &property.property_type) {
                    parameter.value = default;
                }
            }
            None => asset.inputs.values.push(MaterialParameter {
                name: property.name.clone(),
                value: default,
            }),
        }
    }

    Ok(())
}
