use std::borrow::Cow;
use std::cmp::Ordering;
use std::str::FromStr;

use citationberg::LongShortForm;
use serde::ser::SerializeMap;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use unicode_segmentation::UnicodeSegmentation;

use crate::lang::en::ARTICLES;
use crate::lang::is_cjk;
use crate::lang::name::NAME_PARTICLES;
use crate::util::{deserialize_one_or_many, serialize_one_or_many};

use super::derive_or_from_str;

/// A list of persons with a common role.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct PersonsWithRoles {
    /// The persons.
    #[serde(serialize_with = "serialize_one_or_many")]
    #[serde(deserialize_with = "deserialize_one_or_many")]
    pub names: Vec<Person>,
    /// The role the persons had in the creation of the cited item.
    pub role: PersonRole,
}

impl PersonsWithRoles {
    /// Create a new list of persons with a common role.
    pub fn new(names: Vec<Person>, role: PersonRole) -> Self {
        Self { names, role }
    }
}

/// Specifies the role a group of persons had in the creation to the
/// cited item.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[non_exhaustive]
#[serde(rename_all = "kebab-case")]
pub enum PersonRole {
    /// Translated the work from a foreign language to the cited edition.
    Translator,
    /// Authored an afterword.
    Afterword,
    /// Authored an foreword.
    Foreword,
    /// Authored an introduction.
    Introduction,
    /// Provided value-adding annotations.
    Annotator,
    /// Commented the work.
    Commentator,
    /// Holds a patent or similar.
    Holder,
    /// Compiled the works in an [Anthology](super::EntryType::Anthology).
    Compiler,
    /// Founded the publication.
    Founder,
    /// Collaborated on the cited item.
    Collaborator,
    /// Organized the creation of the cited item.
    Organizer,
    /// Performed in the cited item.
    CastMember,
    /// Composed all or parts of the cited item's musical / audible components.
    Composer,
    /// Produced the cited item.
    Producer,
    /// Lead Producer for the cited item.
    ExecutiveProducer,
    /// Did the writing for the cited item.
    Writer,
    /// Shot film/video for the cited item.
    Cinematography,
    /// Directed the cited item.
    Director,
    /// Illustrated the cited item.
    Illustrator,
    /// Provided narration or voice-over for the cited item.
    Narrator,

    /// Various other roles described by the contained string.
    #[serde(skip)]
    Unknown(String),
}

derive_or_from_str! {
    /// Holds the name of a person.
    #[derive(Clone, Debug, PartialEq, Eq, Hash)]
    pub struct Person where "a name string or a dictionary with a \"name\" key" {
        /// The family name.
        pub name: String,
        /// The given name / forename.
        pub given_name: Option<String>,
        /// A prefix of the family name such as 'van' or 'de'.
        pub prefix: Option<String>,
        /// A suffix of the family name such as 'Jr.' or 'IV'.
        pub suffix: Option<String>,
        /// Whether a comma should be inserted before the suffix.
        #[serde(default)]
        pub comma_suffix: bool,
        /// Another name (often user name) the person might be known under.
        pub alias: Option<String>,
    }
}

impl Serialize for Person {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        // Aliases are not represented in the string, prefixes can create
        // ambiguity.
        if self.alias.is_none() && self.prefix.is_none() {
            serializer.serialize_str(&self.name_first(false, false))
        } else {
            let entries = [
                ("name", Some(&self.name)),
                ("given-name", self.given_name.as_ref()),
                ("prefix", self.prefix.as_ref()),
                ("suffix", self.suffix.as_ref()),
                ("alias", self.alias.as_ref()),
            ];

            let map_len = entries.iter().filter(|(_, v)| v.is_some()).count();

            let mut map = serializer.serialize_map(Some(map_len))?;

            for (key, value) in entries.iter() {
                if let Some(value) = value {
                    map.serialize_entry(key, value)?;
                }
            }

            map.end()
        }
    }
}

/// Error that may occur when parsing a slice of strings as a name.
#[derive(Clone, Copy, Debug, Error, PartialEq, Eq)]
pub enum PersonError {
    /// The name has too many parts to be appropriately parsed.
    #[error("too many parts")]
    TooManyParts,
    /// The name is empty.
    #[error("part list is empty")]
    Empty,
    /// The role is unknown.
    #[error("unknown role")]
    UnknownRole,
}

