use std::error::Error;
use std::fmt;
use std::result;

use serde::de;
use serde::ser;

#[derive(Debug)]
pub enum Unexpected {
    Bool(bool),
    I64(i64),
    I128(i128),
    U64(u64),
    U128(u128),
    Float(f64),
    Str(String),
    Unit,
    Seq,
    Map,
}

impl fmt::Display for Unexpected {
    fn fmt(&self, f: &mut fmt::Formatter) -> result::Result<(), fmt::Error> {
        match *self {
            Unexpected::Bool(b) => write!(f, "boolean `{}`", b),
            Unexpected::I64(i) => write!(f, "integer 64 bit `{}`", i),
            Unexpected::I128(i) => write!(f, "integer 128 bit `{}`", i),
            Unexpected::U64(i) => write!(f, "unsigned integer 64 bit `{}`", i),
            Unexpected::U128(i) => write!(f, "unsigned integer 128 bit `{}`", i),
            Unexpected::Float(v) => write!(f, "floating point `{}`", v),
            Unexpected::Str(ref s) => write!(f, "string {:?}", s),
            Unexpected::Unit => write!(f, "unit value"),
            Unexpected::Seq => write!(f, "sequence"),
            Unexpected::Map => write!(f, "map"),
        }
    }
}

/// Represents all possible errors that can occur when working with
/// configuration.
pub enum ConfigError {
    /// Configuration is frozen and no further mutations can be made.
    Frozen,

    /// Configuration property was not found
    NotFound(String),

    /// Configuration path could not be parsed.
    PathParse(nom::error::ErrorKind),

    /// Configuration could not be parsed from file.
    FileParse {
        /// The URI used to access the file (if not loaded from a string).
        /// Example: `/path/to/config.json`
        uri: Option<String>,

        /// The captured error from attempting to parse the file in its desired format.
        /// This is the actual error object from the library used for the parsing.
        cause: Box<dyn Error + Send + Sync>,
    },

    /// Value could not be converted into the requested type.
    Type {
        /// The URI that references the source that the value came from.
        /// Example: `/path/to/config.json` or `Environment` or `etcd://localhost`
        // TODO: Why is this called Origin but FileParse has a uri field?
        origin: Option<String>,

        /// What we found when parsing the value
        unexpected: Unexpected,

        /// What was expected when parsing the value
        expected: &'static str,

        /// The key in the configuration hash of this value (if available where the
        /// error is generated).
        key: Option<String>,
    },

    /// Custom message
    Message(String),

    /// Unadorned error from a foreign origin.
    Foreign(Box<dyn Error + Send + Sync>),
}

impl ConfigError {
    // FIXME: pub(crate)
    #[doc(hidden)]
    pub fn invalid_type(
        origin: Option<String>,
        unexpected: Unexpected,
        expected: &'static str,
    ) -> Self {
        Self::Type {
            origin,
            unexpected,
            expected,
            key: None,
        }
    }

    // Have a proper error fire if the root of a file is ever not a Table
    // TODO: for now only json5 checked, need to finish others
    #[doc(hidden)]
    pub fn invalid_root(origin: Option<&String>, unexpected: Unexpected) -> Box<Self> {
        Box::new(Self::Type {
            origin: origin.cloned(),
            unexpected,
            expected: "a map",
            key: None,
        })
    }

    // FIXME: pub(crate)
    #[doc(hidden)]
    #[must_use]
    pub fn extend_with_key(self, key: &str) -> Self {
        match self {
            Self::Type {
                origin,
                unexpected,
                expected,
                ..
            } => Self::Type {
                origin,
                unexpected,
                expected,
                key: Some(key.into()),
            },

            _ => self,
        }
    }

    #[must_use]
    fn prepend(self, segment: &str, add_dot: bool) -> Self {
        let concat = |key: Option<String>| {
            let key = key.unwrap_or_default();
            let dot = if add_dot && key.as_bytes().get(0).unwrap_or(&b'[') != &b'[' {
                "."
            } else {
                ""
            };
            format!("{}{}{}", segment, dot, key)
        };
        match self {
            Self::Type {
                origin,
                unexpected,
                expected,
                key,
            } => Self::Type {
                origin,
                unexpected,
                expected,
                key: Some(concat(key)),
            },
            Self::NotFound(key) => Self::NotFound(concat(Some(key))),
            _ => self,
        }
    }

    #[must_use]
    pub(crate) fn prepend_key(self, key: &str) -> Self {
        self.prepend(key, true)
    }

    #[must_use]
    pub(crate) fn prepend_index(self, idx: usize) -> Self {
        self.prepend(&format!("[{}]", idx), false)
    }
}

/// Alias for a `Result` with the error type set to `ConfigError`.
pub type Result<T> = result::Result<T, ConfigError>;

// Forward Debug to Display for readable panic! messages
impl fmt::Debug for ConfigError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", *self)
    }
}

impl fmt::Display for ConfigError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            ConfigError::Frozen => write!(f, "configuration is frozen"),

            ConfigError::PathParse(ref kind) => write!(f, "{}", kind.description()),

            ConfigError::Message(ref s) => write!(f, "{}", s),

            ConfigError::Foreign(ref cause) => write!(f, "{}", cause),

            ConfigError::NotFound(ref key) => {
                write!(f, "configuration property {:?} not found", key)
            }

            ConfigError::Type {
                ref origin,
                ref unexpected,
                expected,
                ref key,
            } => {
                write!(f, "invalid type: {}, expected {}", unexpected, expected)?;

                if let Some(ref key) = *key {
                    write!(f, " for key `{}`", key)?;
                }

                if let Some(ref origin) = *origin {
                    write!(f, " in {}", origin)?;
                }

                Ok(())
            }

            ConfigError::FileParse { ref cause, ref uri } => {
                write!(f, "{}", cause)?;

                if let Some(ref uri) = *uri {
                    write!(f, " in {}", uri)?;
                }

                Ok(())
            }
        }
    }
}

impl Error for ConfigError {}

impl de::Error for ConfigError {
    fn custom<T: fmt::Display>(msg: T) -> Self {
        Self::Message(msg.to_string())
    }
}

impl ser::Error for ConfigError {
    fn custom<T: fmt::Display>(msg: T) -> Self {
        Self::Message(msg.to_string())
    }
}
