use base_db::semantics::bib::Semantics;
use bibtex_utils::field::{
    author::AuthorField,
    date::DateField,
    number::{NumberField, NumberFieldData},
    text::TextField,
};
use isocountry::CountryCode;
use itertools::Itertools;
use syntax::bibtex;
use titlecase::titlecase;
use url::Url;

use super::{
    entry::{EntryData, EntryKind},
    output::{Inline, InlineBuilder, Punct},
};

#[derive(Debug, Default)]
pub struct Driver {
    builder: InlineBuilder,
}

impl Driver {
    pub fn process(&mut self, entry: &bibtex::Entry, semantics: &Semantics) {
        let entry = EntryData::from_entry(entry, semantics);
        match entry.kind {
            EntryKind::Article
            | EntryKind::DataSet
            | EntryKind::Manual
            | EntryKind::Misc
            | EntryKind::Online
            | EntryKind::Electronic
            | EntryKind::Www
            | EntryKind::Proceedings
            | EntryKind::MVProceedings
            | EntryKind::Set
            | EntryKind::Software
            | EntryKind::Thesis
            | EntryKind::MasterThesis
            | EntryKind::PhdThesis
            | EntryKind::Unknown => self.article_like(entry),
            EntryKind::Book
            | EntryKind::MVBook
            | EntryKind::InBook
            | EntryKind::BookInBook
            | EntryKind::SuppBook
            | EntryKind::Booklet
            | EntryKind::Collection
            | EntryKind::MVCollection
            | EntryKind::InCollection
            | EntryKind::SuppCollection
            | EntryKind::Periodical
            | EntryKind::SuppPeriodical
            | EntryKind::InProceedings
            | EntryKind::Conference
            | EntryKind::Reference
            | EntryKind::MVReference
            | EntryKind::InReference
            | EntryKind::Report
            | EntryKind::TechReport => self.book_like(entry),
            EntryKind::Patent => self.patent(entry),
        };
    }

    fn article_like(&mut self, mut entry: EntryData) {
        self.author(&mut entry);
        self.title(&mut entry);
        self.translator(&mut entry);
        self.commentator(&mut entry);
        self.annotator(&mut entry);
        self.version(&mut entry);
        self.journal(&mut entry);
        self.article_series(&mut entry);

        if let Some(volume) = entry.number.remove(&NumberField::Volume) {
            let number = entry
                .number
                .remove(&NumberField::Number)
                .map(|data| format!(".{}", data))
                .unwrap_or_default();

            self.builder.push(
                Inline::Regular(format!("{}{}", volume, number)),
                Punct::Space,
                Punct::Space,
            );
        }

        self.eid(&mut entry);
        self.issue_and_date(&mut entry);
        self.issue_title(&mut entry);
        self.editors(&mut entry);
        self.note(&mut entry);
        self.pages(&mut entry);
        self.issn(&mut entry);
        self.doi(&mut entry);
        self.eprint(&mut entry);
        self.url(&mut entry);
        self.addendum(&mut entry);
        self.pubstate(&mut entry);
    }

    fn book_like(&mut self, mut entry: EntryData) {
        self.author(&mut entry);
        self.title(&mut entry);
        self.inbook_title(&mut entry);
        self.main_title(&mut entry);
        self.event(&mut entry);
        self.editors(&mut entry);
        self.translator(&mut entry);
        self.commentator(&mut entry);
        self.annotator(&mut entry);
        self.introduction(&mut entry);
        self.foreword(&mut entry);
        self.afterword(&mut entry);
        self.edition(&mut entry);
        self.book_volume(&mut entry);
        self.volumes(&mut entry);
        self.book_series(&mut entry);
        self.how_published(&mut entry);
        self.note(&mut entry);
        self.publisher(&mut entry);
        self.date(&mut entry);
        self.chapter(&mut entry);
        self.eid(&mut entry);
        self.pages(&mut entry);
        self.page_total(&mut entry);
        self.isbn(&mut entry);
        self.doi(&mut entry);
        self.eprint(&mut entry);
        self.url(&mut entry);
        self.addendum(&mut entry);
        self.pubstate(&mut entry);
    }

    fn patent(&mut self, mut entry: EntryData) {
        self.author(&mut entry);
        self.title(&mut entry);
        self.patent_number(&mut entry);
        self.holder(&mut entry);
        self.date(&mut entry);
        self.doi(&mut entry);
        self.eprint(&mut entry);
        self.url(&mut entry);
        self.addendum(&mut entry);
        self.pubstate(&mut entry);
    }

