/*
use std::{
    collections::HashMap,
    fs::File,
    hash::Hash,
    io::{ErrorKind, Read},
    path::PathBuf,
};
*/

pub use config_derive::ConfigOption;
use thiserror::Error;

#[derive(Debug, Error)]
pub enum ConfigReadError {
    #[error("Cannot read the configuration file: {0}")]
    CannotRead(std::io::Error),
    #[error("Cannot open the configuration file for reading: {0}")]
    CannotOpen(std::io::Error),
    #[error("Invalid json data found in the configurationfile: {0}")]
    InvalidJSON(serde_json::Error),
}

#[macro_export]
macro_rules! define_config {
    ($($name:ident($struct:ident),)+) => (
        #[derive(Clone, Debug, Hash, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
        pub enum ConfigName {
            $($name),+
        }

        #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
        #[serde(untagged)]
        pub enum ConfigOption {
            $($name($struct)),+
        }

        #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
        pub struct Config {
            values: std::collections::HashMap<ConfigName, ConfigOption>,
        }

        impl Config {
            pub fn new() -> Self {
                Self {
                    values: std::collections::HashMap::new(),
                }
            }

            pub fn from_path(config_path: std::path::PathBuf) -> Result<Self, $crate::ConfigReadError> {
                let mut settings = config_path.clone();
                settings.push("config");

                match std::fs::File::open(settings) {
                    Ok(mut file) => {
                        let mut buf = String::new();
                        std::io::Read::read_to_string(&mut file, &mut buf)
                            .map_err(|err| $crate::ConfigReadError::CannotRead(err))?;
                        let values = serde_json::from_str(buf.as_ref())
                            .map_err(|err| $crate::ConfigReadError::InvalidJSON(err))?;
                        Ok(Self {
                            values,
                        })
                    }
                    Err(io_err) => {
                        match io_err.kind() {
                            std::io::ErrorKind::NotFound => {
                                /* create the path and an empty file */
                                Ok(Self {
                                    values: std::collections::HashMap::new(),
                                })
                            }
                            _ => Err($crate::ConfigReadError::CannotOpen(io_err)),
                        }
                    }
                }
            }

            pub fn set(&mut self, val: ConfigOption) {
                let _ = match val {
                    $(ConfigOption::$struct(_) => self.values.insert(ConfigName::$name, val)),+
                };
            }

            pub fn get<'a, T>(&'a self) -> Option<T>
            where
                Option<T>: From<&'a Self>,
            {
                self.into()
            }
        }
    )
}

#[cfg(test)]
mod test {
    use super::*;
    use cool_asserts::assert_matches;
    use serde::{Deserialize, Serialize};
    use std::collections::HashMap;
    use std::path::PathBuf;

    define_config! {
        DatabasePath(DatabasePath),
        Me(Me),
    }

    #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, ConfigOption)]
    pub struct DatabasePath(PathBuf);

    #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
    enum Rank {
        Kyu(i8),
        Dan(i8),
    }

    #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, ConfigOption)]
    pub struct Me {
        name: String,
        rank: Option<Rank>,
    }

    #[test]
    fn it_can_set_and_get_options() {
        let mut config: Config = Config::new();
        config.set(ConfigOption::DatabasePath(DatabasePath(PathBuf::from(
            "./fixtures/five_games",
        ))));

        assert_eq!(
            Some(DatabasePath(PathBuf::from("./fixtures/five_games"))),
            config.get()
        );
    }

    #[test]
    fn it_can_serialize_and_deserialize() {
        let mut config = Config::new();
        config.set(ConfigOption::DatabasePath(DatabasePath(PathBuf::from(
            "fixtures/five_games",
        ))));
        config.set(ConfigOption::Me(Me {
            name: "Savanni".to_owned(),
            rank: Some(Rank::Kyu(10)),
        }));
        let s = serde_json::to_string(&config.values).unwrap();
        println!("{}", s);
        let values: HashMap<ConfigName, ConfigOption> = serde_json::from_str(s.as_ref()).unwrap();
        println!("options: {:?}", values);

        assert_matches!(values.get(&ConfigName::DatabasePath),
                        Some(ConfigOption::DatabasePath(ref db_path)) =>
                        assert_eq!(Some(db_path.clone()), config.get())
        );

        assert_matches!(values.get(&ConfigName::Me), Some(ConfigOption::Me(val)) =>
            assert_eq!(Some(val.clone()), config.get())
        );
    }
}
