use super::*;

pub(super) fn assign_extracted_material_to_actor_slot(
    world: &mut World,
    target: &GltfMaterialExtractionTarget,
    reference: MaterialRef,
) -> Result<(), String> {
    use crate::history::reflected_component_transaction;

    if let Some(mut primitive) = world.get::<shared::Primitive>(target.entity).cloned() {
        if primitive.surface.id == target.slot_id {
            primitive.surface.material = Some(reference);
            return reflected_component_transaction(
                world,
                target.entity,
                "Extract and Assign Material",
                shared::AUTHORING_COMPONENT_PRIMITIVE,
                shared::COMPONENT_PRIMITIVE,
                move |world, entity| {
                    world.entity_mut(entity).insert(primitive);
                    Ok(())
                },
            );
        }
    }
    if let Some(mut renderer) = world
        .get::<shared::StaticMeshRenderer>(target.entity)
        .cloned()
    {
        if let Some(slot) = renderer.materials.slot_mut(&target.slot_id) {
            slot.material = Some(reference);
            return reflected_component_transaction(
                world,
                target.entity,
                "Extract and Assign Material",
                shared::AUTHORING_COMPONENT_STATIC_MESH_RENDERER,
                shared::COMPONENT_STATIC_MESH_RENDERER,
                move |world, entity| {
                    world.entity_mut(entity).insert(renderer);
                    Ok(())
                },
            );
        }
    }
    if let Some(mut renderer) = world
        .get::<shared::SkinnedMeshRenderer>(target.entity)
        .cloned()
    {
        if let Some(slot) = renderer.materials.slot_mut(&target.slot_id) {
            slot.material = Some(reference);
            return reflected_component_transaction(
                world,
                target.entity,
                "Extract and Assign Material",
                shared::AUTHORING_COMPONENT_SKINNED_MESH_RENDERER,
                shared::COMPONENT_SKINNED_MESH_RENDERER,
                move |world, entity| {
                    world.entity_mut(entity).insert(renderer);
                    Ok(())
                },
            );
        }
    }
    Err("The actor or exact material slot changed before assignment".into())
}

pub(super) fn bind_extracted_material_slots(
    document: &mut AssetRegistryDocument,
    source: &str,
    manifest: &crate::assets::static_mesh::StaticMeshManifest,
    bindings: &[ExtractedMaterialBinding],
) -> Result<usize, String> {
    let material_bindings = bindings
        .iter()
        .map(|binding| {
            let record = document
                .records
                .iter()
                .find(|record| record.path == binding.path)
                .ok_or_else(|| {
                    format!("Extracted Material was not registered: {}", binding.path)
                })?;
            let expected_source_id =
                material_id_from_label(&format!("Material{}", binding.source_index));
            let reference = MaterialRef::new(
                EditorAssetRef::new(record.id.as_string(), "", record.label.clone())
                    .with_source_path(record.path.clone()),
            );
            Ok((expected_source_id, reference))
        })
        .collect::<Result<Vec<_>, String>>()?;
    let model = document
        .records
        .iter_mut()
        .find(|record| record.path == source)
        .ok_or_else(|| format!("Model is not registered: {source}"))?;
    let settings = model
        .import_settings
        .model_mut()
        .ok_or_else(|| format!("Asset is not a model: {source}"))?;
    let mut mapped_slots = 0;
    for part in &manifest.parts {
        let Some((_, reference)) = material_bindings.iter().find(|(source_id, _)| {
            part_effective_material_id(part).as_deref() == Some(source_id.as_str())
        }) else {
            continue;
        };
        let slot_id = shared::ComponentInstanceId::new(format!("slot:{}", part_effective_id(part)));
        let selection = ModelMaterialSelection::Project(reference.clone());
        if let Some(existing) = settings
            .material_slots
            .iter_mut()
            .find(|entry| entry.slot_id == slot_id)
        {
            existing.selection = selection;
        } else {
            settings
                .material_slots
                .push(ModelMaterialSlotSelection { slot_id, selection });
        }
        mapped_slots += 1;
    }
    settings
        .material_slots
        .sort_by(|left, right| left.slot_id.0.cmp(&right.slot_id.0));
    Ok(mapped_slots)
}