    fn author(&mut self, entry: &mut EntryData) -> Option<()> {
        let author = entry.author.remove(&AuthorField::Author)?;
        self.builder.push(
            Inline::Regular(author.to_string()),
            Punct::Nothing,
            Punct::Colon,
        );

        Some(())
    }

    fn commentator(&mut self, entry: &mut EntryData) -> Option<()> {
        let commentator = entry.author.remove(&AuthorField::Commentator)?;
        self.builder.push(
            Inline::Regular(format!("With a comment. by {}", commentator)),
            Punct::Dot,
            Punct::Dot,
        );

        Some(())
    }

    fn translator(&mut self, entry: &mut EntryData) -> Option<()> {
        let translator = entry.author.remove(&AuthorField::Translator)?;
        self.builder.push(
            Inline::Regular(format!("Trans. by {}", translator)),
            Punct::Dot,
            Punct::Dot,
        );

        Some(())
    }

    fn annotator(&mut self, entry: &mut EntryData) -> Option<()> {
        let annotator = entry.author.remove(&AuthorField::Annotator)?;
        self.builder.push(
            Inline::Regular(format!("With annots. by {}", annotator)),
            Punct::Dot,
            Punct::Dot,
        );

        Some(())
    }

    fn editors(&mut self, entry: &mut EntryData) {
        self.editor(entry, AuthorField::Editor, TextField::EditorType);
        self.editor(entry, AuthorField::EditorA, TextField::EditorTypeA);
        self.editor(entry, AuthorField::EditorB, TextField::EditorTypeB);
        self.editor(entry, AuthorField::EditorC, TextField::EditorTypeC);
    }

    fn editor(
        &mut self,
        entry: &mut EntryData,
        name_field: AuthorField,
        type_field: TextField,
    ) -> Option<()> {
        let editor = entry.author.remove(&name_field)?;
        let editor_type = entry
            .text
            .remove(&type_field)
            .map_or_else(|| "Ed. by".to_string(), |data| data.text);

        self.builder.push(
            Inline::Regular(format!("{editor_type} {editor}")),
            Punct::Dot,
            Punct::Dot,
        );

        Some(())
    }

    fn title(&mut self, entry: &mut EntryData) -> Option<()> {
        let title = entry.text.remove(&TextField::Title)?;
        let subtitle = entry
            .text
            .remove(&TextField::Subtitle)
            .map(|data| format!(". {}", data.text))
            .unwrap_or_default();

        self.builder.push(
            Inline::Quoted(format!("{}{}", title.text, subtitle)),
            Punct::Nothing,
            Punct::Dot,
        );

        let addon = entry.text.remove(&TextField::TitleAddon)?;
        self.builder
            .push(Inline::Regular(addon.text), Punct::Dot, Punct::Dot);

        Some(())
    }

    fn main_title(&mut self, entry: &mut EntryData) -> Option<()> {
        let title = entry.text.remove(&TextField::MainTitle)?;
        let subtitle = entry
            .text
            .remove(&TextField::MainSubtitle)
            .map(|data| format!(". {}", data.text))
            .unwrap_or_default();

        self.builder.push(
            Inline::Italic(titlecase(&format!("{}{}", title.text, subtitle))),
            Punct::Dot,
            Punct::Dot,
        );

        let addon = entry.text.remove(&TextField::MainTitleAddon)?;
        self.builder
            .push(Inline::Regular(addon.text), Punct::Dot, Punct::Dot);

        Some(())
    }

    fn inbook_title(&mut self, entry: &mut EntryData) -> Option<()> {
        let title = entry.text.remove(&TextField::BookTitle)?;
        let subtitle = entry
            .text
            .remove(&TextField::BookSubtitle)
            .map(|data| format!(". {}", data.text))
            .unwrap_or_default();

        self.builder.push(
            Inline::Italic(titlecase(&format!("{}{}", title.text, subtitle))),
            Punct::Dot,
            Punct::Dot,
        );

        let addon = entry.text.remove(&TextField::BookTitleAddon)?;
        self.builder
            .push(Inline::Regular(addon.text), Punct::Dot, Punct::Dot);

        Some(())
    }

