hgame 0.26.4

CG production management structs, e.g. of assets, personnels, progress, etc.
Documentation
use std::collections::HashSet;

use super::*;

pub mod criterion;
pub mod ext;
pub mod htree;
pub mod lod;
pub mod org;
pub mod semantic_series;
pub mod standard;
pub mod status;
pub mod status_action;

pub use criterion::*;
pub use ext::*;
pub use htree::SubTreeGrouping;
pub use lod::AssetLod;
pub use org::{AssetGov, AssetOrg, StatusFilter};
pub use semantic_series::*;
pub use standard::*;
pub use status::AssetStatus;
pub use status_action::*;

#[cfg(feature = "gui")]
use mktree::MkTree;

use mkentity::{Entity, ProjectSource};

pub type SemanticMap = HashMap<Semantic, Vec<ProductionAsset>>;

const EMPTY_ASSET_NAME: &str = "";
const MAX_ASSET_NAME_LENGTH: u8 = 80;

// ----------------------------------------------------------------------------

#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
/// An element which usually needs depiction in production, e.g. a car, a villain.
///
/// Currently this has support for documents fetched from `MongoDB` and `Kitsu`, while `Manual`
/// variant means we'll scan folders on disk for what likely are assets.
///
/// Central to such "manual" asset, or a `Mongo` asset alike, is the [`StandardAsset`].
///
/// [`ProductionAsset`] must implement `Default` since
/// we want to implement `mktree::Entity` for it.
pub enum ProductionAsset {
    #[default]
    Null,

    Manual(StandardAsset),

    #[cfg(feature = "mongo")]
    Mongo(StandardAsset),

    #[cfg(feature = "kitsu")]
    Kitsu(KitsuAsset),
}

impl Entity for ProductionAsset {
    fn empty() -> Self {
        Self::Null
    }

    fn as_group(group_name: &str, typ: &ProjectSource) -> Self {
        match typ {
            ProjectSource::Offline => Self::Manual(StandardAsset::as_group(group_name, typ)),

            #[cfg(feature = "mongo")]
            ProjectSource::Mongo => Self::Mongo(StandardAsset::as_group(group_name, typ)),

            #[cfg(feature = "kitsu")]
            ProjectSource::Zou => Self::Kitsu(KitsuAsset::as_group(group_name, typ)),
        }
    }

    fn name(&self) -> Option<&String> {
        match self {
            Self::Null => None,
            Self::Manual(inner) => inner.name(),
            #[cfg(feature = "mongo")]
            Self::Mongo(inner) => inner.name(),
            #[cfg(feature = "kitsu")]
            Self::Kitsu(inner) => inner.name(),
        }
    }
}

impl ProductionAsset {
    #[cfg(feature = "mongo")]
    pub fn mongo(sem: &Semantic, asset_name: &str) -> Self {
        Self::Mongo(StandardAsset::new(sem, asset_name))
    }

    pub fn inner_standard_owned(self) -> Option<StandardAsset> {
        match self {
            Self::Manual(inner) => Some(inner),
            #[cfg(feature = "mongo")]
            Self::Mongo(inner) => Some(inner),
            // TODO@kitsu
            _ => None,
        }
    }

    pub fn inner_standard(&self) -> Option<&StandardAsset> {
        match self {
            Self::Manual(inner) => Some(inner),
            #[cfg(feature = "mongo")]
            Self::Mongo(inner) => Some(inner),
            // TODO@kitsu
            _ => None,
        }
    }

    #[cfg(feature = "kitsu")]
    pub fn inner_kitsu(&self) -> Option<&KitsuAsset> {
        match self {
            Self::Kitsu(inner) => Some(inner),
            _ => None,
        }
    }

    pub fn name_or_empty(&self) -> &str {
        match self {
            Self::Null => EMPTY_ASSET_NAME,
            Self::Manual(inner) => inner.name_or_empty(),
            #[cfg(feature = "mongo")]
            Self::Mongo(inner) => inner.name_or_empty(),
            #[cfg(feature = "kitsu")]
            Self::Kitsu(inner) => inner.name_or_empty(),
        }
    }

    /// Gets `\n`-joined string of asset names, with an optional bullet character appended
    /// to each line.
    pub fn selection_names(selection: &HashSet<Self>, bullet: Option<&str>) -> String {
        let join = match bullet {
            Some(b) => format!("\n{}", b),
            None => String::from("\n"),
        };
        let mut names = selection
            .iter()
            .filter_map(|a| a.name())
            .map(|n| n.as_str())
            .collect::<Vec<&str>>()
            .join(&join);
        if let Some(b) = bullet {
            names = format!("{}{}", b, names);
        };
        names
    }

    pub fn semantic_str_and_name(&self) -> AnyResult<(String, String)> {
        match self {
            Self::Null => Err(anyhow!("Null asset doesn\'t have category and name")),
            Self::Manual(inner) => inner.semantic_and_name(),
            #[cfg(feature = "mongo")]
            Self::Mongo(inner) => inner.semantic_and_name(),
            #[cfg(feature = "kitsu")]
            // TODO@kitsu
            Self::Kitsu(_) => Err(anyhow!("kitsu category and asset name: unimplemented")),
        }
    }

