use std::sync::MutexGuard;
use std::{fmt, io, ptr};

use bitflags::bitflags;
use dpi::{PhysicalPosition, PhysicalSize, Size};
use windows_sys::Win32::Foundation::{HWND, RECT};
use windows_sys::Win32::Graphics::Gdi::InvalidateRgn;
use windows_sys::Win32::UI::WindowsAndMessaging::{
    AdjustWindowRectEx, EnableMenuItem, GWL_EXSTYLE, GWL_STYLE, GetMenu, GetSystemMenu,
    GetWindowLongW, HWND_BOTTOM, HWND_NOTOPMOST, HWND_TOPMOST, MF_BYCOMMAND, MF_DISABLED,
    MF_ENABLED, SC_CLOSE, SW_HIDE, SW_MAXIMIZE, SW_MINIMIZE, SW_RESTORE, SW_SHOW,
    SW_SHOWNOACTIVATE, SWP_ASYNCWINDOWPOS, SWP_FRAMECHANGED, SWP_NOACTIVATE, SWP_NOMOVE,
    SWP_NOREPOSITION, SWP_NOSIZE, SWP_NOZORDER, SendMessageW, SetWindowLongW, SetWindowPos,
    ShowWindow, WINDOW_EX_STYLE, WINDOW_STYLE, WINDOWPLACEMENT, WS_BORDER, WS_CAPTION, WS_CHILD,
    WS_CLIPCHILDREN, WS_CLIPSIBLINGS, WS_EX_ACCEPTFILES, WS_EX_APPWINDOW, WS_EX_LAYERED,
    WS_EX_NOACTIVATE, WS_EX_NOREDIRECTIONBITMAP, WS_EX_TOPMOST, WS_EX_TRANSPARENT,
    WS_EX_WINDOWEDGE, WS_MAXIMIZE, WS_MAXIMIZEBOX, WS_MINIMIZE, WS_MINIMIZEBOX,
    WS_OVERLAPPEDWINDOW, WS_POPUP, WS_SIZEBOX, WS_SYSMENU, WS_VISIBLE,
};
use winit_core::icon::Icon;
use winit_core::keyboard::ModifiersState;
use winit_core::monitor::Fullscreen;
use winit_core::window::{ImeCapabilities, Theme, WindowAttributes, WindowPositioner, WindowType};

use crate::{SelectedCursor, WindowAttributesWindows, event_loop, util};

/// Contains information about states and the window that the callback is going to use.
#[derive(Debug)]
pub(crate) struct WindowState {
    pub mouse: MouseProperties,

    /// Used by `WM_GETMINMAXINFO`.
    pub min_size: Option<Size>,
    pub max_size: Option<Size>,

    /// The last known size of the window surface
    pub surface_size: PhysicalSize<u32>,

    pub surface_resize_increments: Option<Size>,

    pub window_icon: Option<Icon>,
    pub taskbar_icon: Option<Icon>,

    pub saved_window: Option<SavedWindow>,
    pub scale_factor: f64,

    pub modifiers_state: ModifiersState,
    pub fullscreen: Option<Fullscreen>,
    pub current_theme: Theme,
    pub preferred_theme: Option<Theme>,

    pub window_flags: WindowFlags,

    /// The role of this window. Governs creation/role behavior (OS window style, decorations)
    /// only -- see `anchored` for whether this window is positioned via the anchor system.
    pub window_type: WindowType,

    /// Whether this window is positioned relative to its parent using the anchor/gravity/
    /// positioner system, either because it's a [`WindowType::Popup`] or because anchor
    /// attributes were set on a [`WindowType::Window`]. Stored here (rather than only on the
    /// `Window` struct) so it stays reachable from just an `hwnd` via `GWL_USERDATA` -- e.g. to
    /// reposition an anchored window when its parent moves.
    pub anchored: bool,

    /// The positioner state backing `anchored` placement, meaningful only when `anchored` is
    /// `true`.
    pub positioner: WindowPositioner,

    pub ime_state: ImeState,
    pub ime_capabilities: Option<ImeCapabilities>,

    // Used by WM_NCACTIVATE, WM_SETFOCUS and WM_KILLFOCUS
    pub is_active: bool,
    pub is_focused: bool,

    // Flag whether redraw was requested.
    pub redraw_requested: bool,

    pub dragging: bool,

    pub skip_taskbar: bool,

    pub use_system_wheel_speed: bool,
}

#[derive(Clone)]
pub struct SavedWindow {
    pub placement: WINDOWPLACEMENT,
}

