// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0

use std::{
  ops::Deref,
  sync::{Mutex, Weak},
};

use cocoa::{
  appkit::{CGFloat, NSScreen, NSWindow, NSWindowStyleMask},
  base::{id, nil},
  foundation::{NSPoint, NSSize, NSString},
};
use dispatch::Queue;
use objc::{
  rc::autoreleasepool,
  runtime::{BOOL, NO, YES},
};

use crate::{
  dpi::LogicalSize,
  event::{Event, WindowEvent},
  platform_impl::platform::{
    app_state::AppState,
    event::EventWrapper,
    ffi,
    util::IdRef,
    window::{get_window_id, SharedState},
  },
  window::WindowId,
};

// Unsafe wrapper type that allows us to dispatch things that aren't Send.
// This should *only* be used to dispatch to the main queue.
// While it is indeed not guaranteed that these types can safely be sent to
// other threads, we know that they're safe to use on the main thread.
struct MainThreadSafe<T>(T);

unsafe impl<T> Send for MainThreadSafe<T> {}

impl<T> Deref for MainThreadSafe<T> {
  type Target = T;
  fn deref(&self) -> &T {
    &self.0
  }
}

unsafe fn set_style_mask(ns_window: id, ns_view: id, mask: NSWindowStyleMask) {
  ns_window.setStyleMask_(mask);
  // If we don't do this, key handling will break
  // (at least until the window is clicked again/etc.)
  ns_window.makeFirstResponder_(ns_view);
}

// Always use this function instead of trying to modify `styleMask` directly!
// `setStyleMask:` isn't thread-safe, so we have to use Grand Central Dispatch.
// Otherwise, this would vomit out errors about not being on the main thread
// and fail to do anything.
pub unsafe fn set_style_mask_async(ns_window: id, ns_view: id, mask: NSWindowStyleMask) {
  let ns_window = MainThreadSafe(ns_window);
  let ns_view = MainThreadSafe(ns_view);
  Queue::main().exec_async(move || {
    set_style_mask(*ns_window, *ns_view, mask);
  });
}
pub unsafe fn set_style_mask_sync(ns_window: id, ns_view: id, mask: NSWindowStyleMask) {
  let is_main_thread: BOOL = msg_send!(class!(NSThread), isMainThread);
  if is_main_thread != NO {
    set_style_mask(ns_window, ns_view, mask);
  } else {
    let ns_window = MainThreadSafe(ns_window);
    let ns_view = MainThreadSafe(ns_view);
    Queue::main().exec_sync(move || {
      set_style_mask(*ns_window, *ns_view, mask);
    })
  }
}

// `setContentSize:` isn't thread-safe either, though it doesn't log any errors
// and just fails silently. Anyway, GCD to the rescue!
pub unsafe fn set_content_size_async(ns_window: id, size: LogicalSize<f64>) {
  let ns_window = MainThreadSafe(ns_window);
  Queue::main().exec_async(move || {
    ns_window.setContentSize_(NSSize::new(size.width as CGFloat, size.height as CGFloat));
  });
}

// `setFrameTopLeftPoint:` isn't thread-safe, but fortunately has the courtesy
// to log errors.
pub unsafe fn set_frame_top_left_point_async(ns_window: id, point: NSPoint) {
  let ns_window = MainThreadSafe(ns_window);
  Queue::main().exec_async(move || {
    ns_window.setFrameTopLeftPoint_(point);
  });
}

// `setFrameTopLeftPoint:` isn't thread-safe, and fails silently.
pub unsafe fn set_level_async(ns_window: id, level: ffi::NSWindowLevel) {
  let ns_window = MainThreadSafe(ns_window);
  Queue::main().exec_async(move || {
    ns_window.setLevel_(level as _);
  });
}

// `toggleFullScreen` is thread-safe, but our additional logic to account for
// window styles isn't.
pub unsafe fn toggle_full_screen_async(
  ns_window: id,
  ns_view: id,
  not_fullscreen: bool,
  shared_state: Weak<Mutex<SharedState>>,
) {
  let ns_window = MainThreadSafe(ns_window);
  let ns_view = MainThreadSafe(ns_view);
  let shared_state = MainThreadSafe(shared_state);
  Queue::main().exec_async(move || {
    // `toggleFullScreen` doesn't work if the `StyleMask` is none, so we
    // set a normal style temporarily. The previous state will be
    // restored in `WindowDelegate::window_did_exit_fullscreen`.
    if not_fullscreen {
      let curr_mask = ns_window.styleMask();
      let required =
        NSWindowStyleMask::NSTitledWindowMask | NSWindowStyleMask::NSResizableWindowMask;
      if !curr_mask.contains(required) {
        set_style_mask(*ns_window, *ns_view, required);
        if let Some(shared_state) = shared_state.upgrade() {
          trace!("Locked shared state in `toggle_full_screen_callback`");
          let mut shared_state_lock = shared_state.lock().unwrap();
          (*shared_state_lock).saved_style = Some(curr_mask);
          trace!("Unlocked shared state in `toggle_full_screen_callback`");
        }
      }
    }
    // Window level must be restored from `CGShieldingWindowLevel()
    // + 1` back to normal in order for `toggleFullScreen` to do
    // anything
    ns_window.setLevel_(0);
    ns_window.toggleFullScreen_(nil);
  });
}

