use super::*;

pub(super) const RECOVERY_IDLE: Duration = Duration::from_secs(2);
pub(super) const RECOVERY_GENERATIONS: usize = 5;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum AuthoredAssetDocumentKind {
    Material,
    MaterialInstance,
    ModelImportSettings,
    TextureImportSettings,
    ProjectContentDefaults,
}

impl AuthoredAssetDocumentKind {
    pub(super) fn slug(self) -> &'static str {
        match self {
            Self::Material => "material",
            Self::MaterialInstance => "material-instance",
            Self::ModelImportSettings => "model-import",
            Self::TextureImportSettings => "texture-import",
            Self::ProjectContentDefaults => "content-defaults",
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct AuthoredAssetDocumentKey {
    pub asset_id: String,
    pub kind: AuthoredAssetDocumentKind,
}

impl AuthoredAssetDocumentKey {
    pub(super) fn new(asset_id: impl Into<String>, kind: AuthoredAssetDocumentKind) -> Self {
        Self {
            asset_id: asset_id.into(),
            kind,
        }
    }
}

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum AuthoredDocumentState {
    #[default]
    Clean,
    Dirty,
    Saving,
    ExternalConflict,
    SaveFailed,
}

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum DerivedProcessingState {
    #[default]
    Idle,
    Queued,
    Processing,
    Failed,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub(super) enum AuthoredAssetValue {
    Material(MaterialAsset),
    MaterialInstance(MaterialInstanceAsset),
    ModelImportSettings(ModelImportSettings),
    TextureImportSettings(TextureImportSettings),
    ProjectContentDefaults(ProjectContentDefaults),
}

impl PartialEq for AuthoredAssetValue {
    fn eq(&self, other: &Self) -> bool {
        match (self, other) {
            (Self::Material(left), Self::Material(right)) => left == right,
            (Self::MaterialInstance(left), Self::MaterialInstance(right)) => left == right,
            (Self::ModelImportSettings(left), Self::ModelImportSettings(right)) => left == right,
            (Self::TextureImportSettings(left), Self::TextureImportSettings(right)) => {
                left == right
            }
            (Self::ProjectContentDefaults(left), Self::ProjectContentDefaults(right)) => {
                left == right
            }
            _ => false,
        }
    }
}

#[derive(Debug, Clone)]
pub(super) struct AuthoredAssetDocument {
    pub(super) key: AuthoredAssetDocumentKey,
    pub(super) path: String,
    pub(super) clean: AuthoredAssetValue,
    pub(super) current: AuthoredAssetValue,
    pub(super) disk_snapshot: FileSnapshot,
    pub(super) revision: u64,
    pub(super) state: AuthoredDocumentState,
    pub(super) processing: DerivedProcessingState,
    pub(super) error: Option<String>,
    pub(super) changed_at: Option<Instant>,
    pub(super) recovery_revision: Option<u64>,
}

impl AuthoredAssetDocument {
    pub(super) fn new(
        key: AuthoredAssetDocumentKey,
        path: String,
        value: AuthoredAssetValue,
        disk_snapshot: FileSnapshot,
    ) -> Self {
        Self {
            key,
            path,
            clean: value.clone(),
            current: value,
            disk_snapshot,
            revision: 0,
            state: AuthoredDocumentState::Clean,
            processing: DerivedProcessingState::Idle,
            error: None,
            changed_at: None,
            recovery_revision: None,
        }
    }

    pub(super) fn dirty(&self) -> bool {
        self.current != self.clean
    }

    pub(super) fn mark_edited(&mut self) {
        self.revision = self.revision.wrapping_add(1);
        self.state = AuthoredDocumentState::Dirty;
        self.error = None;
        self.changed_at = Some(Instant::now());
    }

    pub(super) fn refresh_state_from_value(&mut self) {
        if self.dirty() {
            self.mark_edited();
        } else {
            self.state = AuthoredDocumentState::Clean;
            self.error = None;
            self.changed_at = None;
        }
    }
}

#[derive(Debug, Clone)]
pub struct AuthoredDocumentSnapshot {
    pub key: AuthoredAssetDocumentKey,
    pub path: String,
    pub state: AuthoredDocumentState,
    pub processing: DerivedProcessingState,
    pub dirty: bool,
    pub error: Option<String>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AssetSaveRequest {
    Active,
    All,
}

#[derive(Resource, Debug, Default)]
pub struct AuthoredAssetDocumentStore {
    pub(super) documents: HashMap<AuthoredAssetDocumentKey, AuthoredAssetDocument>,
    pub(super) path_index: HashMap<String, AuthoredAssetDocumentKey>,
    pub(super) active: Option<AuthoredAssetDocumentKey>,
    pub(super) pending_save: Option<AssetSaveRequest>,
    pub(super) registry_clean: Option<AssetRegistryDocument>,
    pub(super) registry_snapshot: Option<FileSnapshot>,
    pub(super) pending_recovery: Vec<RecoveryEnvelope>,
    pub(super) recovery_prompt_open: bool,
    pub(super) recovery_reviewing: bool,
}

impl AuthoredAssetDocumentStore {
    pub fn active_key(&self) -> Option<&AuthoredAssetDocumentKey> {
        self.active.as_ref()
    }

    pub fn set_active_scene(&mut self) {
        self.active = None;
    }

    pub fn request_save(&mut self, request: AssetSaveRequest) {
        self.pending_save = Some(request);
    }

    pub fn dirty_count(&self) -> usize {
        self.documents
            .values()
            .filter(|document| document.dirty())
            .count()
    }

    pub fn has_dirty_documents(&self) -> bool {
        self.dirty_count() > 0
    }

    pub fn has_pending_or_failed_processing(&self) -> bool {
        self.documents.values().any(|document| {
            matches!(
                document.processing,
                DerivedProcessingState::Queued
                    | DerivedProcessingState::Processing
                    | DerivedProcessingState::Failed
            )
        })
    }

    pub fn snapshot_for_path(&self, path: &str) -> Option<AuthoredDocumentSnapshot> {
        let key = self.path_index.get(&normalize_path(path))?;
        self.snapshot(key)
    }

    pub fn snapshot(&self, key: &AuthoredAssetDocumentKey) -> Option<AuthoredDocumentSnapshot> {
        let document = self.documents.get(key)?;
        Some(AuthoredDocumentSnapshot {
            key: key.clone(),
            path: document.path.clone(),
            state: document.state,
            processing: document.processing,
            dirty: document.dirty(),
            error: document.error.clone(),
        })
    }

    pub fn material(&self, key: &AuthoredAssetDocumentKey) -> Option<&MaterialAsset> {
        match &self.documents.get(key)?.current {
            AuthoredAssetValue::Material(asset) => Some(asset),
            _ => None,
        }
    }

    pub fn material_instance(
        &self,
        key: &AuthoredAssetDocumentKey,
    ) -> Option<&MaterialInstanceAsset> {
        match &self.documents.get(key)?.current {
            AuthoredAssetValue::MaterialInstance(asset) => Some(asset),
            _ => None,
        }
    }

    pub fn model_import(&self, key: &AuthoredAssetDocumentKey) -> Option<&ModelImportSettings> {
        match &self.documents.get(key)?.current {
            AuthoredAssetValue::ModelImportSettings(settings) => Some(settings),
            _ => None,
        }
    }

    pub fn texture_import(&self, key: &AuthoredAssetDocumentKey) -> Option<&TextureImportSettings> {
        match &self.documents.get(key)?.current {
            AuthoredAssetValue::TextureImportSettings(settings) => Some(settings),
            _ => None,
        }
    }

    pub fn content_defaults(
        &self,
        key: &AuthoredAssetDocumentKey,
    ) -> Option<&ProjectContentDefaults> {
        match &self.documents.get(key)?.current {
            AuthoredAssetValue::ProjectContentDefaults(defaults) => Some(defaults),
            _ => None,
        }
    }

    pub fn update_material(&mut self, key: &AuthoredAssetDocumentKey, asset: MaterialAsset) {
        self.update_value(key, AuthoredAssetValue::Material(asset));
    }

    pub fn update_material_instance(
        &mut self,
        key: &AuthoredAssetDocumentKey,
        asset: MaterialInstanceAsset,
    ) {
        self.update_value(key, AuthoredAssetValue::MaterialInstance(asset));
    }

    pub fn update_model_import(
        &mut self,
        key: &AuthoredAssetDocumentKey,
        settings: ModelImportSettings,
    ) {
        self.update_value(key, AuthoredAssetValue::ModelImportSettings(settings));
    }

    pub fn update_texture_import(
        &mut self,
        key: &AuthoredAssetDocumentKey,
        settings: TextureImportSettings,
    ) {
        self.update_value(key, AuthoredAssetValue::TextureImportSettings(settings));
    }

    pub fn update_content_defaults(
        &mut self,
        key: &AuthoredAssetDocumentKey,
        defaults: ProjectContentDefaults,
    ) {
        self.update_value(key, AuthoredAssetValue::ProjectContentDefaults(defaults));
    }

    pub(super) fn update_value(
        &mut self,
        key: &AuthoredAssetDocumentKey,
        value: AuthoredAssetValue,
    ) {
        let Some(document) = self.documents.get_mut(key) else {
            return;
        };
        if document.current == value {
            self.active = Some(key.clone());
            return;
        }
        document.current = value;
        document.refresh_state_from_value();
        self.active = Some(key.clone());
    }

    pub fn discard(&mut self, key: &AuthoredAssetDocumentKey) -> bool {
        let Some(document) = self.documents.get_mut(key) else {
            return false;
        };
        document.current = document.clean.clone();
        document.state = AuthoredDocumentState::Clean;
        document.error = None;
        document.changed_at = None;
        true
    }

    pub fn discard_all(&mut self) {
        let keys = self.documents.keys().cloned().collect::<Vec<_>>();
        for key in keys {
            self.discard(&key);
        }
    }
}
