/*
SPDX-License-Identifier: MIT OR Apache-2.0
SPDX-FileCopyrightText: © 2023-2026 Bruce D'Arcus and Citum contributors
*/

//! Shared primitive types used across all reference categories.
//!
//! Includes multilingual string support, title representation, date types,
//! and reusable metadata structs (archive, eprint).

use crate::reference::contributor::Contributor;
#[cfg(feature = "schema")]
use schemars::JsonSchema;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
#[cfg(feature = "bindings")]
use specta::Type;
use std::borrow::Borrow;
use std::borrow::Cow;
use std::collections::HashMap;
use std::fmt;
use std::fmt::{Display, Formatter};
use std::hash::{Hash, Hasher};
use std::ops::Deref;
use std::str::FromStr;
use url::Url;

/// Unique identifier for a reference item.
#[derive(Debug, Deserialize, Serialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(feature = "bindings", derive(Type))]
#[serde(transparent)]
pub struct RefID(pub String);

impl RefID {
    /// Borrow the reference identifier as a string slice.
    #[must_use]
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl Display for RefID {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        f.write_str(&self.0)
    }
}

impl AsRef<str> for RefID {
    fn as_ref(&self) -> &str {
        self.as_str()
    }
}

impl Borrow<str> for RefID {
    fn borrow(&self) -> &str {
        self.as_str()
    }
}

impl Deref for RefID {
    type Target = str;

    fn deref(&self) -> &Self::Target {
        self.as_str()
    }
}

impl From<String> for RefID {
    fn from(value: String) -> Self {
        Self(value)
    }
}

impl From<&str> for RefID {
    fn from(value: &str) -> Self {
        Self(value.to_string())
    }
}

impl From<RefID> for String {
    fn from(value: RefID) -> Self {
        value.0
    }
}

impl FromStr for RefID {
    type Err = std::convert::Infallible;

    fn from_str(value: &str) -> Result<Self, Self::Err> {
        Ok(Self::from(value))
    }
}

impl PartialEq<&str> for RefID {
    fn eq(&self, other: &&str) -> bool {
        self.as_str() == *other
    }
}

impl PartialEq<str> for RefID {
    fn eq(&self, other: &str) -> bool {
        self.as_str() == other
    }
}

impl PartialEq<String> for RefID {
    fn eq(&self, other: &String) -> bool {
        self.as_str() == other
    }
}

impl PartialEq<RefID> for &str {
    fn eq(&self, other: &RefID) -> bool {
        *self == other.as_str()
    }
}

impl PartialEq<RefID> for String {
    fn eq(&self, other: &RefID) -> bool {
        self.as_str() == other.as_str()
    }
}

/// Geographic place name used for publication and archive locations.
#[derive(Debug, Deserialize, Serialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(feature = "bindings", derive(Type))]
#[serde(transparent)]
pub struct Place(
    /// The rendered place string, such as a city or city-country label.
    pub String,
);

impl Place {
    /// Borrow the place name as a string slice.
    #[must_use]
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl Display for Place {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        f.write_str(&self.0)
    }
}

impl AsRef<str> for Place {
    fn as_ref(&self) -> &str {
        self.as_str()
    }
}

impl Borrow<str> for Place {
    fn borrow(&self) -> &str {
        self.as_str()
    }
}

impl Deref for Place {
    type Target = str;

    fn deref(&self) -> &Self::Target {
        self.as_str()
    }
}

impl From<String> for Place {
    fn from(value: String) -> Self {
        Self(value)
    }
}

impl From<&str> for Place {
    fn from(value: &str) -> Self {
        Self(value.to_string())
    }
}

impl From<Place> for String {
    fn from(value: Place) -> Self {
        value.0
    }
}

impl FromStr for Place {
    type Err = std::convert::Infallible;

    fn from_str(value: &str) -> Result<Self, Self::Err> {
        Ok(Self::from(value))
    }
}

impl PartialEq<&str> for Place {
    fn eq(&self, other: &&str) -> bool {
        self.as_str() == *other
    }
}

impl PartialEq<str> for Place {
    fn eq(&self, other: &str) -> bool {
        self.as_str() == other
    }
}

impl PartialEq<String> for Place {
    fn eq(&self, other: &String) -> bool {
        self.as_str() == other
    }
}

