extern crate libc;

use libc::{c_int};
use nanomsg_sys;

use std::str;
use std::fmt;
use std::io;
use std::convert::From;
use std::ffi::CStr;
use std::result;
use std::error;

pub type Result<T> = result::Result<T, Error>;

#[derive(Clone, Copy, Eq, PartialEq)]
pub enum Error {
    Unknown                    = 0 as isize,
    OperationNotSupported      = nanomsg_sys::ENOTSUP          as isize,
    ProtocolNotSupported       = nanomsg_sys::EPROTONOSUPPORT  as isize,
    NoBufferSpace              = nanomsg_sys::ENOBUFS          as isize,
    NetworkDown                = nanomsg_sys::ENETDOWN         as isize,
    AddressInUse               = nanomsg_sys::EADDRINUSE       as isize,
    AddressNotAvailable        = nanomsg_sys::EADDRNOTAVAIL    as isize,
    ConnectionRefused          = nanomsg_sys::ECONNREFUSED     as isize,
    OperationNowInProgress     = nanomsg_sys::EINPROGRESS      as isize,
    NotSocket                  = nanomsg_sys::ENOTSOCK         as isize,
    AddressFamilyNotSupported  = nanomsg_sys::EAFNOSUPPORT     as isize,
    WrongProtocol              = nanomsg_sys::EPROTO           as isize,
    TryAgain                   = nanomsg_sys::EAGAIN           as isize,
    BadFileDescriptor          = nanomsg_sys::EBADF            as isize,
    InvalidInput               = nanomsg_sys::EINVAL           as isize,
    TooManyOpenFiles           = nanomsg_sys::EMFILE           as isize,
    BadAddress                 = nanomsg_sys::EFAULT           as isize,
    PermissionDenied           = nanomsg_sys::EACCESS          as isize,
    NetworkReset               = nanomsg_sys::ENETRESET        as isize,
    NetworkUnreachable         = nanomsg_sys::ENETUNREACH      as isize,
    HostUnreachable            = nanomsg_sys::EHOSTUNREACH     as isize,
    NotConnected               = nanomsg_sys::ENOTCONN         as isize,
    MessageTooLong             = nanomsg_sys::EMSGSIZE         as isize,
    TimedOut                   = nanomsg_sys::ETIMEDOUT        as isize,
    ConnectionAborted          = nanomsg_sys::ECONNABORTED     as isize,
    ConnectionReset            = nanomsg_sys::ECONNRESET       as isize,
    ProtocolNotAvailable       = nanomsg_sys::ENOPROTOOPT      as isize,
    AlreadyConnected           = nanomsg_sys::EISCONN          as isize,
    SocketTypeNotSupported     = nanomsg_sys::ESOCKTNOSUPPORT  as isize,
    Terminating                = nanomsg_sys::ETERM            as isize,
    NameTooLong                = nanomsg_sys::ENAMETOOLONG     as isize,
    NoDevice                   = nanomsg_sys::ENODEV           as isize,
    FileStateMismatch          = nanomsg_sys::EFSM             as isize,
    Interrupted                = nanomsg_sys::EINTR            as isize
}

impl Error {
    pub fn to_raw(&self) -> c_int {
        *self as c_int
    }