impl fmt::Debug for SavedWindow {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("SavedWindow").finish_non_exhaustive()
    }
}

#[derive(Clone, Debug)]
pub struct MouseProperties {
    pub(crate) selected_cursor: SelectedCursor,
    pub capture_count: u32,
    cursor_flags: CursorFlags,
    pub last_position: Option<PhysicalPosition<f64>>,
}

bitflags! {
    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
    pub struct CursorFlags: u8 {
        const GRABBED   = 1 << 0;
        const HIDDEN    = 1 << 1;
        const IN_WINDOW = 1 << 2;
        const LOCKED    = 1 << 3;
    }
}
bitflags! {
    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
    pub struct WindowFlags: u32 {
        const RESIZABLE         = 1 << 0;
        const MINIMIZABLE       = 1 << 1;
        const MAXIMIZABLE       = 1 << 2;
        const CLOSABLE          = 1 << 3;
        const VISIBLE           = 1 << 4;
        const ON_TASKBAR        = 1 << 5;
        const ALWAYS_ON_TOP     = 1 << 6;
        const ALWAYS_ON_BOTTOM  = 1 << 7;
        const NO_BACK_BUFFER    = 1 << 8;
        const TRANSPARENT       = 1 << 9;
        const CHILD             = 1 << 10;
        const MAXIMIZED         = 1 << 11;
        const POPUP             = 1 << 12;

        /// Marker flag for fullscreen. Should always match `WindowState::fullscreen`, but is
        /// included here to make masking easier.
        const MARKER_EXCLUSIVE_FULLSCREEN = 1 << 13;
        const MARKER_BORDERLESS_FULLSCREEN = 1 << 14;

        /// The `WM_SIZE` event contains some parameters that can effect the state of `WindowFlags`.
        /// In most cases, it's okay to let those parameters change the state. However, when we're
        /// running the `WindowFlags::apply_diff` function, we *don't* want those parameters to
        /// effect our stored state, because the purpose of `apply_diff` is to update the actual
        /// window's state to match our stored state. This controls whether to accept those changes.
        const MARKER_RETAIN_STATE_ON_SIZE = 1 << 15;

        const MARKER_IN_SIZE_MOVE = 1 << 16;

        const MINIMIZED = 1 << 17;

        const IGNORE_CURSOR_EVENT = 1 << 18;

        /// Fully decorated window (incl. caption, border and drop shadow).
        const MARKER_DECORATIONS = 1 << 19;
        /// Drop shadow for undecorated windows.
        const MARKER_UNDECORATED_SHADOW = 1 << 20;

        const MARKER_ACTIVATE = 1 << 21;

        const CLIP_CHILDREN = 1 << 22;

        /// Whether this window is positioned relative to its parent via the anchor/gravity/
        /// positioner system. Independent of `POPUP`, which only selects the OS window style --
        /// a `WindowType::Window` can be `ANCHORED` too. Used to pick the coordinate frame in
        /// `translate_outer_position`/`translate_outer_position_to_parent`.
        const ANCHORED = 1 << 23;

        const EXCLUSIVE_FULLSCREEN_OR_MASK = WindowFlags::ALWAYS_ON_TOP.bits();
    }
}

#[derive(Debug, Eq, PartialEq, Hash)]
pub enum ImeState {
    Disabled,
    Enabled,
    Preedit,
}

impl WindowState {
    pub(crate) fn new(
        attributes: &WindowAttributes,
        _win_attributes: &WindowAttributesWindows,
        scale_factor: f64,
        current_theme: Theme,
        preferred_theme: Option<Theme>,
    ) -> WindowState {
        WindowState {
            mouse: MouseProperties {
                selected_cursor: SelectedCursor::default(),
                capture_count: 0,
                cursor_flags: CursorFlags::empty(),
                last_position: None,
            },

            min_size: attributes.min_surface_size,
            max_size: attributes.max_surface_size,

            surface_size: PhysicalSize::default(),

            surface_resize_increments: attributes.surface_resize_increments,

            window_icon: attributes.window_icon.clone(),
            taskbar_icon: None,

            saved_window: None,
            scale_factor,

            modifiers_state: ModifiersState::default(),
            fullscreen: None,
            current_theme,
            preferred_theme,
            window_flags: WindowFlags::empty(),

            window_type: attributes.window_type,
            anchored: matches!(attributes.window_type, WindowType::Popup)
                || attributes.positioner.is_some(),
            positioner: attributes.positioner.unwrap_or_default(),

            ime_state: ImeState::Disabled,
            ime_capabilities: None,

            is_active: false,
            is_focused: false,
            redraw_requested: false,

            dragging: false,

            skip_taskbar: false,

            use_system_wheel_speed: true,
        }
    }

