use raw_window_handle::{ios::IOSHandle, RawWindowHandle};
use std::{
    collections::VecDeque,
    ops::{Deref, DerefMut},
};

use objc::runtime::{Class, Object, BOOL, NO, YES};

use crate::{
    dpi::{self, LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Position, Size},
    error::{ExternalError, NotSupportedError, OsError as RootOsError},
    event::{Event, WindowEvent},
    icon::Icon,
    monitor::MonitorHandle as RootMonitorHandle,
    platform::ios::{MonitorHandleExtIOS, ScreenEdge, ValidOrientations},
    platform_impl::platform::{
        app_state,
        event_loop::{self, EventProxy, EventWrapper},
        ffi::{
            id, CGFloat, CGPoint, CGRect, CGSize, UIEdgeInsets, UIInterfaceOrientationMask,
            UIRectEdge, UIScreenOverscanCompensation,
        },
        monitor, view, EventLoopWindowTarget, MonitorHandle,
    },
    window::{CursorIcon, Fullscreen, WindowAttributes, WindowId as RootWindowId},
};

pub struct Inner {
    pub window: id,
    pub view_controller: id,
    pub view: id,
    gl_or_metal_backed: bool,
}

impl Drop for Inner {
    fn drop(&mut self) {
        unsafe {
            let () = msg_send![self.view, release];
            let () = msg_send![self.view_controller, release];
            let () = msg_send![self.window, release];
        }
    }
}

impl Inner {
    pub fn set_title(&self, _title: &str) {
        debug!("`Window::set_title` is ignored on iOS")
    }

    pub fn set_visible(&self, visible: bool) {
        match visible {
            true => unsafe {
                let () = msg_send![self.window, setHidden: NO];
            },
            false => unsafe {
                let () = msg_send![self.window, setHidden: YES];
            },
        }
    }

    pub fn request_redraw(&self) {
        unsafe {
            if self.gl_or_metal_backed {
                // `setNeedsDisplay` does nothing on UIViews which are directly backed by CAEAGLLayer or CAMetalLayer.
                // Ordinarily the OS sets up a bunch of UIKit state before calling drawRect: on a UIView, but when using
                // raw or gl/metal for drawing this work is completely avoided.
                //
                // The docs for `setNeedsDisplay` don't mention `CAMetalLayer`; however, this has been confirmed via
                // testing.
                //
                // https://developer.apple.com/documentation/uikit/uiview/1622437-setneedsdisplay?language=objc
                app_state::queue_gl_or_metal_redraw(self.window);
            } else {
                let () = msg_send![self.view, setNeedsDisplay];
            }
        }
    }

    pub fn inner_position(&self) -> Result<PhysicalPosition<i32>, NotSupportedError> {
        unsafe {
            let safe_area = self.safe_area_screen_space();
            let position = LogicalPosition {
                x: safe_area.origin.x as f64,
                y: safe_area.origin.y as f64,
            };
            let scale_factor = self.scale_factor();
            Ok(position.to_physical(scale_factor))
        }
    }

    pub fn outer_position(&self) -> Result<PhysicalPosition<i32>, NotSupportedError> {
        unsafe {
            let screen_frame = self.screen_frame();
            let position = LogicalPosition {
                x: screen_frame.origin.x as f64,
                y: screen_frame.origin.y as f64,
            };
            let scale_factor = self.scale_factor();
            Ok(position.to_physical(scale_factor))
        }
    }

    pub fn set_outer_position(&self, physical_position: Position) {
        unsafe {
            let scale_factor = self.scale_factor();
            let position = physical_position.to_logical::<f64>(scale_factor);
            let screen_frame = self.screen_frame();
            let new_screen_frame = CGRect {
                origin: CGPoint {
                    x: position.x as _,
                    y: position.y as _,
                },
                size: screen_frame.size,
            };
            let bounds = self.from_screen_space(new_screen_frame);
            let () = msg_send![self.window, setBounds: bounds];
        }
    }