    fn issue_title(&mut self, entry: &mut EntryData) -> Option<()> {
        let issue = entry.text.remove(&TextField::IssueTitle)?;
        let subtitle = entry
            .text
            .remove(&TextField::IssueSubtitle)
            .map(|data| format!(" {}", data.text))
            .unwrap_or_default();

        self.builder.push(
            Inline::Italic(titlecase(&format!("{}{}", issue.text, subtitle))),
            Punct::Colon,
            Punct::Space,
        );

        if let Some(addon) = entry.text.remove(&TextField::IssueTitleAddon) {
            self.builder
                .push(Inline::Regular(addon.text), Punct::Dot, Punct::Dot);
        }

        Some(())
    }

    fn event(&mut self, entry: &mut EntryData) -> Option<()> {
        let title = entry.text.remove(&TextField::EventTitle)?;
        let addon = entry
            .text
            .remove(&TextField::EventTitleAddon)
            .map_or(String::new(), |addon| format!(". {}", addon.text));

        let venue = entry.text.remove(&TextField::Venue);
        let date = entry.date.remove(&DateField::EventDate);
        let venue_and_date = match (venue, date) {
            (None, None) => String::new(),
            (None, Some(date)) => format!(" ({date})"),
            (Some(venue), None) => format!(" ({})", venue.text),
            (Some(venue), Some(date)) => format!(" ({}, {})", venue.text, date),
        };

        self.builder.push(
            Inline::Regular(format!("{}{}{}", title.text, addon, venue_and_date)),
            Punct::Dot,
            Punct::Dot,
        );

        Some(())
    }

    fn patent_number(&mut self, entry: &mut EntryData) -> Option<()> {
        let number = entry.number.remove(&NumberField::Number)?;

        let location = entry
            .text
            .remove(&TextField::Location)
            .map_or(String::new(), |location| {
                let text = location
                    .text
                    .split_whitespace()
                    .map(|word| {
                        if word == "countryuk" {
                            "United Kingdom"
                        } else {
                            word.strip_prefix("country")
                                .and_then(|code| CountryCode::for_alpha2_caseless(code).ok())
                                .map_or(word, |country| country.name())
                        }
                    })
                    .join(" ");

                format!(" ({})", text)
            });

        self.builder.push(
            Inline::Regular(format!("{}{}", number, location)),
            Punct::Dot,
            Punct::Dot,
        );

        Some(())
    }

    fn holder(&mut self, entry: &mut EntryData) -> Option<()> {
        let holder = entry.text.remove(&TextField::Holder)?;
        self.builder
            .push(Inline::Regular(holder.text), Punct::Dot, Punct::Dot);

        Some(())
    }

    fn version(&mut self, entry: &mut EntryData) -> Option<()> {
        let version = entry.text.remove(&TextField::Version)?;
        self.builder.push(
            Inline::Regular(format!("Version {}", version.text)),
            Punct::Dot,
            Punct::Dot,
        );

        Some(())
    }

    fn article_series(&mut self, entry: &mut EntryData) -> Option<()> {
        let series = entry.text.remove(&TextField::Series)?;
        self.builder
            .push(Inline::Regular(series.text), Punct::Space, Punct::Space);

        Some(())
    }

    fn issue_and_date(&mut self, entry: &mut EntryData) -> Option<()> {
        let date = [DateField::Date, DateField::Year, DateField::Month]
            .iter()
            .filter_map(|field| entry.date.remove(field))
            .reduce(|a, b| a + b)?;

        let issue = entry
            .text
            .remove(&TextField::Issue)
            .map(|data| format!("{} ", data.text))
            .unwrap_or_default();

        self.builder.push(
            Inline::Regular(format!("({issue}{date})")),
            Punct::Space,
            Punct::Colon,
        );

        Some(())
    }

    fn chapter(&mut self, entry: &mut EntryData) -> Option<()> {
        let chapter = entry.text.remove(&TextField::Chapter)?;
        self.builder.push(
            Inline::Regular(format!("Chap. {}", chapter.text)),
            Punct::Dot,
            Punct::Dot,
        );

        Some(())
    }

    fn date(&mut self, entry: &mut EntryData) -> Option<()> {
        let date = [DateField::Date, DateField::Year, DateField::Month]
            .iter()
            .filter_map(|field| entry.date.remove(field))
            .reduce(|a, b| a + b)?;

        self.builder
            .push(Inline::Regular(format!("{date}")), Punct::Dot, Punct::Dot);

        Some(())
    }

