use std::fmt;
use std::rc::Rc;
use std::sync::Arc;
use std::time::{Duration, Instant};

use objc2::rc::{Retained, autoreleasepool};
use objc2::runtime::ProtocolObject;
use objc2::{AnyThread, MainThreadMarker, available};
use objc2_app_kit::{
    NSApplication, NSApplicationActivationPolicy, NSApplicationDidFinishLaunchingNotification,
    NSApplicationWillTerminateNotification, NSDraggingItem, NSWindow,
};
use objc2_core_foundation::{
    CFIndex, CFRunLoopActivity, CGPoint, CGRect, CGSize, kCFRunLoopCommonModes,
};
use objc2_foundation::{NSArray, NSNotificationCenter, NSObjectProtocol, NSString};
use rwh_06::HasDisplayHandle;
use tracing::debug_span;
use winit_common::core_foundation::{MainRunLoop, MainRunLoopObserver, tracing_observers};
use winit_common::foundation::create_observer;
use winit_core::application::ApplicationHandler;
use winit_core::cursor::{CustomCursor as CoreCustomCursor, CustomCursorSource};
use winit_core::data_transfer::{
    DataTransfer, DataTransferId, DataTransferSend, SendData, TransferType, TypeHint,
};
use winit_core::error::{EventLoopError, RequestError};
use winit_core::event::WindowEvent;
use winit_core::event_loop::pump_events::PumpStatus;
use winit_core::event_loop::{
    ActiveEventLoop as RootActiveEventLoop, AsyncRequestSerial, ControlFlow, DeviceEvents,
    DndAction, DragIcon, EventLoopProvider, EventLoopProxy as CoreEventLoopProxy,
    OwnedDisplayHandle as CoreOwnedDisplayHandle,
};
use winit_core::monitor::MonitorHandle as CoreMonitorHandle;
use winit_core::window::{Theme, WindowId};

use super::app::override_send_event;
use super::app_state::AppState;
use super::cursor::CustomCursor;
use super::event::dummy_event;
use super::monitor;
use crate::ActivationPolicy;
use crate::cursor::image_from_icon;
use crate::dnd::{PasteboardTypeSpec, PasteboardWriter, dnd_actions_to_ns_drag_operation};
use crate::window::Window;

#[derive(Debug)]
pub struct ActiveEventLoop {
    pub(super) app_state: Rc<AppState>,
    pub(super) mtm: MainThreadMarker,
}

impl ActiveEventLoop {
    pub(crate) fn hide_application(&self) {
        NSApplication::sharedApplication(self.mtm).hide(None)
    }

    pub(crate) fn hide_other_applications(&self) {
        NSApplication::sharedApplication(self.mtm).hideOtherApplications(None)
    }

    pub(crate) fn set_allows_automatic_window_tabbing(&self, enabled: bool) {
        NSWindow::setAllowsAutomaticWindowTabbing(enabled, self.mtm)
    }

    pub(crate) fn allows_automatic_window_tabbing(&self) -> bool {
        NSWindow::allowsAutomaticWindowTabbing(self.mtm)
    }
}

impl RootActiveEventLoop for ActiveEventLoop {
    fn create_proxy(&self) -> CoreEventLoopProxy {
        CoreEventLoopProxy::new(self.app_state.event_loop_proxy().clone())
    }

    fn create_window(
        &self,
        window_attributes: winit_core::window::WindowAttributes,
    ) -> Result<Box<dyn winit_core::window::Window>, RequestError> {
        Ok(Box::new(Window::new(self, window_attributes)?))
    }

    fn create_custom_cursor(
        &self,
        source: CustomCursorSource,
    ) -> Result<CoreCustomCursor, RequestError> {
        Ok(CoreCustomCursor(Arc::new(CustomCursor::new(source)?)))
    }

    fn available_monitors(&self) -> Box<dyn Iterator<Item = CoreMonitorHandle>> {
        Box::new(
            monitor::available_monitors()
                .into_iter()
                .map(|monitor| CoreMonitorHandle(Arc::new(monitor))),
        )
    }

    fn primary_monitor(&self) -> Option<winit_core::monitor::MonitorHandle> {
        let monitor = monitor::primary_monitor();
        Some(CoreMonitorHandle(Arc::new(monitor)))
    }

    fn listen_device_events(&self, _allowed: DeviceEvents) {}