impl PartialEq<Place> for &str {
    fn eq(&self, other: &Place) -> bool {
        *self == other.as_str()
    }
}

impl PartialEq<Place> for String {
    fn eq(&self, other: &Place) -> bool {
        self.as_str() == other.as_str()
    }
}

/// Inline markup for freeform text fields (note, abstract).
///
/// Serializes from a plain string or a `{ djot: "..." }` object.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(feature = "bindings", derive(Type))]
#[serde(untagged)]
pub enum RichText {
    /// Plain text with no inline markup.
    Plain(String),
    /// Djot inline markup.
    Djot {
        /// Djot inline markup source.
        djot: String,
    },
}

impl Default for RichText {
    fn default() -> Self {
        RichText::Plain(String::new())
    }
}

impl RichText {
    /// Return the raw source string regardless of format.
    pub fn raw(&self) -> &str {
        match self {
            RichText::Plain(s) | RichText::Djot { djot: s } => s,
        }
    }

    /// Return true when the content is empty.
    pub fn is_empty(&self) -> bool {
        self.raw().is_empty()
    }
}

/// A numbering identifier for a work (e.g., volume, issue, number).
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(feature = "bindings", derive(Type))]
pub struct Numbering {
    /// The type of the numbering.
    pub r#type: NumberingType,
    /// The value of the numbering (e.g., "4", "B").
    pub value: String,
}

/// Controlled vocabulary for numbering types.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "bindings", derive(Type))]
#[non_exhaustive]
pub enum NumberingType {
    /// A volume number.
    Volume,
    /// An issue number.
    Issue,
    /// A generic document number.
    Number,
    /// A report identifier.
    Report,
    /// A part number.
    Part,
    /// A supplement number.
    Supplement,
    /// A printing run identifier.
    Printing,
    /// A chapter number.
    Chapter,
    /// A section identifier.
    Section,
    /// An edition identifier.
    Edition,
    /// A season number.
    Season,
    /// An episode number.
    Episode,
    /// A custom numbering identifier for domain-specific kinds.
    Custom(String),
}

impl NumberingType {
    /// Return the canonical kebab-case key for this numbering kind.
    #[must_use]
    pub fn as_key(&self) -> Cow<'_, str> {
        match self {
            Self::Volume => Cow::Borrowed("volume"),
            Self::Issue => Cow::Borrowed("issue"),
            Self::Number => Cow::Borrowed("number"),
            Self::Report => Cow::Borrowed("report"),
            Self::Part => Cow::Borrowed("part"),
            Self::Supplement => Cow::Borrowed("supplement"),
            Self::Printing => Cow::Borrowed("printing"),
            Self::Chapter => Cow::Borrowed("chapter"),
            Self::Section => Cow::Borrowed("section"),
            Self::Edition => Cow::Borrowed("edition"),
            Self::Season => Cow::Borrowed("season"),
            Self::Episode => Cow::Borrowed("episode"),
            Self::Custom(value) => normalize_kind_key(value)
                .map(Cow::Owned)
                .unwrap_or_else(|| Cow::Borrowed(value.as_str())),
        }
    }

    /// Parse a numbering kind from a known keyword or custom identifier.
    ///
    /// # Errors
    ///
    /// Returns an error when the input is empty or normalizes to an empty key.
    pub fn from_key(value: &str) -> Result<Self, String> {
        let canonical = normalize_kind_key(value)
            .ok_or_else(|| "numbering kind must not be empty".to_string())?;
        Ok(match canonical.as_str() {
            "volume" => Self::Volume,
            "issue" => Self::Issue,
            "number" => Self::Number,
            "report" => Self::Report,
            "part" => Self::Part,
            "supplement" => Self::Supplement,
            "printing" => Self::Printing,
            "chapter" => Self::Chapter,
            "section" => Self::Section,
            "edition" => Self::Edition,
            "season" => Self::Season,
            "episode" => Self::Episode,
            _ => Self::Custom(canonical),
        })
    }
}

impl PartialEq for NumberingType {
    fn eq(&self, other: &Self) -> bool {
        self.as_key().as_ref() == other.as_key().as_ref()
    }
}

impl Eq for NumberingType {}

