mod animation_frame;
mod canvas;
pub mod event;
mod event_handle;
mod fullscreen;
mod intersection_handle;
mod media_query_handle;
mod pointer;
mod resize_scaling;
mod safe_area;
mod schedule;

use std::cell::OnceCell;

use dpi::{LogicalPosition, LogicalSize};
use js_sys::Array;
use wasm_bindgen::JsCast;
use wasm_bindgen::closure::Closure;
use wasm_bindgen::prelude::wasm_bindgen;
use web_sys::{Document, HtmlCanvasElement, Navigator, PageTransitionEvent, VisibilityState};

pub use self::canvas::{Canvas, Style};
pub use self::event_handle::EventListenerHandle;
pub use self::resize_scaling::ResizeScaleHandle;
pub use self::safe_area::SafeAreaHandle;
pub use self::schedule::Schedule;

pub struct PageTransitionEventHandle {
    _show_listener: event_handle::EventListenerHandle<dyn FnMut(PageTransitionEvent)>,
    _hide_listener: event_handle::EventListenerHandle<dyn FnMut(PageTransitionEvent)>,
}

pub fn on_page_transition(
    window: web_sys::Window,
    show_handler: impl FnMut(PageTransitionEvent) + 'static,
    hide_handler: impl FnMut(PageTransitionEvent) + 'static,
) -> PageTransitionEventHandle {
    let show_closure = Closure::new(show_handler);
    let hide_closure = Closure::new(hide_handler);

    let show_listener =
        event_handle::EventListenerHandle::new(window.clone(), "pageshow", show_closure);
    let hide_listener = event_handle::EventListenerHandle::new(window, "pagehide", hide_closure);
    PageTransitionEventHandle { _show_listener: show_listener, _hide_listener: hide_listener }
}

pub fn scale_factor(window: &web_sys::Window) -> f64 {
    window.device_pixel_ratio()
}

fn fix_canvas_size(style: &Style, mut size: LogicalSize<f64>) -> LogicalSize<f64> {
    if style.get("box-sizing") == "border-box" {
        size.width += style_size_property(style, "border-left-width")
            + style_size_property(style, "border-right-width")
            + style_size_property(style, "padding-left")
            + style_size_property(style, "padding-right");
        size.height += style_size_property(style, "border-top-width")
            + style_size_property(style, "border-bottom-width")
            + style_size_property(style, "padding-top")
            + style_size_property(style, "padding-bottom");
    }

    size
}

pub fn set_canvas_size(
    document: &Document,
    raw: &HtmlCanvasElement,
    style: &Style,
    new_size: LogicalSize<f64>,
) {
    if !document.contains(Some(raw)) || style.get("display") == "none" {
        return;
    }

    let new_size = fix_canvas_size(style, new_size);

    style.set("width", &format!("{}px", new_size.width));
    style.set("height", &format!("{}px", new_size.height));
}

pub fn set_canvas_min_size(
    document: &Document,
    raw: &HtmlCanvasElement,
    style: &Style,
    dimensions: Option<LogicalSize<f64>>,
) {
    if let Some(dimensions) = dimensions {
        if !document.contains(Some(raw)) || style.get("display") == "none" {
            return;
        }

        let new_size = fix_canvas_size(style, dimensions);

        style.set("min-width", &format!("{}px", new_size.width));
        style.set("min-height", &format!("{}px", new_size.height));
    } else {
        style.remove("min-width");
        style.remove("min-height");
    }
}

pub fn set_canvas_max_size(
    document: &Document,
    raw: &HtmlCanvasElement,
    style: &Style,
    dimensions: Option<LogicalSize<f64>>,
) {
    if let Some(dimensions) = dimensions {
        if !document.contains(Some(raw)) || style.get("display") == "none" {
            return;
        }

        let new_size = fix_canvas_size(style, dimensions);

        style.set("max-width", &format!("{}px", new_size.width));
        style.set("max-height", &format!("{}px", new_size.height));
    } else {
        style.remove("max-width");
        style.remove("max-height");
    }
}