    pub fn inner_size(&self) -> PhysicalSize<u32> {
        unsafe {
            let scale_factor = self.scale_factor();
            let safe_area = self.safe_area_screen_space();
            let size = LogicalSize {
                width: safe_area.size.width as f64,
                height: safe_area.size.height as f64,
            };
            size.to_physical(scale_factor)
        }
    }

    pub fn outer_size(&self) -> PhysicalSize<u32> {
        unsafe {
            let scale_factor = self.scale_factor();
            let screen_frame = self.screen_frame();
            let size = LogicalSize {
                width: screen_frame.size.width as f64,
                height: screen_frame.size.height as f64,
            };
            size.to_physical(scale_factor)
        }
    }

    pub fn set_inner_size(&self, _size: Size) {
        unimplemented!("not clear what `Window::set_inner_size` means on iOS");
    }

    pub fn set_min_inner_size(&self, _dimensions: Option<Size>) {
        warn!("`Window::set_min_inner_size` is ignored on iOS")
    }

    pub fn set_max_inner_size(&self, _dimensions: Option<Size>) {
        warn!("`Window::set_max_inner_size` is ignored on iOS")
    }

    pub fn set_resizable(&self, _resizable: bool) {
        warn!("`Window::set_resizable` is ignored on iOS")
    }

    pub fn scale_factor(&self) -> f64 {
        unsafe {
            let hidpi: CGFloat = msg_send![self.view, contentScaleFactor];
            hidpi as _
        }
    }

    pub fn set_cursor_icon(&self, _cursor: CursorIcon) {
        debug!("`Window::set_cursor_icon` ignored on iOS")
    }

    pub fn set_cursor_position(&self, _position: Position) -> Result<(), ExternalError> {
        Err(ExternalError::NotSupported(NotSupportedError::new()))
    }

    pub fn set_cursor_grab(&self, _grab: bool) -> Result<(), ExternalError> {
        Err(ExternalError::NotSupported(NotSupportedError::new()))
    }

    pub fn set_cursor_visible(&self, _visible: bool) {
        debug!("`Window::set_cursor_visible` is ignored on iOS")
    }

    pub fn set_minimized(&self, _minimized: bool) {
        warn!("`Window::set_minimized` is ignored on iOS")
    }

    pub fn set_maximized(&self, _maximized: bool) {
        warn!("`Window::set_maximized` is ignored on iOS")
    }

    pub fn set_fullscreen(&self, monitor: Option<Fullscreen>) {
        unsafe {
            let uiscreen = match monitor {
                Some(Fullscreen::Exclusive(video_mode)) => {
                    let uiscreen = video_mode.video_mode.monitor.ui_screen() as id;
                    let () = msg_send![uiscreen, setCurrentMode: video_mode.video_mode.screen_mode];
                    uiscreen
                }
                Some(Fullscreen::Borderless(monitor)) => monitor.ui_screen() as id,
                None => {
                    warn!("`Window::set_fullscreen(None)` ignored on iOS");
                    return;
                }
            };

            // this is pretty slow on iOS, so avoid doing it if we can
            let current: id = msg_send![self.window, screen];
            if uiscreen != current {
                let () = msg_send![self.window, setScreen: uiscreen];
            }

            let bounds: CGRect = msg_send![uiscreen, bounds];
            let () = msg_send![self.window, setFrame: bounds];

            // For external displays, we must disable overscan compensation or
            // the displayed image will have giant black bars surrounding it on
            // each side
            let () = msg_send![
                uiscreen,
                setOverscanCompensation: UIScreenOverscanCompensation::None
            ];
        }
    }

    pub fn fullscreen(&self) -> Option<Fullscreen> {
        unsafe {
            let monitor = self.current_monitor();
            let uiscreen = monitor.inner.ui_screen();
            let screen_space_bounds = self.screen_frame();
            let screen_bounds: CGRect = msg_send![uiscreen, bounds];

            // TODO: track fullscreen instead of relying on brittle float comparisons
            if screen_space_bounds.origin.x == screen_bounds.origin.x
                && screen_space_bounds.origin.y == screen_bounds.origin.y
                && screen_space_bounds.size.width == screen_bounds.size.width
                && screen_space_bounds.size.height == screen_bounds.size.height
            {
                Some(Fullscreen::Borderless(monitor))
            } else {
                None
            }
        }
    }