impl Hash for NumberingType {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.as_key().as_ref().hash(state);
    }
}

impl Serialize for NumberingType {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        serializer.serialize_str(self.as_key().as_ref())
    }
}

impl<'de> Deserialize<'de> for NumberingType {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let value = String::deserialize(deserializer)?;
        Self::from_key(&value).map_err(serde::de::Error::custom)
    }
}

#[cfg(feature = "schema")]
impl JsonSchema for NumberingType {
    fn schema_name() -> std::borrow::Cow<'static, str> {
        "NumberingType".into()
    }

    fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
        schemars::json_schema!({
            "type": "string",
            "description": "Known numbering kind keyword or custom kebab-case identifier."
        })
    }
}

pub(crate) trait HasNumbering {
    fn numbering(&self) -> &[Numbering];

    fn find_numbering(&self, numbering_type: NumberingType) -> Option<String> {
        self.numbering()
            .iter()
            .find(|numbering| numbering.r#type == numbering_type)
            .map(|numbering| numbering.value.clone())
    }
}

pub(crate) trait NormalizeNumbering {
    fn numbering_mut(&mut self) -> &mut Vec<Numbering>;
    fn volume_mut(&mut self) -> &mut Option<String>;
    fn issue_mut(&mut self) -> &mut Option<String>;
    fn edition_mut(&mut self) -> &mut Option<String>;
    fn number_mut(&mut self) -> &mut Option<String>;
    fn part_number_mut(&mut self) -> &mut Option<String>;
    fn supplement_number_mut(&mut self) -> &mut Option<String>;
    fn printing_number_mut(&mut self) -> &mut Option<String>;

    fn normalize_numbering(&mut self) {
        let volume = self.volume_mut().take();
        let issue = self.issue_mut().take();
        let edition = self.edition_mut().take();
        let number = self.number_mut().take();
        let part_number = self.part_number_mut().take();
        let supplement_number = self.supplement_number_mut().take();
        let printing_number = self.printing_number_mut().take();
        let has_volume = volume.is_some();
        let has_issue = issue.is_some();
        let has_edition = edition.is_some();
        let has_number = number.is_some();
        let has_part = part_number.is_some();
        let has_supplement = supplement_number.is_some();
        let has_printing = printing_number.is_some();
        let existing = std::mem::take(self.numbering_mut());

        let mut normalized = Vec::with_capacity(existing.len() + 7);
        let mut push_shorthand = |r#type, value: Option<String>| {
            if let Some(value) = value {
                normalized.push(Numbering { r#type, value });
            }
        };

        push_shorthand(NumberingType::Volume, volume);
        push_shorthand(NumberingType::Issue, issue);
        push_shorthand(NumberingType::Edition, edition);
        push_shorthand(NumberingType::Number, number);
        push_shorthand(NumberingType::Part, part_number);
        push_shorthand(NumberingType::Supplement, supplement_number);
        push_shorthand(NumberingType::Printing, printing_number);

        normalized.extend(existing.into_iter().filter(|entry| match entry.r#type {
            NumberingType::Volume => !has_volume,
            NumberingType::Issue => !has_issue,
            NumberingType::Edition => !has_edition,
            NumberingType::Number => !has_number,
            NumberingType::Part => !has_part,
            NumberingType::Supplement => !has_supplement,
            NumberingType::Printing => !has_printing,
            _ => true,
        }));

        *self.numbering_mut() = normalized;
    }
}

fn normalize_kind_key(value: &str) -> Option<String> {
    let mut normalized = String::new();
    let mut pending_dash = false;

    for ch in value.trim().chars() {
        if ch.is_ascii_alphanumeric() {
            if pending_dash && !normalized.is_empty() {
                normalized.push('-');
            }
            normalized.push(ch.to_ascii_lowercase());
            pending_dash = false;
        } else if !normalized.is_empty() {
            pending_dash = true;
        }
    }

    if normalized.is_empty() {
        None
    } else {
        Some(normalized)
    }
}

/// BCP 47 language tag (e.g., `"en"`, `"de"`, `"ja"`).
#[derive(Debug, Deserialize, Serialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(feature = "bindings", derive(Type))]
#[serde(transparent)]
pub struct LangID(pub String);

impl LangID {
    /// Borrow the language identifier as a string slice.
    #[must_use]
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl Display for LangID {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        f.write_str(&self.0)
    }
}

impl AsRef<str> for LangID {
    fn as_ref(&self) -> &str {
        self.as_str()
    }
}

impl Borrow<str> for LangID {
    fn borrow(&self) -> &str {
        self.as_str()
    }
}

impl Deref for LangID {
    type Target = str;