pub(super) fn refresh_extracted_model_artifacts(
    world: &mut World,
    source: &str,
) -> Result<(), String> {
    let mut registry = world.resource_mut::<AssetRegistry>();
    let record = find_asset_mut_by_path(&mut registry, source)
        .ok_or_else(|| format!("Model is no longer registered: {source}"))?;
    refresh_model_artifacts(record)?;
    save_registry(&registry)?;
    registry.index_dirty = false;
    Ok(())
}

pub(super) fn begin_pbr_grouping(world: &mut World, folder: String) {
    let paths = match fs::read_dir(&folder) {
        Ok(entries) => entries
            .filter_map(Result::ok)
            .filter_map(|entry| {
                entry
                    .file_type()
                    .ok()
                    .filter(|kind| kind.is_file())
                    .map(|_| entry.path())
            })
            .collect::<Vec<_>>(),
        Err(error) => {
            world.resource_mut::<SceneIo>().status =
                format!("Could not inspect {folder} for PBR textures: {error}");
            return;
        }
    };
    let groups = content_pipeline::detect_pbr_texture_groups(paths);
    let textures = groups
        .into_iter()
        .flat_map(|group| {
            group
                .textures
                .into_iter()
                .map(move |texture| PbrTextureDraft {
                    path: texture.path.to_string_lossy().replace('\\', "/"),
                    target: group.key.clone(),
                    role: texture.role,
                    confidence: texture.confidence,
                    included: texture.role != content_pipeline::PbrTextureRole::Unknown,
                })
        })
        .collect::<Vec<_>>();
    if textures.is_empty() {
        world.resource_mut::<SceneIo>().status =
            format!("No supported texture files found directly in {folder}");
        return;
    }
    world
        .resource_mut::<AssetBrowserUiState>()
        .pending_pbr_grouping = Some(PbrGroupingDraft { folder, textures });
}

pub(super) fn draw_pbr_grouping_modal(world: &mut World, ctx: &egui::Context) {
    let Some(mut draft) = world
        .resource::<AssetBrowserUiState>()
        .pending_pbr_grouping
        .clone()
    else {
        return;
    };
    let mut cancel = false;
    let mut create = false;
    egui::Window::new("Create PBR Materials From Folder")
        .collapsible(false)
        .resizable(true)
        .default_width(820.0)
        .min_width(620.0)
        .show(ctx, |ui| {
            ui.label(format!("Source folder: {}", draft.folder));
            ui.small(
                egui::RichText::new(
                    "Review detected maps. Edit Target to merge or split material groups; unresolved files remain visible but are excluded by default.",
                )
                .color(TEXT_DIM),
            );
            ui.separator();
            egui::Grid::new("pbr_grouping_review")
                .num_columns(5)
                .striped(true)
                .spacing([10.0, 6.0])
                .show(ui, |ui| {
                    ui.strong("Use");
                    ui.strong("Texture");
                    ui.strong("Target");
                    ui.strong("Role");
                    ui.strong("Confidence");
                    ui.end_row();
                    for (index, texture) in draft.textures.iter_mut().enumerate() {
                        ui.checkbox(&mut texture.included, "");
                        let name = Path::new(&texture.path)
                            .file_name()
                            .and_then(|name| name.to_str())
                            .unwrap_or(&texture.path);
                        ui.add(egui::Label::new(name).truncate()).on_hover_text(&texture.path);
                        ui.add_sized([170.0, 20.0], egui::TextEdit::singleline(&mut texture.target));
                        egui::ComboBox::from_id_salt(("pbr_role", index))
                            .selected_text(texture.role.label())
                            .show_ui(ui, |ui| {
                                for role in content_pipeline::PbrTextureRole::ALL {
                                    ui.selectable_value(&mut texture.role, role, role.label());
                                }
                            });
                        ui.label(pbr_confidence_label(texture.confidence));
                        ui.end_row();
                    }
                });
            ui.separator();
            let group_count = pbr_draft_group_count(&draft);
            let valid = group_count > 0
                && draft.textures.iter().filter(|texture| texture.included).all(|texture| {
                    valid_content_name(&texture.target)
                        && texture.role != content_pipeline::PbrTextureRole::Unknown
                });
            ui.horizontal(|ui| {
                if ui.button("Cancel").clicked() {
                    cancel = true;
                }
                if ui
                    .add_enabled(
                        valid,
                        egui::Button::new(format!("Create {group_count} Material(s)")),
                    )
                    .clicked()
                {
                    create = true;
                }
            });
            if !valid {
                ui.small(
                    egui::RichText::new(
                        "Included rows need a valid Target and a resolved texture role.",
                    )
                    .color(WARNING),
                );
            }
        });

    if cancel {
        world
            .resource_mut::<AssetBrowserUiState>()
            .pending_pbr_grouping = None;
    } else if create {
        match create_pbr_materials(world, &draft) {
            Ok(()) => {
                world
                    .resource_mut::<AssetBrowserUiState>()
                    .pending_pbr_grouping = None;
            }
            Err(error) => {
                world.resource_mut::<SceneIo>().status = error;
                world
                    .resource_mut::<AssetBrowserUiState>()
                    .pending_pbr_grouping = Some(draft);
            }
        }
    } else {
        world
            .resource_mut::<AssetBrowserUiState>()
            .pending_pbr_grouping = Some(draft);
    }
}