    pub fn set_decorations(&self, _decorations: bool) {
        warn!("`Window::set_decorations` is ignored on iOS")
    }

    pub fn set_always_on_top(&self, _always_on_top: bool) {
        warn!("`Window::set_always_on_top` is ignored on iOS")
    }

    pub fn set_window_icon(&self, _icon: Option<Icon>) {
        warn!("`Window::set_window_icon` is ignored on iOS")
    }

    pub fn set_ime_position(&self, _position: Position) {
        warn!("`Window::set_ime_position` is ignored on iOS")
    }

    pub fn current_monitor(&self) -> RootMonitorHandle {
        unsafe {
            let uiscreen: id = msg_send![self.window, screen];
            RootMonitorHandle {
                inner: MonitorHandle::retained_new(uiscreen),
            }
        }
    }

    pub fn available_monitors(&self) -> VecDeque<MonitorHandle> {
        unsafe { monitor::uiscreens() }
    }

    pub fn primary_monitor(&self) -> MonitorHandle {
        unsafe { monitor::main_uiscreen() }
    }

    pub fn id(&self) -> WindowId {
        self.window.into()
    }

    pub fn raw_window_handle(&self) -> RawWindowHandle {
        let handle = IOSHandle {
            ui_window: self.window as _,
            ui_view: self.view as _,
            ui_view_controller: self.view_controller as _,
            ..IOSHandle::empty()
        };
        RawWindowHandle::IOS(handle)
    }
}

pub struct Window {
    pub inner: Inner,
}

impl Drop for Window {
    fn drop(&mut self) {
        unsafe {
            assert_main_thread!("`Window::drop` can only be run on the main thread on iOS");
        }
    }
}

unsafe impl Send for Window {}
unsafe impl Sync for Window {}

impl Deref for Window {
    type Target = Inner;

    fn deref(&self) -> &Inner {
        unsafe {
            assert_main_thread!("`Window` methods can only be run on the main thread on iOS");
        }
        &self.inner
    }
}

impl DerefMut for Window {
    fn deref_mut(&mut self) -> &mut Inner {
        unsafe {
            assert_main_thread!("`Window` methods can only be run on the main thread on iOS");
        }
        &mut self.inner
    }
}

