use super::*;

#[derive(Clone)]
pub(super) struct AssetBrowserStateSnapshot {
    pub(super) search: String,
    pub(super) view: AssetBrowserView,
    pub(super) sort: AssetSort,
    pub(super) kind_filter: AssetKindFilter,
    pub(super) thumbnail_size: f32,
    pub(super) recursive: bool,
    pub(super) show_details: bool,
    pub(super) details_width: f32,
}

pub(super) fn state_snapshot(state: &AssetBrowserUiState) -> AssetBrowserStateSnapshot {
    AssetBrowserStateSnapshot {
        search: state.search.clone(),
        view: state.view,
        sort: state.sort,
        kind_filter: state.kind_filter,
        thumbnail_size: state.thumbnail_size,
        recursive: state.recursive,
        show_details: state.show_details,
        details_width: state.details_width,
    }
}

pub(super) fn embedded_assets_for_asset(world: &World, asset: &EditorAsset) -> Vec<EmbeddedAsset> {
    if !matches!(asset.kind, EditorAssetKind::Model) {
        return Vec::new();
    }
    let Some(parent_path) = asset.path.as_ref() else {
        return Vec::new();
    };
    let Some(record) = world
        .get_resource::<crate::asset_db::AssetRegistry>()
        .and_then(|registry| find_asset_by_path(registry, parent_path))
    else {
        return Vec::new();
    };
    let mut embedded = Vec::new();
    let settings = record.model_import();
    let static_manifest = settings
        .static_mesh_manifest_path
        .as_deref()
        .and_then(|path| load_static_mesh_manifest(path).ok());
    let legacy_skinned_primitives = static_manifest
        .as_ref()
        .filter(|manifest| manifest.schema_version < 2)
        .map(|_| crate::assets::gltf_skinned_primitive_labels(parent_path.as_str()))
        .unwrap_or_default();
    if let Some(manifest) = static_manifest.as_ref() {
        for part in &manifest.parts {
            let mesh_id = part_effective_id(part);
            let material_label = part.material_label.clone();
            let requires_skinned_hierarchy = manifest.metadata.animation_count > 0
                || part.skinned
                || legacy_skinned_primitives.contains(&part.mesh_label);
            embedded.push(EmbeddedAsset {
                selection: AssetSelection::SubAsset {
                    parent_path: parent_path.clone(),
                    sub_asset_id: mesh_id,
                    label: part.name.clone(),
                    kind: AssetSubAssetKind::Mesh,
                    source_path: None,
                },
                label: part.name.clone(),
                kind: AssetSubAssetKind::Mesh,
                detail: if requires_skinned_hierarchy {
                    "Skinned | dedicated renderer".to_string()
                } else {
                    part.source_mesh
                        .clone()
                        .unwrap_or_else(|| part.mesh_label.clone())
                },
                texture_path: None,
                thumbnail_key: subasset_thumbnail_key(
                    parent_path,
                    AssetSubAssetKind::Mesh,
                    &part_effective_id(part),
                ),
                mesh_label: Some(part.mesh_label.clone()),
                material_label,
                requires_skinned_hierarchy,
            });
        }

        let mut material_ids = HashSet::new();
        for part in &manifest.parts {
            let Some(material_id) = part_effective_material_id(part) else {
                continue;
            };
            if !material_ids.insert(material_id.clone()) {
                continue;
            }
            let label = if part.material_slot_name.trim().is_empty() {
                part.material_label
                    .clone()
                    .unwrap_or_else(|| "Source Material".to_string())
            } else {
                part.material_slot_name.clone()
            };
            let source_used = manifest.parts.iter().any(|candidate| {
                part_effective_material_id(candidate).as_deref() == Some(material_id.as_str())
                    && matches!(
                        crate::assets::static_mesh::material_selection(
                            settings,
                            &format!("slot:{}", part_effective_id(candidate)),
                        ),
                        ModelMaterialSelection::Source
                    )
            });
            embedded.push(EmbeddedAsset {
                selection: AssetSelection::SubAsset {
                    parent_path: parent_path.clone(),
                    sub_asset_id: material_id,
                    label: label.clone(),
                    kind: AssetSubAssetKind::Material,
                    source_path: None,
                },
                label,
                kind: AssetSubAssetKind::Material,
                detail: if source_used {
                    "Embedded source material · active model default".to_string()
                } else {
                    "Embedded source material · available".to_string()
                },
                texture_path: None,
                thumbnail_key: subasset_thumbnail_key(
                    parent_path,
                    AssetSubAssetKind::Material,
                    &part_effective_material_id(part).unwrap_or_else(|| {
                        material_id_from_label(
                            part.material_label
                                .as_deref()
                                .unwrap_or(&part.material_slot_name),
                        )
                    }),
                ),
                mesh_label: None,
                material_label: part.material_label.clone(),
                requires_skinned_hierarchy: false,
            });
        }

        let mut texture_paths = HashSet::new();
        for dependency in &manifest.source.dependencies {
            if !is_texture_path(dependency) || !texture_paths.insert(dependency.clone()) {
                continue;
            }
            let label = Path::new(dependency)
                .file_stem()
                .and_then(|stem| stem.to_str())
                .unwrap_or(dependency)
                .to_string();
            embedded.push(EmbeddedAsset {
                selection: AssetSelection::SubAsset {
                    parent_path: parent_path.clone(),
                    sub_asset_id: format!("texture:{}", stable_subasset_slug(dependency)),
                    label: label.clone(),
                    kind: AssetSubAssetKind::Texture,
                    source_path: Some(dependency.clone()),
                },
                label,
                kind: AssetSubAssetKind::Texture,
                detail: dependency.clone(),
                texture_path: Some(dependency.clone()),
                thumbnail_key: subasset_thumbnail_key(
                    parent_path,
                    AssetSubAssetKind::Texture,
                    &stable_subasset_slug(dependency),
                ),
                mesh_label: None,
                material_label: None,
                requires_skinned_hierarchy: false,
            });
        }
    }

    let animation_manifest = settings
        .animation_manifest_path
        .as_deref()
        .and_then(|path| load_animation_manifest(path).ok());
    if let Some(manifest) = animation_manifest {
        for skeleton in manifest.skeletons {
            let signature = skeleton.signature.0;
            let signature_short = signature.get(..8).unwrap_or(&signature);
            embedded.push(EmbeddedAsset {
                selection: AssetSelection::SubAsset {
                    parent_path: parent_path.clone(),
                    sub_asset_id: skeleton.id.clone(),
                    label: skeleton.label.clone(),
                    kind: AssetSubAssetKind::Skeleton,
                    source_path: Some(parent_path.clone()),
                },
                label: skeleton.label,
                kind: AssetSubAssetKind::Skeleton,
                detail: format!(
                    "{} joints | rig {}",
                    skeleton.joint_paths.len(),
                    signature_short
                ),
                texture_path: None,
                thumbnail_key: subasset_thumbnail_key(
                    parent_path,
                    AssetSubAssetKind::Skeleton,
                    &skeleton.id,
                ),
                mesh_label: None,
                material_label: None,
                requires_skinned_hierarchy: false,
            });
        }
        for clip in manifest.clips {
            let event_suffix = if clip.events.is_empty() {
                String::new()
            } else {
                format!(" | {} events", clip.events.len())
            };
            embedded.push(EmbeddedAsset {
                selection: AssetSelection::SubAsset {
                    parent_path: parent_path.clone(),
                    sub_asset_id: clip.id.clone(),
                    label: clip.label.clone(),
                    kind: AssetSubAssetKind::AnimationClip,
                    source_path: Some(parent_path.clone()),
                },
                label: clip.label,
                kind: AssetSubAssetKind::AnimationClip,
                detail: format!("{:.2}s{event_suffix}", clip.duration_seconds),
                texture_path: None,
                thumbnail_key: subasset_thumbnail_key(
                    parent_path,
                    AssetSubAssetKind::AnimationClip,
                    &clip.id,
                ),
                mesh_label: None,
                material_label: None,
                requires_skinned_hierarchy: false,
            });
        }
    }

    embedded
}