    fn deref(&self) -> &Self::Target {
        self.as_str()
    }
}

impl From<String> for LangID {
    fn from(value: String) -> Self {
        Self(value)
    }
}

impl From<&str> for LangID {
    fn from(value: &str) -> Self {
        Self(value.to_string())
    }
}

impl From<LangID> for String {
    fn from(value: LangID) -> Self {
        value.0
    }
}

impl FromStr for LangID {
    type Err = std::convert::Infallible;

    fn from_str(value: &str) -> Result<Self, Self::Err> {
        Ok(Self::from(value))
    }
}

impl PartialEq<&str> for LangID {
    fn eq(&self, other: &&str) -> bool {
        self.as_str() == *other
    }
}

impl PartialEq<str> for LangID {
    fn eq(&self, other: &str) -> bool {
        self.as_str() == other
    }
}

impl PartialEq<String> for LangID {
    fn eq(&self, other: &String) -> bool {
        self.as_str() == other
    }
}

impl PartialEq<LangID> for &str {
    fn eq(&self, other: &LangID) -> bool {
        *self == other.as_str()
    }
}

impl PartialEq<LangID> for String {
    fn eq(&self, other: &LangID) -> bool {
        self.as_str() == other.as_str()
    }
}
/// Maps field names to their language tags for multilingual references.
pub type FieldLanguageMap = HashMap<String, LangID>;

/// A value that could be either a number or a string.
///
/// Used for fields that may contain numeric or string values, such as issue numbers,
/// volume numbers, or similar identifiers that can be formatted as either type.
/// The `Display` implementation shows the value in its appropriate form.
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(feature = "bindings", derive(Type))]
#[serde(untagged)]
pub enum NumOrStr {
    /// A numeric value.
    Number(i64),
    /// A string value.
    Str(String),
}

impl Display for NumOrStr {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
        match self {
            Self::Number(i) => write!(f, "{}", i),
            Self::Str(s) => write!(f, "{}", s),
        }
    }
}

/// A string that can be represented in multiple languages and scripts.
///
/// This is an enum that supports both simple strings and complex multilingual representations.
/// Use `Simple` for basic strings and `Complex` when you need to track original language,
/// transliterations, and translations.
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(feature = "bindings", derive(Type))]
#[serde(untagged)]
pub enum MultilingualString {
    /// A simple string in a single language.
    Simple(String),
    /// A complex multilingual string with original, transliterations, and translations.
    Complex(MultilingualComplex),
}

/// Complex multilingual representation with original, transliterations, and translations.
///
/// Allows capturing the original text in its native script, along with transliterations
/// (phonetic representations in different scripts) and translations (semantic equivalents
/// in different languages). This is essential for accurately rendering bibliographies in
/// multilingual and non-Latin-script contexts.
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(feature = "bindings", derive(Type))]
#[serde(rename_all = "kebab-case")]
pub struct MultilingualComplex {
    /// The text in its original script and language.
    pub original: String,
    /// ISO 639/BCP 47 language code for the original text.
    pub lang: Option<LangID>,
    /// Transliterations/Transcriptions of the original text into other scripts.
    ///
    /// Keys are typically script codes (e.g., "Latn" for Latin) or full BCP 47 tags.
    /// Values are the transliterated or transcribed text.
    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
    pub transliterations: HashMap<String, String>,
    /// Translations of the text into other languages.
    ///
    /// Keys are ISO 639/BCP 47 language codes (e.g., "en" for English, "fr" for French).
    /// Values are the translated text.
    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
    pub translations: HashMap<LangID, String>,
}

impl From<String> for MultilingualString {
    fn from(s: String) -> Self {
        Self::Simple(s)
    }
}

impl From<&str> for MultilingualString {
    fn from(s: &str) -> Self {
        Self::Simple(s.to_string())
    }
}

