// Copyright 2019 The Druid Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Windows implementation of features at the application scope.

use std::cell::RefCell;
use std::collections::HashMap;
use std::mem;
use std::ptr;
use std::rc::Rc;
use std::sync::atomic::{AtomicBool, Ordering};

use winapi::shared::minwindef::{BOOL, FALSE, HINSTANCE};
use winapi::shared::ntdef::LPCWSTR;
use winapi::shared::windef::{DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2, HCURSOR, HWND};
use winapi::shared::winerror::HRESULT_FROM_WIN32;
use winapi::um::errhandlingapi::GetLastError;
use winapi::um::shellscalingapi::PROCESS_PER_MONITOR_DPI_AWARE;
use winapi::um::winnls::GetUserDefaultLocaleName;
use winapi::um::winnt::LOCALE_NAME_MAX_LENGTH;
use winapi::um::winuser::{
    DispatchMessageW, EnableWindow, GetAncestor, GetMessageW, LoadIconW, PeekMessageW,
    PostMessageW, PostQuitMessage, RegisterClassW, TranslateAcceleratorW, TranslateMessage,
    GA_ROOT, IDI_APPLICATION, MSG, PM_NOREMOVE, WM_TIMER, WNDCLASSW,
};

use piet_common::D2DLoadedFonts;

use crate::application::AppHandler;

use super::accels;
use super::clipboard::Clipboard;
use super::error::Error;
use super::util::{self, FromWide, ToWide, CLASS_NAME, OPTIONAL_FUNCTIONS};
use super::window::{self, DS_REQUEST_DESTROY};

#[derive(Clone)]
pub(crate) struct Application {
    state: Rc<RefCell<State>>,
    pub(crate) fonts: D2DLoadedFonts,
}

struct State {
    quitting: bool,
    windows: HashMap<HWND, WindowState>,
}

struct WindowState {
    disable_count: usize,
}

impl WindowState {
    fn new() -> Self {
        Self { disable_count: 0 }
    }
}

/// Used to ensure the window class is registered only once per process.
static WINDOW_CLASS_REGISTERED: AtomicBool = AtomicBool::new(false);

impl Application {
    pub fn new() -> Result<Application, Error> {
        Application::init()?;
        let state = Rc::new(RefCell::new(State {
            quitting: false,
            windows: HashMap::new(),
        }));
        let fonts = D2DLoadedFonts::default();
        Ok(Application { state, fonts })
    }

