use crate::types::Player;
use serde::{Deserialize, Serialize};
use std::{
    collections::HashMap,
    fs::File,
    io::{ErrorKind, Read},
    path::PathBuf,
};
use thiserror::Error;

/*
pub trait ConfigOption {
    type Value;
}

pub struct DatabasePath(PathBuf);

impl ConfigOption for DatabasePath {
    type Value = PathBuf;
}

impl ConfigOption for Player {
    type Value = Player;
}

pub trait Config {
    // fn set_option(option: ConfigOption);
    fn get_option<N, C: ConfigOption>(name: Name) -> C<Name = Name>
}
*/

#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
enum OptionNames {
    DatabasePath,
    Me,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ConfigOption {
    DatabasePath(DatabasePath),
    Me(Me),
}

#[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),
}

#[derive(Clone, Debug)]
pub struct Config {
    config_path: PathBuf,
    values: HashMap<OptionNames, ConfigOption>,
}

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

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

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

    pub fn set(&mut self, val: ConfigOption) {
        let _ = match val {
            ConfigOption::DatabasePath(_) => self.values.insert(OptionNames::DatabasePath, val),
            ConfigOption::Me(_) => self.values.insert(OptionNames::Me, val),
        };
    }

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

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

impl std::ops::Deref for DatabasePath {
    type Target = PathBuf;
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl From<&Config> for DatabasePath {
    fn from(config: &Config) -> Self {
        match config.values.get(&OptionNames::DatabasePath) {
            Some(ConfigOption::DatabasePath(path)) => path.clone(),
            _ => DatabasePath(config.config_path.clone()),
        }
    }
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Me(Player);

impl From<&Config> for Option<Me> {
    fn from(config: &Config) -> Self {
        config
            .values
            .get(&OptionNames::Me)
            .and_then(|val| match val {
                ConfigOption::Me(me) => Some(me.clone()),
                _ => None,
            })
    }
}

impl std::ops::Deref for Me {
    type Target = Player;
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

#[cfg(test)]
mod test {
    use super::*;
    use crate::types::Rank;
    use cool_asserts::assert_matches;

    #[test]
    fn it_can_set_and_get_options() {
        let mut config = Config::new(PathBuf::from("."));
        config.set(ConfigOption::DatabasePath(DatabasePath(PathBuf::from(
            "fixtures/five_games",
        ))));
        config.set(ConfigOption::Me(Me(Player {
            name: "Savanni".to_owned(),
            rank: Some(Rank::Kyu(10)),
        })));
    }

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

        assert_matches!(values.get(&OptionNames::DatabasePath),
                        Some(ConfigOption::DatabasePath(db_path)) =>
                        assert_eq!(*db_path, config.get())
        );

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