//! Stable project-content registry and import contracts shared by editor, runtime, and tools.

use bevy::prelude::Resource;
use serde::{Deserialize, Serialize};
use uuid::Uuid;

use crate::{AssetSourceFingerprint, ComponentInstanceId, EditorAssetRef, MaterialRef};

pub const ASSET_REGISTRY_SCHEMA_VERSION: u32 = 3;
pub const RUNTIME_CONTENT_CATALOG_VERSION: u32 = 2;

/// Stable identity for one top-level project asset.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct AssetId(pub Uuid);

impl AssetId {
    pub fn new() -> Self {
        Self(Uuid::new_v4())
    }

    pub fn as_string(&self) -> String {
        self.0.to_string()
    }
}

impl Default for AssetId {
    fn default() -> Self {
        Self::new()
    }
}

/// Folder-independent authored/imported asset classification.
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum AssetKind {
    Model,
    Texture,
    Material,
    MaterialInstance,
    AudioClip,
    Level,
    Prefab,
    PostProcessEffect,
    RenderingProfile,
    ShaderSchema,
    #[default]
    Unknown,
}

impl AssetKind {
    pub fn from_legacy_tag(tag: &str) -> Self {
        match tag {
            "Model" => Self::Model,
            "Texture" => Self::Texture,
            "Material" => Self::Material,
            "MaterialInstance" => Self::MaterialInstance,
            "AudioClip" => Self::AudioClip,
            "Level" => Self::Level,
            "Prefab" => Self::Prefab,
            "PostProcessEffect" => Self::PostProcessEffect,
            "RenderingProfile" => Self::RenderingProfile,
            "ShaderSchema" => Self::ShaderSchema,
            _ => Self::Unknown,
        }
    }

    pub const fn is_imported_source(self) -> bool {
        matches!(self, Self::Model | Self::Texture | Self::AudioClip)
    }
}

#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
pub enum ModelPlacementMode {
    #[default]
    StaticAsset,
    SceneInstance,
}

#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
pub enum ModelHierarchyMode {
    #[default]
    SingleActor,
    SourceHierarchy,
}

/// Legacy whole-model policy retained only for registry-v1 migration.
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
pub enum MaterialImportPolicy {
    #[default]
    SourceMaterials,
    AuthoringOverride,
}

/// Persisted model-asset default for one stable imported material slot.
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub enum ModelMaterialSelection {
    #[default]
    Source,
    Project(MaterialRef),
    Default,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct ModelMaterialSlotSelection {
    pub slot_id: ComponentInstanceId,
    #[serde(default)]
    pub selection: ModelMaterialSelection,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct OrphanedModelMaterialSelection {
    pub slot_id: ComponentInstanceId,
    #[serde(default)]
    pub last_known_name: String,
    pub material: MaterialRef,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ModelImportSettings {
    pub scale: f32,
    pub generate_collider: bool,
    pub lod0_only: bool,
    #[serde(default)]
    pub placement_mode: ModelPlacementMode,
    #[serde(default)]
    pub hierarchy_mode: ModelHierarchyMode,
    /// Registry-v1 compatibility input. Registry-v2 writers keep this at its default and use
    /// `material_slots` instead.
    #[serde(default, skip_serializing_if = "material_import_policy_is_default")]
    pub material_policy: MaterialImportPolicy,
    #[serde(default)]
    pub material_slots: Vec<ModelMaterialSlotSelection>,
    #[serde(default)]
    pub orphaned_material_slots: Vec<OrphanedModelMaterialSelection>,
    #[serde(default)]
    pub static_mesh_manifest_path: Option<String>,
    #[serde(default)]
    pub animation_manifest_path: Option<String>,
    #[serde(default)]
    pub default_animation_clip_id: Option<String>,
}

fn material_import_policy_is_default(policy: &MaterialImportPolicy) -> bool {
    *policy == MaterialImportPolicy::SourceMaterials
}

impl Default for ModelImportSettings {
    fn default() -> Self {
        Self {
            scale: 1.0,
            generate_collider: true,
            lod0_only: true,
            placement_mode: ModelPlacementMode::default(),
            hierarchy_mode: ModelHierarchyMode::default(),
            material_policy: MaterialImportPolicy::default(),
            material_slots: Vec::new(),
            orphaned_material_slots: Vec::new(),
            static_mesh_manifest_path: None,
            animation_manifest_path: None,
            default_animation_clip_id: None,
        }
    }
}

/// Source role used to infer safe color-space and compression defaults.
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum TextureAssetSemantic {
    #[default]
    Auto,
    Color,
    Normal,
    MaskData,
    Ui,
    Hdr,
}

#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum TextureColorSpace {
    #[default]
    Auto,
    Srgb,
    Linear,
}

#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum TextureMipmapMode {
    #[default]
    Generate,
    PreserveSource,
    None,
}

#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum TextureCompression {
    #[default]
    Auto,
    Uastc,
    Uncompressed,
}