pub fn set_canvas_position(
    document: &Document,
    raw: &HtmlCanvasElement,
    style: &Style,
    mut position: LogicalPosition<f64>,
) {
    if document.contains(Some(raw)) && style.get("display") != "none" {
        position.x -= style_size_property(style, "margin-left")
            + style_size_property(style, "border-left-width")
            + style_size_property(style, "padding-left");
        position.y -= style_size_property(style, "margin-top")
            + style_size_property(style, "border-top-width")
            + style_size_property(style, "padding-top");
    }

    style.set("position", "fixed");
    style.set("left", &format!("{}px", position.x));
    style.set("top", &format!("{}px", position.y));
}

/// This function will panic if the element is not inserted in the DOM
/// or is not a CSS property that represents a size in pixel.
pub fn style_size_property(style: &Style, property: &str) -> f64 {
    let prop = style.get(property);
    prop.strip_suffix("px")
        .expect("Element was not inserted into the DOM or is not a size in pixel")
        .parse()
        .expect("CSS property is not a size in pixel")
}

pub fn is_dark_mode(window: &web_sys::Window) -> Option<bool> {
    window.match_media("(prefers-color-scheme: dark)").ok().flatten().map(|media| media.matches())
}

pub fn is_visible(document: &Document) -> bool {
    document.visibility_state() == VisibilityState::Visible
}

pub type RawCanvasType = HtmlCanvasElement;

#[derive(Clone, Copy)]
pub enum Engine {
    Chromium,
    Gecko,
    WebKit,
}

struct UserAgentData {
    engine: Option<Engine>,
    chrome_linux: bool,
}

thread_local! {
    static USER_AGENT_DATA: OnceCell<UserAgentData> = const { OnceCell::new() };
}

pub fn chrome_linux(navigator: &Navigator) -> bool {
    USER_AGENT_DATA.with(|data| data.get_or_init(|| user_agent(navigator)).chrome_linux)
}

pub fn engine(navigator: &Navigator) -> Option<Engine> {
    USER_AGENT_DATA.with(|data| data.get_or_init(|| user_agent(navigator)).engine)
}

fn user_agent(navigator: &Navigator) -> UserAgentData {
    #[wasm_bindgen]
    extern "C" {
        #[wasm_bindgen(extends = Navigator)]
        type NavigatorExt;

        #[wasm_bindgen(method, getter, js_name = userAgentData)]
        fn user_agent_data(this: &NavigatorExt) -> Option<NavigatorUaData>;

        type NavigatorUaData;

        #[wasm_bindgen(method, getter)]
        fn brands(this: &NavigatorUaData) -> Array;

        #[wasm_bindgen(method, getter)]
        fn platform(this: &NavigatorUaData) -> String;

        type NavigatorUaBrandVersion;

        #[wasm_bindgen(method, getter)]
        fn brand(this: &NavigatorUaBrandVersion) -> String;
    }

    let navigator: &NavigatorExt = navigator.unchecked_ref();

    if let Some(data) = navigator.user_agent_data() {
        let engine = 'engine: {
            for brand in data
                .brands()
                .iter()
                .map(NavigatorUaBrandVersion::unchecked_from_js)
                .map(|brand| brand.brand())
            {
                match brand.as_str() {
                    "Chromium" => break 'engine Some(Engine::Chromium),
                    // TODO: verify when Firefox actually implements it.
                    "Gecko" => break 'engine Some(Engine::Gecko),
                    // TODO: verify when Safari actually implements it.
                    "WebKit" => break 'engine Some(Engine::WebKit),
                    _ => (),
                }
            }

            None
        };

        let chrome_linux = matches!(engine, Some(Engine::Chromium))
            .then(|| data.platform() == "Linux")
            .unwrap_or(false);

        UserAgentData { engine, chrome_linux }
    } else {
        let engine = 'engine: {
            let Ok(data) = navigator.user_agent() else {
                break 'engine None;
            };

            if data.contains("Chrome/") {
                Some(Engine::Chromium)
            } else if data.contains("Gecko/") {
                Some(Engine::Gecko)
            } else if data.contains("AppleWebKit/") {
                Some(Engine::WebKit)
            } else {
                None
            }
        };

        UserAgentData { engine, chrome_linux: false }
    }
}
