#import <AppKit/AppKit.h>
#import <CoreServices/CoreServices.h>
#import <Foundation/Foundation.h>
#import <objc/objc-runtime.h>

#include <node_api.h>

@interface PanelDelegate : NSObject <NSWindowDelegate>
@end

@interface Panel : NSPanel
@property(strong, nonatomic) NSWindow *originalWindow;
@end

void HidePanel(Panel *window, bool animate);

@implementation PanelDelegate {
}

- (void)windowDidResize:(NSNotification *)notification {
  Panel *window = (Panel *)[notification object];
  [window.originalWindow setFrame:window.frame display:NO];
}

- (void)windowDidMove:(NSNotification *)notification {
  Panel *window = (Panel *)[notification object];
  [window.originalWindow setFrame:window.frame display:NO];
}

- (void)windowDidResignKey:(NSNotification *)notification {
  HidePanel((Panel *)[notification object], false);
}

@end

@implementation Panel
@synthesize originalWindow;
- (id)init {
  self = [super init];
  if (self) {
    self.delegate = [[PanelDelegate alloc] init];
  }
  return self;
}
- (BOOL)canBecomeKeyWindow {
  return YES;
}
@end

napi_value IsFocused(napi_env env, napi_callback_info info) {
  napi_value napi_false;

  napi_get_boolean(env, false, &napi_false);

  napi_status status;
  size_t argc = 1;
  napi_value handleBuffer[1];

  status = napi_get_cb_info(env, info, &argc, handleBuffer, 0, 0);
  if (status != napi_ok || argc < 1) {
    napi_throw_type_error(env, NULL, "Wrong number of arguments");
    return napi_false;
  }

  napi_handle_scope scope;
  status = napi_open_handle_scope(env, &scope);
  if (status != napi_ok) {
    return napi_false;
  }

  void *buffer;
  size_t bufferLength;
  status = napi_get_buffer_info(env, handleBuffer[0], &buffer, &bufferLength);
  if (status != napi_ok) {
    return napi_false;
  }

  NSView *mainContentView = *reinterpret_cast<NSView **>(buffer);
  if (!mainContentView) {
    return napi_false;
  }

  Panel *panel = (Panel *)mainContentView.window;

  napi_value result;
  napi_get_boolean(env, [panel isKeyWindow], &result);

  status = napi_close_handle_scope(env, scope);
  if (status != napi_ok) {
    return napi_false;
  }

  return result;
}

enum Action { Close, Hide, Show, Sync };

void ShowPanel(Panel *window, bool animate) {
  if (animate) {
    [window setAlphaValue:0.0];
    [window orderFrontRegardless];

    NSRect originalFrame = [window frame];
    NSRect offsetFrame = NSOffsetRect(originalFrame, 0, -12);
    [window setFrame:offsetFrame display:true];

    [NSAnimationContext beginGrouping];
    [[window animator] setAlphaValue:1.0];
    [[window animator] setFrame:originalFrame display:true];
    [NSAnimationContext endGrouping];
  } else {
    [window orderFrontRegardless];
  }

  [window makeKeyWindow];
}

void HidePanel(Panel *window, bool animate) {
  if (animate) {
    [NSAnimationContext beginGrouping];
    __block __unsafe_unretained Panel *_window = window;
    [[NSAnimationContext currentContext] setCompletionHandler:^{
      [_window orderOut:nil];
    }];
    [[window animator] setAlphaValue:0.0];
    [NSAnimationContext endGrouping];
  } else {
    [window orderOut:nil];
  }
}

void ClosePanel(Panel *window, bool animate) {
  if (animate) {
    [NSAnimationContext beginGrouping];
    __block __unsafe_unretained NSWindow *_originalWindow =
        window.originalWindow;
    __block __unsafe_unretained Panel *_window = window;
    [[NSAnimationContext currentContext] setCompletionHandler:^{
      [_originalWindow close];
      [_window close];
    }];
    [[window animator] setAlphaValue:0.0];
    [NSAnimationContext endGrouping];
  } else {
    [window.originalWindow close];
    [window close];
  }
}

void SyncPanel(Panel *window, bool animate) {
  NSWindow *originalWindow = window.originalWindow;

  [window setFrame:originalWindow.frame display:YES animate:animate];
  [window setContentMinSize:originalWindow.contentMinSize];
  [window setContentMaxSize:originalWindow.contentMaxSize];

  if (originalWindow.resizable) {
    window.styleMask |= NSWindowStyleMaskResizable;
  } else {
    window.styleMask &= ~NSWindowStyleMaskResizable;
  }

  NSSize size = originalWindow.aspectRatio;
  NSSize zeroSize = NSMakeSize(0.0, 0.0);
  if (CGSizeEqualToSize(size, zeroSize)) {
    [window setResizeIncrements:NSMakeSize(1.0, 1.0)];
  } else {
    [window setAspectRatio:size];
  }
}

