/*
 * meli - themes conf module
 *
 * Copyright 2019  Manos Pitsidianakis
 *
 * This file is part of meli.
 *
 * meli is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * meli is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with meli. If not, see <http://www.gnu.org/licenses/>.
 */

//! Application themes.
//!
//! * An attribute is a triple of foreground color, background color and terminal attribute `ThemeValue`s.
//! * A `ThemeValue<T>` is either an actual value or the key name of another value to which it depends. The value is either `Color` or `Attr`.
//! * `ThemeAttributeInner` is an attribute triplet.
//! * `ThemeAttribute` is an attribute triplet with the links resolved.
//!
//! On startup a [DFS](https://en.wikipedia.org/wiki/Depth-first_search) is performed to see if there are any cycles in the link graph.

use crate::terminal::{Attr, Color};
use crate::Context;
use indexmap::IndexMap;
use melib::{MeliError, Result};
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use smallvec::SmallVec;
use std::borrow::Cow;
use std::collections::HashSet;

#[inline(always)]
pub fn value(context: &Context, key: &'static str) -> ThemeAttribute {
    let theme = match context.settings.terminal.theme.as_str() {
        "light" => &context.settings.terminal.themes.light,
        "dark" => &context.settings.terminal.themes.dark,
        t => context
            .settings
            .terminal
            .themes
            .other_themes
            .get(t)
            .unwrap_or(&context.settings.terminal.themes.dark),
    };
    unlink(theme, &Cow::from(key))
}

#[inline(always)]
pub fn fg_color(context: &Context, key: &'static str) -> Color {
    let theme = match context.settings.terminal.theme.as_str() {
        "light" => &context.settings.terminal.themes.light,
        "dark" => &context.settings.terminal.themes.dark,
        t => context
            .settings
            .terminal
            .themes
            .other_themes
            .get(t)
            .unwrap_or(&context.settings.terminal.themes.dark),
    };
    unlink_fg(theme, &ColorField::Fg, &Cow::from(key))
}

#[inline(always)]
pub fn bg_color(context: &Context, key: &'static str) -> Color {
    let theme = match context.settings.terminal.theme.as_str() {
        "light" => &context.settings.terminal.themes.light,
        "dark" => &context.settings.terminal.themes.dark,
        t => context
            .settings
            .terminal
            .themes
            .other_themes
            .get(t)
            .unwrap_or(&context.settings.terminal.themes.dark),
    };
    unlink_bg(theme, &ColorField::Bg, &Cow::from(key))
}

#[inline(always)]
pub fn attrs(context: &Context, key: &'static str) -> Attr {
    let theme = match context.settings.terminal.theme.as_str() {
        "light" => &context.settings.terminal.themes.light,
        "dark" => &context.settings.terminal.themes.dark,
        t => context
            .settings
            .terminal
            .themes
            .other_themes
            .get(t)
            .unwrap_or(&context.settings.terminal.themes.dark),
    };
    unlink_attrs(theme, &Cow::from(key))
}

#[inline(always)]
fn unlink<'k, 't: 'k>(theme: &'t Theme, key: &'k Cow<'static, str>) -> ThemeAttribute {
    ThemeAttribute {
        fg: unlink_fg(theme, &ColorField::Fg, key),
        bg: unlink_bg(theme, &ColorField::Bg, key),
        attrs: unlink_attrs(theme, key),
    }
}

#[inline(always)]
fn unlink_fg<'k, 't: 'k>(
    theme: &'t Theme,
    mut field: &'k ColorField,
    mut key: &'k Cow<'static, str>,
) -> Color {
    loop {
        match field {
            ColorField::LikeSelf | ColorField::Fg => match &theme[key].fg {
                ThemeValue::Link(ref new_key, ref new_field) => {
                    key = new_key;
                    field = new_field
                }
                ThemeValue::Alias(ref alias_ident) => {
                    let mut alias_ident = alias_ident;
                    'self_alias_loop: loop {
                        match &theme.color_aliases[alias_ident.as_ref()] {
                            ThemeValue::Link(ref new_key, ref new_field) => {
                                key = new_key;
                                field = new_field;
                                break 'self_alias_loop;
                            }
                            ThemeValue::Alias(ref new_alias_ident) => alias_ident = new_alias_ident,
                            ThemeValue::Value(val) => return *val,
                        }
                    }
                }

                ThemeValue::Value(val) => return *val,
            },
            ColorField::Bg => match &theme[key].bg {
                ThemeValue::Link(ref new_key, ref new_field) => {
                    key = new_key;
                    field = new_field
                }
                ThemeValue::Alias(ref alias_ident) => {
                    let mut alias_ident = alias_ident;
                    'other_alias_loop: loop {
                        match &theme.color_aliases[alias_ident.as_ref()] {
                            ThemeValue::Link(ref new_key, ref new_field) => {
                                key = new_key;
                                field = new_field;
                                break 'other_alias_loop;
                            }
                            ThemeValue::Alias(ref new_alias_ident) => alias_ident = new_alias_ident,
                            ThemeValue::Value(val) => return *val,
                        }
                    }
                }
                ThemeValue::Value(val) => return *val,
            },
        }
    }
}

#[inline(always)]
fn unlink_bg<'k, 't: 'k>(
    theme: &'t Theme,
    mut field: &'k ColorField,
    mut key: &'k Cow<'static, str>,
) -> Color {
    loop {
        match field {
            ColorField::LikeSelf | ColorField::Bg => match &theme[key].bg {
                ThemeValue::Link(ref new_key, ref new_field) => {
                    key = new_key;
                    field = new_field
                }
                ThemeValue::Alias(ref alias_ident) => {
                    let mut alias_ident = alias_ident;
                    'self_alias_loop: loop {
                        match &theme.color_aliases[alias_ident.as_ref()] {
                            ThemeValue::Link(ref new_key, ref new_field) => {
                                key = new_key;
                                field = new_field;
                                break 'self_alias_loop;
                            }
                            ThemeValue::Alias(ref new_alias_ident) => alias_ident = new_alias_ident,
                            ThemeValue::Value(val) => return *val,
                        }
                    }
                }
                ThemeValue::Value(val) => return *val,
            },
            ColorField::Fg => match &theme[key].fg {
                ThemeValue::Link(ref new_key, ref new_field) => {
                    key = new_key;
                    field = new_field
                }
                ThemeValue::Alias(ref alias_ident) => {
                    let mut alias_ident = alias_ident;
                    'other_alias_loop: loop {
                        match &theme.color_aliases[alias_ident.as_ref()] {
                            ThemeValue::Link(ref new_key, ref new_field) => {
                                key = new_key;
                                field = new_field;
                                break 'other_alias_loop;
                            }
                            ThemeValue::Alias(ref new_alias_ident) => alias_ident = new_alias_ident,
                            ThemeValue::Value(val) => return *val,
                        }
                    }
                }
                ThemeValue::Value(val) => return *val,
            },
        }
    }
}

#[inline(always)]
fn unlink_attrs<'k, 't: 'k>(theme: &'t Theme, mut key: &'k Cow<'static, str>) -> Attr {
    loop {
        match &theme[key].attrs {
            ThemeValue::Link(ref new_key, ()) => key = new_key,
            ThemeValue::Alias(ref alias_ident) => {
                let mut alias_ident = alias_ident;
                'alias_loop: loop {
                    match &theme.attr_aliases[alias_ident.as_ref()] {
                        ThemeValue::Link(ref new_key, ()) => {
                            key = new_key;
                            break 'alias_loop;
                        }
                        ThemeValue::Alias(ref new_alias_ident) => alias_ident = new_alias_ident,
                        ThemeValue::Value(val) => return *val,
                    }
                }
            }
            ThemeValue::Value(val) => return *val,
        }
    }
}