pub(super) fn texture_asset_from_subasset_selection(
    world: &World,
    selection: &Option<AssetSelection>,
) -> Option<EditorAsset> {
    let Some(AssetSelection::SubAsset {
        label,
        kind: AssetSubAssetKind::Texture,
        source_path: Some(path),
        ..
    }) = selection
    else {
        return None;
    };
    if let Some(asset) = world
        .resource::<EditorAssets>()
        .assets
        .iter()
        .find(|asset| asset.path.as_deref() == Some(path.as_str()))
    {
        return Some(asset.clone());
    }
    Some(EditorAsset {
        label: label.clone(),
        path: Some(path.clone()),
        folder_path: Path::new(path)
            .parent()
            .map(|parent| parent.to_string_lossy().replace('\\', "/"))
            .unwrap_or_else(|| ASSETS_ROOT.to_string()),
        kind: EditorAssetKind::Texture,
    })
}

pub(super) fn model_uses_source_materials(record: &crate::asset_db::AssetRecord) -> bool {
    let settings = record.model_import();
    let Some(manifest_path) = settings.static_mesh_manifest_path.as_deref() else {
        return true;
    };
    let Ok(manifest) = load_static_mesh_manifest(manifest_path) else {
        return true;
    };
    manifest.parts.iter().any(|part| {
        part_effective_material_id(part).is_some()
            && matches!(
                crate::assets::static_mesh::material_selection(
                    settings,
                    &format!("slot:{}", part_effective_id(part)),
                ),
                ModelMaterialSelection::Source
            )
    })
}

pub(super) fn embedded_asset_for_selection(
    world: &World,
    selection: &AssetSelection,
) -> Option<EmbeddedAsset> {
    let AssetSelection::SubAsset { parent_path, .. } = selection else {
        return None;
    };
    let asset = world
        .resource::<EditorAssets>()
        .assets
        .iter()
        .find(|asset| asset.path.as_deref() == Some(parent_path.as_str()))?
        .clone();
    embedded_assets_for_asset(world, &asset)
        .into_iter()
        .find(|embedded| &embedded.selection == selection)
}

pub(super) fn has_embedded_assets(world: &World, asset: &EditorAsset) -> bool {
    !embedded_assets_for_asset(world, asset).is_empty()
}

pub(super) fn part_effective_id(part: &crate::assets::static_mesh::StaticMeshPart) -> String {
    if part.id.trim().is_empty() {
        part_id_from_label(&part.mesh_label)
    } else {
        part.id.clone()
    }
}

pub(super) fn part_effective_material_id(
    part: &crate::assets::static_mesh::StaticMeshPart,
) -> Option<String> {
    part.material_id.clone().or_else(|| {
        part.material_label
            .as_ref()
            .map(|label| material_id_from_label(label))
    })
}

pub(super) fn is_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"
            )
        })
}

pub(super) fn stable_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 subasset_thumbnail_key(
    parent_path: &str,
    kind: AssetSubAssetKind,
    sub_asset_id: &str,
) -> String {
    format!(
        "subasset:{}#{}:{}",
        parent_path,
        subasset_kind_label(kind).to_ascii_lowercase(),
        sub_asset_id
    )
}