pub(super) fn pbr_confidence_label(
    confidence: content_pipeline::PbrMatchConfidence,
) -> &'static str {
    match confidence {
        content_pipeline::PbrMatchConfidence::High => "High",
        content_pipeline::PbrMatchConfidence::Medium => "Medium",
        content_pipeline::PbrMatchConfidence::Low => "Low",
        content_pipeline::PbrMatchConfidence::Unresolved => "Unresolved",
    }
}

pub(super) fn pbr_draft_group_count(draft: &PbrGroupingDraft) -> usize {
    draft
        .textures
        .iter()
        .filter(|texture| texture.included)
        .map(|texture| texture.target.trim())
        .collect::<HashSet<_>>()
        .len()
}

pub(super) fn create_pbr_materials(
    world: &mut World,
    draft: &PbrGroupingDraft,
) -> Result<(), String> {
    let mut groups: BTreeMap<String, Vec<&PbrTextureDraft>> = BTreeMap::new();
    for texture in draft.textures.iter().filter(|texture| texture.included) {
        if !valid_content_name(&texture.target)
            || texture.role == content_pipeline::PbrTextureRole::Unknown
        {
            return Err("Every included texture needs a valid target and resolved role".into());
        }
        groups
            .entry(texture.target.trim().to_string())
            .or_default()
            .push(texture);
    }
    if groups.is_empty() {
        return Err("Select at least one texture before creating materials".into());
    }

    let records = world
        .resource::<crate::asset_db::AssetRegistry>()
        .records
        .clone();
    let mut reserved = HashSet::new();
    let mut operations = Vec::new();
    for (target, textures) in groups {
        let destination = unique_material_destination(&draft.folder, &target, &mut reserved);
        let asset = material_asset_from_pbr_group(&target, &textures, &records);
        let ron = ron::ser::to_string_pretty(&asset, ron::ser::PrettyConfig::default())
            .map_err(|error| format!("Could not serialize material {target}: {error}"))?;
        operations.push(content_pipeline::ContentOperation::WriteFile {
            path: PathBuf::from(destination),
            bytes: ron.into_bytes(),
        });
    }
    if !commit_content_operations(world, operations) {
        return Err(world.resource::<SceneIo>().status.clone());
    }
    Ok(())
}