const DEFAULT_KEYS: &[&str] = &[
    "theme_default",
    "status.bar",
    "status.notification",
    "tab.focused",
    "tab.unfocused",
    "tab.bar",
    "widgets.list.header",
    "widgets.form.label",
    "widgets.form.field",
    "widgets.form.highlighted",
    "widgets.options.highlighted",
    "mail.sidebar",
    "mail.sidebar_account_name",
    "mail.sidebar_unread_count",
    "mail.sidebar_index",
    "mail.sidebar_highlighted",
    "mail.sidebar_highlighted_account_name",
    "mail.sidebar_highlighted_unread_count",
    "mail.sidebar_highlighted_index",
    "mail.sidebar_highlighted_account",
    "mail.sidebar_highlighted_account_unread_count",
    "mail.sidebar_highlighted_account_index",
    "mail.listing.compact.even",
    "mail.listing.compact.odd",
    "mail.listing.compact.even_unseen",
    "mail.listing.compact.odd_unseen",
    "mail.listing.compact.even_selected",
    "mail.listing.compact.odd_selected",
    "mail.listing.compact.even_highlighted",
    "mail.listing.compact.odd_highlighted",
    "mail.listing.plain.even",
    "mail.listing.plain.odd",
    "mail.listing.plain.even_unseen",
    "mail.listing.plain.odd_unseen",
    "mail.listing.plain.even_selected",
    "mail.listing.plain.odd_selected",
    "mail.listing.plain.even_highlighted",
    "mail.listing.plain.odd_highlighted",
    "mail.listing.conversations",
    "mail.listing.conversations.subject",
    "mail.listing.conversations.from",
    "mail.listing.conversations.date",
    "mail.listing.conversations.padding",
    "mail.listing.conversations.unseen",
    "mail.listing.conversations.unseen_padding",
    "mail.listing.conversations.highlighted",
    "mail.listing.conversations.selected",
    "mail.view.headers",
    "mail.view.body",
    "mail.view.thread.indentation.a",
    "mail.view.thread.indentation.b",
    "mail.view.thread.indentation.c",
    "mail.view.thread.indentation.d",
    "mail.view.thread.indentation.e",
    "mail.view.thread.indentation.f",
    "mail.listing.attachment_flag",
    "mail.listing.thread_snooze_flag",
    "mail.listing.tag_default",
    "pager.highlight_search",
    "pager.highlight_search_current",
];

/// `ThemeAttributeInner` but with the links resolved.
#[derive(Debug, PartialEq, Eq, Clone, Default, Copy, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ThemeAttribute {
    pub fg: Color,
    pub bg: Color,
    pub attrs: Attr,
}

/// Holds {fore,back}ground color and terminal attribute values.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ThemeAttributeInner {
    #[serde(default)]
    fg: ThemeValue<Color>,
    #[serde(default)]
    bg: ThemeValue<Color>,
    #[serde(default)]
    attrs: ThemeValue<Attr>,
}

impl Default for ThemeAttributeInner {
    fn default() -> Self {
        Self {
            fg: "theme_default".into(),
            bg: "theme_default".into(),
            attrs: "theme_default".into(),
        }
    }
}

#[derive(Debug, Clone)]
pub enum ColorField {
    // Like self, i.e. either Fg or Bg
    LikeSelf,
    Fg,
    Bg,
}

/// The field a ThemeValue::Link refers to.
trait ThemeLink {
    type LinkType;
}

/// A color value that's a link can either refer to .fg or .bg field
impl ThemeLink for Color {
    type LinkType = ColorField;
}

/// An attr value that's a link can only refer to an .attr field
impl ThemeLink for Attr {
    type LinkType = ();
}

#[derive(Debug, Clone)]
/// Holds either an actual value or refers to the key name of the attribute that holds the value.
enum ThemeValue<T: ThemeLink> {
    Value(T),
    Alias(Cow<'static, str>),
    Link(Cow<'static, str>, T::LinkType),
}

impl From<&'static str> for ThemeValue<Color> {
    fn from(from: &'static str) -> Self {
        ThemeValue::Link(from.into(), ColorField::LikeSelf)
    }
}

impl From<&'static str> for ThemeValue<Attr> {
    fn from(from: &'static str) -> Self {
        ThemeValue::Link(from.into(), ())
    }
}

impl From<Color> for ThemeValue<Color> {
    fn from(from: Color) -> Self {
        ThemeValue::Value(from)
    }
}

impl From<Attr> for ThemeValue<Attr> {
    fn from(from: Attr) -> Self {
        ThemeValue::Value(from)
    }
}

impl Default for ThemeValue<Color> {
    fn default() -> Self {
        ThemeValue::Value(Color::Default)
    }
}

impl Default for ThemeValue<Attr> {
    fn default() -> Self {
        ThemeValue::Value(Attr::DEFAULT)
    }
}

impl<'de> Deserialize<'de> for ThemeValue<Attr> {
    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        if let Ok(s) = <String>::deserialize(deserializer) {
            if s.starts_with("$") {
                Ok(ThemeValue::Alias(s[1..].to_string().into()))
            } else if let Ok(c) = Attr::from_string_de::<'de, D, String>(s.clone()) {
                Ok(ThemeValue::Value(c))
            } else {
                Ok(ThemeValue::Link(s.into(), ()))
            }
        } else {
            Err(de::Error::custom("invalid theme attribute value"))
        }
    }
}

impl Serialize for ThemeValue<Attr> {
    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        match self {
            ThemeValue::Value(s) => s.serialize(serializer),
            ThemeValue::Alias(s) => format!("${}", s).serialize(serializer),
            ThemeValue::Link(s, ()) => serializer.serialize_str(s.as_ref()),
        }
    }
}

impl Serialize for ThemeValue<Color> {
    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        match self {
            ThemeValue::Value(s) => s.serialize(serializer),
            ThemeValue::Alias(s) => format!("${}", s).serialize(serializer),
            ThemeValue::Link(s, ColorField::LikeSelf) => serializer.serialize_str(s.as_ref()),
            ThemeValue::Link(s, ColorField::Fg) => {
                serializer.serialize_str(format!("{}.fg", s).as_ref())
            }
            ThemeValue::Link(s, ColorField::Bg) => {
                serializer.serialize_str(format!("{}.bg", s).as_ref())
            }
        }
    }
}

impl<'de> Deserialize<'de> for ThemeValue<Color> {
    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        if let Ok(s) = <String>::deserialize(deserializer) {
            if s.starts_with("$") {
                Ok(ThemeValue::Alias(s[1..].to_string().into()))
            } else if let Ok(c) = Color::from_string_de::<'de, D>(s.clone()) {
                Ok(ThemeValue::Value(c))
            } else if s.ends_with(".fg") {
                Ok(ThemeValue::Link(
                    s[..s.len() - 3].to_string().into(),
                    ColorField::Fg,
                ))
            } else if s.ends_with(".bg") {
                Ok(ThemeValue::Link(
                    s[..s.len() - 3].to_string().into(),
                    ColorField::Bg,
                ))
            } else {
                Ok(ThemeValue::Link(s.into(), ColorField::LikeSelf))
            }
        } else {
            Err(de::Error::custom("invalid theme color value"))
        }
    }
}

#[derive(Debug, Clone)]
pub struct Themes {
    pub light: Theme,
    pub dark: Theme,
    pub other_themes: IndexMap<String, Theme>,
}

#[derive(Debug, Clone)]
pub struct Theme {
    color_aliases: IndexMap<Cow<'static, str>, ThemeValue<Color>>,
    attr_aliases: IndexMap<Cow<'static, str>, ThemeValue<Attr>>,
    #[cfg(feature = "regexp")]
    text_format_regexps: IndexMap<Cow<'static, str>, SmallVec<[TextFormatterSetting; 32]>>,
    pub keys: IndexMap<Cow<'static, str>, ThemeAttributeInner>,
}

#[cfg(feature = "regexp")]
pub use regexp::text_format_regexps;
#[cfg(feature = "regexp")]
use regexp::*;

#[cfg(feature = "regexp")]
mod regexp {
    use super::*;
    use crate::terminal::FormatTag;

    pub(super) const DEFAULT_TEXT_FORMATTER_KEYS: &[&str] =
        &["pager.envelope.body", "listing.from", "listing.subject"];

    #[derive(Clone)]
    pub struct RegexpWrapper(pub pcre2::bytes::Regex);

    #[derive(Debug, Clone)]
    pub(super) struct TextFormatterSetting {
        pub(super) regexp: RegexpWrapper,
        pub(super) fg: Option<ThemeValue<Color>>,
        pub(super) bg: Option<ThemeValue<Color>>,
        pub(super) attrs: Option<ThemeValue<Attr>>,
        pub(super) priority: u8,
    }

