// LAF OS Library
// Copyright (C) 2019-2025  Igara Studio S.A.
// Copyright (C) 2012-2017  David Capello
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.

#ifdef HAVE_CONFIG_H
  #include "config.h"
#endif

#include "os/osx/window.h"

#include "base/debug.h"
#include "os/event.h"
#include "os/osx/app.h"
#include "os/osx/event_queue.h"
#include "os/osx/view.h"
#include "os/osx/window_delegate.h"
#include "os/surface.h"
#include "os/window_spec.h"

@implementation WindowOSXObjc

- (WindowOSXObjc*)initWithImpl:(os::WindowOSX*)impl spec:(const os::WindowSpec*)spec
{
  // This autoreleasepool reduces the total number of references that
  // the NSWindow object has when we close it.
  @autoreleasepool {
    m_impl = impl;
    m_scale = spec->scale();

    NSScreen* primaryScreen = NSScreen.screens[0];
    NSScreen* nsScreen;
    if (spec->screen())
      nsScreen = (__bridge NSScreen*)spec->screen()->nativeHandle();
    else
      nsScreen = [NSScreen mainScreen];

    NSWindowStyleMask style = 0;
    if (spec->titled())
      style |= NSWindowStyleMaskTitled;
    if (spec->closable())
      style |= NSWindowStyleMaskClosable;
    if (spec->minimizable())
      style |= NSWindowStyleMaskMiniaturizable;
    if (spec->resizable())
      style |= NSWindowStyleMaskResizable;
    if (spec->borderless())
      style |= NSWindowStyleMaskBorderless;

    NSRect contentRect;
    if (!spec->contentRect().isEmpty()) {
      contentRect = NSMakeRect(spec->contentRect().x - nsScreen.frame.origin.x,
                               (primaryScreen.frame.origin.y + primaryScreen.frame.size.height) -
                                 spec->contentRect().y2() - nsScreen.frame.origin.y,
                               spec->contentRect().w,
                               spec->contentRect().h);
    }
    else if (!spec->frame().isEmpty()) {
      NSRect frameRect = NSMakeRect(
        spec->frame().x - nsScreen.frame.origin.x,
        (primaryScreen.frame.origin.y + primaryScreen.frame.size.height) - spec->frame().y2() -
          nsScreen.frame.origin.y,
        spec->frame().w,
        spec->frame().h);

      contentRect = [NSWindow contentRectForFrameRect:frameRect styleMask:style];
    }
    else {
      // TODO is there a default size for macOS apps?
      contentRect = NSMakeRect(0, 0, 400, 300);
    }

    // Align the content size to the scale, because macOS give us the
    // chance to create the frame with any size, then the
    // contentResizeIncrements stablish a delta from this initial size.
    contentRect.size.width = int(contentRect.size.width / m_scale) * m_scale;
    contentRect.size.height = int(contentRect.size.height / m_scale) * m_scale;

    self = [self initWithContentRect:contentRect
                           styleMask:style
                             backing:NSBackingStoreBuffered
                               defer:NO
                              screen:nsScreen];
    if (!self)
      return nil;

    m_delegate = [[WindowOSXDelegate alloc] initWithWindowImpl:impl];

    // The NSView width and height will be a multiple of scale().
    self.contentResizeIncrements = NSMakeSize(m_scale, m_scale);

    ViewOSX* view = [[ViewOSX alloc] initWithFrame:contentRect];
    m_view = view;
    [view setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];

    // Remove shadow for borderless windows (the shadow is too dark and
    // creates a thick black border arround our windows).
    if (spec->borderless())
      self.hasShadow = false;

    if (spec->transparent()) {
      self.hasShadow = false;
      self.opaque = false;
      self.backgroundColor = NSColor.clearColor;
    }

    // Redraw the entire window content when we resize it.
    // TODO add support to avoid redrawing the entire window
    self.preservesContentDuringLiveResize = false;

    [self setDelegate:m_delegate];
    [self setContentView:view];

    if (spec->position() == os::WindowSpec::Position::Center) {
      [self center];
    }

    if (spec->parent())
      self.parentWindow =
        (__bridge NSWindow*)static_cast<os::WindowOSX*>(spec->parent())->nativeHandle();

    [self makeKeyAndOrderFront:self];

    m_floating = spec->floating();
    if (spec->floating())
      self.level = NSFloatingWindowLevel;

    if (spec->modal())
      self.level = NSModalPanelWindowLevel;

    // Hide the "View > Show Tab Bar" menu item
    if ([self respondsToSelector:@selector(setTabbingMode:)])
      [self setTabbingMode:NSWindowTabbingModeDisallowed];
  }
  return self;
}