napi_value MakePanel(napi_env env, napi_callback_info info) {
  napi_status status;
  size_t argc = 1;
  napi_value handleBuffer[1];

  status = napi_get_cb_info(env, info, &argc, handleBuffer, 0, 0);
  if (status != napi_ok || argc < 1) {
    napi_throw_type_error(env, NULL, "Wrong number of arguments");
    return 0;
  }

  napi_handle_scope scope;
  status = napi_open_handle_scope(env, &scope);
  if (status != napi_ok) {
    return 0;
  }

  void *buffer;
  size_t bufferLength;
  status = napi_get_buffer_info(env, handleBuffer[0], &buffer, &bufferLength);
  if (status != napi_ok) {
    return handleBuffer[0];
  }
  NSView *mainContentView = *reinterpret_cast<NSView **>(buffer);
  if (!mainContentView) {
    return handleBuffer[0];
  }

  NSWindow *originalWindow = mainContentView.window;

  Panel *window = [[Panel alloc] init];
  [window setReleasedWhenClosed:YES];
  [window setOriginalWindow:originalWindow];
  [window setFrame:originalWindow.frame display:NO];
  [window setContentSize:mainContentView.frame.size];
  [window setContentMinSize:originalWindow.contentMinSize];
  [window setContentMaxSize:originalWindow.contentMaxSize];
  [window setMovableByWindowBackground:YES];
  [window setFloatingPanel:YES];
  [window setLevel:NSFloatingWindowLevel];
  [window setCanHide:YES];
  [window setTitleVisibility:NSWindowTitleHidden];
  [window setTitlebarAppearsTransparent:YES];
  [window setStyleMask:NSWindowStyleMaskBorderless |
                       NSWindowStyleMaskNonactivatingPanel];
  [window setOpaque:NO];
  [window setHasShadow:NO];
  [window setBackgroundColor:[NSColor clearColor]];

  if (originalWindow.resizable) {
    window.styleMask |= NSWindowStyleMaskResizable;
  }

  [window setCollectionBehavior:NSWindowCollectionBehaviorCanJoinAllSpaces |
                                NSWindowCollectionBehaviorFullScreenAuxiliary];

  [window setHidesOnDeactivate:NO];

  [mainContentView removeFromSuperview];
  [window setContentView:mainContentView];
  [mainContentView viewDidMoveToWindow];

  status = napi_close_handle_scope(env, scope);
  if (status != napi_ok) {
    return 0;
  }

  return handleBuffer[0];
}

napi_value RunAction(Action action, napi_env env, napi_callback_info info) {
  napi_status status;
  size_t argc = 2;
  napi_value handleBuffer[2];

  status = napi_get_cb_info(env, info, &argc, handleBuffer, 0, 0);
  if (status != napi_ok || argc < 2) {
    napi_throw_type_error(env, NULL, "Wrong number of arguments");
    return 0;
  }

  napi_handle_scope scope;
  status = napi_open_handle_scope(env, &scope);
  if (status != napi_ok) {
    return 0;
  }

  void *buffer;
  size_t bufferLength;
  status = napi_get_buffer_info(env, handleBuffer[0], &buffer, &bufferLength);
  if (status != napi_ok) {
    return handleBuffer[0];
  }

  bool animate;
  status = napi_get_value_bool(env, handleBuffer[1], &animate);
  if (status != napi_ok) {
    return handleBuffer[0];
  }

  NSView *mainContentView = *reinterpret_cast<NSView **>(buffer);
  if (!mainContentView) {
    return handleBuffer[0];
  }

  Panel *panel = (Panel *)mainContentView.window;

  switch (action) {
  case Close:
    ClosePanel(panel, animate);
    break;
  case Hide:
    HidePanel(panel, animate);
    break;
  case Show:
    ShowPanel(panel, animate);
    break;
  case Sync:
    SyncPanel(panel, animate);
    break;
  }

  status = napi_close_handle_scope(env, scope);
  if (status != napi_ok) {
    return 0;
  }

  return handleBuffer[0];
}

napi_value ShowPanel(napi_env env, napi_callback_info info) {
  return RunAction(Action::Show, env, info);
}

napi_value HidePanel(napi_env env, napi_callback_info info) {
  return RunAction(Action::Hide, env, info);
}

napi_value ClosePanel(napi_env env, napi_callback_info info) {
  return RunAction(Action::Close, env, info);
}

napi_value SyncPanel(napi_env env, napi_callback_info info) {
  return RunAction(Action::Sync, env, info);
}

#define DECLARE_NAPI_METHOD(name, func)                                        \
  { name, 0, func, 0, 0, 0, napi_default, 0 }

napi_value Init(napi_env env, napi_value exports) {
  napi_status status;

  napi_property_descriptor descriptors[] = {
      DECLARE_NAPI_METHOD("IsFocused", IsFocused),
      DECLARE_NAPI_METHOD("MakePanel", MakePanel),
      DECLARE_NAPI_METHOD("ShowPanel", ShowPanel),
      DECLARE_NAPI_METHOD("HidePanel", HidePanel),
      DECLARE_NAPI_METHOD("ClosePanel", ClosePanel),
      DECLARE_NAPI_METHOD("SyncPanel", SyncPanel)};

  status = napi_define_properties(env, exports, 5, descriptors);
  if (status != napi_ok) {
    return NULL;
  }

  return exports;
}

NAPI_MODULE(Panel, Init)