    #[derive(Debug, Clone)]
    pub struct TextFormatter<'r> {
        pub regexp: &'r RegexpWrapper,
        pub tag: FormatTag,
    }

    impl Default for RegexpWrapper {
        fn default() -> Self {
            Self(pcre2::bytes::Regex::new("").unwrap())
        }
    }

    impl std::fmt::Debug for RegexpWrapper {
        fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
            std::fmt::Debug::fmt(self.0.as_str(), fmt)
        }
    }

    impl std::hash::Hash for RegexpWrapper {
        fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
            self.0.as_str().hash(state)
        }
    }

    impl Eq for RegexpWrapper {}

    impl PartialEq for RegexpWrapper {
        fn eq(&self, other: &RegexpWrapper) -> bool {
            self.0.as_str().eq(other.0.as_str())
        }
    }

    impl RegexpWrapper {
        pub(super) fn new(
            pattern: &str,
            caseless: bool,
            dotall: bool,
            extended: bool,
            multi_line: bool,
            ucp: bool,
            jit_if_available: bool,
        ) -> std::result::Result<Self, pcre2::Error> {
            Ok(Self(unsafe {
                pcre2::bytes::RegexBuilder::new()
                    .caseless(caseless)
                    .dotall(dotall)
                    .extended(extended)
                    .multi_line(multi_line)
                    .ucp(ucp)
                    .jit_if_available(jit_if_available)
                    .disable_utf_check() // We only match on rust strings, which are guaranteed UTF8
                    .build(pattern)?
            }))
        }

        pub fn find_iter<'w, 's>(&'w self, s: &'s str) -> FindIter<'w, 's> {
            FindIter {
                pcre_iter: self.0.find_iter(s.as_bytes()),
                char_indices: s.char_indices(),
                char_offset: 0,
            }
        }
    }

    pub struct FindIter<'r, 's> {
        pcre_iter: pcre2::bytes::Matches<'r, 's>,
        char_indices: std::str::CharIndices<'s>,
        char_offset: usize,
    }

    impl<'r, 's> Iterator for FindIter<'r, 's> {
        type Item = (usize, usize);

        fn next(&mut self) -> Option<Self::Item> {
            loop {
                let next_byte_offset = self.pcre_iter.next();
                if next_byte_offset.is_none() {
                    return None;
                }
                let next_byte_offset = next_byte_offset.unwrap();
                if next_byte_offset.is_err() {
                    continue;
                }
                let next_byte_offset = next_byte_offset.unwrap();

                let mut next_char_index = self.char_indices.next();
                if next_char_index.is_none() {
                    return None;
                }

                while next_byte_offset.start() < next_char_index.unwrap().0 {
                    self.char_offset += 1;
                    next_char_index = self.char_indices.next();
                    if next_char_index.is_none() {
                        return None;
                    }
                }
                let start = self.char_offset;

                while next_byte_offset.end()
                    >= self
                        .char_indices
                        .next()
                        .map(|(v, _)| v)
                        .unwrap_or(next_byte_offset.end())
                        + 1
                {
                    self.char_offset += 1;
                }
                let end = self.char_offset + 1;

                return Some((start, end));
            }
        }
    }

    #[inline(always)]
    pub fn text_format_regexps<'ctx>(
        context: &'ctx Context,
        key: &'static str,
    ) -> SmallVec<[TextFormatter<'ctx>; 64]> {
        let theme = match context.settings.terminal.theme.as_str() {
            "light" => &context.settings.terminal.themes.light,
            "dark" => &context.settings.terminal.themes.dark,
            t => context
                .settings
                .terminal
                .themes
                .other_themes
                .get(t)
                .unwrap_or(&context.settings.terminal.themes.dark),
        };
        theme.text_format_regexps[&Cow::from(key)]
            .iter()
            .map(|v| TextFormatter {
                regexp: &v.regexp,
                tag: FormatTag {
                    fg: v.fg.as_ref().map(|v| match v {
                        ThemeValue::Link(ref key, ref field) => unlink_fg(theme, field, key),
                        ThemeValue::Alias(ref alias_ident) => {
                            let mut alias_ident = alias_ident;
                            let ret;
                            'fg_alias_loop: loop {
                                match &theme.color_aliases[alias_ident.as_ref()] {
                                    ThemeValue::Link(ref new_key, ref new_field) => {
                                        ret = unlink_fg(theme, new_field, new_key);
                                        break 'fg_alias_loop;
                                    }

                                    ThemeValue::Alias(ref new_alias_ident) => {
                                        alias_ident = new_alias_ident
                                    }
                                    ThemeValue::Value(val) => {
                                        ret = *val;
                                        break 'fg_alias_loop;
                                    }
                                }
                            }
                            ret
                        }
                        ThemeValue::Value(val) => *val,
                    }),
                    bg: v.bg.as_ref().map(|v| match v {
                        ThemeValue::Link(ref key, ref field) => unlink_bg(theme, field, key),
                        ThemeValue::Alias(ref alias_ident) => {
                            let mut alias_ident = alias_ident;
                            let ret;
                            'bg_alias_loop: loop {
                                match &theme.color_aliases[alias_ident.as_ref()] {
                                    ThemeValue::Link(ref new_key, ref new_field) => {
                                        ret = unlink_bg(theme, new_field, new_key);
                                        break 'bg_alias_loop;
                                    }

                                    ThemeValue::Alias(ref new_alias_ident) => {
                                        alias_ident = new_alias_ident
                                    }
                                    ThemeValue::Value(val) => {
                                        ret = *val;
                                        break 'bg_alias_loop;
                                    }
                                }
                            }
                            ret
                        }
                        ThemeValue::Value(val) => *val,
                    }),
                    attrs: v.attrs.as_ref().map(|v| match v {
                        ThemeValue::Link(ref key, ()) => unlink_attrs(theme, key),
                        ThemeValue::Alias(ref alias_ident) => {
                            let mut alias_ident = alias_ident;
                            let ret;
                            'attrs_alias_loop: loop {
                                match &theme.attr_aliases[alias_ident.as_ref()] {
                                    ThemeValue::Link(ref new_key, ()) => {
                                        ret = unlink_attrs(theme, new_key);
                                        break 'attrs_alias_loop;
                                    }
                                    ThemeValue::Alias(ref new_alias_ident) => {
                                        alias_ident = new_alias_ident
                                    }
                                    ThemeValue::Value(val) => {
                                        ret = *val;
                                        break 'attrs_alias_loop;
                                    }
                                }
                            }
                            ret
                        }
                        ThemeValue::Value(val) => *val,
                    }),
                    priority: v.priority,
                },
            })
            .collect()
    }
}

use std::ops::{Deref, DerefMut};
impl Deref for Theme {
    type Target = IndexMap<Cow<'static, str>, ThemeAttributeInner>;
    fn deref(&self) -> &Self::Target {
        &self.keys
    }
}

impl DerefMut for Theme {
    fn deref_mut(&mut self) -> &mut IndexMap<Cow<'static, str>, ThemeAttributeInner> {
        &mut self.keys
    }
}

impl<'de> Deserialize<'de> for Themes {
    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        #[cfg(feature = "regexp")]
        const fn false_val() -> bool {
            false
        }
        #[cfg(feature = "regexp")]
        const fn true_val() -> bool {
            true
        }
        #[derive(Deserialize)]
        struct ThemesOptions {
            #[serde(default)]
            light: ThemeOptions,
            #[serde(default)]
            dark: ThemeOptions,
            #[serde(flatten, default)]
            other_themes: IndexMap<String, ThemeOptions>,
        }
        #[derive(Deserialize, Default)]
        struct ThemeOptions {
            #[serde(default)]
            color_aliases: IndexMap<Cow<'static, str>, ThemeValue<Color>>,
            #[serde(default)]
            attr_aliases: IndexMap<Cow<'static, str>, ThemeValue<Attr>>,
            #[cfg(feature = "regexp")]
            #[serde(default)]
            text_format_regexps: IndexMap<Cow<'static, str>, IndexMap<String, RegexpOptions>>,
            #[serde(flatten, default)]
            keys: IndexMap<Cow<'static, str>, ThemeAttributeInnerOptions>,
        }
        #[cfg(feature = "regexp")]
        #[derive(Deserialize, Default)]
        struct RegexpOptions {
            #[serde(default = "false_val")]
            caseless: bool,
            #[serde(default = "false_val")]
            dotall: bool,
            #[serde(default = "false_val")]
            extended: bool,
            #[serde(default = "false_val")]
            multi_line: bool,
            #[serde(default = "true_val")]
            ucp: bool,
            #[serde(default = "false_val")]
            jit_if_available: bool,
            #[serde(default)]
            priority: u8,
            #[serde(flatten)]
            rest: ThemeAttributeInnerOptions,
        }
        #[derive(Deserialize, Default)]
        #[serde(deny_unknown_fields)]
        struct ThemeAttributeInnerOptions {
            #[serde(default)]
            fg: Option<ThemeValue<Color>>,
            #[serde(default)]
            bg: Option<ThemeValue<Color>>,
            #[serde(default)]
            attrs: Option<ThemeValue<Attr>>,
        }