    pub fn from_raw(raw: c_int) -> Error {
        match raw {
            nanomsg_sys::ENOTSUP         => Error::OperationNotSupported    ,
            nanomsg_sys::EPROTONOSUPPORT => Error::ProtocolNotSupported     ,
            nanomsg_sys::ENOBUFS         => Error::NoBufferSpace            ,
            nanomsg_sys::ENETDOWN        => Error::NetworkDown              ,
            nanomsg_sys::EADDRINUSE      => Error::AddressInUse             ,
            nanomsg_sys::EADDRNOTAVAIL   => Error::AddressNotAvailable      ,
            nanomsg_sys::ECONNREFUSED    => Error::ConnectionRefused        ,
            nanomsg_sys::EINPROGRESS     => Error::OperationNowInProgress   ,
            nanomsg_sys::ENOTSOCK        => Error::NotSocket                ,
            nanomsg_sys::EAFNOSUPPORT    => Error::AddressFamilyNotSupported,
            nanomsg_sys::EPROTO          => Error::WrongProtocol            ,
            nanomsg_sys::EAGAIN          => Error::TryAgain                 ,
            nanomsg_sys::EBADF           => Error::BadFileDescriptor        ,
            nanomsg_sys::EINVAL          => Error::InvalidInput             ,
            nanomsg_sys::EMFILE          => Error::TooManyOpenFiles         ,
            nanomsg_sys::EFAULT          => Error::BadAddress               ,
            nanomsg_sys::EACCESS         => Error::PermissionDenied         ,
            nanomsg_sys::ENETRESET       => Error::NetworkReset             ,
            nanomsg_sys::ENETUNREACH     => Error::NetworkUnreachable       ,
            nanomsg_sys::EHOSTUNREACH    => Error::HostUnreachable          ,
            nanomsg_sys::ENOTCONN        => Error::NotConnected             ,
            nanomsg_sys::EMSGSIZE        => Error::MessageTooLong           ,
            nanomsg_sys::ETIMEDOUT       => Error::TimedOut                 ,
            nanomsg_sys::ECONNABORTED    => Error::ConnectionAborted        ,
            nanomsg_sys::ECONNRESET      => Error::ConnectionReset          ,
            nanomsg_sys::ENOPROTOOPT     => Error::ProtocolNotAvailable     ,
            nanomsg_sys::EISCONN         => Error::AlreadyConnected         ,
            nanomsg_sys::ESOCKTNOSUPPORT => Error::SocketTypeNotSupported   ,
            nanomsg_sys::ETERM           => Error::Terminating              ,
            nanomsg_sys::ENAMETOOLONG    => Error::NameTooLong              ,
            nanomsg_sys::ENODEV          => Error::NoDevice                 ,
            nanomsg_sys::EFSM            => Error::FileStateMismatch        ,
            nanomsg_sys::EINTR           => Error::Interrupted              ,
            _                            => Error::Unknown
        }
    }
}

impl error::Error for Error {
    fn description(&self) -> &str {
        unsafe {
            let nn_errno = *self as c_int;
            let c_ptr: *const libc::c_char = nanomsg_sys::nn_strerror(nn_errno);
            let c_str = CStr::from_ptr(c_ptr);
            let bytes = c_str.to_bytes();
            let desc = str::from_utf8(bytes).unwrap_or("Error");

            desc
        }
    }
}

impl From<io::Error> for Error {
    fn from(err: io::Error) -> Error {
        match err.kind() {
            io::ErrorKind::PermissionDenied    => Error::PermissionDenied,
            io::ErrorKind::ConnectionRefused   => Error::ConnectionRefused,
            io::ErrorKind::ConnectionReset     => Error::ConnectionReset,
            io::ErrorKind::ConnectionAborted   => Error::ConnectionAborted,
            io::ErrorKind::NotConnected        => Error::NotConnected,
            io::ErrorKind::AddrInUse           => Error::AddressInUse,
            io::ErrorKind::AddrNotAvailable    => Error::AddressNotAvailable,
            io::ErrorKind::AlreadyExists       => Error::AlreadyConnected,
            io::ErrorKind::WouldBlock          => Error::TryAgain,
            io::ErrorKind::InvalidInput        => Error::InvalidInput,
            io::ErrorKind::TimedOut            => Error::TimedOut,
            io::ErrorKind::Interrupted         => Error::Interrupted,
            _                                  => Error::Unknown
        }
    }
}

