use std::{fmt, io};

use nix::errno::Errno;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorCode {
    Exec = 1,
    CapSet = 2,
    ParentDeathSignal = 3,
    PreExec = 4,
    ProcessStop = 5,
    ResetSignal = 6,
    Seccomp = 7,
    SeccompSendFd = 8,
    SeccompWaitFd = 9,
    UnshareFiles = 10,
    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
    SetTSC = 11,
}

/// Error runnning process
///
/// This type has very large number of options and it's enum only to be
/// compact. Probably you shouldn't match on the error cases but just format
/// it for user into string.
#[derive(Debug)]
pub enum Error {
    /// Unknown nix error
    ///
    /// Frankly, this error should not happen when running process. We just
    /// keep it here in case `nix` returns this error, which should not happen.
    NixError(i32), // Not sure it's possible, but it is here to convert from
    // nix::Error safer
    /// Some invalid error code received from child application
    UnknownError,
    /// Error when calling capset syscall
    CapSet(i32),
    /// Error when running execve() systemcall
    Exec(i32),
    /// Unable to set death signal (probably signal number invalid)
    ParentDeathSignal(i32),
    /// Before unfreeze callback error
    BeforeUnfreeze(Box<dyn (::std::error::Error) + Send + Sync + 'static>),
    /// Before exec callback error
    PreExec(i32),
    /// Error stopping process
    ProcessStop(i32),
    /// Error resetting signals
    ResetSignal(i32),
    /// Seccomp error (loading filter, getting notify fd)
    Seccomp(i32),
    /// Error sending notification fd through the seccomp sender channel
    SeccompSendFd(i32),
    /// Error waiting for parent to receive the seccomp fd
    SeccompWaitFd(i32),
    /// Error unsharing files
    UnshareFiles(i32),
    /// Error calling prctl PR_SET_TSC
    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
    SetTSC(i32),
}

impl std::error::Error for Error {}

impl Error {
    /// Similarly to `io::Error` returns bare error code
    pub fn raw_os_error(&self) -> Option<i32> {
        use self::Error::*;
        match *self {
            UnknownError => None,
            NixError(x) => Some(x),
            CapSet(x) => Some(x),
            Exec(x) => Some(x),
            ParentDeathSignal(x) => Some(x),
            BeforeUnfreeze(..) => None,
            PreExec(x) => Some(x),
            ProcessStop(x) => Some(x),
            ResetSignal(x) => Some(x),
            Seccomp(x) => Some(x),
            SeccompSendFd(x) => Some(x),
            SeccompWaitFd(x) => Some(x),
            UnshareFiles(x) => Some(x),
            #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
            SetTSC(x) => Some(x),
        }
    }
}

impl Error {
    fn title(&self) -> &'static str {
        use self::Error::*;
        match *self {
            UnknownError => "unexpected value received via signal pipe",
            NixError(_) => "some unknown nix error",
            CapSet(_) => "error when setting capabilities",
            Exec(_) => "error when executing",
            ParentDeathSignal(_) => "error when death signal",
            BeforeUnfreeze(_) => "error in before_unfreeze callback",
            PreExec(_) => "error in pre_exec callback",
            ProcessStop(_) => "error stopping process",
            ResetSignal(_) => "error reseting signals",
            Seccomp(_) => "error in seccomp filter load",
            SeccompSendFd(_) => "error sending seccomp file descriptor",
            SeccompWaitFd(_) => "error waiting for parent to receive the seccomp file descriptor",
            UnshareFiles(_) => "error unsharing files",
            #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
            SetTSC(_) => "error setting timestamp counter prctl",
        }
    }
}

impl fmt::Display for Error {
    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
        use crate::unshare::Error::*;
        if let Some(code) = self.raw_os_error() {
            let errno = Errno::from_raw(code);
            if let nix::errno::Errno::UnknownErrno = errno {
                // May be OS knows error name better
                write!(
                    fmt,
                    "{}: {}",
                    self.title(),
                    io::Error::from_raw_os_error(code)
                )
            } else {
                // Format similar to that of std::io::Error
                write!(
                    fmt,
                    "{}: {} (os error {})",
                    self.title(),
                    errno.desc(),
                    code
                )
            }
        } else {
            match self {
                BeforeUnfreeze(err) => {
                    write!(fmt, "{}: {}", self.title(), err)
                }
                _ => write!(fmt, "{}", self.title()),
            }
        }
    }
}