- (os::WindowOSX*)impl
{
  return m_impl;
}

- (void)removeImpl
{
  [m_view removeImpl];

  [self setDelegate:nil];
  [m_delegate removeImpl];
  m_delegate = nil;

  m_view = nil;
  m_impl = nil;

  // After calling this the ViewOSX dealloc should be called.
  [self setContentView:nil];
}

- (int)scale
{
  return m_scale;
}

- (void)setScale:(int)scale
{
  // If the scale is the same, we don't generate a resize event.
  if (m_scale == scale)
    return;

  m_scale = scale;
  self.contentResizeIncrements = NSMakeSize(m_scale, m_scale);

  if (m_impl) {
    // Check if the content needs a new size because it's not a
    // multiple of the new scale.
    gfx::Size content = m_impl->contentRect().size();
    gfx::Size newContent = content;
    newContent.w = int(content.w / m_scale) * m_scale;
    newContent.h = int(content.h / m_scale) * m_scale;
    if (content != newContent) {
      // Resize the frame so the new content size is a multiple of the
      // new given scale.
      gfx::Rect frame = m_impl->frame();
      gfx::Rect oldFrame = frame;
      frame.w += (newContent.w - content.w);
      frame.h += (newContent.h - content.h);
      m_impl->setFrame(frame);

      // onResize() will be called from View's setFrameSize()
    }
    else {
      // There is no need for a resize of the frame, but we inform the
      // new content scale through onResize().
      m_impl->onResize(content);
    }
  }
}

- (gfx::Size)clientSize
{
  return gfx::Size([[self contentView] frame].size.width, [[self contentView] frame].size.height);
}

- (void)setMousePosition:(const gfx::Point&)position
{
  NSView* view = self.contentView;
  NSPoint pt = NSMakePoint(position.x * m_scale, view.frame.size.height - position.y * m_scale);

  pt = [view convertPoint:pt toView:view];
  pt = [view convertPoint:pt toView:nil];
  pt = [self convertBaseToScreen:pt];
  pt.y = [[self screen] frame].size.height - pt.y;

  CGPoint pos = CGPointMake(pt.x, pt.y);
  CGEventRef event = CGEventCreateMouseEvent(NULL, kCGEventMouseMoved, pos, kCGMouseButtonLeft);
  CGEventPost(kCGHIDEventTap, event);
  CFRelease(event);
}

- (BOOL)setNativeCursor:(os::NativeCursor)cursor
{
  NSCursor* nsCursor = nullptr;

  switch (cursor) {
    case os::NativeCursor::Arrow:
    case os::NativeCursor::Wait:
    case os::NativeCursor::Help:
    case os::NativeCursor::SizeNE:
    case os::NativeCursor::SizeNW:
    case os::NativeCursor::SizeSE:
    case os::NativeCursor::SizeSW:    nsCursor = [NSCursor arrowCursor]; break;
    case os::NativeCursor::Crosshair: nsCursor = [NSCursor crosshairCursor]; break;
    case os::NativeCursor::IBeam:     nsCursor = [NSCursor IBeamCursor]; break;
    case os::NativeCursor::Link:      nsCursor = [NSCursor pointingHandCursor]; break;
    case os::NativeCursor::Forbidden: nsCursor = [NSCursor operationNotAllowedCursor]; break;
    case os::NativeCursor::Move:      nsCursor = [NSCursor openHandCursor]; break;
    case os::NativeCursor::SizeNS:    nsCursor = [NSCursor resizeUpDownCursor]; break;
    case os::NativeCursor::SizeWE:    nsCursor = [NSCursor resizeLeftRightCursor]; break;
    case os::NativeCursor::SizeN:     nsCursor = [NSCursor resizeUpCursor]; break;
    case os::NativeCursor::SizeE:     nsCursor = [NSCursor resizeRightCursor]; break;
    case os::NativeCursor::SizeS:     nsCursor = [NSCursor resizeDownCursor]; break;
    case os::NativeCursor::SizeW:     nsCursor = [NSCursor resizeLeftCursor]; break;
    default:                          break;
  }

  [self.contentView setCursor:nsCursor];
  return (nsCursor ? YES : NO);
}