impl Window {
    pub fn new<T>(
        _event_loop: &EventLoopWindowTarget<T>,
        window_attributes: WindowAttributes,
        platform_attributes: PlatformSpecificWindowBuilderAttributes,
    ) -> Result<Window, RootOsError> {
        if let Some(_) = window_attributes.min_inner_size {
            warn!("`WindowAttributes::min_inner_size` is ignored on iOS");
        }
        if let Some(_) = window_attributes.max_inner_size {
            warn!("`WindowAttributes::max_inner_size` is ignored on iOS");
        }
        if window_attributes.always_on_top {
            warn!("`WindowAttributes::always_on_top` is unsupported on iOS");
        }
        // TODO: transparency, visible

        unsafe {
            let screen = match window_attributes.fullscreen {
                Some(Fullscreen::Exclusive(ref video_mode)) => {
                    video_mode.video_mode.monitor.ui_screen() as id
                }
                Some(Fullscreen::Borderless(ref monitor)) => monitor.ui_screen() as id,
                None => monitor::main_uiscreen().ui_screen(),
            };

            let screen_bounds: CGRect = msg_send![screen, bounds];

            let frame = match window_attributes.inner_size {
                Some(dim) => {
                    let scale_factor = msg_send![screen, scale];
                    let size = dim.to_logical::<f64>(scale_factor);
                    CGRect {
                        origin: screen_bounds.origin,
                        size: CGSize {
                            width: size.width as _,
                            height: size.height as _,
                        },
                    }
                }
                None => screen_bounds,
            };

            let view = view::create_view(&window_attributes, &platform_attributes, frame.clone());

            let gl_or_metal_backed = {
                let view_class: id = msg_send![view, class];
                let layer_class: id = msg_send![view_class, layerClass];
                let is_metal: BOOL =
                    msg_send![layer_class, isSubclassOfClass: class!(CAMetalLayer)];
                let is_gl: BOOL = msg_send![layer_class, isSubclassOfClass: class!(CAEAGLLayer)];
                is_metal == YES || is_gl == YES
            };

            let view_controller =
                view::create_view_controller(&window_attributes, &platform_attributes, view);
            let window = view::create_window(
                &window_attributes,
                &platform_attributes,
                frame,
                view_controller,
            );

            let result = Window {
                inner: Inner {
                    window,
                    view_controller,
                    view,
                    gl_or_metal_backed,
                },
            };
            app_state::set_key_window(window);

            // Like the Windows and macOS backends, we send a `ScaleFactorChanged` and `Resized`
            // event on window creation if the DPI factor != 1.0
            let scale_factor: CGFloat = msg_send![view, contentScaleFactor];
            let scale_factor: f64 = scale_factor.into();
            if scale_factor != 1.0 {
                let bounds: CGRect = msg_send![view, bounds];
                let screen: id = msg_send![window, screen];
                let screen_space: id = msg_send![screen, coordinateSpace];
                let screen_frame: CGRect =
                    msg_send![view, convertRect:bounds toCoordinateSpace:screen_space];
                let size = crate::dpi::LogicalSize {
                    width: screen_frame.size.width as _,
                    height: screen_frame.size.height as _,
                };
                app_state::handle_nonuser_events(
                    std::iter::once(EventWrapper::EventProxy(EventProxy::DpiChangedProxy {
                        window_id: window,
                        scale_factor,
                        suggested_size: size,
                    }))
                    .chain(std::iter::once(EventWrapper::StaticEvent(
                        Event::WindowEvent {
                            window_id: RootWindowId(window.into()),
                            event: WindowEvent::Resized(size.to_physical(scale_factor)),
                        },
                    ))),
                );
            }

            Ok(result)
        }
    }
}

// WindowExtIOS
impl Inner {
    pub fn ui_window(&self) -> id {
        self.window
    }
    pub fn ui_view_controller(&self) -> id {
        self.view_controller
    }
    pub fn ui_view(&self) -> id {
        self.view
    }

    pub fn set_scale_factor(&self, scale_factor: f64) {
        unsafe {
            assert!(
                dpi::validate_scale_factor(scale_factor),
                "`WindowExtIOS::set_scale_factor` received an invalid hidpi factor"
            );
            let scale_factor = scale_factor as CGFloat;
            let () = msg_send![self.view, setContentScaleFactor: scale_factor];
        }
    }

    pub fn set_valid_orientations(&self, valid_orientations: ValidOrientations) {
        unsafe {
            let idiom = event_loop::get_idiom();
            let supported_orientations = UIInterfaceOrientationMask::from_valid_orientations_idiom(
                valid_orientations,
                idiom,
            );
            msg_send![
                self.view_controller,
                setSupportedInterfaceOrientations: supported_orientations
            ]
        }
    }

    pub fn set_prefers_home_indicator_hidden(&self, hidden: bool) {
        unsafe {
            let prefers_home_indicator_hidden = if hidden { YES } else { NO };
            let () = msg_send![
                self.view_controller,
                setPrefersHomeIndicatorAutoHidden: prefers_home_indicator_hidden
            ];
        }
    }

    pub fn set_preferred_screen_edges_deferring_system_gestures(&self, edges: ScreenEdge) {
        let edges: UIRectEdge = edges.into();
        unsafe {
            let () = msg_send![
                self.view_controller,
                setPreferredScreenEdgesDeferringSystemGestures: edges
            ];
        }
    }

    pub fn set_prefers_status_bar_hidden(&self, hidden: bool) {
        unsafe {
            let status_bar_hidden = if hidden { YES } else { NO };
            let () = msg_send![
                self.view_controller,
                setPrefersStatusBarHidden: status_bar_hidden
            ];
        }
    }
}