    fn system_theme(&self) -> Option<Theme> {
        let app = NSApplication::sharedApplication(self.mtm);

        // Dark appearance was introduced in macOS 10.14
        if available!(macos = 10.14) {
            Some(super::window_delegate::appearance_to_theme(&app.effectiveAppearance()))
        } else {
            Some(Theme::Light)
        }
    }

    fn set_control_flow(&self, control_flow: ControlFlow) {
        self.app_state.set_control_flow(control_flow)
    }

    fn control_flow(&self) -> ControlFlow {
        self.app_state.control_flow()
    }

    fn exit(&self) {
        self.app_state.exit()
    }

    fn exiting(&self) -> bool {
        self.app_state.exiting()
    }

    fn owned_display_handle(&self) -> CoreOwnedDisplayHandle {
        CoreOwnedDisplayHandle::new(Arc::new(OwnedDisplayHandle))
    }

    fn rwh_06_handle(&self) -> &dyn rwh_06::HasDisplayHandle {
        self
    }

    fn fetch_data_transfer(
        &self,
        id: DataTransferId,
        type_: &dyn TransferType,
    ) -> Result<AsyncRequestSerial, RequestError> {
        let Some(pb) = self.app_state.pasteboards().get(id) else {
            return Err(RequestError::Ignored);
        };
        let Some(window_id) = self.app_state.pasteboards().window_id(id) else {
            return Err(RequestError::Ignored);
        };

        let serial = AsyncRequestSerial::get();

        let Some(type_) = PasteboardTypeSpec::from_dyn(type_) else {
            return Err(os_error!(format!("Pasteboard does not contain type {type_:?}")).into());
        };

        let data = Arc::new(pb.with_type(type_));

        self.app_state.maybe_queue_with_handler(move |app, event_loop| {
            app.window_event(event_loop, window_id, WindowEvent::DataTransferReceived {
                id,
                serial,
                value: data,
            });
        });

        Ok(serial)
    }

    fn data_transfer(&self, id: DataTransferId) -> Result<Box<dyn DataTransfer>, RequestError> {
        let Some(pb) = self.app_state.pasteboards().get(id) else {
            return Err(RequestError::Ignored);
        };

        Ok(Box::new(pb))
    }

    fn set_valid_dnd_actions(
        &self,
        id: DataTransferId,
        actions: &[DndAction],
    ) -> Result<(), RequestError> {
        let mut state = self.app_state.drag_state().borrow_mut();
        let Some(drag_state) = &mut *state else {
            return Err(os_error!(UnknownDataTransfer(id)).into());
        };

        if drag_state.id != id {
            return Err(os_error!(UnknownDataTransfer(id)).into());
        }

        drag_state.valid_actions.clear();
        drag_state.valid_actions.extend_from_slice(actions);

        Ok(())
    }