#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum TextureFilter {
    Nearest,
    #[default]
    Linear,
}

#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum TextureWrap {
    #[default]
    Repeat,
    Clamp,
    Mirror,
}

#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum NormalMapConvention {
    #[default]
    OpenGl,
    DirectX,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct TextureImportSettings {
    #[serde(default)]
    pub semantic: TextureAssetSemantic,
    #[serde(default)]
    pub color_space: TextureColorSpace,
    #[serde(default)]
    pub mipmaps: TextureMipmapMode,
    #[serde(default)]
    pub compression: TextureCompression,
    #[serde(default)]
    pub max_dimension: Option<u32>,
    #[serde(default)]
    pub filter: TextureFilter,
    #[serde(default)]
    pub wrap: TextureWrap,
    #[serde(default = "default_texture_anisotropy")]
    pub anisotropy: u16,
    #[serde(default)]
    pub normal_map_convention: NormalMapConvention,
}

const fn default_texture_anisotropy() -> u16 {
    8
}

impl Default for TextureImportSettings {
    fn default() -> Self {
        Self {
            semantic: TextureAssetSemantic::Auto,
            color_space: TextureColorSpace::Auto,
            mipmaps: TextureMipmapMode::Generate,
            compression: TextureCompression::Auto,
            max_dimension: None,
            filter: TextureFilter::Linear,
            wrap: TextureWrap::Repeat,
            anisotropy: default_texture_anisotropy(),
            normal_map_convention: NormalMapConvention::OpenGl,
        }
    }
}

#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub enum AssetImportSettings {
    #[default]
    None,
    Model(ModelImportSettings),
    Texture(TextureImportSettings),
}

impl AssetImportSettings {
    pub fn for_kind(kind: AssetKind) -> Self {
        match kind {
            AssetKind::Model => Self::Model(ModelImportSettings::default()),
            AssetKind::Texture => Self::Texture(TextureImportSettings::default()),
            _ => Self::None,
        }
    }

    pub fn model(&self) -> Option<&ModelImportSettings> {
        match self {
            Self::Model(settings) => Some(settings),
            _ => None,
        }
    }

    pub fn model_mut(&mut self) -> Option<&mut ModelImportSettings> {
        match self {
            Self::Model(settings) => Some(settings),
            _ => None,
        }
    }

    pub fn texture(&self) -> Option<&TextureImportSettings> {
        match self {
            Self::Texture(settings) => Some(settings),
            _ => None,
        }
    }

    pub fn texture_mut(&mut self) -> Option<&mut TextureImportSettings> {
        match self {
            Self::Texture(settings) => Some(settings),
            _ => None,
        }
    }
}

impl From<ModelImportSettings> for AssetImportSettings {
    fn from(settings: ModelImportSettings) -> Self {
        Self::Model(settings)
    }
}

impl From<TextureImportSettings> for AssetImportSettings {
    fn from(settings: TextureImportSettings) -> Self {
        Self::Texture(settings)
    }
}

/// One-cycle source alias used by model processing APIs while call sites adopt the typed enum.
pub type ImportSettings = ModelImportSettings;

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AssetRecord {
    pub id: AssetId,
    pub path: String,
    pub label: String,
    pub kind: AssetKind,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub source_fingerprint: Option<AssetSourceFingerprint>,
    #[serde(default)]
    pub import_settings: AssetImportSettings,
    #[serde(default)]
    pub dependencies: Vec<String>,
}

impl AssetRecord {
    pub fn model_import(&self) -> &ModelImportSettings {
        self.import_settings
            .model()
            .expect("model asset record must carry model import settings")
    }

    pub fn model_import_mut(&mut self) -> &mut ModelImportSettings {
        self.import_settings
            .model_mut()
            .expect("model asset record must carry model import settings")
    }

    pub fn texture_import(&self) -> Option<&TextureImportSettings> {
        self.import_settings.texture()
    }

    pub fn texture_import_mut(&mut self) -> Option<&mut TextureImportSettings> {
        self.import_settings.texture_mut()
    }
}

#[derive(Resource, Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct ProjectContentDefaults {
    #[serde(default)]
    pub default_material: Option<MaterialRef>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AssetRegistryDocument {
    #[serde(default = "asset_registry_schema_version")]
    pub schema_version: u32,
    #[serde(default)]
    pub defaults: ProjectContentDefaults,
    #[serde(default)]
    pub records: Vec<AssetRecord>,
}

