use itertools::Itertools;
use smallvec::SmallVec;
use std::rc::Rc;
use util::ResultExt;
use uuid::Uuid;
use windows::{
    core::*,
    Win32::{
        Foundation::*,
        Graphics::Gdi::*,
        UI::{
            HiDpi::{GetDpiForMonitor, MDT_EFFECTIVE_DPI},
            WindowsAndMessaging::USER_DEFAULT_SCREEN_DPI,
        },
    },
};

use crate::{logical_point, point, size, Bounds, DevicePixels, DisplayId, Pixels, PlatformDisplay};

#[derive(Debug, Clone, Copy)]
pub(crate) struct WindowsDisplay {
    pub handle: HMONITOR,
    pub display_id: DisplayId,
    scale_factor: f32,
    bounds: Bounds<Pixels>,
    physical_bounds: Bounds<DevicePixels>,
    uuid: Uuid,
}

// The `HMONITOR` is thread-safe.
unsafe impl Send for WindowsDisplay {}
unsafe impl Sync for WindowsDisplay {}

impl WindowsDisplay {
    pub(crate) fn new(display_id: DisplayId) -> Option<Self> {
        let screen = available_monitors().into_iter().nth(display_id.0 as _)?;
        let info = get_monitor_info(screen).log_err()?;
        let monitor_size = info.monitorInfo.rcMonitor;
        let uuid = generate_uuid(&info.szDevice);
        let scale_factor = get_scale_factor_for_monitor(screen).log_err()?;
        let physical_size = size(
            (monitor_size.right - monitor_size.left).into(),
            (monitor_size.bottom - monitor_size.top).into(),
        );

        Some(WindowsDisplay {
            handle: screen,
            display_id,
            scale_factor,
            bounds: Bounds {
                origin: logical_point(
                    monitor_size.left as f32,
                    monitor_size.top as f32,
                    scale_factor,
                ),
                size: physical_size.to_pixels(scale_factor),
            },
            physical_bounds: Bounds {
                origin: point(monitor_size.left.into(), monitor_size.top.into()),
                size: physical_size,
            },
            uuid,
        })
    }

    pub fn new_with_handle(monitor: HMONITOR) -> Self {
        let info = get_monitor_info(monitor).expect("unable to get monitor info");
        let monitor_size = info.monitorInfo.rcMonitor;
        let uuid = generate_uuid(&info.szDevice);
        let display_id = available_monitors()
            .iter()
            .position(|handle| handle.0 == monitor.0)
            .unwrap();
        let scale_factor =
            get_scale_factor_for_monitor(monitor).expect("unable to get scale factor for monitor");
        let physical_size = size(
            (monitor_size.right - monitor_size.left).into(),
            (monitor_size.bottom - monitor_size.top).into(),
        );

        WindowsDisplay {
            handle: monitor,
            display_id: DisplayId(display_id as _),
            scale_factor,
            bounds: Bounds {
                origin: logical_point(
                    monitor_size.left as f32,
                    monitor_size.top as f32,
                    scale_factor,
                ),
                size: physical_size.to_pixels(scale_factor),
            },
            physical_bounds: Bounds {
                origin: point(monitor_size.left.into(), monitor_size.top.into()),
                size: physical_size,
            },
            uuid,
        }
    }

    fn new_with_handle_and_id(handle: HMONITOR, display_id: DisplayId) -> Self {
        let info = get_monitor_info(handle).expect("unable to get monitor info");
        let monitor_size = info.monitorInfo.rcMonitor;
        let uuid = generate_uuid(&info.szDevice);
        let scale_factor =
            get_scale_factor_for_monitor(handle).expect("unable to get scale factor for monitor");
        let physical_size = size(
            (monitor_size.right - monitor_size.left).into(),
            (monitor_size.bottom - monitor_size.top).into(),
        );

        WindowsDisplay {
            handle,
            display_id,
            scale_factor,
            bounds: Bounds {
                origin: logical_point(
                    monitor_size.left as f32,
                    monitor_size.top as f32,
                    scale_factor,
                ),
                size: physical_size.to_pixels(scale_factor),
            },
            physical_bounds: Bounds {
                origin: point(monitor_size.left.into(), monitor_size.top.into()),
                size: physical_size,
            },
            uuid,
        }
    }