impl Person {
    /// This function expects a list of strings with its length between one and
    /// three. The first part will be interpreted as the `<prefix> <Name>`, the
    /// second part as the given name and the third part as the suffix.
    ///
    /// The prefix and name are separated just like in BiBTeX, as described
    /// [Nicolas Markey describes in "Tame the BeaST"][taming], p. 24. The gist
    /// is that the given name will start at the first word with a capital
    /// letter, if there are any such words.
    ///
    /// The call site of this function in the library obtains the slice by
    /// calling `split(",")` on a string like `"Des Egdens, Britta"`.
    ///
    /// [taming]: https://ftp.rrze.uni-erlangen.de/ctan/info/bibtex/tamethebeast/ttb_en.pdf
    pub fn from_strings(mut parts: Vec<&str>) -> Result<Self, PersonError> {
        if parts.is_empty() {
            return Err(PersonError::Empty);
        } else if parts.len() > 3 {
            return Err(PersonError::TooManyParts);
        }

        for part in parts.iter_mut() {
            *part = part.trim();
        }

        let last_pre = parts[0];
        let given_name = (parts.len() > 1)
            .then(|| parts.last().map(|last| last.to_string()))
            .flatten();

        let suffix = if parts.len() > 2 { Some(parts[1].to_string()) } else { None };

        let mut word_start = true;
        let mut last_lower_case_end: i32 = -1;
        let mut is_lowercase = false;
        let mut last_word_start = 0;
        let mut has_seen_uppercase_words = false;

        for (index, c) in last_pre.chars().enumerate() {
            if c.is_whitespace() {
                word_start = true;
                continue;
            }

            if word_start {
                last_word_start = index;

                if c.is_lowercase() {
                    is_lowercase = true;
                } else {
                    is_lowercase = false;
                    has_seen_uppercase_words = true;
                }
            }

            if is_lowercase {
                last_lower_case_end = index as i32;
            }

            word_start = false;
        }

        let mut name = String::new();
        let mut prefix = String::new();
        for (index, c) in last_pre.chars().enumerate() {
            if (index as i32 <= last_lower_case_end && has_seen_uppercase_words)
                || (!has_seen_uppercase_words && index < last_word_start)
            {
                prefix.push(c);
            } else if has_seen_uppercase_words || index >= last_word_start {
                name.push(c);
            }
        }

        let prefix = if prefix.is_empty() { None } else { Some(prefix) };
        if prefix.is_some() {
            name = name.trim_start().to_string();
        }

        Ok(Person {
            name,
            given_name,
            prefix,
            suffix,
            comma_suffix: false,
            alias: None,
        })
    }

