use std::ffi;
use std::io;
#[cfg(unix)]
use std::str;

/// Struct that stores a lookup error from `getaddrinfo`
/// or `getnameinfo`. Can automatically be coerced to an io::Error using `?`.
#[derive(Debug)]
pub struct LookupError {
  kind: LookupErrorKind,
  err_num: i32,
  inner: io::Error,
}

impl LookupError {
  /// Match a `gai` error, returning Ok() if it's
  /// `0`. Otherwise return Err(LookupError) with
  /// the specific error details.
  pub fn match_gai_error(err: i32) -> Result<(), Self> {
    match err {
      0 => Ok(()),
      _ => Err(LookupError::new(err)),
    }
  }

  /// Create a new LookupError from a `gai` error,
  /// returned by `getaddrinfo` and `getnameinfo`.
  pub fn new(err: i32) -> Self {
    LookupError {
      kind: LookupErrorKind::new(err),
      err_num: err,
      inner: gai_err_to_io_err(err),
    }
  }
  /// Get the error kind explicitly. If this is an
  /// io::Error, use From/Into to convert it.
  pub fn kind(&self) -> LookupErrorKind {
    self.kind
  }

  /// Get the actual error number. This can be used
  /// to find non-standard return codes from some
  /// implementations (be careful of portability here).
  pub fn error_num(&self) -> i32 {
    self.err_num
  }
}

/// Different kinds of lookup errors that `getaddrinfo` and
/// `getnameinfo` can return. These can be a little inconsitant
/// between platforms, so it's recommended not to rely on them.
#[derive(Copy, Clone, Debug)]
pub enum LookupErrorKind {
  /// Temporary failure in name resolution.
  ///
  /// May also be returend when DNS server returns a SERVFAIL.
  Again,
  /// Invalid value for `ai_flags' field.
  Badflags,
  /// NAME or SERVICE is unknown.
  ///
  /// May also be returned when domain doesn't exist (NXDOMAIN) or domain
  /// exists but contains no address records (NODATA).
  NoName,
  /// The specified network host exists, but has no data defined.
  ///
  /// This is no longer a POSIX standard, however it's still returned by
  /// some platforms. Be warned that FreeBSD does not include the corresponding
  /// `EAI_NODATA` symbol.
  NoData,
  /// Non-recoverable failure in name resolution.
  Fail,
  /// `ai_family' not supported.
  Family,
  /// `ai_socktype' not supported.
  Socktype,
  /// SERVICE not supported for `ai_socktype'.
  Service,
  /// Memory allocation failure.
  Memory,
  /// System error returned in `errno'.
  System,
  /// An unknown result code was returned.
  ///
  /// For some platforms, you may wish to match on an unknown value directly.
  /// Note that `gai_strerr` is used to get error messages, so the generated IO
  /// error should contain the correct error message for the platform.
  Unknown,
  /// A generic C error or IO error occured.
  ///
  /// You should convert this `LookupError` into an IO error directly. Note
  /// that the error code is set to 0 in the case this is returned.
  IO,
}

impl LookupErrorKind {
  #[cfg(all(not(windows), not(unix)))]
  /// Create a `LookupErrorKind` from a `gai` error.
  pub fn new(err: i32) -> Self {
    LookupErrorKind::IO
  }

  #[cfg(unix)]
  /// Create a `LookupErrorKind` from a `gai` error.
  pub fn new(err: i32) -> Self {
    use libc as c;
    match err {
      c::EAI_AGAIN => LookupErrorKind::Again,
      c::EAI_BADFLAGS => LookupErrorKind::Badflags,
      c::EAI_FAIL => LookupErrorKind::Fail,
      c::EAI_FAMILY => LookupErrorKind::Family,
      c::EAI_MEMORY => LookupErrorKind::Memory,
      c::EAI_NONAME => LookupErrorKind::NoName,
      // FreeBSD has no EAI_NODATA, so don't match it on that platform.
      #[cfg(not(any(target_os="freebsd", target_os="emscripten")))]
      c::EAI_NODATA => LookupErrorKind::NoData,
      c::EAI_SERVICE => LookupErrorKind::Service,
      c::EAI_SOCKTYPE => LookupErrorKind::Socktype,
      c::EAI_SYSTEM => LookupErrorKind::System,
      _ => LookupErrorKind::IO,
    }
  }

  #[cfg(windows)]
  /// Create a `LookupErrorKind` from a `gai` error.
  pub fn new(err: i32) -> Self {
    use winapi::shared::winerror as e;
    match err as u32 {
      e::WSATRY_AGAIN => LookupErrorKind::Again,
      e::WSAEINVAL => LookupErrorKind::Badflags,
      e::WSANO_RECOVERY => LookupErrorKind::Fail,
      e::WSAEAFNOSUPPORT => LookupErrorKind::Family,
      e::ERROR_NOT_ENOUGH_MEMORY => LookupErrorKind::Memory,
      e::WSAHOST_NOT_FOUND => LookupErrorKind::NoName,
      e::WSANO_DATA => LookupErrorKind::NoData,
      e::WSATYPE_NOT_FOUND => LookupErrorKind::Service,
      e::WSAESOCKTNOSUPPORT => LookupErrorKind::Socktype,
      _ => LookupErrorKind::IO,
    }
  }
}

impl From<LookupError> for io::Error {
  fn from(err: LookupError) -> io::Error {
    err.inner
  }
}

impl From<io::Error> for LookupError {
  fn from(err: io::Error) -> LookupError {
    LookupError {
      kind: LookupErrorKind::IO,
      err_num: 0,
      inner: err,
    }
  }
}

impl From<ffi::NulError> for LookupError {
  fn from(err: ffi::NulError) -> LookupError {
    let err: io::Error = err.into();
    err.into()
  }
}

#[cfg(all(not(windows), not(unix)))]
/// Given a gai error, return an `std::io::Error` with
/// the appropriate error message. Note `0` is not an
/// error, but will still map to an error
pub(crate) fn gai_err_to_io_err(err: i32) -> io::Error {
  match (err) {
    0 => io::Error::new(
      io::ErrorKind::Other,
      "address information lookup success"
    ),
    _ => io::Error::new(
      io::ErrorKind::Other,
      "failed to lookup address information"
    ),
  }
}

#[cfg(unix)]
/// Given a gai error, return an `std::io::Error` with
/// the appropriate error message. Note `0` is not an
/// error, but will still map to an error
pub(crate) fn gai_err_to_io_err(err: i32) -> io::Error {
  use libc::{EAI_SYSTEM, gai_strerror};

  match err {
    0 => return io::Error::new(
      io::ErrorKind::Other,
      "address information lookup success"
    ),
    EAI_SYSTEM => return io::Error::last_os_error(),
    _ => {},
  }

  let detail = unsafe {
    str::from_utf8(ffi::CStr::from_ptr(gai_strerror(err)).to_bytes()).unwrap()
      .to_owned()
  };
  io::Error::new(io::ErrorKind::Other,
    &format!("failed to lookup address information: {}", detail)[..]
  )
}

#[cfg(windows)]
/// Given a gai error, return an `std::io::Error` with
/// the appropriate error message. Note `0` is not an
/// error, but will still map to an error
pub(crate) fn gai_err_to_io_err(err: i32) -> io::Error {
  use winapi::um::winsock2::WSAGetLastError;
  match err {
    0 => io::Error::new(
      io::ErrorKind::Other,
      "address information lookup success"
    ),
    _ => {
      io::Error::from_raw_os_error(
        unsafe { WSAGetLastError() }
      )
    }
  }
}