impl Default for AssetRegistryDocument {
    fn default() -> Self {
        Self {
            schema_version: ASSET_REGISTRY_SCHEMA_VERSION,
            defaults: ProjectContentDefaults::default(),
            records: Vec::new(),
        }
    }
}

const fn asset_registry_schema_version() -> u32 {
    ASSET_REGISTRY_SCHEMA_VERSION
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
struct LegacyAssetRecord {
    id: AssetId,
    path: String,
    label: String,
    kind_tag: String,
    #[serde(default)]
    source_fingerprint: Option<AssetSourceFingerprint>,
    #[serde(default)]
    import_settings: ModelImportSettings,
    #[serde(default)]
    dependencies: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
struct AssetRegistryDocumentV2 {
    schema_version: u32,
    #[serde(default)]
    defaults: ProjectContentDefaults,
    #[serde(default)]
    records: Vec<AssetRecordV2>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
struct AssetRecordV2 {
    id: AssetId,
    path: String,
    label: String,
    kind: AssetKind,
    #[serde(default)]
    source_fingerprint: Option<AssetSourceFingerprint>,
    #[serde(default)]
    import_settings: ModelImportSettings,
    #[serde(default)]
    dependencies: Vec<String>,
}

pub struct LoadedAssetRegistry {
    pub document: AssetRegistryDocument,
    pub migration_required: bool,
}

pub fn parse_asset_registry(source: &str) -> Result<LoadedAssetRegistry, String> {
    if let Ok(document) = ron::from_str::<AssetRegistryDocument>(source) {
        if document.schema_version != ASSET_REGISTRY_SCHEMA_VERSION {
            return Err(format!(
                "unsupported asset registry schema {}; expected {}",
                document.schema_version, ASSET_REGISTRY_SCHEMA_VERSION
            ));
        }
        return Ok(LoadedAssetRegistry {
            document,
            migration_required: false,
        });
    }

    if let Ok(document) = ron::from_str::<AssetRegistryDocumentV2>(source) {
        if document.schema_version != 2 {
            return Err(format!(
                "unsupported asset registry schema {}; expected {}",
                document.schema_version, ASSET_REGISTRY_SCHEMA_VERSION
            ));
        }
        return Ok(LoadedAssetRegistry {
            document: AssetRegistryDocument {
                schema_version: ASSET_REGISTRY_SCHEMA_VERSION,
                defaults: document.defaults,
                records: document
                    .records
                    .into_iter()
                    .map(|record| {
                        let import_settings = match record.kind {
                            AssetKind::Model => AssetImportSettings::Model(record.import_settings),
                            AssetKind::Texture => {
                                AssetImportSettings::Texture(TextureImportSettings::default())
                            }
                            _ => AssetImportSettings::None,
                        };
                        AssetRecord {
                            id: record.id,
                            path: record.path,
                            label: record.label,
                            kind: record.kind,
                            source_fingerprint: record.source_fingerprint,
                            import_settings,
                            dependencies: record.dependencies,
                        }
                    })
                    .collect(),
            },
            migration_required: true,
        });
    }

    match ron::from_str::<Vec<LegacyAssetRecord>>(source) {
        Ok(records) => Ok(LoadedAssetRegistry {
            document: AssetRegistryDocument {
                records: records
                    .into_iter()
                    .map(|record| {
                        let kind = AssetKind::from_legacy_tag(&record.kind_tag);
                        let import_settings = match kind {
                            AssetKind::Model => AssetImportSettings::Model(record.import_settings),
                            AssetKind::Texture => {
                                AssetImportSettings::Texture(TextureImportSettings::default())
                            }
                            _ => AssetImportSettings::None,
                        };
                        AssetRecord {
                            id: record.id,
                            path: record.path,
                            label: record.label,
                            kind,
                            source_fingerprint: record.source_fingerprint,
                            import_settings,
                            dependencies: record.dependencies,
                        }
                    })
                    .collect(),
                ..Default::default()
            },
            migration_required: true,
        }),
        Err(error) => Err(format!("invalid asset registry RON: {error}")),
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TextureRuntimeData {
    pub source_path: String,
    #[serde(default)]
    pub processed_path: Option<String>,
    #[serde(default)]
    pub processing_key: Option<String>,
    pub settings: TextureImportSettings,
    pub is_srgb: bool,
}

/// Runtime-only description of one project texture load. This keeps Bevy-specific loading out of
/// the content catalog while ensuring every renderer consumes the same authored sampling state.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RuntimeTextureLoadSpec {
    pub path: String,
    pub is_srgb: bool,
    pub filter: TextureFilter,
    pub wrap: TextureWrap,
    pub anisotropy: u16,
}

impl TextureRuntimeData {
    pub fn load_spec(&self) -> RuntimeTextureLoadSpec {
        RuntimeTextureLoadSpec {
            path: self
                .processed_path
                .clone()
                .unwrap_or_else(|| self.source_path.clone()),
            is_srgb: self.is_srgb,
            filter: self.settings.filter,
            wrap: self.settings.wrap,
            anisotropy: self.settings.anisotropy,
        }
    }
}

impl RuntimeContentCatalog {
    pub fn texture_load_spec(&self, reference: &EditorAssetRef) -> Option<RuntimeTextureLoadSpec> {
        self.records
            .iter()
            .find(|record| record.id.as_string() == reference.asset_id)
            .and_then(|record| record.texture.as_ref())
            .map(TextureRuntimeData::load_spec)
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct MaterialRuntimeData {
    #[serde(default)]
    pub packed_arm_path: Option<String>,
    #[serde(default)]
    pub processing_key: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct RuntimeContentRecord {
    pub id: AssetId,
    pub path: String,
    pub label: String,
    pub kind: AssetKind,
    #[serde(default)]
    pub material_slots: Vec<ModelMaterialSlotSelection>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub texture: Option<TextureRuntimeData>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub material: Option<MaterialRuntimeData>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct RuntimeContentCatalog {
    pub version: u32,
    #[serde(default)]
    pub defaults: ProjectContentDefaults,
    #[serde(default)]
    pub records: Vec<RuntimeContentRecord>,
}

impl From<&AssetRegistryDocument> for RuntimeContentCatalog {
    fn from(document: &AssetRegistryDocument) -> Self {
        Self {
            version: RUNTIME_CONTENT_CATALOG_VERSION,
            defaults: document.defaults.clone(),
            records: document
                .records
                .iter()
                .map(|record| RuntimeContentRecord {
                    id: record.id.clone(),
                    path: record.path.clone(),
                    label: record.label.clone(),
                    kind: record.kind,
                    material_slots: record
                        .import_settings
                        .model()
                        .map(|settings| settings.material_slots.clone())
                        .unwrap_or_default(),
                    texture: record
                        .import_settings
                        .texture()
                        .map(|settings| TextureRuntimeData {
                            source_path: record.path.clone(),
                            processed_path: None,
                            processing_key: None,
                            settings: settings.clone(),
                            is_srgb: resolves_srgb(settings),
                        }),
                    material: None,
                })
                .collect(),
        }
    }
}

pub fn resolves_srgb(settings: &TextureImportSettings) -> bool {
    match settings.color_space {
        TextureColorSpace::Srgb => true,
        TextureColorSpace::Linear => false,
        TextureColorSpace::Auto => matches!(
            settings.semantic,
            TextureAssetSemantic::Auto | TextureAssetSemantic::Color | TextureAssetSemantic::Ui
        ),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn legacy_record_lists_require_explicit_migration() {
        let source = r#"[(id:("00000000-0000-0000-0000-000000000001"),path:"assets/Props/a.glb",label:"a",kind_tag:"Model")]"#;
        let loaded = parse_asset_registry(source).unwrap();
        assert!(loaded.migration_required);
        assert_eq!(loaded.document.records[0].kind, AssetKind::Model);
    }

    #[test]
    fn runtime_catalog_strips_editor_import_state() {
        let record = AssetRecord {
            id: AssetId::new(),
            path: "assets/Props/a.glb".into(),
            label: "a".into(),
            kind: AssetKind::Model,
            source_fingerprint: None,
            import_settings: AssetImportSettings::Model(ImportSettings::default()),
            dependencies: vec!["assets/Props/a.bin".into()],
        };
        let document = AssetRegistryDocument {
            records: vec![record],
            ..Default::default()
        };
        let runtime = RuntimeContentCatalog::from(&document);
        assert_eq!(runtime.records.len(), 1);
        assert_eq!(runtime.records[0].kind, AssetKind::Model);
    }

    #[test]
    fn registry_v2_writers_omit_the_legacy_whole_model_material_policy() {
        let document = AssetRegistryDocument {
            records: vec![AssetRecord {
                id: AssetId::new(),
                path: "assets/model.glb".into(),
                label: "Model".into(),
                kind: AssetKind::Model,
                source_fingerprint: None,
                import_settings: AssetImportSettings::Model(ImportSettings::default()),
                dependencies: Vec::new(),
            }],
            ..Default::default()
        };

        let serialized = ron::ser::to_string(&document).unwrap();

        assert!(!serialized.contains("material_policy"));
    }
}