        let mut ret = Themes::default();
        let mut s = <ThemesOptions>::deserialize(deserializer)?;
        for tk in s.other_themes.keys() {
            ret.other_themes.insert(tk.clone(), ret.dark.clone());
        }

        for (k, v) in ret.light.iter_mut() {
            if let Some(mut att) = s.light.keys.remove(k) {
                if let Some(att) = att.fg.take() {
                    v.fg = att;
                }
                if let Some(att) = att.bg.take() {
                    v.bg = att;
                }
                if let Some(att) = att.attrs.take() {
                    v.attrs = att;
                }
            }
        }
        if !s.light.keys.is_empty() {
            return Err(de::Error::custom(format!(
                "light theme contains unrecognized theme keywords: {}",
                s.light
                    .keys
                    .keys()
                    .into_iter()
                    .map(|k| k.as_ref())
                    .collect::<SmallVec<[_; 128]>>()
                    .join(", ")
            )));
        }
        ret.light.color_aliases = s.light.color_aliases;
        ret.light.attr_aliases = s.light.attr_aliases;
        #[cfg(feature = "regexp")]
        for (k, v) in s.light.text_format_regexps {
            let mut acc = SmallVec::new();
            for (rs, v) in v {
                match RegexpWrapper::new(
                    &rs,
                    v.caseless,
                    v.dotall,
                    v.extended,
                    v.multi_line,
                    v.ucp,
                    v.jit_if_available,
                ) {
                    Ok(regexp) => {
                        acc.push(TextFormatterSetting {
                            regexp,
                            fg: v.rest.fg,
                            bg: v.rest.bg,
                            attrs: v.rest.attrs,
                            priority: v.priority,
                        });
                    }
                    Err(err) => {
                        return Err(de::Error::custom(err.to_string()));
                    }
                }
            }
            ret.light.text_format_regexps.insert(k, acc);
        }
        for (k, v) in ret.dark.iter_mut() {
            if let Some(mut att) = s.dark.keys.remove(k) {
                if let Some(att) = att.fg.take() {
                    v.fg = att;
                }
                if let Some(att) = att.bg.take() {
                    v.bg = att;
                }
                if let Some(att) = att.attrs.take() {
                    v.attrs = att;
                }
            }
        }
        if !s.dark.keys.is_empty() {
            return Err(de::Error::custom(format!(
                "dark theme contains unrecognized theme keywords: {}",
                s.dark
                    .keys
                    .keys()
                    .into_iter()
                    .map(|k| k.as_ref())
                    .collect::<SmallVec<[_; 128]>>()
                    .join(", ")
            )));
        }
        ret.dark.color_aliases = s.dark.color_aliases;
        ret.dark.attr_aliases = s.dark.attr_aliases;
        #[cfg(feature = "regexp")]
        for (k, v) in s.dark.text_format_regexps {
            let mut acc = SmallVec::new();
            for (rs, v) in v {
                match RegexpWrapper::new(
                    &rs,
                    v.caseless,
                    v.dotall,
                    v.extended,
                    v.multi_line,
                    v.ucp,
                    v.jit_if_available,
                ) {
                    Ok(regexp) => {
                        acc.push(TextFormatterSetting {
                            regexp,
                            fg: v.rest.fg,
                            bg: v.rest.bg,
                            attrs: v.rest.attrs,
                            priority: v.priority,
                        });
                    }
                    Err(err) => {
                        return Err(de::Error::custom(err.to_string()));
                    }
                }
            }
            ret.dark.text_format_regexps.insert(k, acc);
        }
        for (tk, t) in ret.other_themes.iter_mut() {
            let mut theme = s.other_themes.remove(tk).unwrap();
            for (k, v) in t.iter_mut() {
                if let Some(mut att) = theme.keys.remove(k) {
                    if let Some(att) = att.fg.take() {
                        v.fg = att;
                    }
                    if let Some(att) = att.bg.take() {
                        v.bg = att;
                    }
                    if let Some(att) = att.attrs.take() {
                        v.attrs = att;
                    }
                }
            }
            if !theme.keys.is_empty() {
                return Err(de::Error::custom(format!(
                    "{} theme contains unrecognized theme keywords: {}",
                    tk,
                    theme
                        .keys
                        .keys()
                        .into_iter()
                        .map(|k| k.as_ref())
                        .collect::<SmallVec<[_; 128]>>()
                        .join(", ")
                )));
            }
            t.color_aliases = theme.color_aliases;
            t.attr_aliases = theme.attr_aliases;
            #[cfg(feature = "regexp")]
            for (k, v) in theme.text_format_regexps {
                let mut acc = SmallVec::new();
                for (rs, v) in v {
                    match RegexpWrapper::new(
                        &rs,
                        v.caseless,
                        v.dotall,
                        v.extended,
                        v.multi_line,
                        v.ucp,
                        v.jit_if_available,
                    ) {
                        Ok(regexp) => {
                            acc.push(TextFormatterSetting {
                                regexp,
                                fg: v.rest.fg,
                                bg: v.rest.bg,
                                attrs: v.rest.attrs,
                                priority: v.priority,
                            });
                        }
                        Err(err) => {
                            return Err(de::Error::custom(err.to_string()));
                        }
                    }
                }
                t.text_format_regexps.insert(k, acc);
            }
        }
        Ok(ret)
    }
}