    /// Formats the given name into initials.
    ///
    /// For example, `"Judith Beatrice"` would yield `"J. B."` if the
    /// `delimiter` argument is set to `Some(". ")`, `"Klaus-Peter"` would
    /// become `"K-P"` without a delimiter (or with an empty delimiter).
    ///
    /// Whitespace at the end of the delimiter is kept when parts are separated
    /// by whitespace (as in the first example), but not when parts of a
    /// compound given name are separated by hyphens (for example,
    /// `"Klaus-Peter"` produces `"K.-P."` instead of `"K. -P."` for a
    /// delimiter of `Some(". ")`).
    pub fn initials(
        &self,
        buf: &mut impl std::fmt::Write,
        delimiter: Option<&str>,
        with_hyphen: bool,
    ) -> std::fmt::Result {
        let Some(gn) = &self.given_name else {
            return Ok(());
        };

        let mut collect = true;
        let mut non_empty = false;
        // Whether we are at the start of a word (i.e., the initial char or following a whitespace)
        let mut start_of_word = true;
        // Whether the last char (excl. whitespace was lower case)
        let mut last_was_lower = false;

        let delim_ends_in_whitespace = delimiter
            .and_then(|d| d.chars().last().map(|d| d.is_whitespace()))
            .unwrap_or_default();

        for (_, gr) in gn.grapheme_indices(true) {
            if let Some(c) = gr.chars().next()
                && (c.is_whitespace() || c == '-')
            {
                // The last word was in lower case. We enounter a whitespace and stop collecting
                if last_was_lower {
                    collect = false;
                }
                if !collect {
                    let hyphenate = with_hyphen && c == '-';
                    // Do not emit delim if we just had a lower case word
                    if !last_was_lower && let Some(delimiter) = delimiter {
                        // Use the given delimiter, including any spaces at
                        // its end if there was a whitespace, but not if we
                        // should add a hyphen in a compound given name.
                        buf.write_str(if hyphenate {
                            delimiter.trim_end()
                        } else {
                            delimiter
                        })?;
                    }

                    collect = true;
                    start_of_word = true;
                    if hyphenate {
                        buf.write_char('-')?;
                    } else if last_was_lower {
                        // Write a space after a lower case word
                        buf.write_char(' ')?;
                    }
                }
                continue;
            }

            if collect {
                if gr.to_lowercase() != gr {
                    // Upper case letter => stop collecting; print initials
                    collect = false;
                    last_was_lower = false;
                } else if start_of_word {
                    // We enter a lower-cased word
                    last_was_lower = true;
                }
                if last_was_lower && start_of_word {
                    start_of_word = false;
                    if !delim_ends_in_whitespace {
                        // Print a space if we just started a lower case word and the delimiter did not already put a space
                        buf.write_char(' ')?;
                    }
                }
                buf.write_str(gr)?;
                non_empty = true;
            }
        }

        if non_empty
            && !collect
            && let Some(delim) = delimiter
        {
            buf.write_str(delim.trim_end())?;
        }

        Ok(())
    }

    /// Yields the first name of a person. Will add the delimiter after initials
    /// / single letters.
    pub fn first_name_with_delimiter(
        &self,
        buf: &mut impl std::fmt::Write,
        delimiter: Option<&str>,
    ) -> std::fmt::Result {
        let Some(name) = &self.given_name else {
            return Ok(());
        };

        let mut first = true;
        for item in name.split(' ') {
            if !first {
                buf.write_char(' ')?;
            }

            buf.write_str(item)?;

            if let Some(delimiter) = delimiter
                && item.graphemes(true).count() == 1
            {
                buf.write_str(delimiter)?;
            }

            first = false;
        }

        Ok(())
    }

    /// Get the name with the family name first, the initials
    /// afterwards, separated by a comma.
    pub fn name_first(&self, initials: bool, prefix_given_name: bool) -> String {
        let mut res = if !prefix_given_name {
            if let Some(prefix) = &self.prefix {
                format!("{} {}", prefix, self.name)
            } else {
                self.name.clone()
            }
        } else {
            self.name.clone()
        };

        if initials {
            if self.given_name.is_some() {
                res += ", ";
                self.initials(&mut res, Some(". "), true).unwrap();
            }
        } else if let Some(given_name) = &self.given_name {
            res += ", ";
            res += given_name;
        }

        if prefix_given_name && let Some(prefix) = &self.prefix {
            if self.given_name.is_some() {
                res.push(' ');
            }

            res += prefix;
        }

        if let Some(suffix) = &self.suffix {
            res += ", ";
            res += suffix;
        }
        res
    }

    /// Get the name with the given name first, the family name afterwards.
    pub fn given_first(&self, initials: bool) -> String {
        let mut res = String::new();

        if initials {
            if self.given_name.is_some() {
                self.initials(&mut res, Some(". "), true).unwrap();
                res.push(' ');
            }
        } else if let Some(given_name) = &self.given_name {
            res += given_name;
            res.push(' ');
        }

        if let Some(prefix) = &self.prefix {
            res += prefix;
            res += " ";
        }

        res += &self.name;

        if let Some(suffix) = &self.suffix {
            res += " ";
            res += suffix;
        }

        res
    }

    /// Get the non-dropping name particle in the family name.
    pub fn name_particle(&self) -> Option<&str> {
        for (idx, char) in self.name.char_indices().rev() {
            if char != ' ' {
                continue;
            }

            let particle = &self.name[0..idx];
            let lowercase = particle.to_lowercase();
            if NAME_PARTICLES.binary_search(&lowercase.as_str()).is_ok() {
                return Some(particle);
            }
        }

        None
    }

