use std::error::Error as StdError;
use std::fmt;
use std::io;
use std::path::PathBuf;

#[derive(Debug, Clone, Copy)]
pub(crate) enum ErrorKind {
    OpenFile,
    CreateFile,
    CreateDir,
    SyncFile,
    SetLen,
    Metadata,
    Clone,
    SetPermissions,
    Read,
    Seek,
    Write,
    Flush,
    ReadDir,
    RemoveFile,
    RemoveDir,
    Canonicalize,
    ReadLink,
    SymlinkMetadata,
    #[allow(dead_code)]
    FileExists,

    #[cfg(windows)]
    SeekRead,
    #[cfg(windows)]
    SeekWrite,

    #[cfg(unix)]
    ReadAt,
    #[cfg(unix)]
    WriteAt,
}

/// Contains an IO error that has a file path attached.
///
/// This type is never returned directly, but is instead wrapped inside yet
/// another IO error.
#[derive(Debug)]
pub(crate) struct Error {
    kind: ErrorKind,
    source: io::Error,
    path: PathBuf,
}

impl Error {
    pub fn build(source: io::Error, kind: ErrorKind, path: impl Into<PathBuf>) -> io::Error {
        io::Error::new(
            source.kind(),
            Self {
                kind,
                source,
                path: path.into(),
            },
        )
    }
}

impl fmt::Display for Error {
    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
        use ErrorKind as E;

        let path = self.path.display();

        match self.kind {
            E::OpenFile => write!(formatter, "failed to open file `{}`", path),
            E::CreateFile => write!(formatter, "failed to create file `{}`", path),
            E::CreateDir => write!(formatter, "failed to create directory `{}`", path),
            E::SyncFile => write!(formatter, "failed to sync file `{}`", path),
            E::SetLen => write!(formatter, "failed to set length of file `{}`", path),
            E::Metadata => write!(formatter, "failed to query metadata of file `{}`", path),
            E::Clone => write!(formatter, "failed to clone handle for file `{}`", path),
            E::SetPermissions => write!(formatter, "failed to set permissions for file `{}`", path),
            E::Read => write!(formatter, "failed to read from file `{}`", path),
            E::Seek => write!(formatter, "failed to seek in file `{}`", path),
            E::Write => write!(formatter, "failed to write to file `{}`", path),
            E::Flush => write!(formatter, "failed to flush file `{}`", path),
            E::ReadDir => write!(formatter, "failed to read directory `{}`", path),
            E::RemoveFile => write!(formatter, "failed to remove file `{}`", path),
            E::RemoveDir => write!(formatter, "failed to remove directory `{}`", path),
            E::Canonicalize => write!(formatter, "failed to canonicalize path `{}`", path),
            E::ReadLink => write!(formatter, "failed to read symbolic link `{}`", path),
            E::SymlinkMetadata => {
                write!(formatter, "failed to query metadata of symlink `{}`", path)
            }
            E::FileExists => write!(formatter, "failed to check file existence `{}`", path),

            #[cfg(windows)]
            E::SeekRead => write!(formatter, "failed to seek and read from `{}`", path),
            #[cfg(windows)]
            E::SeekWrite => write!(formatter, "failed to seek and write to `{}`", path),

            #[cfg(unix)]
            E::ReadAt => write!(formatter, "failed to read with offset from `{}`", path),
            #[cfg(unix)]
            E::WriteAt => write!(formatter, "failed to write with offset to `{}`", path),
        }?;

        // The `expose_original_error` feature indicates the caller should display the original error
        #[cfg(not(feature = "expose_original_error"))]
        write!(formatter, ": {}", self.source)?;

        Ok(())
    }
}

impl StdError for Error {
    fn cause(&self) -> Option<&dyn StdError> {
        self.source()
    }

    #[cfg(not(feature = "expose_original_error"))]
    fn source(&self) -> Option<&(dyn StdError + 'static)> {
        None
    }

    #[cfg(feature = "expose_original_error")]
    fn source(&self) -> Option<&(dyn StdError + 'static)> {
        Some(&self.source)
    }
}

#[derive(Debug, Clone, Copy)]
pub(crate) enum SourceDestErrorKind {
    Copy,
    HardLink,
    Rename,
    SoftLink,

    #[cfg(unix)]
    Symlink,

    #[cfg(windows)]
    SymlinkDir,
    #[cfg(windows)]
    SymlinkFile,
}

/// Error type used by functions like `fs::copy` that holds two paths.
#[derive(Debug)]
pub(crate) struct SourceDestError {
    kind: SourceDestErrorKind,
    source: io::Error,
    from_path: PathBuf,
    to_path: PathBuf,
}

impl SourceDestError {
    pub fn build(
        source: io::Error,
        kind: SourceDestErrorKind,
        from_path: impl Into<PathBuf>,
        to_path: impl Into<PathBuf>,
    ) -> io::Error {
        io::Error::new(
            source.kind(),
            Self {
                kind,
                source,
                from_path: from_path.into(),
                to_path: to_path.into(),
            },
        )
    }
}

impl fmt::Display for SourceDestError {
    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
        let from = self.from_path.display();
        let to = self.to_path.display();
        match self.kind {
            SourceDestErrorKind::Copy => {
                write!(formatter, "failed to copy file from {} to {}", from, to)
            }
            SourceDestErrorKind::HardLink => {
                write!(formatter, "failed to hardlink file from {} to {}", from, to)
            }
            SourceDestErrorKind::Rename => {
                write!(formatter, "failed to rename file from {} to {}", from, to)
            }
            SourceDestErrorKind::SoftLink => {
                write!(formatter, "failed to softlink file from {} to {}", from, to)
            }

            #[cfg(unix)]
            SourceDestErrorKind::Symlink => {
                write!(formatter, "failed to symlink file from {} to {}", from, to)
            }

            #[cfg(windows)]
            SourceDestErrorKind::SymlinkFile => {
                write!(formatter, "failed to symlink file from {} to {}", from, to)
            }
            #[cfg(windows)]
            SourceDestErrorKind::SymlinkDir => {
                write!(formatter, "failed to symlink dir from {} to {}", from, to)
            }
        }?;

        // The `expose_original_error` feature indicates the caller should display the original error
        #[cfg(not(feature = "expose_original_error"))]
        write!(formatter, ": {}", self.source)?;

        Ok(())
    }
}

impl StdError for SourceDestError {
    fn cause(&self) -> Option<&dyn StdError> {
        self.source()
    }

    #[cfg(not(feature = "expose_original_error"))]
    fn source(&self) -> Option<&(dyn StdError + 'static)> {
        None
    }

    #[cfg(feature = "expose_original_error")]
    fn source(&self) -> Option<&(dyn StdError + 'static)> {
        Some(&self.source)
    }
}