impl Themes {
    fn validate_keys(name: &str, theme: &Theme, hash_set: &HashSet<&'static str>) -> Result<()> {
        let mut keys = theme
            .keys()
            .filter_map(|k| {
                if !hash_set.contains(&k.as_ref()) {
                    Some((None, "key", "invalid key", k.as_ref()))
                } else {
                    None
                }
            })
            .chain(theme.color_aliases.iter().filter_map(|(key, a)| match a {
                ThemeValue::Link(ref r, ref field) => {
                    if !hash_set.contains(&r.as_ref()) {
                        Some((
                            Some(key),
                            match field {
                                ColorField::LikeSelf => "Color alias link",
                                ColorField::Fg => "Color alias fg link",
                                ColorField::Bg => "Color alias bg link",
                            },
                            "invalid key",
                            r.as_ref(),
                        ))
                    } else {
                        None
                    }
                }
                ThemeValue::Alias(ref ident) => {
                    if !theme.color_aliases.contains_key(ident.as_ref()) {
                        Some((Some(key), "alias", "nonexistant color alias", ident))
                    } else {
                        None
                    }
                }
                _ => None,
            }))
            .chain(theme.attr_aliases.iter().filter_map(|(key, a)| match a {
                ThemeValue::Link(ref r, ()) => {
                    if !hash_set.contains(&r.as_ref()) {
                        Some((Some(key), "Attr alias link", "invalid key", r.as_ref()))
                    } else {
                        None
                    }
                }
                ThemeValue::Alias(ref ident) => {
                    if !theme.attr_aliases.contains_key(ident.as_ref()) {
                        Some((Some(key), "alias", "nonexistant color alias", ident))
                    } else {
                        None
                    }
                }
                _ => None,
            }))
            .chain(theme.iter().filter_map(|(key, a)| {
                if let ThemeValue::Link(ref r, _) = a.fg {
                    if !hash_set.contains(&r.as_ref()) {
                        Some((Some(key), "fg link", "invalid key", r.as_ref()))
                    } else {
                        None
                    }
                } else if let ThemeValue::Alias(ref ident) = a.fg {
                    if !theme.color_aliases.contains_key(ident.as_ref()) {
                        Some((Some(key), "fg alias", "nonexistant color alias", ident))
                    } else {
                        None
                    }
                } else {
                    None
                }
            }))
            .chain(theme.iter().filter_map(|(key, a)| {
                if let ThemeValue::Link(ref r, _) = a.bg {
                    if !hash_set.contains(&r.as_ref()) {
                        Some((Some(key), "bg link", "invalid key", r.as_ref()))
                    } else {
                        None
                    }
                } else if let ThemeValue::Alias(ref ident) = a.bg {
                    if !theme.color_aliases.contains_key(ident.as_ref()) {
                        Some((Some(key), "bg alias", "nonexistant color alias", ident))
                    } else {
                        None
                    }
                } else {
                    None
                }
            }))
            .chain(theme.iter().filter_map(|(key, a)| {
                if let ThemeValue::Link(ref r, _) = a.attrs {
                    if !hash_set.contains(&r.as_ref()) {
                        Some((Some(key), "attrs link", "invalid key", r.as_ref()))
                    } else {
                        None
                    }
                } else if let ThemeValue::Alias(ref ident) = a.attrs {
                    if !theme.attr_aliases.contains_key(ident.as_ref()) {
                        Some((
                            Some(key),
                            "attrs alias",
                            "nonexistant text attribute alias",
                            ident,
                        ))
                    } else {
                        None
                    }
                } else {
                    None
                }
            }))
            .collect::<SmallVec<[(Option<_>, &'_ str, &'_ str, &'_ str); 128]>>();
        #[cfg(feature = "regexp")]
        {
            for (key, v) in &theme.text_format_regexps {
                if !regexp::DEFAULT_TEXT_FORMATTER_KEYS.contains(&key.as_ref()) {
                    keys.push((
                        None,
                        "key",
                        "invalid key in `text_format_regexps`",
                        key.as_ref(),
                    ));
                } else {
                    for tfs in v {
                        if let Some(fg) = &tfs.fg {
                            if let ThemeValue::Link(ref r, _) = fg {
                                if !hash_set.contains(&r.as_ref()) {
                                    keys.push((
                                        Some(key),
                                        "fg link",
                                        "invalid key in `text_format_regexps`",
                                        r.as_ref(),
                                    ));
                                }
                            } else if let ThemeValue::Alias(ref ident) = fg {
                                if !theme.color_aliases.contains_key(ident.as_ref()) {
                                    keys.push((
                                        Some(key),
                                        "fg alias",
                                        "nonexistant color alias in `text_format_regexps`",
                                        ident,
                                    ));
                                }
                            }
                        }
                        if let Some(bg) = &tfs.bg {
                            if let ThemeValue::Link(ref r, _) = bg {
                                if !hash_set.contains(&r.as_ref()) {
                                    keys.push((
                                        Some(key),
                                        "bg link",
                                        "invalid key in `text_format_regexps`",
                                        r.as_ref(),
                                    ));
                                }
                            } else if let ThemeValue::Alias(ref ident) = bg {
                                if !theme.color_aliases.contains_key(ident.as_ref()) {
                                    keys.push((
                                        Some(key),
                                        "bg alias",
                                        "nonexistant color alias in `text_format_regexps`",
                                        ident,
                                    ));
                                }
                            }
                        }
                        if let Some(attrs) = &tfs.attrs {
                            if let ThemeValue::Link(ref r, _) = attrs {
                                if !hash_set.contains(&r.as_ref()) {
                                    keys.push((
                                        Some(key),
                                        "attrs link",
                                        "invalid key in `text_format_regexps`",
                                        r.as_ref(),
                                    ));
                                }
                            } else if let ThemeValue::Alias(ref ident) = attrs {
                                if !theme.attr_aliases.contains_key(ident.as_ref()) {
                                    keys.push((
                                        Some(key),
                                        "attrs alias",
                                        "nonexistant text attribute alias in `text_format_regexps`",
                                        ident,
                                    ));
                                }
                            }
                        }
                    }
                }
            }
        }

        if !keys.is_empty() {
            return Err(format!(
                "{} theme contains invalid data: {}",
                name,
                keys.into_iter()
                    .map(|(key_opt, desc, kind, link)| if let Some(key) = key_opt {
                        format!("{} {}: {} \"{}\"", key, desc, kind, link)
                    } else {
                        format!("{}: {} \"{}\"", desc, kind, link)
                    })
                    .collect::<SmallVec<[String; 128]>>()
                    .join(", ")
            )
            .into());
        }
        Ok(())
    }
    pub fn validate(&self) -> Result<()> {
        let hash_set: HashSet<&'static str> = DEFAULT_KEYS.into_iter().map(|k| *k).collect();
        Themes::validate_keys("light", &self.light, &hash_set)?;
        Themes::validate_keys("dark", &self.dark, &hash_set)?;
        for (name, t) in self.other_themes.iter() {
            Themes::validate_keys(name, t, &hash_set)?;
        }
        if let Err(err) = is_cyclic(&self.light) {
            return Err(MeliError::new(format!(
                "light theme contains a cycle: {}",
                err
            )));
        }
        if let Err(err) = is_cyclic(&self.dark) {
            return Err(MeliError::new(format!(
                "dark theme contains a cycle: {}",
                err
            )));
        }
        for (k, t) in self.other_themes.iter() {
            if let Err(err) = is_cyclic(t) {
                return Err(MeliError::new(format!(
                    "{} theme contains a cycle: {}",
                    k, err
                )));
            }
        }
        Ok(())
    }

    pub fn key_to_string(&self, key: &str, unlink: bool) -> String {
        let theme = match key {
            "light" => &self.light,
            "dark" => &self.dark,
            t => self.other_themes.get(t).unwrap_or(&self.dark),
        };
        let mut ret = String::new();
        ret.extend(format!("[terminal.themes.{}]\n", key).chars());
        if unlink {
            for k in theme.keys() {
                ret.extend(
                    format!(
                        "\"{}\" = {{ fg = {}, bg = {}, attrs = {} }}\n",
                        k,
                        toml::to_string(&unlink_fg(&theme, &ColorField::Fg, k)).unwrap(),
                        toml::to_string(&unlink_bg(&theme, &ColorField::Bg, k)).unwrap(),
                        toml::to_string(&unlink_attrs(&theme, k)).unwrap(),
                    )
                    .chars(),
                );
            }
        } else {
            for k in theme.keys() {
                ret.extend(
                    format!(
                        "\"{}\" = {{ fg = {}, bg = {}, attrs = {} }}\n",
                        k,
                        toml::to_string(&theme[k].fg).unwrap(),
                        toml::to_string(&theme[k].bg).unwrap(),
                        toml::to_string(&theme[k].attrs).unwrap(),
                    )
                    .chars(),
                );
            }
        }
        ret
    }
    pub fn to_string(&self) -> String {
        let mut ret = String::new();
        ret.extend(self.key_to_string("dark", true).chars());

        ret.push_str("\n\n");
        ret.extend(self.key_to_string("light", true).chars());
        for name in self.other_themes.keys() {
            ret.push_str("\n\n");
            ret.extend(self.key_to_string(name, true).chars());
        }
        ret
    }
}

impl Default for Themes {
    fn default() -> Themes {
        let mut light = IndexMap::default();
        let mut dark = IndexMap::default();
        let other_themes = IndexMap::default();

        macro_rules! add {
            ($key:literal, $($theme:ident={ $($name:ident : $val:expr),*$(,)? }),*$(,)?) => {
                add!($key);
                $($theme.insert($key.into(), ThemeAttributeInner {
                    $($name: $val.into()),*
                        ,..ThemeAttributeInner::default() }));*
            };
            ($key:literal) => {
                light.insert($key.into(), ThemeAttributeInner::default());
                dark.insert($key.into(), ThemeAttributeInner::default());
            };
        }
        add!("theme_default", dark = { fg: Color::Default, bg: Color::Default, attrs: Attr::DEFAULT }, light = { fg: Color::Default, bg: Color::Default, attrs: Attr::DEFAULT });
        add!("status.bar", dark = { fg: Color::Byte(123), bg: Color::Byte(26) }, light = { fg: Color::Byte(123), bg: Color::Byte(26) });
        add!("status.notification", dark = { fg: Color::Byte(219), bg: Color::Byte(88) }, light = { fg: Color::Byte(219), bg: Color::Byte(88) });

        add!("tab.focused");
        add!("tab.unfocused", dark = { fg: Color::Byte(15), bg: Color::Byte(8), }, light = { fg: Color::Byte(15), bg: Color::Byte(8), });
        add!("tab.bar");
        add!(
            "widgets.list.header",
            dark = { fg: Color::Black, bg: Color::White, attrs: Attr::BOLD },
            light = {fg: Color::White, bg: Color::Black, attrs: Attr::BOLD }
        );
        add!(
            "widgets.form.label",
            dark = { attrs: Attr::BOLD },
            light = { attrs: Attr::BOLD }
        );
        add!("widgets.form.field");
        add!("widgets.form.highlighted", light = { bg: Color::Byte(246) }, dark = { bg: Color::Byte(246) });
        add!("widgets.options.highlighted", light = { bg: Color::Byte(8) }, dark = { bg: Color::Byte(8) });

        /* Mail Sidebar */

        add!("mail.sidebar");
        add!(
            "mail.sidebar_account_name",
            dark = {
                fg: "mail.sidebar",
                bg: "mail.sidebar",
                attrs: Attr::BOLD,
            },
            light = {
                fg: "mail.sidebar",
                bg: "mail.sidebar",
                attrs: Attr::BOLD,
            }
        );
        add!("mail.sidebar_unread_count", dark = { fg: Color::Byte(243) });
        add!("mail.sidebar_index", dark = { fg: Color::Byte(243) });
        add!("mail.sidebar_highlighted", dark = { fg: Color::Byte(233), bg: Color::Byte(15) });
        add!(
            "mail.sidebar_highlighted_unread_count",
            light = {
                fg: "mail.sidebar_highlighted",
                bg: "mail.sidebar_highlighted"
            },
            dark = {
                fg: "mail.sidebar_highlighted",
                bg: "mail.sidebar_highlighted"
            }
        );
        add!(
            "mail.sidebar_highlighted_index",
            light = {
                fg: "mail.sidebar_index",
                bg: "mail.sidebar_highlighted",
            },
            dark = {
                fg: "mail.sidebar_index",
                bg: "mail.sidebar_highlighted",
            },
        );
        add!(
            "mail.sidebar_highlighted_account",
            dark = {
                fg: Color::Byte(15),
                bg: Color::Byte(233),
            }
        );
        add!(
            "mail.sidebar_highlighted_account_name",
            dark = {
                fg: "mail.sidebar_highlighted_account",
                bg: "mail.sidebar_highlighted_account",
                attrs: Attr::BOLD,
            },
            light = {
                fg: "mail.sidebar_highlighted_account",
                bg: "mail.sidebar_highlighted_account",
                attrs: Attr::BOLD,
            }
        );
        add!(
            "mail.sidebar_highlighted_account_unread_count",
            light = {
                fg: "mail.sidebar_unread_count",
                bg: "mail.sidebar_highlighted_account",
            },
            dark = {
                fg: "mail.sidebar_unread_count",
                bg: "mail.sidebar_highlighted_account"
            }
        );
        add!(
            "mail.sidebar_highlighted_account_index",
            light = {
                fg: "mail.sidebar_index",
                bg: "mail.sidebar_highlighted_account"
            },
            dark = {
                fg: "mail.sidebar_index",
                bg: "mail.sidebar_highlighted_account"
            }
        );

        /* CompactListing */
        add!("mail.listing.compact.even",
            dark = {
                bg: Color::Byte(236)
            },
            light = {
                bg: Color::Byte(252)
            }
        );
        add!("mail.listing.compact.odd");
        add!(
            "mail.listing.compact.even_unseen",
            dark = {
                fg: Color::Byte(0),
                bg: Color::Byte(251)
            },
            light = {
                fg: Color::Byte(0),
                bg: Color::Byte(251)
            }
        );
        add!(
            "mail.listing.compact.odd_unseen",
            dark = {
                fg: Color::Byte(0),
                bg: Color::Byte(251)
            },
            light = {
                fg: Color::Byte(0),
                bg: Color::Byte(251)
            }
        );
        add!("mail.listing.compact.even_selected",
            dark = {
                bg: Color::Byte(210)
            },
            light = {
                bg: Color::Byte(210)
            }
        );
        add!("mail.listing.compact.odd_selected",
            dark = {
                bg: Color::Byte(210)
            },
            light = {
                bg: Color::Byte(210)
            }
        );
        add!(
            "mail.listing.compact.even_highlighted",
            dark = {
                bg: Color::Byte(246)
            },
            light = {
                bg: Color::Byte(244)
            }
        );
        add!(
            "mail.listing.compact.odd_highlighted",
            dark = {
                bg: Color::Byte(246)
            },
            light = {
                bg: Color::Byte(244)
            }
        );

        /* ConversationsListing */

        add!("mail.listing.conversations");
        add!("mail.listing.conversations.subject");
        add!("mail.listing.conversations.from");
        add!("mail.listing.conversations.date");
        add!(
            "mail.listing.conversations.padding",
            dark = {
                fg: Color::Byte(235),
                bg: Color::Byte(235),
            },
            light = {
                fg: Color::Byte(254),
                bg: Color::Byte(254),
            }
        );
        add!(
            "mail.listing.conversations.unseen_padding",
            dark = {
                fg: Color::Byte(235),
                bg: Color::Byte(235),
            },
            light = {
                fg: Color::Byte(254),
                bg: Color::Byte(254),
            }
        );
        add!(
            "mail.listing.conversations.unseen",
            dark = {
                fg: Color::Byte(0),
                bg: Color::Byte(251)
            },
            light = {
                fg: Color::Byte(0),
                bg: Color::Byte(251)
            }
        );
        add!(
            "mail.listing.conversations.highlighted",
            dark = {
                bg: Color::Byte(246),
                attrs: Attr::BOLD,
            },
            light = {
                bg: Color::Byte(246),
                attrs: Attr::BOLD,
            }
        );
        add!("mail.listing.conversations.selected",
            dark = {
                bg: Color::Byte(210),
            },
            light = {
                bg: Color::Byte(210)
            }
        );

        /* PlainListing */
        add!("mail.listing.plain.even",
            dark = {
                bg: Color::Byte(236)
            },
            light = {
                bg: Color::Byte(252)
            }
        );
        add!("mail.listing.plain.odd");
        add!(
            "mail.listing.plain.even_unseen",
            dark = {
                fg: Color::Byte(0),
                bg: Color::Byte(251)

            },
            light = {
                fg: Color::Byte(0),
                bg: Color::Byte(251)
            }
        );
        add!(
            "mail.listing.plain.odd_unseen",
            dark = {
                fg: Color::Byte(0),
                bg: Color::Byte(251)

            },
            light = {
                fg: Color::Byte(0),
                bg: Color::Byte(251)
            }
        );
        add!("mail.listing.plain.even_selected",
            dark = {
                bg: Color::Byte(210)
            },
            light = {
                bg: Color::Byte(210)
            }
        );
        add!("mail.listing.plain.odd_selected",
            dark = {
                bg: Color::Byte(210)
            },
            light = {
                bg: Color::Byte(210)
            }
        );
        add!(
            "mail.listing.plain.even_highlighted",
            dark = {
                bg: Color::Byte(246)
            },
            light = {
                bg: Color::Byte(244)
            }
        );
        add!(
            "mail.listing.plain.odd_highlighted",
            dark = {
                bg: Color::Byte(246)
            },
            light = {
                bg: Color::Byte(244)
            }
        );

        add!(
            "mail.view.headers",
            dark = {
                fg: Color::Byte(33),
            },
            light = {
                fg: Color::Black,
            }
        );
        add!("mail.view.body");
        add!("mail.view.thread.indentation.a", light = { bg: Color::Byte(69) }, dark = { bg: Color::Byte(69) }); // CornflowerBlue
        add!("mail.view.thread.indentation.b", light = { bg: Color::Byte(196) }, dark = { bg: Color::Byte(196) }); // Red1
        add!("mail.view.thread.indentation.c", light = { bg: Color::Byte(175) }, dark = { bg: Color::Byte(175) }); // Pink3
        add!("mail.view.thread.indentation.d", light = { bg: Color::Byte(220) }, dark = { bg: Color::Byte(220) }); // Gold1
        add!("mail.view.thread.indentation.e", light = { bg: Color::Byte(172) }, dark = { bg: Color::Byte(172) }); // Orange3
        add!("mail.view.thread.indentation.f", light = { bg: Color::Byte(72) }, dark = { bg: Color::Byte(72) }); // CadetBlue

        add!(
            "mail.listing.attachment_flag",
            light = {
                fg: Color::Byte(103),
            },
            dark = {
                fg: Color::Byte(103)
            }
        );

        add!(
            "mail.listing.thread_snooze_flag",
            light = {
                fg: Color::Red,
            },
            dark = {
                fg: Color::Red,
            }
        );

        add!(
            "mail.listing.tag_default",
            light = {
                fg: Color::White,
                bg: Color::Byte(8),
                attrs: Attr::BOLD
            },
            dark = {
                fg: Color::White,
                bg: Color::Byte(8),
                attrs: Attr::BOLD
            }
        );

        add!("pager.highlight_search", light = { fg: Color::White, bg: Color::Byte(6) /* Teal */, attrs: Attr::BOLD }, dark = { fg: Color::White, bg: Color::Byte(6) /* Teal */, attrs: Attr::BOLD });
        add!("pager.highlight_search_current", light = { fg: Color::White, bg: Color::Byte(17) /* NavyBlue */, attrs: Attr::BOLD }, dark = { fg: Color::White, bg: Color::Byte(17) /* NavyBlue */, attrs: Attr::BOLD });
        Themes {
            light: Theme {
                keys: light,
                attr_aliases: Default::default(),
                color_aliases: Default::default(),
                #[cfg(feature = "regexp")]
                text_format_regexps: DEFAULT_TEXT_FORMATTER_KEYS
                    .iter()
                    .map(|&k| (k.into(), SmallVec::new()))
                    .collect(),
            },
            dark: Theme {
                keys: dark,
                attr_aliases: Default::default(),
                color_aliases: Default::default(),
                #[cfg(feature = "regexp")]
                text_format_regexps: DEFAULT_TEXT_FORMATTER_KEYS
                    .iter()
                    .map(|&k| (k.into(), SmallVec::new()))
                    .collect(),
            },
            other_themes,
        }
    }
}

impl Serialize for Themes {
    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let mut dark: IndexMap<Cow<'static, str>, ThemeAttribute> = Default::default();
        let mut light: IndexMap<Cow<'static, str>, ThemeAttribute> = Default::default();
        let mut other_themes: IndexMap<String, _> = Default::default();

        for k in self.dark.keys() {
            dark.insert(
                k.clone(),
                ThemeAttribute {
                    fg: unlink_fg(&self.dark, &ColorField::Fg, k),
                    bg: unlink_bg(&self.dark, &ColorField::Bg, k),
                    attrs: unlink_attrs(&self.dark, k),
                },
            );
        }

        for k in self.light.keys() {
            light.insert(
                k.clone(),
                ThemeAttribute {
                    fg: unlink_fg(&self.light, &ColorField::Fg, k),
                    bg: unlink_bg(&self.light, &ColorField::Bg, k),
                    attrs: unlink_attrs(&self.light, k),
                },
            );
        }

        for (name, t) in self.other_themes.iter() {
            let mut new_map: IndexMap<Cow<'static, str>, ThemeAttribute> = Default::default();

            for k in t.keys() {
                new_map.insert(
                    k.clone(),
                    ThemeAttribute {
                        fg: unlink_fg(&t, &ColorField::Fg, k),
                        bg: unlink_bg(&t, &ColorField::Bg, k),
                        attrs: unlink_attrs(&t, k),
                    },
                );
            }
            other_themes.insert(name.to_string(), new_map);
        }

        other_themes.insert("light".to_string(), light);
        other_themes.insert("dark".to_string(), dark);
        other_themes.serialize(serializer)
    }
}

/* Check Theme linked values for cycles */
fn is_cyclic(theme: &Theme) -> std::result::Result<(), String> {
    #[derive(Hash, Copy, Clone, PartialEq, Eq)]
    enum Course {
        Fg,
        Bg,
        Attrs,
        ColorAliasFg,
        ColorAliasBg,
        AttrAlias,
    }
    fn is_cyclic_util<'a>(
        course: Course,
        k: &'a Cow<'static, str>,
        visited: &mut IndexMap<(&'a Cow<'static, str>, Course), bool>,
        stack: &mut IndexMap<(&'a Cow<'static, str>, Course), bool>,
        path: &mut SmallVec<[(&'a Cow<'static, str>, Course); 16]>,
        theme: &'a Theme,
    ) -> bool {
        if !visited[&(k, course)] {
            visited.entry((k, course)).and_modify(|e| *e = true);
            stack.entry((k, course)).and_modify(|e| *e = true);

            match course {
                Course::Fg => match theme[k].fg {
                    ThemeValue::Link(ref l, ColorField::LikeSelf)
                    | ThemeValue::Link(ref l, ColorField::Fg) => {
                        path.push((l, Course::Fg));
                        if (!visited[&(l, Course::Fg)]
                            && is_cyclic_util(course, l, visited, stack, path, theme))
                            || stack[&(l, Course::Fg)]
                        {
                            return true;
                        }
                        path.pop();
                    }
                    ThemeValue::Link(ref l, ColorField::Bg) => {
                        path.push((l, Course::Bg));
                        if (!visited[&(l, Course::Bg)]
                            && is_cyclic_util(Course::Bg, l, visited, stack, path, theme))
                            || stack[&(l, Course::Bg)]
                        {
                            return true;
                        }
                        path.pop();
                    }
                    ThemeValue::Alias(ref ident) => {
                        path.push((ident, Course::ColorAliasFg));
                        if (!visited[&(ident, Course::ColorAliasFg)]
                            && is_cyclic_util(
                                Course::ColorAliasFg,
                                ident,
                                visited,
                                stack,
                                path,
                                theme,
                            ))
                            || stack[&(ident, Course::ColorAliasFg)]
                        {
                            return true;
                        }
                        path.pop();
                    }
                    _ => {}
                },
                Course::Bg => match theme[k].bg {
                    ThemeValue::Link(ref l, ColorField::LikeSelf)
                    | ThemeValue::Link(ref l, ColorField::Bg) => {
                        path.push((l, Course::Bg));
                        if (!visited[&(l, Course::Bg)]
                            && is_cyclic_util(Course::Bg, l, visited, stack, path, theme))
                            || stack[&(l, Course::Bg)]
                        {
                            return true;
                        }
                        path.pop();
                    }
                    ThemeValue::Link(ref l, ColorField::Fg) => {
                        path.push((l, Course::Fg));
                        if (!visited[&(l, Course::Fg)]
                            && is_cyclic_util(Course::Fg, l, visited, stack, path, theme))
                            || stack[&(l, Course::Fg)]
                        {
                            return true;
                        }
                        path.pop();
                    }
                    ThemeValue::Alias(ref ident) => {
                        path.push((ident, Course::ColorAliasBg));
                        if (!visited[&(ident, Course::ColorAliasBg)]
                            && is_cyclic_util(
                                Course::ColorAliasBg,
                                ident,
                                visited,
                                stack,
                                path,
                                theme,
                            ))
                            || stack[&(ident, Course::ColorAliasBg)]
                        {
                            return true;
                        }
                        path.pop();
                    }
                    _ => {}
                },
                Course::Attrs => match theme[k].attrs {
                    ThemeValue::Link(ref l, _) => {
                        path.push((l, course));
                        if (!visited[&(l, course)]
                            && is_cyclic_util(course, l, visited, stack, path, theme))
                            || stack[&(l, course)]
                        {
                            return true;
                        }
                        path.pop();
                    }
                    ThemeValue::Alias(ref ident) => {
                        path.push((ident, Course::AttrAlias));
                        if (!visited[&(ident, Course::AttrAlias)]
                            && is_cyclic_util(
                                Course::AttrAlias,
                                ident,
                                visited,
                                stack,
                                path,
                                theme,
                            ))
                            || stack[&(ident, Course::AttrAlias)]
                        {
                            return true;
                        }
                        path.pop();
                    }
                    _ => {}
                },
                Course::ColorAliasFg | Course::ColorAliasBg => match &theme.color_aliases[k] {
                    ThemeValue::Link(ref l, ref field) => {
                        let course = match (course, field) {
                            (Course::ColorAliasFg, ColorField::LikeSelf) => Course::Fg,
                            (Course::ColorAliasBg, ColorField::LikeSelf) => Course::Bg,
                            (_, ColorField::LikeSelf) => unsafe {
                                std::hint::unreachable_unchecked()
                            },
                            (_, ColorField::Fg) => Course::Fg,
                            (_, ColorField::Bg) => Course::Bg,
                        };
                        path.push((l, course));
                        if (!visited[&(l, course)]
                            && is_cyclic_util(course, l, visited, stack, path, theme))
                            || stack[&(l, course)]
                        {
                            return true;
                        }
                        path.pop();
                    }
                    ThemeValue::Alias(ref ident) => {
                        path.push((ident, course));
                        if (!visited[&(ident, course)]
                            && is_cyclic_util(course, ident, visited, stack, path, theme))
                            || stack[&(ident, course)]
                        {
                            return true;
                        }
                        path.pop();
                    }
                    _ => {}
                },
                Course::AttrAlias => match &theme.attr_aliases[k] {
                    ThemeValue::Link(ref l, ()) => {
                        path.push((l, Course::Attrs));
                        if (!visited[&(l, Course::Attrs)]
                            && is_cyclic_util(Course::Attrs, l, visited, stack, path, theme))
                            || stack[&(l, Course::Attrs)]
                        {
                            return true;
                        }
                        path.pop();
                    }
                    ThemeValue::Alias(ref ident) => {
                        path.push((ident, course));
                        if (!visited[&(ident, course)]
                            && is_cyclic_util(course, ident, visited, stack, path, theme))
                            || stack[&(ident, course)]
                        {
                            return true;
                        }
                        path.pop();
                    }
                    _ => {}
                },
            }
        }
        stack.entry((k, course)).and_modify(|e| *e = false);
        false
    }