impl From<Error> for io::Error {
    fn from(err: Error) -> io::Error {
        let as_std_error: &error::Error = &err;
        let description = as_std_error.description();
        match err {
            Error::PermissionDenied      => io::Error::new(io::ErrorKind::PermissionDenied,  description ),
            Error::ConnectionRefused     => io::Error::new(io::ErrorKind::ConnectionRefused, description ),
            Error::ConnectionReset       => io::Error::new(io::ErrorKind::ConnectionReset,   description ),
            Error::ConnectionAborted     => io::Error::new(io::ErrorKind::ConnectionAborted, description ),
            Error::NotConnected          => io::Error::new(io::ErrorKind::NotConnected,      description ),
            Error::AddressInUse          => io::Error::new(io::ErrorKind::AddrInUse,         description ),
            Error::AddressNotAvailable   => io::Error::new(io::ErrorKind::AddrNotAvailable,  description ),
            Error::AlreadyConnected      => io::Error::new(io::ErrorKind::AlreadyExists,     description ),
            Error::TryAgain              => io::Error::new(io::ErrorKind::WouldBlock,        description ),
            Error::InvalidInput          => io::Error::new(io::ErrorKind::InvalidInput,      description ),
            Error::TimedOut              => io::Error::new(io::ErrorKind::TimedOut,          description ),
            Error::Interrupted           => io::Error::new(io::ErrorKind::Interrupted,       description ),
            _                            => io::Error::new(io::ErrorKind::Other,             description )
        }
    }
}

impl fmt::Debug for Error {
    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
        let as_std_error: &error::Error = self;
        let description = as_std_error.description();
        write!(formatter, "{}", description)
    }
}

impl fmt::Display for Error {
    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
        let as_std_error: &error::Error = self;
        let description = as_std_error.description();
        write!(formatter, "{}", description)
    }
}

pub fn last_nano_error() -> Error {
    let nn_errno = unsafe { nanomsg_sys::nn_errno() };

    Error::from_raw(nn_errno)
}

#[cfg(test)]
#[allow(unused_must_use)]
mod tests {
    use nanomsg_sys;
    use libc;
    use super::{Error};
    use std::io;
    use std::convert::From;

    fn assert_convert_error_code_to_error(error_code: libc::c_int, expected_error: Error) {
        let converted_error = Error::from_raw(error_code);
        assert_eq!(expected_error, converted_error)
    }

    #[test]
    fn can_convert_error_code_to_error() {
        assert_convert_error_code_to_error(nanomsg_sys::ENOTSUP, Error::OperationNotSupported);
        assert_convert_error_code_to_error(nanomsg_sys::EPROTONOSUPPORT, Error::ProtocolNotSupported);
        assert_convert_error_code_to_error(nanomsg_sys::EADDRINUSE, Error::AddressInUse);
        assert_convert_error_code_to_error(nanomsg_sys::EHOSTUNREACH, Error::HostUnreachable);
    }

    fn check_error_kind_match(nano_err: Error, io_err_kind: io::ErrorKind) {
        let io_err: io::Error = From::from(nano_err);

        assert_eq!(io_err_kind, io_err.kind())
    }

    #[test]
    fn nano_err_can_be_converted_to_io_err() {
        check_error_kind_match(Error::TimedOut, io::ErrorKind::TimedOut);
        check_error_kind_match(Error::PermissionDenied, io::ErrorKind::PermissionDenied);
        check_error_kind_match(Error::ConnectionRefused, io::ErrorKind::ConnectionRefused);
        check_error_kind_match(Error::OperationNotSupported, io::ErrorKind::Other);
        check_error_kind_match(Error::NotConnected, io::ErrorKind::NotConnected);
        check_error_kind_match(Error::Interrupted, io::ErrorKind::Interrupted);
    }

    #[test]
    fn nano_err_can_be_converted_from_io_err() {
        let io_err = io::Error::new(io::ErrorKind::TimedOut, "Timed out");
        let nano_err: Error = From::from(io_err);

        assert_eq!(Error::TimedOut, nano_err)
    }
}