    pub fn window_flags(&self) -> WindowFlags {
        self.window_flags
    }

    pub fn set_window_flags<F>(mut this: MutexGuard<'_, Self>, window: HWND, f: F)
    where
        F: FnOnce(&mut WindowFlags),
    {
        let old_flags = this.window_flags;
        f(&mut this.window_flags);
        let new_flags = this.window_flags;

        drop(this);
        old_flags.apply_diff(window, new_flags);
    }

    pub fn set_window_flags_in_place<F>(&mut self, f: F)
    where
        F: FnOnce(&mut WindowFlags),
    {
        f(&mut self.window_flags);
    }

    pub fn has_active_focus(&self) -> bool {
        self.is_active && self.is_focused
    }

    // Updates is_active and returns whether active-focus state has changed
    pub fn set_active(&mut self, is_active: bool) -> bool {
        let old = self.has_active_focus();
        self.is_active = is_active;
        old != self.has_active_focus()
    }

    // Updates is_focused and returns whether active-focus state has changed
    pub fn set_focused(&mut self, is_focused: bool) -> bool {
        let old = self.has_active_focus();
        self.is_focused = is_focused;
        old != self.has_active_focus()
    }
}

impl MouseProperties {
    pub fn cursor_flags(&self) -> CursorFlags {
        self.cursor_flags
    }

    pub fn set_cursor_flags<F>(&mut self, window: HWND, f: F) -> Result<(), io::Error>
    where
        F: FnOnce(&mut CursorFlags),
    {
        let old_flags = self.cursor_flags;
        f(&mut self.cursor_flags);
        match self.cursor_flags.refresh_os_cursor(window) {
            Ok(()) => (),
            Err(e) => {
                self.cursor_flags = old_flags;
                return Err(e);
            },
        }

        Ok(())
    }
}

impl WindowFlags {
    fn mask(mut self) -> WindowFlags {
        if self.contains(WindowFlags::MARKER_EXCLUSIVE_FULLSCREEN) {
            self |= WindowFlags::EXCLUSIVE_FULLSCREEN_OR_MASK;
        }
        self
    }

    pub fn to_window_styles(self) -> (WINDOW_STYLE, WINDOW_EX_STYLE) {
        // Required styles to properly support common window functionality like aero snap.
        let mut style = WS_CLIPSIBLINGS;
        let mut style_ex = WS_EX_WINDOWEDGE | WS_EX_ACCEPTFILES;
        if self.contains(WindowFlags::POPUP) {
            style |= WS_POPUP;
            // Don't activate the popup (and thus don't deactivate the parent) when it is shown or
            // clicked, unless the caller requested activation via `WindowAttributes::active`.
            if !self.contains(WindowFlags::MARKER_ACTIVATE) {
                style_ex |= WS_EX_NOACTIVATE;
            }
        } else {
            style |= WS_CAPTION | WS_SYSMENU | WS_BORDER;

            if self.contains(WindowFlags::RESIZABLE) {
                style |= WS_SIZEBOX;
            }
            if self.contains(WindowFlags::MAXIMIZABLE) {
                style |= WS_MAXIMIZEBOX;
            }
            if self.contains(WindowFlags::MINIMIZABLE) {
                style |= WS_MINIMIZEBOX;
            }

            if self.contains(WindowFlags::ON_TASKBAR) {
                style_ex |= WS_EX_APPWINDOW;
            }
        };

        if self.contains(WindowFlags::VISIBLE) {
            style |= WS_VISIBLE;
        }
        if self.contains(WindowFlags::ALWAYS_ON_TOP) {
            style_ex |= WS_EX_TOPMOST;
        }
        if self.contains(WindowFlags::NO_BACK_BUFFER) {
            style_ex |= WS_EX_NOREDIRECTIONBITMAP;
        }
        if self.contains(WindowFlags::CHILD) && !self.contains(WindowFlags::POPUP) {
            style |= WS_CHILD;

            // Remove decorations window styles for child
            if !self.contains(WindowFlags::MARKER_DECORATIONS) {
                style &= !(WS_CAPTION | WS_BORDER);
                style_ex &= !WS_EX_WINDOWEDGE;
            }
        }
        if self.contains(WindowFlags::MINIMIZED) {
            style |= WS_MINIMIZE;
        }
        if self.contains(WindowFlags::MAXIMIZED) {
            style |= WS_MAXIMIZE;
        }
        if self.contains(WindowFlags::IGNORE_CURSOR_EVENT) {
            style_ex |= WS_EX_TRANSPARENT | WS_EX_LAYERED;
        }
        if self.contains(WindowFlags::CLIP_CHILDREN) {
            style |= WS_CLIPCHILDREN;
        }

        if self.intersects(
            WindowFlags::MARKER_EXCLUSIVE_FULLSCREEN | WindowFlags::MARKER_BORDERLESS_FULLSCREEN,
        ) {
            style &= !WS_OVERLAPPEDWINDOW;
        }

        (style, style_ex)
    }