    fn start_drag(
        &self,
        source: WindowId,
        send_data: Box<dyn DataTransferSend>,
        actions: &[DndAction],
        icon: Option<DragIcon>,
    ) -> Result<DataTransferId, RequestError> {
        let drag_operation = dnd_actions_to_ns_drag_operation(actions);

        self.app_state
            .with_window_delegate_on_main(source, move |delegate| {
                let (dragging_rect_offset_x, dragging_rect_offset_y) =
                    icon.as_ref().map(|icon| (icon.offset_x, icon.offset_y)).unwrap_or_default();
                let drag_image = icon.and_then(|icon| image_from_icon(&icon.icon).ok());

                let Some(event) = delegate.window().currentEvent() else {
                    return Err(RequestError::Ignored);
                };

                let dragging_rect_size = drag_image
                    .as_ref()
                    .map(|img| img.size())
                    // Seemingly we need some kind of dragging rectangle even if no icon is
                    // supplied.
                    .unwrap_or(CGSize::new(16., 16.));

                let event_location = event.locationInWindow();
                let dragging_rect_location = CGPoint::new(
                    event_location.x + dragging_rect_offset_x as f64,
                    // Convert generic coordinates (y=0 is top of image) to AppKit coordinates (y=0
                    // is bottom of image)
                    event_location.y - dragging_rect_size.height - dragging_rect_offset_y as f64,
                );
                let dragging_rect = CGRect::new(dragging_rect_location, dragging_rect_size);

                let mut uris = send_data
                    .data_for_type(&TypeHint::UriList)
                    .and_then(|file_uris| {
                        // TODO: Might not be ideal to do this
                        let ns_url_from_str = |str: String| NSString::from_str(&str);
                        // Slightly complicated use of iterators in order to ensure that branches
                        // have the same opaque type
                        match file_uris {
                            SendData::Uris(os_strings) => Some(
                                None.into_iter().chain(os_strings.into_iter().map(ns_url_from_str)),
                            ),
                            SendData::String(string) => Some(
                                Some(NSString::from_str(&string))
                                    .into_iter()
                                    .chain(Vec::new().into_iter().map(ns_url_from_str)),
                            ),
                            SendData::Bytes(_) => None,
                            _ => None,
                        }
                    })
                    .into_iter()
                    .flatten();

                let first_uri = uris.next();

                let mut pasteboard_items = uris
                    .map(|ns_url| {
                        let dragging_item = NSDraggingItem::initWithPasteboardWriter(
                            NSDraggingItem::alloc(),
                            ProtocolObject::from_ref(&*ns_url),
                        );

                        // No dragging frame/contents, icon only applies to the first item.

                        dragging_item
                    })
                    .collect::<Vec<_>>();

                let first_dragging_item = NSDraggingItem::initWithPasteboardWriter(
                    NSDraggingItem::alloc(),
                    ProtocolObject::from_ref(&*PasteboardWriter::new(send_data, first_uri)),
                );

                unsafe {
                    first_dragging_item.setDraggingFrame_contents(
                        dragging_rect,
                        drag_image.as_ref().map(AsRef::as_ref),
                    )
                };

                pasteboard_items.insert(0, first_dragging_item);

                let pasteboard_items = NSArray::from_retained_slice(&pasteboard_items);

                let session = delegate.window().beginDraggingSessionWithItems_event_source(
                    &pasteboard_items,
                    &event,
                    ProtocolObject::from_ref(&*delegate),
                );

                let id = DataTransferId::from_raw(session.draggingSequenceNumber() as i64);

                delegate.view().set_dragging_session(session, drag_operation);

                Ok(id)
            })
            .ok_or(RequestError::Ignored)?
    }
}

/// An operation was attempted on a data transfer ID, but that ID was invalid.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct UnknownDataTransfer(pub DataTransferId);

impl fmt::Display for UnknownDataTransfer {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let id = self.0.into_raw();
        write!(f, "Unknown data transfer with ID {id}")
    }
}

impl std::error::Error for UnknownDataTransfer {}

impl rwh_06::HasDisplayHandle for ActiveEventLoop {
    fn display_handle(&self) -> Result<rwh_06::DisplayHandle<'_>, rwh_06::HandleError> {
        let raw = rwh_06::RawDisplayHandle::AppKit(rwh_06::AppKitDisplayHandle::new());
        unsafe { Ok(rwh_06::DisplayHandle::borrow_raw(raw)) }
    }
}

#[derive(Debug)]
pub struct EventLoop {
    /// Store a reference to the application for convenience.
    ///
    /// We intentionally don't store `WinitApplication` since we want to have
    /// the possibility of swapping that out at some point.
    app: Retained<NSApplication>,
    app_state: Rc<AppState>,

    window_target: ActiveEventLoop,

    // Since macOS 10.11, we no longer need to remove the observers before they are deallocated;
    // the system instead cleans it up next time it would have posted a notification to it.
    //
    // Though we do still need to keep the observers around to prevent them from being deallocated.
    _did_finish_launching_observer: Retained<ProtocolObject<dyn NSObjectProtocol>>,
    _will_terminate_observer: Retained<ProtocolObject<dyn NSObjectProtocol>>,

    _tracing_observers: Option<(MainRunLoopObserver, MainRunLoopObserver)>,
    _before_waiting_observer: MainRunLoopObserver,
    _after_waiting_observer: MainRunLoopObserver,
}

#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub struct PlatformSpecificEventLoopAttributes {
    pub activation_policy: Option<ActivationPolicy>,
    pub default_menu: bool,
    pub activate_ignoring_other_apps: bool,
}

impl Default for PlatformSpecificEventLoopAttributes {
    fn default() -> Self {
        Self { activation_policy: None, default_menu: true, activate_ignoring_other_apps: true }
    }
}