    let mut path = SmallVec::new();
    let mut visited = theme
        .keys()
        .map(|k| {
            std::iter::once(((k, Course::Fg), false))
                .chain(std::iter::once(((k, Course::Bg), false)))
                .chain(std::iter::once(((k, Course::Attrs), false)))
        })
        .flatten()
        .chain(
            theme
                .color_aliases
                .keys()
                .map(|k| {
                    std::iter::once(((k, Course::ColorAliasFg), false))
                        .chain(std::iter::once(((k, Course::ColorAliasBg), false)))
                })
                .flatten(),
        )
        .chain(
            theme
                .attr_aliases
                .keys()
                .map(|k| ((k, Course::AttrAlias), false)),
        )
        .collect::<IndexMap<(&Cow<'static, str>, Course), bool>>();

    let mut stack = visited.clone();
    for k in theme.keys() {
        for &course in [Course::Fg, Course::Bg, Course::Attrs].iter() {
            path.push((k, course));
            if is_cyclic_util(course, k, &mut visited, &mut stack, &mut path, &theme) {
                let path = path
                    .into_iter()
                    .map(|(k, c)| match c {
                        Course::Fg => format!("{}.fg", k,),
                        Course::Bg => format!("{}.fg", k,),
                        Course::Attrs => format!("{}.attrs", k,),
                        Course::ColorAliasFg => format!("(Color fg) ${}", k),
                        Course::ColorAliasBg => format!("(Color bg) ${}", k),
                        Course::AttrAlias => format!("(Attr) ${}", k),
                    })
                    .collect::<Vec<String>>();
                return Err(format!(
                    "{} {}",
                    match course {
                        Course::Fg => "fg: ",
                        Course::Bg => "bg: ",
                        Course::Attrs => "attrs: ",
                        Course::ColorAliasFg => "color alias fg: ",
                        Course::ColorAliasBg => "color alias bg: ",
                        Course::AttrAlias => "attribute alias: ",
                    },
                    path.join(" -> ")
                ));
            }
            for v in visited.values_mut() {
                *v = false;
            }
            path.pop();
        }
    }

    return Ok(());
}

#[test]
fn test_theme_parsing() {
    /* MUST SUCCEED: default themes should be valid */
    let def = Themes::default();
    assert!(def.validate().is_ok());
    /* MUST SUCCEED: new user theme `hunter2`, theme `dark` has user redefinitions */
    const TEST_STR: &str = r##"[dark]
"mail.listing.tag_default" = { fg = "White", bg = "HotPink3" }
"mail.listing.attachment_flag" = { fg = "mail.listing.tag_default.bg" }
"mail.view.headers" = { bg = "mail.listing.tag_default.fg" }

["hunter2"]
"mail.view.body" = { fg = "Black", bg = "White"}"##;
    let parsed: Themes = toml::from_str(TEST_STR).unwrap();
    assert!(parsed.other_themes.contains_key("hunter2"));
    assert_eq!(
        unlink_bg(
            &parsed.dark,
            &ColorField::Bg,
            &Cow::from("mail.listing.tag_default")
        ),
        Color::Byte(132)
    );
    assert_eq!(
        unlink_fg(
            &parsed.dark,
            &ColorField::Fg,
            &Cow::from("mail.listing.attachment_flag")
        ),
        Color::Byte(132)
    );
    assert_eq!(
        unlink_bg(
            &parsed.dark,
            &ColorField::Bg,
            &Cow::from("mail.view.headers")
        ),
        Color::Byte(15), // White
    );
    assert!(parsed.validate().is_ok());
    /* MUST FAIL: theme `dark` contains a cycle */
    const HAS_CYCLE: &str = r##"[dark]
"mail.listing.compact.even" = { fg = "mail.listing.compact.odd" }
"mail.listing.compact.odd" = { fg = "mail.listing.compact.even" }
"##;
    let parsed: Themes = toml::from_str(HAS_CYCLE).unwrap();
    assert!(parsed.validate().is_err());
    /* MUST FAIL: theme `dark` contains an invalid key */
    const HAS_INVALID_KEYS: &str = r##"[dark]
"asdfsafsa" = { fg = "Black" }
"##;
    let parsed: std::result::Result<Themes, _> = toml::from_str(HAS_INVALID_KEYS);
    assert!(parsed.is_err());
    /* MUST SUCCEED: alias $Jebediah resolves to a valid color */
    const TEST_ALIAS_STR: &str = r##"[dark]
color_aliases= { "Jebediah" = "#b4da55" }
"mail.listing.tag_default" = { fg = "$Jebediah" }
"##;
    let parsed: Themes = toml::from_str(TEST_ALIAS_STR).unwrap();
    assert!(parsed.validate().is_ok());
    assert_eq!(
        unlink_fg(
            &parsed.dark,
            &ColorField::Fg,
            &Cow::from("mail.listing.tag_default")
        ),
        Color::Rgb(180, 218, 85)
    );
    /* MUST FAIL: Mispell color alias $Jebediah as $Jebedia */
    const TEST_INVALID_ALIAS_STR: &str = r##"[dark]
color_aliases= { "Jebediah" = "#b4da55" }
"mail.listing.tag_default" = { fg = "$Jebedia" }
"##;
    let parsed: Themes = toml::from_str(TEST_INVALID_ALIAS_STR).unwrap();
    assert!(parsed.validate().is_err());
    /* MUST FAIL: Color alias $Jebediah is defined as itself */
    const TEST_CYCLIC_ALIAS_STR: &str = r##"[dark]
color_aliases= { "Jebediah" = "$Jebediah" }
"mail.listing.tag_default" = { fg = "$Jebediah" }
"##;
    let parsed: Themes = toml::from_str(TEST_CYCLIC_ALIAS_STR).unwrap();
    assert!(parsed.validate().is_err());
    /* MUST FAIL: Attr alias $Jebediah is defined as itself */
    const TEST_CYCLIC_ALIAS_ATTR_STR: &str = r##"[dark]
attr_aliases= { "Jebediah" = "$Jebediah" }
"mail.listing.tag_default" = { attrs = "$Jebediah" }
"##;
    let parsed: Themes = toml::from_str(TEST_CYCLIC_ALIAS_ATTR_STR).unwrap();
    assert!(parsed.validate().is_err());
    /* MUST FAIL: alias $Jebediah resolves to a cycle */
    const TEST_CYCLIC_ALIAS_STR_2: &str = r##"[dark]
color_aliases= { "Jebediah" = "$JebediahJr", "JebediahJr" = "mail.listing.tag_default" }
"mail.listing.tag_default" = { fg = "$Jebediah" }
"##;
    let parsed: Themes = toml::from_str(TEST_CYCLIC_ALIAS_STR_2).unwrap();
    assert!(parsed.validate().is_err());
    /* MUST SUCCEED: alias $Jebediah resolves to a key's field */
    const TEST_CYCLIC_ALIAS_STR_3: &str = r##"[dark]
color_aliases= { "Jebediah" = "$JebediahJr", "JebediahJr" = "mail.listing.tag_default.bg" }
"mail.listing.tag_default" = { fg = "$Jebediah", bg = "Black" }
"##;
    let parsed: Themes = toml::from_str(TEST_CYCLIC_ALIAS_STR_3).unwrap();
    assert!(parsed.validate().is_ok());
    /* MUST FAIL: alias $Jebediah resolves to an invalid key */
    const TEST_INVALID_LINK_KEY_FIELD_STR: &str = r##"[dark]
color_aliases= { "Jebediah" = "$JebediahJr", "JebediahJr" = "mail.listing.tag_default.attrs" }
"mail.listing.tag_default" = { fg = "$Jebediah", bg = "Black" }
"##;
    let parsed: Themes = toml::from_str(TEST_INVALID_LINK_KEY_FIELD_STR).unwrap();
    assert!(parsed.validate().is_err());
}