    pub fn format_with_active_project(&self, project: &Project) -> String {
        format!("🚩[{}]:: {}", project, self.name_or_empty())
    }

    pub fn bson_id_owned(self) -> Option<ObjectId> {
        match self {
            Self::Null => None,
            Self::Manual(inner) => inner.id,
            #[cfg(feature = "mongo")]
            Self::Mongo(inner) => inner.id,
            #[cfg(feature = "kitsu")]
            Self::Kitsu(_) => None,
        }
    }

    /// For `MongoAsset` this only checks if its semantic (the "category.main_type") is not empty.
    pub fn has_semantic(&self) -> bool {
        self.semantic().is_some()
    }

    pub fn semantic(&self) -> Option<&Semantic> {
        match self {
            Self::Null => None,
            Self::Manual(inner) => Some(inner.semantic()),
            #[cfg(feature = "mongo")]
            Self::Mongo(inner) => Some(inner.semantic()),
            #[cfg(feature = "kitsu")]
            // TODO@kitsu
            Self::Kitsu(_) => None,
        }
    }

    pub fn has_criteria(&self) -> bool {
        self.criteria().is_some()
    }

    pub fn criteria(&self) -> Option<&Vec<ObjectId>> {
        match self {
            Self::Null => None,
            Self::Manual(inner) => inner.criteria(),
            #[cfg(feature = "mongo")]
            Self::Mongo(inner) => inner.criteria(),
            #[cfg(feature = "kitsu")]
            // TODO@kitsu
            Self::Kitsu(_) => None,
        }
    }

    pub fn group_by_semantic(assets: Vec<Self>) -> Vec<SemanticSeries> {
        // builds a map first
        let mut map = HashMap::new();

        for a in assets.into_iter().filter(|a| a.has_semantic()) {
            let sem = a.semantic().unwrap();
            match map.contains_key(sem) {
                false => {
                    map.insert(sem.to_owned(), vec![a]);
                }
                true => {
                    if let Some(assets) = map.get_mut(sem) {
                        assets.push(a);
                    };
                }
            }
        }

        // converts into `Vec<SemanticSeries>`
        let mut groups: Vec<SemanticSeries> = map
            .into_iter()
            .map(|(sem, mut assets)| {
                // sorts the `ProductionAsset`
                assets.sort();
                SemanticSeries::from_semantic(sem).with_assets(assets)
            })
            .collect();
        // sorts the `Semantic`
        groups.sort_by(|a, b| a.semantic.partial_cmp(&b.semantic).unwrap());
        groups
    }
}

impl BsonId for ProductionAsset {
    fn bson_id_as_ref(&self) -> Option<&ObjectId> {
        match self {
            Self::Null => None,
            Self::Manual(inner) => inner.bson_id_as_ref(),
            #[cfg(feature = "mongo")]
            Self::Mongo(inner) => inner.bson_id_as_ref(),
            #[cfg(feature = "kitsu")]
            Self::Kitsu(inner) => inner.bson_id_as_ref(),
        }
    }

    fn bson_id(&self) -> AnyResult<&ObjectId> {
        match self {
            Self::Null => Err(anyhow!(
                "Null ProductionAsset does not contain BSON ObjectId"
            )),
            Self::Manual(inner) => inner.bson_id(),
            #[cfg(feature = "mongo")]
            Self::Mongo(inner) => inner.bson_id(),
            #[cfg(feature = "kitsu")]
            Self::Kitsu(inner) => inner.bson_id(),
        }
    }
}

// ----------------------------------------------------------------------------
#[cfg(feature = "kitsu")]
#[derive(Serialize, Deserialize, Debug, Clone, Default, Eq, Hash)]
/// External API: DO NOT change field names.
pub struct KitsuAsset {
    id: String,
    // parent_id: String,
    name: String,
    description: String,
    entity_type_id: String,
    // this should be mutated post-fetch
    #[serde(skip)]
    entity_type: String,
}

#[cfg(feature = "kitsu")]
impl Entity for KitsuAsset {
    fn empty() -> Self {
        Self::default()
    }

    fn as_group(entity_type: &str, _typ: &ProjectSource) -> Self {
        Self {
            entity_type: entity_type.to_owned(),
            ..Self::default()
        }
    }

    fn name(&self) -> Option<&String> {
        Some(&self.name)
    }
}

#[cfg(feature = "kitsu")]
impl KitsuAsset {
    fn name_or_empty(&self) -> &str {
        &self.name
    }
}

#[cfg(feature = "kitsu")]
impl BsonId for KitsuAsset {
    fn bson_id_as_ref(&self) -> Option<&ObjectId> {
        None
    }

    fn bson_id(&self) -> AnyResult<&ObjectId> {
        self.bson_id_as_ref()
            .context("kitsu asset BSON ID: unimplemented")
    }
}

#[cfg(feature = "kitsu")]
impl PartialEq for KitsuAsset {
    fn eq(&self, other: &Self) -> bool {
        self.id == other.id
    }
}

#[cfg(feature = "kitsu")]
impl Ord for KitsuAsset {
    fn cmp(&self, other: &Self) -> Ordering {
        self.name.cmp(&other.name)
    }
}

#[cfg(feature = "kitsu")]
impl PartialOrd for KitsuAsset {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}
// ----------------------------------------------------------------------------