use serde::{Deserialize, Serialize};

/// This is a small support module that contains some of the default values
/// for [`Config`]. The reason they are functions is to feed them to [`serde`]
/// but they are also used in [`Config::default()`] for consistency
mod defaults {
    pub fn title() -> String {
        "Rubedo".into()
    }

    pub fn language() -> String {
        "en-US".into()
    }

    pub fn output_dir() -> String {
        "public".into()
    }

    pub fn static_dir() -> String {
        "static".into()
    }
}

/// Represents the `config.toml` file of the website
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
    /// Optional base URL of the website
    pub base_url: Option<String>,

    /// The title of the website
    #[serde(default = "defaults::title")]
    pub title: String,

    /// ISO language code; examples: en-US; en-GB; it; fr
    #[serde(default = "defaults::language")]
    pub language: String,

    /// Array of strings, each representing an author of the website
    #[serde(default)]
    pub authors: Vec<String>,

    /// Directory name of the theme to use, relative to the themes folder
    /// inside the website's base path
    #[serde(default)]
    pub themes: Option<Vec<String>>,

    /// Path relative to the website base path where the generated website
    /// content along with all static assets will be written
    #[serde(default = "defaults::output_dir")]
    pub output_dir: String,

    /// Path relative to the website base path to store the static data that
    /// will be copied over to the `output_dir`
    #[serde(default = "defaults::static_dir")]
    pub static_dir: String,

    /// File path relative to the website base path pointing to a `.tmTheme` file
    /// representing the definition of a syntax highlighting theme compatible
    /// with [syntect](https://github.com/trishume/syntect). Leave unset to
    /// disable syntax highlighting
    pub codeblock_theme: Option<String>,

    /// Setting this to true will generate ids for markdown headings and add
    /// anchor tags before the text. These will be invisible by default and
    /// need to be configured by the theme's CSS
    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
    pub heading_ids: bool,

    /// Whether to generate additional pages to allow navigation to a page
    /// without specifying the file extension. This is accomplished by creating
    /// directories with the path of the extensionless page path, then
    /// generating pages called `index.{extension}` inside them
    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
    pub extensionless_pages: bool,

    /// Whether to allow inline HTML inside markdown files. Depending on your
    /// usecase this might or might not be dangerous. With this option enabled,
    /// never generate content automatically from untrusted sources.
    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
    pub unsafe_html_in_markdown: bool,

    /// Freeform object containing arbitrary data. This is useful for
    /// additional configuration used in themes or other form of templates
    #[serde(default)]
    pub data: toml::map::Map<String, toml::Value>,
}

impl Default for Config {
    fn default() -> Self {
        Self {
            base_url: None,
            title: defaults::title(),
            language: defaults::language(),
            authors: Vec::default(),
            themes: None,
            output_dir: defaults::output_dir(),
            static_dir: defaults::static_dir(),
            data: toml::map::Map::default(),
            codeblock_theme: None,
            heading_ids: false,
            extensionless_pages: false,
            unsafe_html_in_markdown: false,
        }
    }
}

impl Config {
    /// Serializes a [`Config`] object to toml
    pub fn toml(&self) -> String {
        toml::to_string_pretty(self).expect("Failed to serialize config to toml")
    }

    /// Parses a string, resulting in a new instance of [`Config`]
    pub fn parse(s: &str) -> Result<Config, toml::de::Error> {
        toml::from_str::<Self>(s)
    }
}

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

    #[test]
    fn generate_default_config() {
        assert_eq!(
            Config::default().toml(),
            r#"title = "Rubedo"
language = "en-US"
authors = []
output_dir = "public"
static_dir = "static"

[data]
"#
        );
    }

    #[test]
    fn deserialize_empty_string() {
        let config = Config::parse("").unwrap();
        let def = Config::default();
        assert_eq!(config.title, def.title);
        assert_eq!(config.base_url, def.base_url);
        assert_eq!(config.data.len(), 0);
        assert_eq!(config.authors.len(), 0);
    }

    #[test]
    fn deserialize() {
        let c = r#"
title = "Foo bar baz"
language = "fr"

authors = [
    "Gabriele Musco",
    "John Doe",
]

[data]
foo = "bar"
lorem = 123
ipsum = [
    "dolor",
    "sit",
]
# multiline inline tables are unsupported: https://github.com/toml-rs/toml/issues/397
amet = { consectetur = "adipiscing", elit = ["sed"] }
"#;
        let config = Config::parse(c).unwrap();

        assert_eq!(config.title, "Foo bar baz");
        assert_eq!(config.language, "fr");
        assert_eq!(config.authors.len(), 2);
        assert_eq!(config.authors.get(0).unwrap(), "Gabriele Musco");
        assert_eq!(config.authors.get(1).unwrap(), "John Doe");
        assert_eq!(config.data.len(), 4);
        assert_eq!(
            config.data.get("foo").unwrap(),
            &toml::Value::String("bar".into())
        );
        assert_eq!(
            config.data.get("lorem").unwrap(),
            &toml::Value::Integer(123)
        );
        assert_eq!(
            config.data.get("ipsum").unwrap(),
            &toml::Value::Array(vec![
                toml::Value::String("dolor".into()),
                toml::Value::String("sit".into()),
            ])
        );
        match config.data.get("amet").unwrap() {
            toml::Value::Table(map) => {
                assert_eq!(map.len(), 2);
                assert_eq!(
                    map.get("consectetur").unwrap(),
                    &toml::Value::String("adipiscing".into())
                );
                assert_eq!(
                    map.get("elit").unwrap(),
                    &toml::Value::Array(vec![toml::Value::String("sed".into()),])
                );
            }
            _ => panic!("key 'amet' is not a table"),
        }
    }
}
