use std::fmt;
use std::path;

use log::debug;
use serde::Serialize;
use serde_yaml;

use crate::error::*;

use super::assets;
use super::collection;
use super::mark;
use super::site;
use super::template;
use super::vwiki;
use crate::SyntaxHighlight;

#[derive(Debug, Clone, Serialize)]
#[serde(deny_unknown_fields, default)]
pub struct Config {
    pub source: path::PathBuf,
    pub destination: path::PathBuf,
    pub ignore: Vec<liquid::model::KString>,
    pub page_extensions: Vec<liquid::model::KString>,
    pub include_drafts: bool,
    pub pages: collection::Collection,
    pub posts: collection::Collection,
    pub site: site::Site,
    pub layouts_path: path::PathBuf,
    pub liquid: template::LiquidBuilder,
    pub markdown: mark::MarkdownBuilder,
    pub vimwiki: vwiki::VimwikiBuilder,
    #[serde(skip)]
    pub syntax: std::sync::Arc<SyntaxHighlight>,
    pub assets: assets::AssetsBuilder,
    pub minify: cobalt_config::Minify,
}

impl Config {
    pub fn from_config(source: cobalt_config::Config) -> Result<Self> {
        let cobalt_config::Config {
            root,
            source,
            destination,
            abs_dest,
            include_drafts,
            default,
            pages,
            posts,
            site,
            template_extensions,
            ignore: custom_ignore,
            syntax_highlight,
            layouts_dir,
            includes_dir,
            assets,
            minify,
        } = source;

        if include_drafts {
            debug!("Draft mode enabled");
        }

        if template_extensions.is_empty() {
            failure::bail!("`template_extensions` should not be empty.");
        }

        let source = source.to_path(&root);
        let destination = abs_dest.unwrap_or_else(|| destination.to_path(root));

        let pages = collection::Collection::from_page_config(pages, &site, &default)?;

        let posts =
            collection::Collection::from_post_config(posts, &site, include_drafts, &default)?;

        let site = site::Site::from_config(site);

        let mut ignore: Vec<liquid::model::KString> = vec![".*".into(), "_*".into()];
        if let Ok(rel_dest) = path::Path::new(&destination).strip_prefix(&source) {
            let rel_dest = rel_dest.to_str().expect("started as a utf-8 string");
            if !rel_dest.is_empty() {
                ignore.push(format!("/{}", rel_dest.to_owned()).into());
            }
        }
        ignore.push(format!("/{}", includes_dir).into());
        ignore.push(format!("/{}", layouts_dir).into());
        ignore.push("/_defaults".into());
        ignore.push(format!("/{}", assets.sass.import_dir).into());
        assert_eq!(pages.dir, "");
        assert_eq!(pages.drafts_dir, None);
        ignore.push(format!("!/{}", posts.dir).into());
        if let Some(dir) = posts.drafts_dir.as_deref() {
            ignore.push(format!("!/{}", dir).into());
        }
        ignore.extend(custom_ignore);

        let assets = assets::AssetsBuilder::from_config(assets, &source);

        let includes_path = source.join(includes_dir);
        let layouts_path = source.join(layouts_dir);

        let mut highlight = SyntaxHighlight::new();
        let syntaxes_path = source.join("_syntaxes");
        if syntaxes_path.exists() {
            highlight.load_custom_syntaxes(&syntaxes_path);
        }

        let syntax = std::sync::Arc::new(highlight);

        let liquid = template::LiquidBuilder {
            includes_path,
            syntax: syntax.clone(),
            theme: syntax_highlight
                .enabled
                .then(|| syntax_highlight.theme.clone()),
        };
        let markdown = mark::MarkdownBuilder {
            syntax: syntax.clone(),
            theme: syntax_highlight
                .enabled
                .then(|| syntax_highlight.theme.clone()),
        };
        let vimwiki = vwiki::VimwikiBuilder {
            theme: syntax_highlight.theme,
            syntax_highlight_enabled: syntax_highlight.enabled,
        };

        let config = Config {
            source,
            destination,
            ignore,
            page_extensions: template_extensions,
            include_drafts,
            pages,
            posts,
            site,
            layouts_path,
            liquid,
            markdown,
            vimwiki,
            syntax,
            assets,
            minify,
        };

        Ok(config)
    }
}

impl Default for Config {
    fn default() -> Config {
        Config::from_config(cobalt_config::Config::default())
            .expect("default config should not fail")
    }
}

impl fmt::Display for Config {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let mut converted = serde_yaml::to_string(self).map_err(|_| fmt::Error)?;
        converted.drain(..4);
        write!(f, "{}", converted)
    }
}

#[test]
fn test_build_default() {
    let config = cobalt_config::Config::default();
    Config::from_config(config).unwrap();
}

#[test]
fn test_build_dest() {
    let config = cobalt_config::Config::from_file("tests/fixtures/config/_cobalt.yml").unwrap();
    let result = Config::from_config(config).unwrap();
    assert_eq!(
        result.source,
        path::Path::new("tests/fixtures/config").to_path_buf()
    );
    assert_eq!(
        result.destination,
        path::Path::new("tests/fixtures/config/dest").to_path_buf()
    );
}

#[test]
fn test_build_abs_dest() {
    let mut config = cobalt_config::Config::from_file("tests/fixtures/config/_cobalt.yml").unwrap();
    config.abs_dest = Some(path::PathBuf::from("hello/world"));
    let result = Config::from_config(config).unwrap();
    assert_eq!(
        result.source,
        path::Path::new("tests/fixtures/config").to_path_buf()
    );
    assert_eq!(
        result.destination,
        path::Path::new("hello/world").to_path_buf()
    );
}