    fn how_published(&mut self, entry: &mut EntryData) -> Option<()> {
        let how_published = entry.text.remove(&TextField::HowPublished)?;
        self.builder
            .push(Inline::Regular(how_published.text), Punct::Dot, Punct::Dot);

        Some(())
    }

    fn publisher(&mut self, entry: &mut EntryData) -> Option<()> {
        let publisher = entry.text.remove(&TextField::Publisher)?.text;
        let location = entry
            .text
            .remove(&TextField::Location)
            .map_or(String::new(), |location| format!("{}: ", location.text));

        self.builder.push(
            Inline::Regular(format!("{location}{publisher}")),
            Punct::Dot,
            Punct::Comma,
        );

        Some(())
    }

    fn book_series(&mut self, entry: &mut EntryData) -> Option<()> {
        let series = entry.text.remove(&TextField::Series)?;
        let number = entry
            .number
            .remove(&NumberField::Number)
            .map_or(String::new(), |number| format!(" {}", number));

        self.builder.push(
            Inline::Regular(format!("{}{}", series.text, number)),
            Punct::Dot,
            Punct::Dot,
        );

        Some(())
    }

    fn volumes(&mut self, entry: &mut EntryData) -> Option<()> {
        let volumes = entry.number.remove(&NumberField::Volume)?;
        self.builder.push(
            Inline::Regular(format!("{volumes} vols")),
            Punct::Dot,
            Punct::Dot,
        );

        Some(())
    }

    fn book_volume(&mut self, entry: &mut EntryData) -> Option<()> {
        let volume = entry.number.remove(&NumberField::Volume)?;
        let part = entry
            .number
            .remove(&NumberField::Part)
            .map_or(String::new(), |part| format!(".{part}"));

        self.builder.push(
            Inline::Regular(format!("Vol. {}{}", volume, part)),
            Punct::Dot,
            Punct::Dot,
        );

        Some(())
    }

    fn pubstate(&mut self, entry: &mut EntryData) -> Option<()> {
        let pubstate = entry.text.remove(&TextField::Pubstate)?;
        self.builder
            .push(Inline::Regular(pubstate.text), Punct::Dot, Punct::Dot);

        Some(())
    }

    fn addendum(&mut self, entry: &mut EntryData) -> Option<()> {
        let addendum = entry.text.remove(&TextField::Addendum)?;
        self.builder
            .push(Inline::Regular(addendum.text), Punct::Dot, Punct::Dot);

        Some(())
    }

    fn edition(&mut self, entry: &mut EntryData) -> Option<()> {
        let text = match entry.number.remove(&NumberField::Edition)? {
            NumberFieldData::Scalar(1) => "1st".to_string(),
            NumberFieldData::Scalar(2) => "2nd".to_string(),
            NumberFieldData::Scalar(3) => "3rd".to_string(),
            NumberFieldData::Scalar(number) => format!("{}th", number),
            NumberFieldData::Range(_, _) => return None,
            NumberFieldData::Other(text) => text,
        };

        self.builder
            .push(Inline::Regular(text), Punct::Dot, Punct::Dot);
        Some(())
    }

    fn introduction(&mut self, entry: &mut EntryData) -> Option<()> {
        let author = entry.author.remove(&AuthorField::Introduction)?;
        self.builder.push(
            Inline::Regular(format!("With an intro. by {}", author)),
            Punct::Dot,
            Punct::Dot,
        );

        Some(())
    }

    fn foreword(&mut self, entry: &mut EntryData) -> Option<()> {
        let author = entry.author.remove(&AuthorField::Commentator)?;
        self.builder.push(
            Inline::Regular(format!("With a forew. by {}", author)),
            Punct::Dot,
            Punct::Dot,
        );

        Some(())
    }

    fn afterword(&mut self, entry: &mut EntryData) -> Option<()> {
        let author = entry.author.remove(&AuthorField::Commentator)?;
        self.builder.push(
            Inline::Regular(format!("With an afterw. by {}", author)),
            Punct::Dot,
            Punct::Dot,
        );

        Some(())
    }

    fn note(&mut self, entry: &mut EntryData) -> Option<()> {
        let note = entry.text.remove(&TextField::Note)?;
        self.builder
            .push(Inline::Regular(note.text), Punct::Dot, Punct::Comma);

        Some(())
    }