    /// Initialize the app. At the moment, this is mostly needed for hi-dpi.
    // TODO: Report back an error instead of panicking
    #[allow(clippy::unnecessary_wraps)]
    fn init() -> Result<(), Error> {
        util::attach_console();
        if let Some(func) = OPTIONAL_FUNCTIONS.SetProcessDpiAwarenessContext {
            // This function is only supported on windows 10
            unsafe {
                func(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
            }
        } else if let Some(func) = OPTIONAL_FUNCTIONS.SetProcessDpiAwareness {
            unsafe {
                func(PROCESS_PER_MONITOR_DPI_AWARE);
            }
        }
        if WINDOW_CLASS_REGISTERED
            .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
            .is_ok()
        {
            unsafe {
                let class_name = CLASS_NAME.to_wide();
                let icon = LoadIconW(0 as HINSTANCE, IDI_APPLICATION);
                let wnd = WNDCLASSW {
                    style: 0,
                    lpfnWndProc: Some(window::win_proc_dispatch),
                    cbClsExtra: 0,
                    cbWndExtra: 0,
                    hInstance: 0 as HINSTANCE,
                    hIcon: icon,
                    hCursor: 0 as HCURSOR,
                    hbrBackground: ptr::null_mut(), // We control all the painting
                    lpszMenuName: 0 as LPCWSTR,
                    lpszClassName: class_name.as_ptr(),
                };
                let class_atom = RegisterClassW(&wnd);
                if class_atom == 0 {
                    panic!("Error registering class");
                }
            }
        }
        Ok(())
    }

    pub fn add_window(&self, hwnd: HWND) -> bool {
        self.state
            .borrow_mut()
            .windows
            .insert(hwnd, WindowState::new())
            .is_some()
    }

    pub fn remove_window(&self, hwnd: HWND) -> bool {
        self.state.borrow_mut().windows.remove(&hwnd).is_some()
    }

    pub fn current_windows(&self) -> Vec<HWND> {
        self.state.borrow().windows.keys().copied().collect()
    }

    pub fn disable_window(&self, hwnd: HWND) {
        if let Some(win_state) = self.state.borrow_mut().windows.get_mut(&hwnd) {
            if win_state.disable_count == 0 {
                unsafe { EnableWindow(hwnd, false as BOOL) };
            }
            win_state.disable_count += 1;
        } else {
            tracing::warn!("Tried to disable an unknown window")
        }
    }

    pub fn enable_window(&self, hwnd: HWND) {
        if let Some(win_state) = self.state.borrow_mut().windows.get_mut(&hwnd) {
            if win_state.disable_count == 0 {
                tracing::warn!("Tried to enable an enabled window");
                return;
            } else if win_state.disable_count == 1 {
                unsafe { EnableWindow(hwnd, true as BOOL) };
            }
            win_state.disable_count -= 1;
        } else {
            tracing::warn!("Tried to enable an unknown window")
        }
    }

    pub fn run(self, _handler: Option<Box<dyn AppHandler>>) {
        unsafe {
            // Handle windows messages.
            //
            // NOTE: Code here will not run when we aren't in charge of the message loop. That
            // will include when moving or resizing the window, and when showing modal dialogs.
            loop {
                let mut msg = mem::MaybeUninit::uninit();

                // Timer messages have a low priority and tend to get delayed. Peeking for them
                // helps for some reason; see
                // https://devblogs.microsoft.com/oldnewthing/20191108-00/?p=103080
                PeekMessageW(
                    msg.as_mut_ptr(),
                    ptr::null_mut(),
                    WM_TIMER,
                    WM_TIMER,
                    PM_NOREMOVE,
                );

                let res = GetMessageW(msg.as_mut_ptr(), ptr::null_mut(), 0, 0);
                if res <= 0 {
                    if res == -1 {
                        tracing::error!(
                            "GetMessageW failed: {}",
                            Error::Hr(HRESULT_FROM_WIN32(GetLastError()))
                        );
                    }
                    break;
                }
                let mut msg: MSG = msg.assume_init();
                let accels = accels::find_accels(GetAncestor(msg.hwnd, GA_ROOT));
                let translated = accels.map_or(false, |it| {
                    TranslateAcceleratorW(msg.hwnd, it.handle(), &mut msg) != 0
                });
                if !translated {
                    TranslateMessage(&msg);
                    DispatchMessageW(&msg);
                }
            }
        }
    }

    pub fn quit(&self) {
        if let Ok(mut state) = self.state.try_borrow_mut() {
            if !state.quitting {
                state.quitting = true;
                unsafe {
                    // We want to queue up the destruction of all our windows.
                    // Failure to do so will lead to resource leaks
                    // and an eventual error code exit for the process.
                    for hwnd in state.windows.keys() {
                        if PostMessageW(*hwnd, DS_REQUEST_DESTROY, 0, 0) == FALSE {
                            tracing::warn!(
                                "PostMessageW DS_REQUEST_DESTROY failed: {}",
                                Error::Hr(HRESULT_FROM_WIN32(GetLastError()))
                            );
                        }
                    }
                    // PostQuitMessage sets a quit request flag in the OS.
                    // The actual WM_QUIT message is queued but won't be sent
                    // until all other important events have been handled.
                    PostQuitMessage(0);
                }
            }
        } else {
            tracing::warn!("Application state already borrowed");
        }
    }

    pub fn clipboard(&self) -> Clipboard {
        Clipboard
    }

    pub fn get_locale() -> String {
        let mut buf = [0u16; LOCALE_NAME_MAX_LENGTH];
        let len_with_null =
            unsafe { GetUserDefaultLocaleName(buf.as_mut_ptr(), buf.len() as _) as usize };
        let locale = if len_with_null > 0 {
            buf.get(..len_with_null - 1).and_then(FromWide::from_wide)
        } else {
            None
        };
        locale.unwrap_or_else(|| {
            tracing::warn!("Failed to get user locale");
            "en-US".into()
        })
    }
}