    /// Adjust the window client rectangle to the return value, if present.
    fn apply_diff(mut self, window: HWND, mut new: WindowFlags) {
        self = self.mask();
        new = new.mask();

        let mut diff = self ^ new;

        if diff == WindowFlags::empty() {
            return;
        }

        if new.contains(WindowFlags::VISIBLE) {
            let flag = if !self.contains(WindowFlags::MARKER_ACTIVATE) {
                self.set(WindowFlags::MARKER_ACTIVATE, true);
                SW_SHOWNOACTIVATE
            } else {
                SW_SHOW
            };
            unsafe {
                ShowWindow(window, flag);
            }
        }

        if diff.intersects(WindowFlags::ALWAYS_ON_TOP | WindowFlags::ALWAYS_ON_BOTTOM) {
            unsafe {
                SetWindowPos(
                    window,
                    match (
                        new.contains(WindowFlags::ALWAYS_ON_TOP),
                        new.contains(WindowFlags::ALWAYS_ON_BOTTOM),
                    ) {
                        (true, false) => HWND_TOPMOST,
                        (false, false) => HWND_NOTOPMOST,
                        (false, true) => HWND_BOTTOM,
                        (true, true) => unreachable!(),
                    },
                    0,
                    0,
                    0,
                    0,
                    SWP_ASYNCWINDOWPOS | SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE,
                );
                InvalidateRgn(window, ptr::null_mut(), false.into());
            }
        }

        if diff.contains(WindowFlags::MAXIMIZED) || new.contains(WindowFlags::MAXIMIZED) {
            unsafe {
                ShowWindow(window, match new.contains(WindowFlags::MAXIMIZED) {
                    true => SW_MAXIMIZE,
                    false => SW_RESTORE,
                });
            }
        }

        // Minimize operations should execute after maximize for proper window animations
        if diff.contains(WindowFlags::MINIMIZED) {
            unsafe {
                ShowWindow(window, match new.contains(WindowFlags::MINIMIZED) {
                    true => SW_MINIMIZE,
                    false => SW_RESTORE,
                });
            }

            diff.remove(WindowFlags::MINIMIZED);
        }

        if diff.contains(WindowFlags::CLOSABLE) || new.contains(WindowFlags::CLOSABLE) {
            let flags = MF_BYCOMMAND
                | if new.contains(WindowFlags::CLOSABLE) { MF_ENABLED } else { MF_DISABLED };

            unsafe {
                EnableMenuItem(GetSystemMenu(window, 0), SC_CLOSE, flags);
            }
        }

        if !new.contains(WindowFlags::VISIBLE) {
            unsafe {
                ShowWindow(window, SW_HIDE);
            }
        }

        if diff != WindowFlags::empty() {
            let (style, style_ex) = new.to_window_styles();

            unsafe {
                SendMessageW(window, event_loop::SET_RETAIN_STATE_ON_SIZE_MSG_ID.get(), 1, 0);

                // This condition is necessary to avoid having an unrestorable window
                if !new.contains(WindowFlags::MINIMIZED) {
                    SetWindowLongW(window, GWL_STYLE, style as i32);
                    SetWindowLongW(window, GWL_EXSTYLE, style_ex as i32);
                }

                let mut flags = SWP_NOZORDER | SWP_NOMOVE | SWP_NOSIZE | SWP_FRAMECHANGED;

                // We generally don't want style changes here to affect window
                // focus, but for fullscreen windows they must be activated
                // (i.e. focused) so that they appear on top of the taskbar
                if !new.contains(WindowFlags::MARKER_EXCLUSIVE_FULLSCREEN)
                    && !new.contains(WindowFlags::MARKER_BORDERLESS_FULLSCREEN)
                {
                    flags |= SWP_NOACTIVATE;
                }

                // Refresh the window frame
                SetWindowPos(window, ptr::null_mut(), 0, 0, 0, 0, flags);
                SendMessageW(window, event_loop::SET_RETAIN_STATE_ON_SIZE_MSG_ID.get(), 0, 0);
            }
        }
    }

