macro_rules! lock_impl {
    ($file: ty) => {
        pub fn lock_shared(file: &$file) -> Result<()> {
            lock_file(file, 0)
        }

        pub fn lock_exclusive(file: &$file) -> Result<()> {
            lock_file(file, LOCKFILE_EXCLUSIVE_LOCK)
        }

        pub fn try_lock_shared(file: &$file) -> Result<bool> {
            try_lock_file(file, LOCKFILE_FAIL_IMMEDIATELY)
        }

        pub fn try_lock_exclusive(file: &$file) -> Result<bool> {
            try_lock_file(file, LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY)
        }

        pub fn unlock(file: &$file) -> Result<()> {
            unsafe {
                let ret = UnlockFile(file.as_raw_handle() as HANDLE, 0, 0, !0, !0);
                if ret == 0 {
                    Err(Error::last_os_error())
                } else {
                    Ok(())
                }
            }
        }

        fn lock_file(file: &$file, flags: u32) -> Result<()> {
            unsafe {
                let mut overlapped = mem::zeroed();
                let ret = LockFileEx(
                    file.as_raw_handle() as HANDLE,
                    flags,
                    0,
                    !0,
                    !0,
                    &mut overlapped,
                );
                if ret == 0 {
                    Err(Error::last_os_error())
                } else {
                    Ok(())
                }
            }
        }

        fn try_lock_file(file: &$file, flags: u32) -> Result<bool> {
            unsafe {
                let mut overlapped = mem::zeroed();
                let ret = LockFileEx(
                    file.as_raw_handle() as HANDLE,
                    flags,
                    0,
                    !0,
                    !0,
                    &mut overlapped,
                );
                if ret == 0 {
                    let err = Error::last_os_error();
                    if err.raw_os_error()
                        == Some(::windows_sys::Win32::Foundation::ERROR_IO_PENDING as i32)
                        || err.raw_os_error()
                            == Some(::windows_sys::Win32::Foundation::ERROR_LOCK_VIOLATION as i32)
                    {
                        return Ok(false);
                    }

                    Err(err)
                } else {
                    Ok(true)
                }
            }
        }
    };
}

#[cfg(any(
    feature = "smol",
    feature = "async-std",
    feature = "tokio",
    feature = "fs-err-tokio"
))]
pub(crate) mod async_impl;
#[cfg(any(feature = "sync", feature = "fs-err"))]
pub(crate) mod sync_impl;

use crate::FsStats;
use std::io::{Error, Result};
use std::os::windows::ffi::OsStrExt;
use std::path::Path;
use windows_sys::Win32::Foundation::ERROR_LOCK_VIOLATION;
use windows_sys::Win32::Storage::FileSystem::{
    GetDiskFreeSpaceExW, GetDiskFreeSpaceW, GetVolumePathNameW,
};

pub fn lock_error() -> Error {
    Error::from_raw_os_error(ERROR_LOCK_VIOLATION as i32)
}

fn volume_path(path: &Path, volume_path: &mut [u16]) -> Result<()> {
    let path_utf8: Vec<u16> = path.as_os_str().encode_wide().chain(Some(0)).collect();
    unsafe {
        let ret = GetVolumePathNameW(
            path_utf8.as_ptr(),
            volume_path.as_mut_ptr(),
            volume_path.len() as u32,
        );
        if ret == 0 {
            Err(Error::last_os_error())
        } else {
            Ok(())
        }
    }
}

pub fn statvfs(path: &Path) -> Result<FsStats> {
    let root_path: &mut [u16] = &mut [0; 261];
    volume_path(path, root_path)?;
    unsafe {
        let mut free_space = 0;
        let mut total_space = 0;
        let ret = GetDiskFreeSpaceExW(
            root_path.as_ptr(),
            &mut free_space,
            &mut total_space,
            std::ptr::null_mut(),
        );
        if ret == 0 {
            return Err(Error::last_os_error());
        }

        let mut sectors_per_cluster = 0;
        let mut bytes_per_sector = 0;
        let mut _number_of_free_clusters = 0;
        let mut _total_number_of_clusters = 0;
        let ret = GetDiskFreeSpaceW(
            root_path.as_ptr(),
            &mut sectors_per_cluster,
            &mut bytes_per_sector,
            &mut _number_of_free_clusters,
            &mut _total_number_of_clusters,
        );
        if ret == 0 {
            Err(Error::last_os_error())
        } else {
            let bytes_per_cluster = sectors_per_cluster as u64 * bytes_per_sector as u64;
            Ok(FsStats {
                free_space,
                available_space: free_space,
                total_space,
                allocation_granularity: bytes_per_cluster,
            })
        }
    }
}
