include!(concat!(env!("OUT_DIR"), "/templates.rs"));

#[macro_use]
mod actix_ructe;

pub mod about;
pub mod direct;
pub mod help_page;
pub mod index;
pub mod liveness;
pub mod news_ep;
pub mod og_tags;
pub mod search_ep;
//pub mod search_help;
mod session;
pub mod templ_utils;
pub mod unescaped;
mod url_query;
pub mod user_settings;
pub mod web_error;

use std::fmt::Display;

use config::Config;
use localization::{
    language::Language,
    traits::{Translatable, TranslatablePlural},
    TranslationDict,
};
use news::NewsEntry;
use og_tags::TagKeyName;
use search::{executor::search_result::SearchResult as SearchResult2, query::Query};

use search::{kanji::result::Item as KanjiItem, query::UserSettings};
use types::jotoba::{
    names::Name,
    pagination::Pagination,
    search::{help::SearchHelp, SearchTarget},
    words::Word,
};
use unescaped::{UnescapedStr, UnescapedString};

/// Data for the base template
pub struct BaseData<'a> {
    pub site: Site<'a>,
    pub dict: &'a TranslationDict,
    pub user_settings: UserSettings,
    pub pagination: Option<Pagination>,
    pub asset_hash: &'a str,
    pub config: &'a Config,
    pub og_tags: Option<og_tags::TagSet>,
}

/// The site to display
#[derive(Clone)]
pub enum Site<'a> {
    SearchResult(SearchResult<'a>),
    Index,
    About,
    InfoPage,
    News(Vec<NewsEntry>),
}

/// Search result data. Required by individual templates to render the result items
#[derive(Clone, Debug)]
pub struct SearchResult<'a> {
    pub query: &'a Query,
    pub result: ResultData,
    pub search_help: Option<SearchHelp>,
}

/// The particular search result items
#[derive(Clone, Debug)]
pub enum ResultData {
    Word(SearchResult2<Word, search::word::result::AddResData>),
    KanjiInfo(Vec<KanjiItem>),
    Name(Vec<&'static Name>),
    Sentence(SearchResult2<search::sentence::result::Sentence, search::sentence::result::ResData>),
}

impl<'a> BaseData<'a> {
    #[inline]
    pub fn new(
        dict: &'a TranslationDict,
        user_settings: UserSettings,
        asset_hash: &'a str,
        config: &'a Config,
    ) -> Self {
        Self {
            site: Site::Index,
            dict,
            user_settings,
            pagination: None,
            asset_hash,
            config,
            og_tags: None,
        }
    }

    #[inline]
    pub fn with_site(mut self, site: Site<'a>) -> Self {
        self.site = site;
        self
    }

    #[inline]
    pub fn with_cust_pages(
        &mut self,
        items: u32,
        curr_page: u32,
        items_per_page: u32,
        max_pages: u32,
    ) {
        let mut pagination = Pagination {
            items,
            curr_page,
            items_per_page,
            max_pages,
        };

        // Don't show paginator if there is only one or no page
        if pagination.get_last() <= 1 {
            return;
        }

        if curr_page > pagination.get_last() {
            pagination.curr_page = pagination.get_last();
        }

        self.pagination = Some(pagination);
    }

    #[inline]
    pub fn with_pages(&mut self, items: u32, curr_page: u32) {
        self.with_cust_pages(items, curr_page, self.user_settings.page_size, 100);
    }

    #[inline]
    pub fn get_search_help(&self) -> Option<&SearchHelp> {
        let help = self.site.as_search_result()?.search_help.as_ref()?;
        (!help.is_empty()).then(|| help)
    }

    #[inline]
    pub fn get_search_site_id(&self) -> u8 {
        if let Site::SearchResult(ref res) = self.site {
            return match res.result {
                ResultData::Word(_) => 0,
                ResultData::KanjiInfo(_) => 1,
                ResultData::Sentence(_) => 2,
                ResultData::Name(_) => 3,
            };
        }

        0
    }

    #[inline]
    pub fn get_search_site_name(&self) -> &str {
        if let Site::SearchResult(ref res) = self.site {
            return match res.result {
                ResultData::Word(_) => self.gettext("Words").as_str(),
                ResultData::KanjiInfo(_) => self.gettext("Kanji").as_str(),
                ResultData::Sentence(_) => self.gettext("Sentences").as_str(),
                ResultData::Name(_) => self.gettext("Names").as_str(),
            };
        }

        self.gettext("Words").as_str()
    }

