use crate::errors::{Error, ErrorKind};
use std::fs::{Metadata, Permissions};
use std::io;
use std::io::{IoSlice, SeekFrom};
use std::path::{Path, PathBuf};
use std::pin::Pin;
use std::task::{ready, Context, Poll};
use tokio::fs;
use tokio::fs::File as TokioFile;
use tokio::io::{AsyncRead, AsyncSeek, AsyncWrite, ReadBuf};

use super::OpenOptions;

#[allow(unused_imports)]
use crate as fs_err; // for docs

/// Wrapper around [`tokio::fs::File`] which adds more helpful
/// information to all errors.
#[derive(Debug)]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))]
pub struct File {
    tokio: fs::File,
    path: PathBuf,
}

impl File {
    /// Attempts to open a file in read-only mode.
    ///
    /// Wrapper for [`tokio::fs::File::open`].
    pub async fn open(path: impl Into<PathBuf>) -> io::Result<File> {
        let path = path.into();
        let f = TokioFile::open(&path)
            .await
            .map_err(|err| Error::build(err, ErrorKind::OpenFile, &path))?;
        Ok(File::from_parts(f, path))
    }

    /// Opens a file in write-only mode.
    ///
    /// Wrapper for [`tokio::fs::File::create`].
    pub async fn create(path: impl Into<PathBuf>) -> io::Result<File> {
        let path = path.into();
        match TokioFile::create(&path).await {
            Ok(f) => Ok(File::from_parts(f, path)),
            Err(err) => Err(Error::build(err, ErrorKind::CreateFile, &path)),
        }
    }

    /// Opens a file in read-write mode.
    ///
    /// Wrapper for [`tokio::fs::File::create_new`].
    pub async fn create_new(path: impl Into<PathBuf>) -> Result<Self, io::Error> {
        let path = path.into();
        match fs::File::create_new(&path).await {
            Ok(file) => Ok(File::from_parts(file, path)),
            Err(err) => Err(Error::build(err, ErrorKind::CreateFile, path)),
        }
    }

    /// Returns a new `OpenOptions` object.
    ///
    /// Wrapper for [`tokio::fs::File::options`].
    pub fn options() -> OpenOptions {
        OpenOptions::new()
    }

    /// Converts a [`fs_err::File`] to a [`fs_err::tokio::File`].
    ///
    /// Wrapper for [`tokio::fs::File::from_std`].
    pub fn from_std(std: crate::File) -> File {
        let (std, path) = std.into_parts();
        File::from_parts(TokioFile::from_std(std), path)
    }

    /// Attempts to sync all OS-internal metadata to disk.
    ///
    /// Wrapper for [`tokio::fs::File::sync_all`].
    pub async fn sync_all(&self) -> io::Result<()> {
        self.tokio
            .sync_all()
            .await
            .map_err(|err| self.error(err, ErrorKind::SyncFile))
    }

    /// This function is similar to `sync_all`, except that it may not
    /// synchronize file metadata to the filesystem.
    ///
    /// Wrapper for [`tokio::fs::File::sync_data`].
    pub async fn sync_data(&self) -> io::Result<()> {
        self.tokio
            .sync_data()
            .await
            .map_err(|err| self.error(err, ErrorKind::SyncFile))
    }

    /// Truncates or extends the underlying file, updating the size of this file to become size.
    ///
    /// Wrapper for [`tokio::fs::File::set_len`].
    pub async fn set_len(&self, size: u64) -> io::Result<()> {
        self.tokio
            .set_len(size)
            .await
            .map_err(|err| self.error(err, ErrorKind::SetLen))
    }

    /// Queries metadata about the underlying file.
    ///
    /// Wrapper for [`tokio::fs::File::metadata`].
    pub async fn metadata(&self) -> io::Result<Metadata> {
        self.tokio
            .metadata()
            .await
            .map_err(|err| self.error(err, ErrorKind::Metadata))
    }

    /// Creates a new `File` instance that shares the same underlying file handle
    /// as the existing `File` instance. Reads, writes, and seeks will affect both
    /// `File` instances simultaneously.
    ///
    /// Wrapper for [`tokio::fs::File::try_clone`].
    pub async fn try_clone(&self) -> io::Result<File> {
        match self.tokio.try_clone().await {
            Ok(file) => Ok(File::from_parts(file, self.path.clone())),
            Err(err) => Err(self.error(err, ErrorKind::Clone)),
        }
    }

    /// Destructures `File` into a [`fs_err::File`]. This function is async
    /// to allow any in-flight operations to complete.
    ///
    /// Wrapper for [`tokio::fs::File::into_std`].
    pub async fn into_std(self) -> crate::File {
        crate::File::from_parts(self.tokio.into_std().await, self.path)
    }