    fn pages(&mut self, entry: &mut EntryData) -> Option<()> {
        let pages = entry.number.remove(&NumberField::Pages)?;
        self.builder.push(
            Inline::Regular(pages.to_string()),
            Punct::Comma,
            Punct::Space,
        );

        Some(())
    }

    fn page_total(&mut self, entry: &mut EntryData) -> Option<()> {
        let page_total = entry.number.remove(&NumberField::PageTotal)?;
        self.builder.push(
            Inline::Regular(format!("{} pp", page_total)),
            Punct::Dot,
            Punct::Dot,
        );

        Some(())
    }

    fn journal(&mut self, entry: &mut EntryData) -> Option<()> {
        let title = entry
            .text
            .remove(&TextField::Journal)
            .or_else(|| entry.text.remove(&TextField::JournalTitle))?;

        let subtitle = entry
            .text
            .remove(&TextField::JournalSubtitle)
            .map_or(String::new(), |data| format!(" {}", data.text));

        self.builder.push(
            Inline::Italic(titlecase(&format!("{}{}", title.text, subtitle))),
            Punct::Dot,
            Punct::Space,
        );

        Some(())
    }

    fn eid(&mut self, entry: &mut EntryData) -> Option<()> {
        let eid = entry.text.remove(&TextField::Eid)?;
        self.builder
            .push(Inline::Regular(eid.text), Punct::Comma, Punct::Space);

        Some(())
    }

    fn isbn(&mut self, entry: &mut EntryData) -> Option<()> {
        let isbn = entry.text.remove(&TextField::Isbn)?;
        self.builder.push(
            Inline::Regular("ISBN".to_string()),
            Punct::Dot,
            Punct::Colon,
        );

        self.builder
            .push(Inline::Regular(isbn.text), Punct::Space, Punct::Dot);

        Some(())
    }

    fn issn(&mut self, entry: &mut EntryData) -> Option<()> {
        let issn = entry.text.remove(&TextField::Issn)?;
        self.builder.push(
            Inline::Regular("ISSN".to_string()),
            Punct::Dot,
            Punct::Colon,
        );

        self.builder
            .push(Inline::Regular(issn.text), Punct::Space, Punct::Dot);

        Some(())
    }

    fn url(&mut self, entry: &mut EntryData) -> Option<()> {
        let url = entry.text.remove(&TextField::Url)?;

        self.builder
            .push(Inline::Regular("URL".to_string()), Punct::Dot, Punct::Colon);

        let url = url.text;
        let alt = url.clone();

        self.builder
            .push(Inline::Link { url, alt }, Punct::Space, Punct::Space);

        let date = entry.date.remove(&DateField::UrlDate)?;
        self.builder.push(
            Inline::Regular(format!("({date})")),
            Punct::Space,
            Punct::Space,
        );

        Some(())
    }

    fn doi(&mut self, entry: &mut EntryData) -> Option<()> {
        let doi = entry.text.remove(&TextField::Doi)?;
        self.builder
            .push(Inline::Regular("DOI".to_string()), Punct::Dot, Punct::Colon);

        let alt = Url::parse(&doi.text)
            .ok()
            .filter(|url| !url.cannot_be_a_base())
            .map_or(doi.text, |url| url.path()[1..].to_string());

        let url = format!("https://doi.org/{alt}");

        self.builder
            .push(Inline::Link { url, alt }, Punct::Space, Punct::Dot);

        Some(())
    }

    fn eprint(&mut self, entry: &mut EntryData) -> Option<()> {
        let eprint = entry.text.remove(&TextField::Eprint)?;
        let eprint_type = entry
            .text
            .remove(&TextField::EprintType)
            .map_or_else(|| "eprint".to_string(), |data| data.text);

        if eprint_type.eq_ignore_ascii_case("arxiv") {
            self.builder.push(
                Inline::Regular("arXiv".to_string()),
                Punct::Dot,
                Punct::Colon,
            );

            self.builder.push(
                Inline::Link {
                    url: format!("https://arxiv.org/abs/{}", eprint.text),
                    alt: eprint.text,
                },
                Punct::Space,
                Punct::Dot,
            );
        } else {
            self.builder
                .push(Inline::Regular(eprint_type), Punct::Dot, Punct::Colon);

            self.builder
                .push(Inline::Regular(eprint.text), Punct::Space, Punct::Dot);
        }

        Some(())
    }

    pub fn finish(self) -> impl Iterator<Item = (Inline, Punct)> {
        self.builder.finish()
    }
}