    pub fn adjust_rect(self, hwnd: HWND, mut rect: RECT) -> Result<RECT, io::Error> {
        unsafe {
            let mut style = GetWindowLongW(hwnd, GWL_STYLE) as u32;
            let style_ex = GetWindowLongW(hwnd, GWL_EXSTYLE) as u32;

            // Frameless style implemented by manually overriding the non-client area in
            // `WM_NCCALCSIZE`.
            if !self.contains(WindowFlags::MARKER_DECORATIONS) {
                style &= !(WS_CAPTION | WS_SIZEBOX);
            }

            util::win_to_err({
                let b_menu = !GetMenu(hwnd).is_null();
                if let (Some(get_dpi_for_window), Some(adjust_window_rect_ex_for_dpi)) =
                    (*util::GET_DPI_FOR_WINDOW, *util::ADJUST_WINDOW_RECT_EX_FOR_DPI)
                {
                    let dpi = get_dpi_for_window(hwnd);
                    adjust_window_rect_ex_for_dpi(&mut rect, style, b_menu.into(), style_ex, dpi)
                } else {
                    AdjustWindowRectEx(&mut rect, style, b_menu.into(), style_ex)
                }
            })?;
            Ok(rect)
        }
    }

    pub fn adjust_size(self, hwnd: HWND, size: PhysicalSize<u32>) -> PhysicalSize<u32> {
        let (width, height): (u32, u32) = size.into();
        let rect = RECT { left: 0, right: width as i32, top: 0, bottom: height as i32 };
        let rect = self.adjust_rect(hwnd, rect).unwrap_or(rect);

        let outer_x = (rect.right - rect.left).abs();
        let outer_y = (rect.top - rect.bottom).abs();

        PhysicalSize::new(outer_x as _, outer_y as _)
    }

    pub fn set_size(self, hwnd: HWND, size: PhysicalSize<u32>) {
        unsafe {
            let (width, height): (u32, u32) = self.adjust_size(hwnd, size).into();
            SetWindowPos(
                hwnd,
                ptr::null_mut(),
                0,
                0,
                width as _,
                height as _,
                SWP_ASYNCWINDOWPOS | SWP_NOZORDER | SWP_NOREPOSITION | SWP_NOMOVE | SWP_NOACTIVATE,
            );
            InvalidateRgn(hwnd, ptr::null_mut(), false.into());
        }
    }
}

impl CursorFlags {
    fn refresh_os_cursor(self, window: HWND) -> Result<(), io::Error> {
        let client_rect = util::WindowArea::Inner.get_rect(window)?;

        if util::is_focused(window) {
            let cursor_clip = match self.contains(CursorFlags::GRABBED) {
                true => {
                    if self.contains(CursorFlags::LOCKED) {
                        if let Ok(pos) = util::get_cursor_position() {
                            Some(RECT {
                                left: pos.x,
                                right: pos.x + 1,
                                top: pos.y,
                                bottom: pos.y + 1,
                            })
                        } else {
                            // If lock is applied while the cursor is not available, lock it to the
                            // middle of the window.
                            let cx = (client_rect.left + client_rect.right) / 2;
                            let cy = (client_rect.top + client_rect.bottom) / 2;
                            Some(RECT { left: cx, right: cx + 1, top: cy, bottom: cy + 1 })
                        }
                    } else {
                        Some(client_rect)
                    }
                },
                false => None,
            };

            let rect_to_tuple = |rect: RECT| (rect.left, rect.top, rect.right, rect.bottom);
            let active_cursor_clip = rect_to_tuple(util::get_cursor_clip()?);
            let desktop_rect = rect_to_tuple(util::get_desktop_rect());

            let active_cursor_clip = match desktop_rect == active_cursor_clip {
                true => None,
                false => Some(active_cursor_clip),
            };

            // We do this check because calling `set_cursor_clip` incessantly will flood the event
            // loop with `WM_MOUSEMOVE` events, and `refresh_os_cursor` is called by
            // `set_cursor_flags` which at times gets called once every iteration of the
            // eventloop.
            if active_cursor_clip != cursor_clip.map(rect_to_tuple) {
                util::set_cursor_clip(cursor_clip)?;
            }
        }

        let cursor_in_client = self.contains(CursorFlags::IN_WINDOW);
        if cursor_in_client {
            util::set_cursor_hidden(self.contains(CursorFlags::HIDDEN));
        } else {
            util::set_cursor_hidden(false);
        }

        Ok(())
    }
}