pub unsafe fn restore_display_mode_async(ns_screen: u32) {
  Queue::main().exec_async(move || {
    ffi::CGRestorePermanentDisplayConfiguration();
    assert_eq!(ffi::CGDisplayRelease(ns_screen), ffi::kCGErrorSuccess);
  });
}

// `setMaximized` is not thread-safe
pub unsafe fn set_maximized_async(
  ns_window: id,
  is_zoomed: bool,
  maximized: bool,
  shared_state: Weak<Mutex<SharedState>>,
) {
  let ns_window = MainThreadSafe(ns_window);
  let shared_state = MainThreadSafe(shared_state);
  Queue::main().exec_async(move || {
    if let Some(shared_state) = shared_state.upgrade() {
      trace!("Locked shared state in `set_maximized`");
      let mut shared_state_lock = shared_state.lock().unwrap();

      // Save the standard frame sized if it is not zoomed
      if !is_zoomed {
        shared_state_lock.standard_frame = Some(NSWindow::frame(*ns_window));
      }

      shared_state_lock.maximized = maximized;

      let curr_mask = ns_window.styleMask();
      if shared_state_lock.fullscreen.is_some() {
        // Handle it in window_did_exit_fullscreen
        return;
      } else if curr_mask.contains(NSWindowStyleMask::NSResizableWindowMask) {
        // Just use the native zoom if resizable
        ns_window.zoom_(nil);
      } else {
        // if it's not resizable, we set the frame directly
        let new_rect = if maximized {
          let screen = NSScreen::mainScreen(nil);
          NSScreen::visibleFrame(screen)
        } else {
          shared_state_lock.saved_standard_frame()
        };
        ns_window.setFrame_display_(new_rect, NO);
      }

      trace!("Unlocked shared state in `set_maximized`");
    }
  });
}

// `orderOut:` isn't thread-safe. Calling it from another thread actually works,
// but with an odd delay.
pub unsafe fn order_out_async(ns_window: id) {
  let ns_window = MainThreadSafe(ns_window);
  Queue::main().exec_async(move || {
    ns_window.orderOut_(nil);
  });
}

// `makeKeyAndOrderFront:` isn't thread-safe. Calling it from another thread
// actually works, but with an odd delay.
pub unsafe fn make_key_and_order_front_async(ns_window: id) {
  let ns_window = MainThreadSafe(ns_window);
  Queue::main().exec_async(move || {
    ns_window.makeKeyAndOrderFront_(nil);
  });
}

// `setTitle:` isn't thread-safe. Calling it from another thread invalidates the
// window drag regions, which throws an exception when not done in the main
// thread
pub unsafe fn set_title_async(ns_window: id, title: String) {
  let ns_window = MainThreadSafe(ns_window);
  Queue::main().exec_async(move || {
    let title = IdRef::new(NSString::alloc(nil).init_str(&title));
    ns_window.setTitle_(*title);
  });
}

// `setFocus:` isn't thread-safe.
pub unsafe fn set_focus(ns_window: id) {
  let ns_window = MainThreadSafe(ns_window);
  Queue::main().exec_async(move || {
    ns_window.makeKeyAndOrderFront_(nil);
    let app: id = msg_send![class!(NSApplication), sharedApplication];
    let () = msg_send![app, activateIgnoringOtherApps: YES];
  });
}

// `close:` is thread-safe, but we want the event to be triggered from the main
// thread. Though, it's a good idea to look into that more...
//
// ArturKovacs: It's important that this operation keeps the underlying window alive
// through the `IdRef` because otherwise it would dereference free'd memory
pub unsafe fn close_async(ns_window: IdRef) {
  let ns_window = MainThreadSafe(ns_window);
  Queue::main().exec_async(move || {
    autoreleasepool(move || {
      ns_window.close();
      let event = Event::WindowEvent {
        window_id: WindowId(get_window_id(*ns_window.0)),
        event: WindowEvent::Destroyed,
      };
      AppState::queue_event(EventWrapper::StaticEvent(event));
    });
  });
}