pub(super) fn unique_material_destination(
    folder: &str,
    target: &str,
    reserved: &mut HashSet<String>,
) -> String {
    let stem = target
        .trim()
        .chars()
        .map(|character| {
            if character.is_ascii_alphanumeric() || matches!(character, '-' | '_') {
                character
            } else {
                '_'
            }
        })
        .collect::<String>();
    let stem = stem.trim_matches('_');
    let stem = if stem.is_empty() { "Material" } else { stem };
    for suffix in 1.. {
        let file = if suffix == 1 {
            format!("{stem}.material.ron")
        } else {
            format!("{stem}_{suffix}.material.ron")
        };
        let path = Path::new(folder)
            .join(file)
            .to_string_lossy()
            .replace('\\', "/");
        if !Path::new(&path).exists() && reserved.insert(path.clone()) {
            return path;
        }
    }
    unreachable!()
}

pub(super) fn material_asset_from_pbr_group(
    target: &str,
    textures: &[&PbrTextureDraft],
    records: &[crate::asset_db::AssetRecord],
) -> MaterialAsset {
    let mut material = MaterialDesc::default();
    for texture in textures {
        match texture.role {
            content_pipeline::PbrTextureRole::BaseColor => {
                material.base_color_texture = Some(texture.path.clone());
            }
            content_pipeline::PbrTextureRole::Normal => {
                material.normal_map_texture = Some(texture.path.clone());
            }
            content_pipeline::PbrTextureRole::PackedOrm => {
                material.metallic_roughness_texture = Some(texture.path.clone());
                material.textures.push(MaterialTextureBinding {
                    name: "occlusion_texture".into(),
                    texture: Some(pbr_texture_reference(&texture.path, records)),
                    channel: TextureChannel::R,
                });
            }
            content_pipeline::PbrTextureRole::Roughness => {
                material.textures.push(MaterialTextureBinding {
                    name: "roughness_texture".into(),
                    texture: Some(pbr_texture_reference(&texture.path, records)),
                    channel: TextureChannel::G,
                });
            }
            content_pipeline::PbrTextureRole::Metallic => {
                material.textures.push(MaterialTextureBinding {
                    name: "metallic_texture".into(),
                    texture: Some(pbr_texture_reference(&texture.path, records)),
                    channel: TextureChannel::B,
                });
            }
            content_pipeline::PbrTextureRole::Occlusion => {
                material.textures.push(MaterialTextureBinding {
                    name: "occlusion_texture".into(),
                    texture: Some(pbr_texture_reference(&texture.path, records)),
                    channel: TextureChannel::R,
                });
            }
            content_pipeline::PbrTextureRole::Height => {
                material.textures.push(MaterialTextureBinding {
                    name: "height_texture".into(),
                    texture: Some(pbr_texture_reference(&texture.path, records)),
                    channel: TextureChannel::R,
                });
            }
            content_pipeline::PbrTextureRole::Unknown => {}
        }
    }
    MaterialAsset {
        schema_version: shared::MATERIAL_ASSET_SCHEMA_VERSION,
        label: target.replace(['_', '-'], " "),
        shader: material.shader.clone(),
        shader_ref: None,
        render_state: shared::MaterialRenderState::default(),
        provenance: None,
        inputs: MaterialInputSet::from_material_desc(&material),
    }
}

pub(super) fn pbr_texture_reference(
    path: &str,
    records: &[crate::asset_db::AssetRecord],
) -> EditorAssetRef {
    let label = Path::new(path)
        .file_stem()
        .and_then(|stem| stem.to_str())
        .unwrap_or("Texture");
    let id = records
        .iter()
        .find(|record| record.path == path)
        .map(|record| record.id.as_string())
        .unwrap_or_default();
    EditorAssetRef::new(id, "texture:source", label).with_source_path(path)
}

pub(super) fn commit_content_operations(
    world: &mut World,
    operations: Vec<content_pipeline::ContentOperation>,
) -> bool {
    commit_content_operations_internal(world, operations, true)
}

