//! This module contains some logic for working with the console handle.

use std::io::Result;
use std::ops::Deref;
use std::ptr::null_mut;
use std::sync::Arc;

use winapi::shared::minwindef::DWORD;
use winapi::um::{
    fileapi::{CreateFileW, OPEN_EXISTING},
    handleapi::{CloseHandle, INVALID_HANDLE_VALUE},
    processenv::GetStdHandle,
    winbase::{STD_INPUT_HANDLE, STD_OUTPUT_HANDLE},
    winnt::{FILE_SHARE_READ, FILE_SHARE_WRITE, GENERIC_READ, GENERIC_WRITE, HANDLE},
};

use super::handle_result;

/// The standard handles of a process.
///
/// See [the Windows documentation on console
/// handles](https://docs.microsoft.com/en-us/windows/console/console-handles) for more info.
#[derive(Debug, Clone, Copy)]
pub enum HandleType {
    /// The process' standard output.
    OutputHandle,
    /// The process' standard input.
    InputHandle,
    /// The process' active console screen buffer, `CONOUT$`.
    CurrentOutputHandle,
    /// The process' console input buffer, `CONIN$`.
    CurrentInputHandle,
}

/// Inner structure for closing a handle on Drop.
///
/// The second parameter indicates if the HANDLE is exclusively owned or not.
/// A non-exclusive handle can be created using for example
/// `Handle::input_handle` or `Handle::output_handle`, which corresponds to
/// stdin and stdout respectively.
#[derive(Debug)]
struct Inner {
    handle: HANDLE,
    is_exclusive: bool,
}

impl Inner {
    fn new_exclusive(handle: HANDLE) -> Self {
        Inner {
            handle,
            is_exclusive: true,
        }
    }

    fn new_shared(handle: HANDLE) -> Self {
        Inner {
            handle,
            is_exclusive: false,
        }
    }
}

impl Drop for Inner {
    fn drop(&mut self) {
        if self.is_exclusive {
            assert!(
                unsafe { CloseHandle(self.handle) != 0 },
                "failed to close handle"
            )
        }
    }
}

unsafe impl Send for Inner {}

unsafe impl Sync for Inner {}

/// This abstracts away some WinAPI calls to set and get some console handles.
///
/// It wraps WinAPI's [`HANDLE`] type.
#[derive(Debug, Clone)]
pub struct Handle {
    handle: Arc<Inner>,
}

impl Handle {
    /// Create a new handle of a certaint type.
    pub fn new(handle: HandleType) -> Result<Handle> {
        match handle {
            HandleType::OutputHandle => Handle::output_handle(),
            HandleType::InputHandle => Handle::input_handle(),
            HandleType::CurrentOutputHandle => Handle::current_out_handle(),
            HandleType::CurrentInputHandle => Handle::current_in_handle(),
        }
    }

    /// Construct a handle from a raw handle.
    ///
    /// # Safety
    ///
    /// This is unsafe since there is not guarantee that the underlying HANDLE is thread-safe to implement `Send` and `Sync`.
    /// Most HANDLE's however, are thread safe.
    pub unsafe fn from_raw(handle: HANDLE) -> Self {
        Self {
            handle: Arc::new(Inner::new_exclusive(handle)),
        }
    }

    /// Get the handle of the active screen buffer.
    /// When using multiple screen buffers this will always point to the to the current screen output buffer.
    ///
    /// This function uses `CONOUT$` to create a file handle to the current output buffer.
    ///
    /// This wraps
    /// [`CreateFileW`](https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew).
    pub fn current_out_handle() -> Result<Handle> {
        let utf16: Vec<u16> = "CONOUT$\0".encode_utf16().collect();
        let utf16_ptr: *const u16 = utf16.as_ptr();

        let handle = handle_result(unsafe {
            CreateFileW(
                utf16_ptr,
                GENERIC_READ | GENERIC_WRITE,
                FILE_SHARE_READ | FILE_SHARE_WRITE,
                null_mut(),
                OPEN_EXISTING,
                0,
                null_mut(),
            )
        })?;

        Ok(Handle {
            handle: Arc::new(Inner::new_exclusive(handle)),
        })
    }

    /// Get the handle of the console input buffer.
    ///
    /// This function uses `CONIN$` to create a file handle to the current input buffer.
    ///
    /// This wraps
    /// [`CreateFileW`](https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew).
    pub fn current_in_handle() -> Result<Handle> {
        let utf16: Vec<u16> = "CONIN$\0".encode_utf16().collect();
        let utf16_ptr: *const u16 = utf16.as_ptr();

        let handle = handle_result(unsafe {
            CreateFileW(
                utf16_ptr,
                GENERIC_READ | GENERIC_WRITE,
                FILE_SHARE_READ | FILE_SHARE_WRITE,
                null_mut(),
                OPEN_EXISTING,
                0,
                null_mut(),
            )
        })?;

        Ok(Handle {
            handle: Arc::new(Inner::new_exclusive(handle)),
        })
    }

    /// Get the handle of the standard output.
    ///
    /// On success this function returns the `HANDLE` to `STD_OUTPUT_HANDLE`.
    ///
    /// This wraps [`GetStdHandle`](https://docs.microsoft.com/en-us/windows/console/getstdhandle)
    /// called with `STD_OUTPUT_HANDLE`.
    pub fn output_handle() -> Result<Handle> {
        Self::std_handle(STD_OUTPUT_HANDLE)
    }

    /// Get the handle of the input screen buffer.
    ///
    /// On success this function returns the `HANDLE` to `STD_INPUT_HANDLE`.
    ///
    /// This wraps [`GetStdHandle`](https://docs.microsoft.com/en-us/windows/console/getstdhandle)
    /// called with `STD_INPUT_HANDLE`.
    pub fn input_handle() -> Result<Handle> {
        Self::std_handle(STD_INPUT_HANDLE)
    }

    fn std_handle(which_std: DWORD) -> Result<Handle> {
        let handle = handle_result(unsafe { GetStdHandle(which_std) })?;

        Ok(Handle {
            handle: Arc::new(Inner::new_shared(handle)),
        })
    }

    /// Checks if the console handle is an invalid handle value.
    ///
    /// This is done by checking if the passed `HANDLE` is equal to `INVALID_HANDLE_VALUE`.
    pub fn is_valid_handle(handle: &HANDLE) -> bool {
        *handle != INVALID_HANDLE_VALUE
    }
}

impl Deref for Handle {
    type Target = HANDLE;

    fn deref(&self) -> &HANDLE {
        &self.handle.handle
    }
}

#[cfg(test)]
mod tests {
    use super::{Handle, HandleType};

    #[test]
    fn test_get_handle() {
        assert!(Handle::new(HandleType::OutputHandle).is_ok());
        assert!(Handle::new(HandleType::InputHandle).is_ok());
        assert!(Handle::new(HandleType::CurrentOutputHandle).is_ok());
        assert!(Handle::new(HandleType::CurrentInputHandle).is_ok());
    }
}