impl EventLoop {
    pub fn new(attributes: &PlatformSpecificEventLoopAttributes) -> Result<Self, EventLoopError> {
        let mtm = MainThreadMarker::new()
            .expect("on macOS, `EventLoop` must be created on the main thread!");

        let activation_policy = match attributes.activation_policy {
            None => None,
            Some(ActivationPolicy::Regular) => Some(NSApplicationActivationPolicy::Regular),
            Some(ActivationPolicy::Accessory) => Some(NSApplicationActivationPolicy::Accessory),
            Some(ActivationPolicy::Prohibited) => Some(NSApplicationActivationPolicy::Prohibited),
        };

        let app_state = AppState::setup_global(
            mtm,
            activation_policy,
            attributes.default_menu,
            attributes.activate_ignoring_other_apps,
        )
        .ok_or_else(|| EventLoopError::RecreationAttempt)?;

        // Initialize the application (if it has not already been).
        let app = NSApplication::sharedApplication(mtm);

        // Override `sendEvent:` on the application to forward to our application state.
        override_send_event(&app);

        let center = NSNotificationCenter::defaultCenter();

        let weak_app_state = Rc::downgrade(&app_state);
        let _did_finish_launching_observer = create_observer(
            &center,
            // `applicationDidFinishLaunching:`
            unsafe { NSApplicationDidFinishLaunchingNotification },
            move |notification| {
                let _entered = debug_span!("NSApplicationDidFinishLaunchingNotification").entered();
                if let Some(app_state) = weak_app_state.upgrade() {
                    app_state.did_finish_launching(notification);
                }
            },
        );

        let weak_app_state = Rc::downgrade(&app_state);
        let _will_terminate_observer = create_observer(
            &center,
            // `applicationWillTerminate:`
            unsafe { NSApplicationWillTerminateNotification },
            move |notification| {
                let _entered = debug_span!("NSApplicationWillTerminateNotification").entered();
                if let Some(app_state) = weak_app_state.upgrade() {
                    app_state.will_terminate(notification);
                }
            },
        );

        let main_loop = MainRunLoop::get(mtm);
        let mode = unsafe { kCFRunLoopCommonModes }.unwrap();

        // Tracing observers have the lowest and highest orderings.
        let _tracing_observers = tracing_observers(mtm).inspect(|(start, end)| {
            main_loop.add_observer(start, mode);
            main_loop.add_observer(end, mode);
        });

        let app_state_clone = Rc::clone(&app_state);
        let _before_waiting_observer = MainRunLoopObserver::new(
            mtm,
            CFRunLoopActivity::BeforeWaiting,
            true,
            // Queued with the second-lowest priority (tracing observers use the lowest) to ensure
            // it is processed after other observers.
            CFIndex::MAX - 1,
            move |_| app_state_clone.cleared(),
        );
        main_loop.add_observer(&_before_waiting_observer, mode);

        let app_state_clone = Rc::clone(&app_state);
        let _after_waiting_observer = MainRunLoopObserver::new(
            mtm,
            CFRunLoopActivity::AfterWaiting,
            true,
            // Queued with the second-highest priority (tracing observers use the highest) to
            // ensure it is processed before other observers.
            CFIndex::MIN + 1,
            move |_| app_state_clone.wakeup(),
        );
        main_loop.add_observer(&_after_waiting_observer, mode);

        Ok(EventLoop {
            app,
            app_state: app_state.clone(),
            window_target: ActiveEventLoop { app_state, mtm },
            _did_finish_launching_observer,
            _will_terminate_observer,
            _tracing_observers,
            _before_waiting_observer,
            _after_waiting_observer,
        })
    }

    pub fn window_target(&self) -> &dyn RootActiveEventLoop {
        &self.window_target
    }

    // NB: we don't base this on `pump_events` because for `MacOs` we can't support
    // `pump_events` elegantly (we just ask to run the loop for a "short" amount of
    // time and so a layered implementation would end up using a lot of CPU due to
    // redundant wake ups.
    pub fn run_app_on_demand<A: ApplicationHandler>(
        &mut self,
        app: A,
    ) -> Result<(), EventLoopError> {
        self.app_state.clear_exit();
        self.app_state.set_event_handler(app, || {
            autoreleasepool(|_| {
                // clear / normalize pump_events state
                self.app_state.set_wait_timeout(None);
                self.app_state.set_stop_before_wait(false);
                self.app_state.set_stop_after_wait(false);
                self.app_state.set_stop_on_redraw(false);

                if self.app_state.is_launched() {
                    debug_assert!(!self.app_state.is_running());
                    self.app_state.set_is_running(true);
                    self.app_state.dispatch_init_events();
                }

                // NOTE: Make sure to not run the application re-entrantly, as that'd be confusing.
                self.app.run();

                self.app_state.internal_exit()
            })
        });

        Ok(())
    }