    /// Get the family name without the non-dropping particle.
    pub fn name_without_particle(&self) -> &str {
        if let Some(particle) = self.name_particle() {
            self.name[particle.len()..].trim_start()
        } else {
            self.name.as_str()
        }
    }

    /// Get only the dropping and non-dropping particle of the family name.
    pub fn name_particles(&self) -> Option<Cow<'_, str>> {
        match (&self.prefix, self.name_particle()) {
            (Some(dropping), Some(non_dropping)) => {
                Some(Cow::Owned(format!("{dropping} {non_dropping}")))
            }
            (Some(dropping), None) => Some(Cow::Borrowed(dropping.as_str())),
            (None, Some(non_dropping)) => Some(Cow::Borrowed(non_dropping)),
            (None, None) => None,
        }
    }

    /// Whether to treat this as an institutional name.
    pub fn is_institutional(&self) -> bool {
        self.given_name.is_none() && self.suffix.is_none() && self.prefix.is_none()
    }

    /// Whether the name contains CJK characters.
    pub fn is_cjk(&self) -> bool {
        self.name.chars().any(is_cjk)
            || self.given_name.as_ref().is_some_and(|gn| gn.chars().any(is_cjk))
    }

    /// Get the name without the leading article.
    pub fn name_without_article(&self) -> &str {
        let space_idx = self.name.find(' ');
        if let Some(space_idx) = space_idx {
            if space_idx + 1 != self.name.len()
                && ARTICLES.binary_search(&&self.name[0..space_idx]).is_ok()
            {
                &self.name[space_idx + 1..]
            } else {
                self.name.as_str()
            }
        } else {
            self.name.as_str()
        }
    }

    /// Order according to the CSL specification.
    pub(crate) fn csl_cmp(
        &self,
        other: &Self,
        form: LongShortForm,
        demote_particle: bool,
    ) -> std::cmp::Ordering {
        let self_cjk = self.is_cjk();
        let other_cjk = other.is_cjk();

        let name_a = self.name.to_lowercase();
        let name_b = other.name.to_lowercase();
        let given_a = self.given_name.as_ref().map(|s| s.to_lowercase());
        let given_b = other.given_name.as_ref().map(|s| s.to_lowercase());

        if self_cjk != other_cjk {
            // Put CJK names last.
            return self_cjk.cmp(&other_cjk);
        } else if self_cjk && other_cjk {
            // Apply special CJK rules.
            return match form {
                LongShortForm::Long => {
                    name_a.cmp(&name_b).then_with(|| given_a.cmp(&given_b))
                }
                LongShortForm::Short => name_a.cmp(&name_b),
            };
        }

        let prefix_a = self.prefix.as_ref().map(|s| s.to_lowercase());
        let prefix_b = other.prefix.as_ref().map(|s| s.to_lowercase());
        let suffix_a = self.suffix.as_ref().map(|s| s.to_lowercase());
        let suffix_b = other.suffix.as_ref().map(|s| s.to_lowercase());

        if demote_particle {
            self.name_without_particle()
                .to_lowercase()
                .cmp(&other.name_without_particle().to_lowercase())
                .then_with(|| {
                    self.name_particles()
                        .map(|c| c.to_lowercase())
                        .cmp(&other.name_particles().map(|c| c.to_lowercase()))
                })
                .then_with(|| given_a.cmp(&given_b))
                .then_with(|| suffix_a.cmp(&suffix_b))
        } else {
            name_a
                .cmp(&name_b)
                .then_with(|| prefix_a.cmp(&prefix_b))
                .then_with(|| given_a.cmp(&given_b))
                .then_with(|| suffix_a.cmp(&suffix_b))
        }
    }
}

impl Ord for Person {
    fn cmp(&self, other: &Self) -> Ordering {
        self.name
            .cmp(&other.name)
            .then(self.given_name.cmp(&other.given_name))
            .then(self.suffix.cmp(&other.suffix))
            .then(self.prefix.cmp(&other.prefix))
    }
}