    #[inline]
    pub fn with_search_result(
        self,
        query: &'a Query,
        result: ResultData,
        search_help: Option<SearchHelp>,
    ) -> Self {
        let search_result = SearchResult {
            query,
            result,
            search_help,
        };
        self.with_site(Site::SearchResult(search_result))
    }

    /// Gets an owned String of the query
    pub fn get_query_str(&self) -> String {
        let query = match &self.site {
            Site::SearchResult(search_result) => {
                Some(search_result.query.without_search_type_tags())
            }
            _ => None,
        }
        .unwrap_or_default();
        query
    }

    /// Return a string 'selected' if the query_type in qs is equal to i
    pub fn sel_str(&self, i: SearchTarget) -> &'static str {
        let is_selected = match &self.site {
            Site::SearchResult(search_result) => search_result.query.target == i,
            _ => false,
        };

        if is_selected {
            "selected"
        } else {
            ""
        }
    }

    /// Returns true if the kanji compounds should be collapsed by default
    pub fn kanji_copounds_collapsed(&self) -> bool {
        self.pagination.as_ref().map(|i| i.get_last()).unwrap_or(0) > 1
    }

    /// Sets og tags which will overwrite the site-defaults if existing
    pub fn set_og_tags(&mut self, tags: og_tags::TagSet) {
        self.og_tags = Some(tags);
    }

    /// returns OG Tags
    pub fn get_og_tags(&self) -> Option<og_tags::TagSet> {
        if let Some(override_tags) = &self.og_tags {
            return Some(override_tags.clone());
        }
        self.site.og_tags()
    }

    #[inline]
    pub fn assets_path(&self) -> &str {
        self.config.server.get_html_files()
    }
}

impl<'a> Site<'a> {
    #[inline]
    pub fn as_search_result(&self) -> Option<&SearchResult<'a>> {
        if let Self::SearchResult(v) = self {
            Some(v)
        } else {
            None
        }
    }

    /// Returns proper OG tags for the current site
    pub fn og_tags(&self) -> Option<og_tags::TagSet> {
        Some(match self {
            Site::SearchResult(rs) => rs.og_tags(),
            _ => default_og_tags(),
        })
    }
}

impl ResultData {
    /// Returns `true` if the ResultData does not contain any items
    #[inline]
    pub fn is_empty(&self) -> bool {
        match self {
            ResultData::Word(w) => w.items.is_empty() && w.sentence.is_none(),
            ResultData::KanjiInfo(k) => k.is_empty(),
            ResultData::Name(n) => n.is_empty(),
            ResultData::Sentence(s) => s.items.is_empty(),
        }
    }
}

impl<'a> SearchResult<'a> {
    pub fn og_tags(&self) -> og_tags::TagSet {
        let mut tags = og_tags::TagSet::with_capacity(5);

        let search_type_name = self.search_type_ogg();
        let query = &self.query.query_str;
        let title = format!("Jotoba {search_type_name} search result for '{query}'");
        let description = self.og_tag_description();

        tags.add_og(TagKeyName::Title, &title);
        tags.add_og(TagKeyName::Description, &description);
        tags.add_twitter(TagKeyName::Title, &title);
        tags.add_twitter(TagKeyName::Description, &description);
        tags.add_twitter(TagKeyName::Card, "summary");

        tags
    }

    pub(crate) fn og_tag_description(&self) -> String {
        format!("{} results. See more...", self.result_count())
    }

    pub(crate) fn search_type_ogg(&self) -> &'static str {
        match self.result {
            ResultData::Word(_) => "words",
            ResultData::KanjiInfo(_) => "kanji",
            ResultData::Sentence(_) => "sentences",
            ResultData::Name(_) => "names",
        }
    }

    fn result_count(&self) -> usize {
        match &self.result {
            ResultData::Word(w) => w.items.len(),
            ResultData::KanjiInfo(k) => k.len(),
            ResultData::Name(n) => n.len(),
            ResultData::Sentence(s) => s.items.len(),
        }
    }
}