impl Inner {
    // requires main thread
    unsafe fn screen_frame(&self) -> CGRect {
        self.to_screen_space(msg_send![self.window, bounds])
    }

    // requires main thread
    unsafe fn to_screen_space(&self, rect: CGRect) -> CGRect {
        let screen: id = msg_send![self.window, screen];
        if !screen.is_null() {
            let screen_space: id = msg_send![screen, coordinateSpace];
            msg_send![self.window, convertRect:rect toCoordinateSpace:screen_space]
        } else {
            rect
        }
    }

    // requires main thread
    unsafe fn from_screen_space(&self, rect: CGRect) -> CGRect {
        let screen: id = msg_send![self.window, screen];
        if !screen.is_null() {
            let screen_space: id = msg_send![screen, coordinateSpace];
            msg_send![self.window, convertRect:rect fromCoordinateSpace:screen_space]
        } else {
            rect
        }
    }

    // requires main thread
    unsafe fn safe_area_screen_space(&self) -> CGRect {
        let bounds: CGRect = msg_send![self.window, bounds];
        if app_state::os_capabilities().safe_area {
            let safe_area: UIEdgeInsets = msg_send![self.window, safeAreaInsets];
            let safe_bounds = CGRect {
                origin: CGPoint {
                    x: bounds.origin.x + safe_area.left,
                    y: bounds.origin.y + safe_area.top,
                },
                size: CGSize {
                    width: bounds.size.width - safe_area.left - safe_area.right,
                    height: bounds.size.height - safe_area.top - safe_area.bottom,
                },
            };
            self.to_screen_space(safe_bounds)
        } else {
            let screen_frame = self.to_screen_space(bounds);
            let status_bar_frame: CGRect = {
                let app: id = msg_send![class!(UIApplication), sharedApplication];
                assert!(
                    !app.is_null(),
                    "`Window::get_inner_position` cannot be called before `EventLoop::run` on iOS"
                );
                msg_send![app, statusBarFrame]
            };
            let (y, height) = if screen_frame.origin.y > status_bar_frame.size.height {
                (screen_frame.origin.y, screen_frame.size.height)
            } else {
                let y = status_bar_frame.size.height;
                let height = screen_frame.size.height
                    - (status_bar_frame.size.height - screen_frame.origin.y);
                (y, height)
            };
            CGRect {
                origin: CGPoint {
                    x: screen_frame.origin.x,
                    y,
                },
                size: CGSize {
                    width: screen_frame.size.width,
                    height,
                },
            }
        }
    }
}

#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct WindowId {
    window: id,
}

impl WindowId {
    pub unsafe fn dummy() -> Self {
        WindowId {
            window: std::ptr::null_mut(),
        }
    }
}

unsafe impl Send for WindowId {}
unsafe impl Sync for WindowId {}

impl From<&Object> for WindowId {
    fn from(window: &Object) -> WindowId {
        WindowId {
            window: window as *const _ as _,
        }
    }
}

impl From<&mut Object> for WindowId {
    fn from(window: &mut Object) -> WindowId {
        WindowId {
            window: window as _,
        }
    }
}

impl From<id> for WindowId {
    fn from(window: id) -> WindowId {
        WindowId { window }
    }
}

#[derive(Clone)]
pub struct PlatformSpecificWindowBuilderAttributes {
    pub root_view_class: &'static Class,
    pub scale_factor: Option<f64>,
    pub valid_orientations: ValidOrientations,
    pub prefers_home_indicator_hidden: bool,
    pub prefers_status_bar_hidden: bool,
    pub preferred_screen_edges_deferring_system_gestures: ScreenEdge,
}

impl Default for PlatformSpecificWindowBuilderAttributes {
    fn default() -> PlatformSpecificWindowBuilderAttributes {
        PlatformSpecificWindowBuilderAttributes {
            root_view_class: class!(UIView),
            scale_factor: None,
            valid_orientations: Default::default(),
            prefers_home_indicator_hidden: false,
            prefers_status_bar_hidden: false,
            preferred_screen_edges_deferring_system_gestures: Default::default(),
        }
    }
}