- (BOOL)canBecomeKeyWindow
{
  if (m_impl)
    return YES;
  else
    return NO;
}

- (BOOL)isFloating
{
  return m_floating;
}

- (void)noResponderFor:(SEL)eventSelector
{
  if (eventSelector == @selector(keyDown:)) {
    // Do nothing (avoid beep)
  }
  else {
    [super noResponderFor:eventSelector];
  }
}

@end

namespace os {

void WindowOSX::createWindow(const os::WindowSpec& spec)
{
  m_nsWindow = [[WindowOSXObjc alloc] initWithImpl:this spec:&spec];
  m_nsWindow.releasedWhenClosed = true;
}

void WindowOSX::destroyWindow()
{
  if (!m_nsWindow)
    return;

  // This autoreleasepool reduces the total number of references that
  // the NSWindow object has when we destroy it.
  @autoreleasepool {
    [m_nsWindow removeImpl];

    // Select other window
    {
      auto app = [NSApplication sharedApplication];
      auto index = [app.windows indexOfObject:m_nsWindow];
      if (index + 1 < app.windows.count) {
        ++index;
      }
      else {
        --index;
      }
      if (index >= 0 && index < app.windows.count)
        [[app.windows objectAtIndex:index] makeKeyWindow];
    }

    [m_nsWindow discardEventsMatchingMask:NSEventMaskAny beforeEvent:nullptr];
    [m_nsWindow close];
  }
  m_nsWindow = nil;
}

gfx::Size WindowOSX::clientSize() const
{
  return [m_nsWindow clientSize];
}

gfx::Rect WindowOSX::frame() const
{
  NSRect r = m_nsWindow.frame;
  NSScreen* primaryScreen = NSScreen.screens[0];

  return gfx::Rect(
    r.origin.x,
    (primaryScreen.frame.origin.y + primaryScreen.frame.size.height) - (r.origin.y + r.size.height),
    r.size.width,
    r.size.height);
}

void WindowOSX::setFrame(const gfx::Rect& bounds)
{
  NSScreen* primaryScreen = NSScreen.screens[0];

  [m_nsWindow setFrame:NSMakeRect(bounds.x,
                                  (primaryScreen.frame.origin.y + primaryScreen.frame.size.height) -
                                    bounds.y2(),
                                  bounds.w,
                                  bounds.h)
               display:YES];
}

gfx::Rect WindowOSX::contentRect() const
{
  NSRect r = [m_nsWindow contentRectForFrameRect:m_nsWindow.frame];
  NSScreen* primaryScreen = NSScreen.screens[0];

  return gfx::Rect(
    r.origin.x,
    (primaryScreen.frame.origin.y + primaryScreen.frame.size.height) - (r.origin.y + r.size.height),
    r.size.width,
    r.size.height);
}

gfx::Rect WindowOSX::restoredFrame() const
{
  return m_restoredFrame;
}

void WindowOSX::activate()
{
  if ([m_nsWindow.delegate respondsToSelector:@selector(windowShouldBecomeKey:)] &&
      ![(id)m_nsWindow.delegate windowShouldBecomeKey:m_nsWindow])
    return;
  [m_nsWindow makeKeyAndOrderFront:nil];
}

void WindowOSX::maximize()
{
  [m_nsWindow zoom:m_nsWindow];
}

void WindowOSX::minimize()
{
  [m_nsWindow miniaturize:m_nsWindow];
}

bool WindowOSX::isMaximized() const
{
  return [m_nsWindow isZoomed];
}

bool WindowOSX::isMinimized() const
{
  // Return true if the NSWindow is minimized or if the NSApplication
  // is hidden.
  return (m_nsWindow.miniaturized || os::AppOSX::instance()->isHidden());
}

bool WindowOSX::isFullscreen() const
{
  return ((m_nsWindow.styleMask & NSWindowStyleMaskFullScreen) == NSWindowStyleMaskFullScreen);
}

void WindowOSX::setFullscreen(bool state)
{
  if (state) {
    if (!isFullscreen()) {
      // TODO this doesn't work for borderless windows
      [m_nsWindow toggleFullScreen:m_nsWindow];
    }
  }
  else {
    if (isFullscreen()) {
      [m_nsWindow toggleFullScreen:m_nsWindow];
    }
  }
}

std::string WindowOSX::title() const
{
  return [m_nsWindow.title UTF8String];
}

void WindowOSX::setTitle(const std::string& title)
{
  [m_nsWindow setTitle:[NSString stringWithUTF8String:title.c_str()]];
}

void WindowOSX::captureMouse()
{
  // TODO
}

void WindowOSX::releaseMouse()
{
  // TODO
}

void WindowOSX::setMousePosition(const gfx::Point& position)
{
  [m_nsWindow setMousePosition:position];
}

void WindowOSX::performWindowAction(const WindowAction action, const Event* event)
{
  if (action == WindowAction::Move) {
    // We cannot use the "m_nsWindow.currentEvent" event directly on
    // [NSWindow performWindowDragWithEvent:] because sometimes it's a
    // NSEventTypeAppKitDefined instead of the mouse event and the
    // mouse location inside that event is invalid. So we just use the
    // current mouse position from [NSEvent mouseLocation] to avoid
    // this creating a new mouse event.

    NSPoint mousePosOnWindow;
    {
      NSPoint mousePosOnScreen = [NSEvent mouseLocation];
      NSRect rc = NSMakeRect(mousePosOnScreen.x, mousePosOnScreen.y, 1, 1);

      // Note: We cannot use [NSWindow convertPointFromScreen] because
      // it is a function available only since macOS 10.12. So we just
      // convert a dummy rectangle.
      rc = [m_nsWindow convertRectFromScreen:rc];

      mousePosOnWindow = rc.origin;
    }

    NSEvent* newEvent = [NSEvent mouseEventWithType:NSEventTypeLeftMouseDown
                                           location:mousePosOnWindow
                                      modifierFlags:0
                                          timestamp:0
                                       windowNumber:m_nsWindow.windowNumber
                                            context:nil
                                        eventNumber:0
                                         clickCount:1
                                           pressure:1.0];

    [m_nsWindow performWindowDragWithEvent:newEvent];
  }
}

os::ScreenRef WindowOSX::screen() const
{
  ASSERT(m_nsWindow);
  return os::make_ref<os::ScreenOSX>(m_nsWindow.screen);
}

os::ColorSpaceRef WindowOSX::colorSpace() const
{
  if (auto cs = Window::colorSpace())
    return cs;

  ASSERT(m_nsWindow);
  return os::convert_nscolorspace_to_os_colorspace([m_nsWindow colorSpace]);
}

int WindowOSX::scale() const
{
  return [m_nsWindow scale];
}

void WindowOSX::setScale(int scale)
{
  [m_nsWindow setScale:scale];
}

bool WindowOSX::isVisible() const
{
  return m_nsWindow.isVisible;
}

void WindowOSX::setVisible(bool visible)
{
  if (visible) {
    // The main window can be changed only when the NSWindow
    // is visible (i.e. when NSWindow::canBecomeMainWindow
    // returns YES).
    if (m_nsWindow.canBecomeMainWindow)
      [m_nsWindow makeMainWindow];
    else
      [m_nsWindow setIsVisible:true];
  }
  else {
    [m_nsWindow setIsVisible:false];
  }
}

bool WindowOSX::setCursor(NativeCursor cursor)
{
  return ([m_nsWindow setNativeCursor:cursor] ? true : false);
}

bool WindowOSX::setCursor(const CursorRef& cursor)
{
  [m_nsWindow.contentView
    setCursor:(cursor ? (__bridge NSCursor*)cursor->nativeHandle() : nullptr)];
  return true;
}

bool WindowOSX::isTransparent() const
{
  return (m_nsWindow.opaque ? false : true);
}

void* WindowOSX::nativeHandle() const
{
  return (__bridge void*)m_nsWindow;
}

void WindowOSX::onBeforeMaximizeFrame()
{
  m_restoredFrame = frame();
}

} // namespace os