fn default_og_tags() -> og_tags::TagSet {
    let mut tags = og_tags::TagSet::new();
    let description =  "A powerful and free Japanese dictionary supporting words, kanji, sentences, and many different languages.";

    tags.add_og(TagKeyName::Title, "Jotoba");
    tags.add_og(TagKeyName::Description, description);
    tags.add_og(TagKeyName::URL, "https://jotoba.de");

    tags.add_twitter(TagKeyName::Title, "Jotoba");
    tags.add_twitter(TagKeyName::Description, description);

    tags
}

/// Translation helper
impl<'a> BaseData<'a> {
    #[inline]
    pub fn get_lang(&self) -> Language {
        self.user_settings.page_lang
    }

    #[inline]
    pub fn gettext<T: Translatable>(&self, t: T) -> UnescapedStr<'a> {
        t.gettext(&self.dict, Some(self.get_lang())).into()
    }

    #[inline]
    pub fn gettext_custom<T: Translatable>(&self, t: T) -> UnescapedString {
        t.gettext_custom(&self.dict, Some(self.get_lang())).into()
    }

    #[inline]
    pub fn pgettext<T: Translatable>(&self, t: T, context: &'a str) -> UnescapedStr<'a> {
        t.pgettext(&self.dict, context, Some(self.get_lang()))
            .into()
    }

    #[inline]
    pub fn ngettext<T: TranslatablePlural>(&self, t: T, n: u64) -> UnescapedStr<'a> {
        t.ngettext(&self.dict, n, Some(self.get_lang())).into()
    }

    #[inline]
    pub fn pngettext<T: TranslatablePlural>(
        &self,
        t: T,
        context: &'a str,
        n: u64,
    ) -> UnescapedStr<'a> {
        t.npgettext(&self.dict, context, n, Some(self.get_lang()))
            .into()
    }

    // Format functions

    #[inline]
    pub fn gettext_fmt<T: Translatable, V: Display + Sized + Clone>(
        &self,
        t: T,
        values: &[V],
    ) -> UnescapedString {
        t.gettext_fmt(&self.dict, values, Some(self.get_lang()))
            .into()
    }

    #[inline]
    pub fn pgettext_fmt<T: Translatable, V: Display + Sized + Clone>(
        &self,
        t: T,
        context: &'a str,
        values: &[V],
    ) -> UnescapedString {
        t.pgettext_fmt(&self.dict, context, values, Some(self.get_lang()))
            .into()
    }

    #[inline]
    pub fn ngettext_fmt<T: TranslatablePlural, V: Display + Sized + Clone>(
        &self,
        t: T,
        n: u64,
        values: &[V],
    ) -> UnescapedString {
        t.ngettext_fmt(&self.dict, n, values, Some(self.get_lang()))
            .into()
    }

    #[inline]
    pub fn pngettext_fmt<T: TranslatablePlural, V: Display + Sized + Clone>(
        &self,
        t: T,
        context: &'a str,
        n: u64,
        values: &[V],
    ) -> UnescapedString {
        t.npgettext_fmt(&self.dict, context, n, values, Some(self.get_lang()))
            .into()
    }

    #[inline]
    pub fn gt_search_link<T: Translatable, V: Display + Sized + Clone>(
        &self,
        t: T,
        value: V,
    ) -> UnescapedString {
        let link = format_search_link(value);
        t.gettext_fmt(&self.dict, &[link], Some(self.get_lang()))
            .into()
    }

    #[inline]
    pub fn gt_search_links<T: Translatable, V: Display + Sized + Clone>(
        &self,
        t: T,
        link: usize,
        values: &[V],
    ) -> UnescapedString {
        let mut values = values.iter().map(|i| i.to_string()).collect::<Vec<_>>();
        values[link] = format_search_link(&values[link]);
        t.gettext_fmt(&self.dict, &values, Some(self.get_lang()))
            .into()
    }

    #[inline]
    pub fn ngt_search_links<T: TranslatablePlural, V: Display + Sized + Clone>(
        &self,
        t: T,
        link: usize,
        values: &[V],
        n: u64,
    ) -> UnescapedString {
        let mut values = values.iter().map(|i| i.to_string()).collect::<Vec<_>>();
        values[link] = format_search_link(&values[link]);
        t.ngettext_fmt(&self.dict, n, &values, Some(self.get_lang()))
            .into()
    }
}

fn format_search_link<V: Display + Sized + Clone>(input: V) -> String {
    format!(
        "<a class='clickable no-align green' href='/search/{}'>{}</a>",
        input, input
    )
}