    /// Tries to immediately destructure `File` into a [`fs_err::File`].
    ///
    /// Wrapper for [`tokio::fs::File::try_into_std`].
    pub fn try_into_std(self) -> Result<crate::File, File> {
        match self.tokio.try_into_std() {
            Ok(f) => Ok(crate::File::from_parts(f, self.path)),
            Err(f) => Err(File::from_parts(f, self.path)),
        }
    }

    /// Changes the permissions on the underlying file.
    ///
    /// Wrapper for [`tokio::fs::File::set_permissions`].
    pub async fn set_permissions(&self, perm: Permissions) -> io::Result<()> {
        self.tokio
            .set_permissions(perm)
            .await
            .map_err(|err| self.error(err, ErrorKind::SetPermissions))
    }
}

/// Methods added by fs-err that are not available on [`tokio::fs::File`].
impl File {
    /// Creates a [`File`] from a raw file and its path.
    pub fn from_parts<P>(file: TokioFile, path: P) -> Self
    where
        P: Into<PathBuf>,
    {
        File {
            tokio: file,
            path: path.into(),
        }
    }

    /// Extract the raw file and its path from this [`File`].
    pub fn into_parts(self) -> (TokioFile, PathBuf) {
        (self.tokio, self.path)
    }

    /// Returns a reference to the underlying [`tokio::fs::File`].
    pub fn file(&self) -> &TokioFile {
        &self.tokio
    }

    /// Returns a mutable reference to the underlying [`tokio::fs::File`].
    pub fn file_mut(&mut self) -> &mut TokioFile {
        &mut self.tokio
    }

    /// Returns a reference to the path that this file was created with.
    pub fn path(&self) -> &Path {
        &self.path
    }

    /// Wrap the error in information specific to this [`File`] object.
    fn error(&self, source: io::Error, kind: ErrorKind) -> io::Error {
        Error::build(source, kind, &self.path)
    }
}

impl From<crate::File> for File {
    fn from(f: crate::File) -> Self {
        let (f, path) = f.into_parts();
        File::from_parts(f.into(), path)
    }
}

impl From<File> for TokioFile {
    fn from(f: File) -> Self {
        f.into_parts().0
    }
}

#[cfg(unix)]
impl std::os::unix::io::AsRawFd for File {
    fn as_raw_fd(&self) -> std::os::unix::io::RawFd {
        self.tokio.as_raw_fd()
    }
}

#[cfg(windows)]
impl std::os::windows::io::AsRawHandle for File {
    fn as_raw_handle(&self) -> std::os::windows::io::RawHandle {
        self.tokio.as_raw_handle()
    }
}

impl AsyncRead for File {
    fn poll_read(
        mut self: Pin<&mut Self>,
        cx: &mut Context<'_>,
        buf: &mut ReadBuf<'_>,
    ) -> Poll<io::Result<()>> {
        Poll::Ready(
            ready!(Pin::new(&mut self.tokio).poll_read(cx, buf))
                .map_err(|err| self.error(err, ErrorKind::Read)),
        )
    }
}

impl AsyncSeek for File {
    fn start_seek(mut self: Pin<&mut Self>, position: SeekFrom) -> io::Result<()> {
        Pin::new(&mut self.tokio)
            .start_seek(position)
            .map_err(|err| self.error(err, ErrorKind::Seek))
    }

    fn poll_complete(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<u64>> {
        Poll::Ready(
            ready!(Pin::new(&mut self.tokio).poll_complete(cx))
                .map_err(|err| self.error(err, ErrorKind::Seek)),
        )
    }
}

impl AsyncWrite for File {
    fn poll_write(
        mut self: Pin<&mut Self>,
        cx: &mut Context<'_>,
        buf: &[u8],
    ) -> Poll<io::Result<usize>> {
        Poll::Ready(
            ready!(Pin::new(&mut self.tokio).poll_write(cx, buf))
                .map_err(|err| self.error(err, ErrorKind::Write)),
        )
    }

    fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
        Poll::Ready(
            ready!(Pin::new(&mut self.tokio).poll_flush(cx))
                .map_err(|err| self.error(err, ErrorKind::Flush)),
        )
    }

    fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
        Poll::Ready(
            ready!(Pin::new(&mut self.tokio).poll_shutdown(cx))
                .map_err(|err| self.error(err, ErrorKind::Flush)),
        )
    }

    fn poll_write_vectored(
        mut self: Pin<&mut Self>,
        cx: &mut Context<'_>,
        bufs: &[IoSlice<'_>],
    ) -> Poll<io::Result<usize>> {
        Poll::Ready(
            ready!(Pin::new(&mut self.tokio).poll_write_vectored(cx, bufs))
                .map_err(|err| self.error(err, ErrorKind::Write)),
        )
    }

    fn is_write_vectored(&self) -> bool {
        self.tokio.is_write_vectored()
    }
}