    pub fn pump_app_events<A: ApplicationHandler>(
        &mut self,
        timeout: Option<Duration>,
        app: A,
    ) -> PumpStatus {
        self.app_state.set_event_handler(app, || {
            autoreleasepool(|_| {
                // As a special case, if the application hasn't been launched yet then we at least
                // run the loop until it has fully launched.
                if !self.app_state.is_launched() {
                    debug_assert!(!self.app_state.is_running());

                    self.app_state.set_stop_on_launch();
                    self.app.run();

                    // Note: we dispatch `NewEvents(Init)` + `Resumed` events after the application
                    // has launched
                } else if !self.app_state.is_running() {
                    // Even though the application may have been launched, it's possible we aren't
                    // running if the `EventLoop` was run before and has since
                    // exited. This indicates that we just starting to re-run
                    // the same `EventLoop` again.
                    self.app_state.set_is_running(true);
                    self.app_state.dispatch_init_events();
                } else {
                    // Only run for as long as the given `Duration` allows so we don't block the
                    // external loop.
                    match timeout {
                        Some(Duration::ZERO) => {
                            self.app_state.set_wait_timeout(None);
                            self.app_state.set_stop_before_wait(true);
                        },
                        Some(duration) => {
                            self.app_state.set_stop_before_wait(false);
                            let timeout = Instant::now() + duration;
                            self.app_state.set_wait_timeout(Some(timeout));
                            self.app_state.set_stop_after_wait(true);
                        },
                        None => {
                            self.app_state.set_wait_timeout(None);
                            self.app_state.set_stop_before_wait(false);
                            self.app_state.set_stop_after_wait(true);
                        },
                    }
                    self.app_state.set_stop_on_redraw(true);
                    self.app.run();
                }

                if self.app_state.exiting() {
                    self.app_state.internal_exit();
                    PumpStatus::Exit(0)
                } else {
                    PumpStatus::Continue
                }
            })
        })
    }
}

impl EventLoopProvider for EventLoop {
    fn run_app<A: ApplicationHandler + 'static>(
        mut self,
        mut app: A,
    ) -> Result<(), EventLoopError> {
        let result = self.run_app_on_demand(&mut app);
        // SAFETY: unsure that the state is dropped before the exit from the event loop.
        drop(app);
        result
    }

    fn create_proxy(&self) -> CoreEventLoopProxy {
        self.window_target().create_proxy()
    }

    fn owned_display_handle(&self) -> CoreOwnedDisplayHandle {
        self.window_target().owned_display_handle()
    }

    fn listen_device_events(&self, allowed: DeviceEvents) {
        self.window_target().listen_device_events(allowed);
    }

    fn set_control_flow(&self, control_flow: ControlFlow) {
        self.window_target().set_control_flow(control_flow);
    }

    fn create_custom_cursor(
        &self,
        custom_cursor: CustomCursorSource,
    ) -> Result<CoreCustomCursor, RequestError> {
        self.window_target().create_custom_cursor(custom_cursor)
    }
}

pub(crate) struct OwnedDisplayHandle;

impl HasDisplayHandle for OwnedDisplayHandle {
    fn display_handle(&self) -> Result<rwh_06::DisplayHandle<'_>, rwh_06::HandleError> {
        let raw = rwh_06::RawDisplayHandle::AppKit(rwh_06::AppKitDisplayHandle::new());
        unsafe { Ok(rwh_06::DisplayHandle::borrow_raw(raw)) }
    }
}

pub(super) fn stop_app_immediately(app: &NSApplication) {
    autoreleasepool(|_| {
        app.stop(None);
        // To stop event loop immediately, we need to post some event here.
        // See: https://stackoverflow.com/questions/48041279/stopping-the-nsapplication-main-event-loop/48064752#48064752
        app.postEvent_atStart(&dummy_event().unwrap(), true);
    });
}

/// Tell all windows to close.
///
/// This will synchronously trigger `WindowEvent::Destroyed` within
/// `windowWillClose:`, giving the application one last chance to handle
/// those events. It doesn't matter if the user also ends up closing the
/// windows in `Window`'s `Drop` impl, once a window has been closed once, it
/// stays closed.
///
/// This ensures that no windows linger on after the event loop has exited,
/// see <https://github.com/rust-windowing/winit/issues/4135>.
pub(super) fn notify_windows_of_exit(app: &NSApplication) {
    for window in app.windows() {
        window.close();
    }
}