impl PartialOrd for Person {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl FromStr for Person {
    type Err = PersonError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Self::from_strings(s.split(',').collect())
    }
}

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

    #[test]
    fn person_initials() {
        let mut s = String::new();
        let p = Person::from_strings(vec!["Dissmer", "Courtney Deliah"]).unwrap();
        p.initials(&mut s, Some("."), true).unwrap();
        assert_eq!("C.D.", s);

        let mut s = String::new();
        let p = Person::from_strings(vec!["Dissmer", "Courtney Deliah"]).unwrap();
        p.initials(&mut s, Some(". "), true).unwrap();
        assert_eq!("C. D.", s);

        let mut s = String::new();
        let p = Person::from_strings(vec!["Dissmer", "Courtney Deliah"]).unwrap();
        p.initials(&mut s, Some(""), true).unwrap();
        assert_eq!("CD", s);

        let mut s = String::new();
        let p = Person::from_strings(vec!["Dissmer", "Courtney Deliah"]).unwrap();
        p.initials(&mut s, None, true).unwrap();
        assert_eq!("CD", s);

        let mut s = String::new();
        let p = Person::from_strings(vec!["Günther", "Hans-Joseph"]).unwrap();
        p.initials(&mut s, Some(". "), true).unwrap();
        assert_eq!("H.-J.", s);

        let mut s = String::new();
        let p = Person::from_strings(vec!["Günther", "Hans-Joseph"]).unwrap();
        p.initials(&mut s, Some("."), true).unwrap();
        assert_eq!("H.-J.", s);

        let mut s = String::new();
        let p = Person::from_strings(vec!["Günther", "Hans-Joseph"]).unwrap();
        p.initials(&mut s, Some(""), true).unwrap();
        assert_eq!("H-J", s);

        let mut s = String::new();
        let p = Person::from_strings(vec!["Günther", "Hans-Joseph"]).unwrap();
        p.initials(&mut s, None, true).unwrap();
        assert_eq!("H-J", s);

        let mut s = String::new();
        let p = Person::from_strings(vec!["Günther", "Hans-Joseph"]).unwrap();
        p.initials(&mut s, Some(". "), false).unwrap();
        assert_eq!("H. J.", s);

        let mut s = String::new();
        let p = Person::from_strings(vec!["Günther", "Hans-Joseph"]).unwrap();
        p.initials(&mut s, Some("."), false).unwrap();
        assert_eq!("H.J.", s);

        let mut s = String::new();
        let p = Person::from_strings(vec!["Günther", "Hans-Joseph"]).unwrap();
        p.initials(&mut s, Some(""), false).unwrap();
        assert_eq!("HJ", s);

        let mut s = String::new();
        let p = Person::from_strings(vec!["Günther", "Hans-Joseph"]).unwrap();
        p.initials(&mut s, None, false).unwrap();
        assert_eq!("HJ", s);
    }

    #[test]
    fn person_non_dropping_initials() {
        let p = Person::from_strings(vec!["Von Der Leyen", "Ursula"]).unwrap();
        assert_eq!("Von Der", p.name_particle().unwrap());
        assert_eq!("Leyen", p.name_without_particle());
    }
    #[test]
    fn person_middle_initial() {
        let p = Person::from_strings(vec!["Kirk", "James T"]).unwrap();
        let mut s = String::new();
        p.first_name_with_delimiter(&mut s, Some(".")).unwrap();
        assert_eq!("James T.", s);
    }

    #[test]
    fn person_name_retrieval_order() {
        let p =
            Person::from_strings(vec!["van Dissmer", "Jr.", "Courtney Deliah"]).unwrap();
        assert_eq!("van Dissmer, Courtney Deliah, Jr.", p.name_first(false, false));
        assert_eq!("Dissmer, Courtney Deliah van, Jr.", p.name_first(false, true));
        assert_eq!("van Dissmer, C. D., Jr.", p.name_first(true, false));
        assert_eq!("Dissmer, C. D. van, Jr.", p.name_first(true, true));
        assert_eq!("Courtney Deliah van Dissmer Jr.", p.given_first(false));
        assert_eq!("C. D. van Dissmer Jr.", p.given_first(true));
    }
}