impl Display for MultilingualString {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
        match self {
            Self::Simple(s) => write!(f, "{}", s),
            Self::Complex(c) => write!(f, "{}", c.original),
        }
    }
}

impl Default for MultilingualString {
    fn default() -> Self {
        Self::Simple(String::new())
    }
}

/// Structured archival location metadata for unpublished and archival material.
///
/// Models the hierarchical location of an item within an archive, following
/// common archival description standards (DACS, ISAD(G), EAD).
#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(feature = "bindings", derive(Type))]
#[serde(rename_all = "kebab-case")]
pub struct ArchiveInfo {
    /// Name of the archive or repository holding the material.
    ///
    /// Uses `MultilingualString` to support international institution names
    /// (e.g., 国立国会図書館 / National Diet Library).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub name: Option<MultilingualString>,
    /// Geographic location (city/country) of the archive.
    ///
    /// Parallel to `SimpleName.location`; both carry geographic-place semantics.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub place: Option<Place>,
    /// Name of the collection within the archive.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub collection: Option<String>,
    /// Identifier for the collection (call number, accession number, etc.).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub collection_id: Option<String>,
    /// Name of the series within the collection.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub series: Option<String>,
    /// Box number within the collection.
    ///
    /// Uses raw identifier syntax `r#box`, which serde serializes as `"box"` transparently.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub r#box: Option<String>,
    /// Folder number within the box.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub folder: Option<String>,
    /// Item identifier within the folder.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub item: Option<String>,
    /// Display override for the archival location (shelfmark, call number, or complex location string).
    ///
    /// When present, overrides the structured fields for display. Acts as the legacy
    /// `archive_location` fallback for complex shelfmarks that don't fit the structured model.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub location: Option<String>,
    /// URL for the archival item (e.g. digitized finding aid or item page).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub url: Option<Url>,
}

/// Preprint server identifier following the biblatex eprint model.
///
/// Used on all three reference classes (Monograph, CollectionComponent, SerialComponent)
/// because journal articles routinely carry arXiv or similar identifiers.
#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(feature = "bindings", derive(Type))]
#[serde(rename_all = "kebab-case")]
pub struct EprintInfo {
    /// The identifier on the preprint server (e.g. "2301.00001").
    pub id: String,
    /// The preprint server name in canonical lowercase form.
    ///
    /// Producers may supply mixed-case values such as "arXiv" or "SSRN";
    /// implementations should treat the field case-insensitively and compare
    /// or normalize on the lowercase form.
    pub server: String,
    /// Subject class or category used by the server (e.g. "cs.CL").
    #[serde(skip_serializing_if = "Option::is_none")]
    pub class: Option<String>,
}

/// A publisher or production company.
///
/// Publisher is always a corporate entity — a person is never a publisher
/// in the bibliographic sense.
#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(feature = "bindings", derive(Type))]
#[serde(rename_all = "kebab-case")]
#[serde(from = "PublisherCompat")]
pub struct Publisher {
    /// Publisher name.
    pub name: MultilingualString,
    /// Geographic place of publication (city, country).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub place: Option<Place>,
}

#[derive(Debug, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(feature = "bindings", derive(Type))]
#[serde(untagged)]
enum PublisherCompat {
    PublisherObject {
        name: MultilingualString,
        place: Option<Place>,
    },
    Contributor(Box<Contributor>),
    Name(MultilingualString),
    String(String),
}

impl From<PublisherCompat> for Publisher {
    fn from(value: PublisherCompat) -> Self {
        match value {
            PublisherCompat::PublisherObject { name, place } => Self { name, place },
            PublisherCompat::Contributor(contributor) => match *contributor {
                Contributor::SimpleName(name) => Self {
                    name: name.name,
                    place: name.location,
                },
                Contributor::StructuredName(name) => Self {
                    name: format!("{} {}", name.given, name.family).into(),
                    place: None,
                },
                Contributor::Multilingual(name) => Self {
                    name: format!("{} {}", name.original.given, name.original.family).into(),
                    place: None,
                },
                Contributor::ContributorList(list) => Self {
                    name: list.to_string().into(),
                    place: None,
                },
            },
            PublisherCompat::Name(name) => Self { name, place: None },
            PublisherCompat::String(name) => Self {
                name: name.into(),
                place: None,
            },
        }
    }
}