pub(super) fn commit_content_operations_internal(
    world: &mut World,
    operations: Vec<content_pipeline::ContentOperation>,
    record_undo: bool,
) -> bool {
    commit_content_operations_internal_with_registry_update(world, operations, record_undo, |_| {
        Ok(())
    })
}

pub(super) fn commit_content_operations_internal_with_registry_update(
    world: &mut World,
    operations: Vec<content_pipeline::ContentOperation>,
    record_undo: bool,
    registry_update: impl FnOnce(&mut AssetRegistryDocument) -> Result<(), String>,
) -> bool {
    let previous_folder = world.resource::<EditorAssets>().current_folder.clone();
    let mut resulting_folder = previous_folder.clone();
    for operation in &operations {
        if let content_pipeline::ContentOperation::Move {
            source,
            destination,
        } = operation
        {
            let source = source.to_string_lossy().replace('\\', "/");
            let destination = destination.to_string_lossy().replace('\\', "/");
            if resulting_folder == source
                || resulting_folder
                    .strip_prefix(&source)
                    .is_some_and(|suffix| suffix.starts_with('/'))
            {
                resulting_folder = format!("{destination}{}", &resulting_folder[source.len()..]);
            }
        }
    }
    let mut document = world
        .resource::<crate::asset_db::AssetRegistry>()
        .document();
    let preview = match content_pipeline::preview_transaction(Path::new("."), operations, &document)
    {
        Ok(preview) => preview,
        Err(error) => {
            world.resource_mut::<SceneIo>().status = format!("Content operation blocked: {error}");
            return false;
        }
    };
    if !preview.is_committable() {
        world.resource_mut::<SceneIo>().status = format!(
            "Content operation has {} collision(s)",
            preview.collisions.len()
        );
        return false;
    }
    crate::assets::begin_content_watch_transaction(world);
    let commit_result = content_pipeline::commit_transaction_with_registry_update(
        Path::new("."),
        &preview,
        &mut document,
        registry_update,
    );
    crate::assets::end_content_watch_transaction(world);
    if let Err(error) = commit_result {
        world.resource_mut::<SceneIo>().status = error;
        return false;
    }
    let undo_entry = record_undo
        .then(|| build_content_undo_entry(Path::new("."), &preview))
        .flatten();
    {
        let mut registry = world.resource_mut::<crate::asset_db::AssetRegistry>();
        registry.schema_version = document.schema_version;
        registry.defaults = document.defaults;
        registry.records = document.records;
        registry.index_dirty = false;
        registry.migration_required = false;
    }
    let resulting_selections: Vec<AssetSelection> = preview
        .operations
        .iter()
        .map(|operation| match operation {
            content_pipeline::ContentOperation::CreateFolder { path } => {
                AssetSelection::Folder(path.to_string_lossy().replace('\\', "/"))
            }
            content_pipeline::ContentOperation::Move { destination, .. }
            | content_pipeline::ContentOperation::Copy { destination, .. } => {
                let normalized = destination.to_string_lossy().replace('\\', "/");
                if Path::new(&normalized).is_dir() {
                    AssetSelection::Folder(normalized)
                } else {
                    AssetSelection::File(normalized)
                }
            }
            content_pipeline::ContentOperation::WriteFile { path, .. }
            | content_pipeline::ContentOperation::ReplaceFile { path, .. } => {
                AssetSelection::File(path.to_string_lossy().replace('\\', "/"))
            }
        })
        .collect();
    {
        let mut assets = world.resource_mut::<EditorAssets>();
        assets.current_folder = resulting_folder;
        assets.refresh();
        assets.select_all(&resulting_selections);
    }
    invalidate_on_catalog_refresh(world);
    if let Some(entry) = undo_entry {
        push_content_undo(world, entry);
    }
    world.resource_mut::<SceneIo>().status = format!(
        "Content transaction committed ({} item(s), {} reference rewrite(s))",
        preview.operations.len(),
        preview.reference_rewrites.len()
    );
    true
}