    pub fn primary_monitor() -> Option<Self> {
        // https://devblogs.microsoft.com/oldnewthing/20070809-00/?p=25643
        const POINT_ZERO: POINT = POINT { x: 0, y: 0 };
        let monitor = unsafe { MonitorFromPoint(POINT_ZERO, MONITOR_DEFAULTTOPRIMARY) };
        if monitor.is_invalid() {
            log::error!(
                "can not find the primary monitor: {}",
                std::io::Error::last_os_error()
            );
            return None;
        }
        Some(WindowsDisplay::new_with_handle(monitor))
    }

    /// Check if the center point of given bounds is inside this monitor
    pub fn check_given_bounds(&self, bounds: Bounds<Pixels>) -> bool {
        let center = bounds.center();
        let center = POINT {
            x: (center.x.0 * self.scale_factor) as i32,
            y: (center.y.0 * self.scale_factor) as i32,
        };
        let monitor = unsafe { MonitorFromPoint(center, MONITOR_DEFAULTTONULL) };
        if monitor.is_invalid() {
            false
        } else {
            let display = WindowsDisplay::new_with_handle(monitor);
            display.uuid == self.uuid
        }
    }

    pub fn displays() -> Vec<Rc<dyn PlatformDisplay>> {
        available_monitors()
            .into_iter()
            .enumerate()
            .map(|(id, handle)| {
                Rc::new(WindowsDisplay::new_with_handle_and_id(
                    handle,
                    DisplayId(id as _),
                )) as Rc<dyn PlatformDisplay>
            })
            .collect()
    }

    pub(crate) fn frequency(&self) -> Option<u32> {
        get_monitor_info(self.handle).ok().and_then(|info| {
            let mut devmode = DEVMODEW::default();
            unsafe {
                EnumDisplaySettingsW(
                    PCWSTR(info.szDevice.as_ptr()),
                    ENUM_CURRENT_SETTINGS,
                    &mut devmode,
                )
            }
            .as_bool()
            .then(|| devmode.dmDisplayFrequency)
        })
    }

    /// Check if this monitor is still online
    pub fn is_connected(hmonitor: HMONITOR) -> bool {
        available_monitors().iter().contains(&hmonitor)
    }

    pub fn physical_bounds(&self) -> Bounds<DevicePixels> {
        self.physical_bounds
    }
}

impl PlatformDisplay for WindowsDisplay {
    fn id(&self) -> DisplayId {
        self.display_id
    }

    fn uuid(&self) -> anyhow::Result<Uuid> {
        Ok(self.uuid)
    }

    fn bounds(&self) -> Bounds<Pixels> {
        self.bounds
    }
}

fn available_monitors() -> SmallVec<[HMONITOR; 4]> {
    let mut monitors: SmallVec<[HMONITOR; 4]> = SmallVec::new();
    unsafe {
        EnumDisplayMonitors(
            None,
            None,
            Some(monitor_enum_proc),
            LPARAM(&mut monitors as *mut _ as _),
        )
        .ok()
        .log_err();
    }
    monitors
}

unsafe extern "system" fn monitor_enum_proc(
    hmonitor: HMONITOR,
    _hdc: HDC,
    _place: *mut RECT,
    data: LPARAM,
) -> BOOL {
    let monitors = data.0 as *mut SmallVec<[HMONITOR; 4]>;
    unsafe { (*monitors).push(hmonitor) };
    BOOL(1)
}

fn get_monitor_info(hmonitor: HMONITOR) -> anyhow::Result<MONITORINFOEXW> {
    let mut monitor_info: MONITORINFOEXW = unsafe { std::mem::zeroed() };
    monitor_info.monitorInfo.cbSize = std::mem::size_of::<MONITORINFOEXW>() as u32;
    let status = unsafe {
        GetMonitorInfoW(
            hmonitor,
            &mut monitor_info as *mut MONITORINFOEXW as *mut MONITORINFO,
        )
    };
    if status.as_bool() {
        Ok(monitor_info)
    } else {
        Err(anyhow::anyhow!(std::io::Error::last_os_error()))
    }
}

fn generate_uuid(device_name: &[u16]) -> Uuid {
    let name = device_name
        .iter()
        .flat_map(|&a| a.to_be_bytes().to_vec())
        .collect_vec();
    Uuid::new_v5(&Uuid::NAMESPACE_DNS, &name)
}

fn get_scale_factor_for_monitor(monitor: HMONITOR) -> Result<f32> {
    let mut dpi_x = 0;
    let mut dpi_y = 0;
    unsafe { GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, &mut dpi_x, &mut dpi_y) }?;
    assert_eq!(dpi_x, dpi_y);
    Ok(dpi_x as f32 / USER_DEFAULT_SCREEN_DPI as f32)
}