/// A title can be a single string, a structured title, or a multilingual title.
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(feature = "bindings", derive(Type))]
#[serde(untagged)]
pub enum Title {
    /// A title in a single language.
    Single(String),
    /// A structured title.
    Structured(StructuredTitle),
    /// A complex multilingual title.
    Multilingual(MultilingualComplex),
    /// A title in multiple languages.
    Multi(Vec<(LangID, String)>),
    /// A structured title in multiple languages.
    MultiStructured(Vec<(LangID, StructuredTitle)>),
    /// An abbreviated title (shorthand, full).
    Shorthand(String, String),
}

/// Where title parts are meaningful, use this struct; Citum processors will not parse title strings.
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(feature = "bindings", derive(Type))]
#[serde(rename_all = "kebab-case")]
pub struct StructuredTitle {
    /// Full rendered title string (optional override).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub full: Option<String>,
    /// Main title component.
    pub main: String,
    /// Subtitle component.
    pub sub: Subtitle,
}

/// The subtitle can either be a string, as is the common case, or a vector of strings.
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(feature = "bindings", derive(Type))]
#[serde(untagged)]
pub enum Subtitle {
    /// A single subtitle string.
    String(String),
    /// Multiple subtitle strings.
    Vector(Vec<String>),
}

impl fmt::Display for Title {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Title::Single(s) => write!(f, "{}", s),
            Title::Multi(_m) => write!(f, "[multilingual title]"),
            Title::Multilingual(m) => write!(f, "{}", m.original),
            Title::Structured(s) => {
                let subtitle = match &s.sub {
                    Subtitle::String(s) => s.clone(),
                    Subtitle::Vector(v) => v.join(", "),
                };
                write!(f, "{}: {}", s.main, subtitle)
            }
            Title::MultiStructured(_m) => write!(f, "[multilingual structured title]"),
            Title::Shorthand(s, t) => write!(f, "{} ({})", s, t),
        }
    }
}

/// Date type.
#[derive(Debug, Clone, PartialEq)]
pub enum RefDate {
    /// A parsed EDTF date.
    Edtf(citum_edtf::Edtf),
    /// A literal date string that could not be parsed as EDTF.
    Literal(String),
}

#[cfg(test)]
#[allow(
    clippy::unwrap_used,
    clippy::expect_used,
    clippy::panic,
    clippy::indexing_slicing,
    clippy::todo,
    clippy::unimplemented,
    clippy::unreachable,
    clippy::get_unwrap,
    reason = "Panicking is acceptable and often desired in tests."
)]
mod tests {
    use super::{LangID, RefID};
    #[cfg(feature = "schema")]
    use schemars::schema_for;
    use std::collections::HashMap;

    #[test]
    fn ref_id_round_trips_as_a_scalar_string() {
        let id = RefID::from("kuhn1962");
        let serialized = serde_json::to_string(&id).expect("ref id should serialize");
        let deserialized: RefID =
            serde_json::from_str(&serialized).expect("ref id should deserialize");

        assert_eq!(serialized, "\"kuhn1962\"");
        assert_eq!(deserialized, "kuhn1962");
        assert_eq!(deserialized.as_ref(), "kuhn1962");
        assert_eq!(String::from(deserialized), "kuhn1962");
    }

    #[test]
    fn lang_id_supports_string_lookup_and_parsing() {
        let lang = "en-US".parse::<LangID>().expect("lang id should parse");
        let mut translations = HashMap::new();
        translations.insert(lang.clone(), "English".to_string());

        assert_eq!(lang, "en-US");
        assert_eq!(
            translations.get("en-US").map(String::as_str),
            Some("English")
        );
    }

    #[cfg(feature = "schema")]
    #[test]
    fn newtypes_stay_string_shaped_in_json_schema() {
        let ref_schema = schema_for!(RefID);
        let lang_schema = schema_for!(LangID);
        let place_schema = schema_for!(super::Place);

        assert_eq!(ref_schema.to_value()["type"], "string");
        assert_eq!(lang_schema.to_value()["type"], "string");
        assert_eq!(place_schema.to_value()["type"], "string");
    }
}
