use std::default::Default;
use std::path::{Path, PathBuf};
use std::fs;
use std::fs::File;
use std::io;
use std::io::ErrorKind;
use std::env;
use std::io::Read;
use std::fmt;
use std::fmt::{Display, Formatter};
use std::error::Error;
use toml;

#[derive(Debug)]
pub enum ConfigError {
    IoError(io::Error),
    TomlParserErrors(Vec<toml::ParserError>),
    InvalidFieldName(String),
    InvalidFieldType(& 'static str, String),
}

impl Error for ConfigError {
    fn description(&self) -> &str {
        match *self {
            ConfigError::IoError(ref e) => e.description(),
            ConfigError::TomlParserErrors(ref v) => v[0].description(),
            ConfigError::InvalidFieldName(_) => "Invalid field name",
            ConfigError::InvalidFieldType(_, _) => "Invalid field type",
        }
    }

    fn cause(&self) -> Option<&Error> {
        match *self {
            ConfigError::IoError(ref e) => Some(e),
            ConfigError::TomlParserErrors(ref v) => Some(&v[0]),
            _ => None
        }
    }
}

impl Display for ConfigError {
    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
        match *self {
            ConfigError::IoError(ref e) => write!(f, "IO Error: {}", e),
            ConfigError::TomlParserErrors(ref v) => write!(f, "Parser Error ({} total): {}",
                    v.len(), v[0]),
            ConfigError::InvalidFieldName(ref s) => write!(f, "Invalid feild name: {}", s),
            ConfigError::InvalidFieldType(expected, ref got) => write!(f, "Expected type {} got {}", expected, got),
        }
    }
}

impl From<io::Error> for ConfigError {
    fn from(e: io::Error) -> ConfigError {
        ConfigError::IoError(e)
    }
}

#[derive(RustcDecodable, Debug)]
pub struct Config {
    pub show_ascii: bool,
    pub show_linenum: bool,
    pub line_width: Option<u32>
}

impl Default for Config {
    fn default() -> Config {
        Config {
            show_ascii: true,
            show_linenum: true,
            line_width: None,
        }
    }
}

macro_rules! try_unwrap_toml {
    ($e:expr, $t:ident) => ({
        match $e {
            toml::Value::$t(v) => v,
            other => {
                return Err(ConfigError::InvalidFieldType(stringify!($t), format!("{}", other)))
            }
        }
    })
}

macro_rules! decode_toml {
    ($obj:expr, $name:ident, $table:expr, $toml_type:ident, $map_func:expr) => ({
        $obj.$name = match $table.remove(stringify!($name)) {
            Some(val) => $map_func(try_unwrap_toml!(val, $toml_type)),
            None => $obj.$name
        };
    });
    ($obj:expr, $name:ident, $table:expr, $toml_type:ident) => ({
        $obj.$name = match $table.remove(stringify!($name)) {
            Some(val) => try_unwrap_toml!(val, $toml_type),
            None => $obj.$name
        };
    });
}

impl Config {
    pub fn get_config_usage() -> &'static str {
        "The supported configuration options with their default values:
    show_ascii=true        Shows an ascii representation in the right hand side
    show_linenum=true      Shows the byte offset in the left hand side
    line_width=0           The number of bytes per line, 0 meaning auto-wrap

The configuration can be set in the rex.conf file located in the
$XDG_CONFIG_HOME path (usually, ~/.config/rex/rex.conf). Additionally,
properties can be set on the commandline as rex -C show_ascii=false.
"
    }

    fn apply_toml(&mut self, mut t: toml::Table) -> Result<(), ConfigError> {
        decode_toml!(self, show_ascii, t, Boolean);
        decode_toml!(self, show_linenum, t, Boolean);
        decode_toml!(self, line_width, t, Integer, |i| if i > 0 { Some(i as u32) } else { None });
        if let Some((key, _)) = t.into_iter().next() {
            Err(ConfigError::InvalidFieldName(key))
        } else {
            Ok(())
        }
    }

    pub fn set_from_string(&mut self, set_line: &str) -> Result<(), ConfigError> {
        let mut parser = toml::Parser::new(&set_line);
        if let Some(table) = parser.parse() {
            return self.apply_toml(table);
        }
        Err(ConfigError::TomlParserErrors(parser.errors))
    }

    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Config, ConfigError> {
        let mut s = String::new();
        let mut f = try!(File::open(path));
        try!(f.read_to_string(&mut s));
        let mut config: Config = Default::default();
        try!(config.set_from_string(&s));
        Ok(config)
    }

    fn get_config_path() -> PathBuf {
        let mut p = PathBuf::new();
        p.push(env::var("XDG_CONFIG_HOME").unwrap_or_else(
                |_| env::var("HOME").unwrap_or("/".into()) + "/.config"
            ));
        p.join("rex").join("rex.conf")
    }

    pub fn open_default() -> Result<Config, ConfigError> {
        let config_path = Config::get_config_path();
        match fs::metadata(config_path) {
            Ok(_) => Config::from_file(Config::get_config_path()),
            Err(ref err) if err.kind() == ErrorKind::NotFound => Ok(Default::default()),
            Err(err) => Err(err.into()),
        }
    }
}