#include "pch.h"
#include "log.h"
#include "shader.h"
#include "shape.h"
#include "texture.h"
#include "image.h"
#include "app.h"
#include "keymap.h"
#include "main.h"
#include "platform_apple/apple_platform_services.h"
#include "settings.h"
#include <CoreFoundation/CoreFoundation.h>
#include <Cocoa/Cocoa.h>
#include <CoreVideo/CoreVideo.h>
#include <OpenGL/OpenGL.h>
#import <Carbon/Carbon.h>
#import "objc_utils.h"

#include <deque>
#include <chrono>

@import AppCenter;
@import AppCenterAnalytics;
@import AppCenterCrashes;

static std::unique_ptr<pp::platform::PlatformServices> g_platform_services;

NSString* keyCodeToString(NSUInteger keyCode, NSUInteger mods)
{
  TISInputSourceRef currentKeyboard = TISCopyCurrentKeyboardInputSource();
  CFDataRef uchr =
    (CFDataRef)TISGetInputSourceProperty(currentKeyboard,
                                         kTISPropertyUnicodeKeyLayoutData);
  const UCKeyboardLayout *keyboardLayout =
    (const UCKeyboardLayout*)CFDataGetBytePtr(uchr);

  if(keyboardLayout)
  {
    UInt32 deadKeyState = 0;
    UniCharCount maxStringLength = 255;
    UniCharCount actualStringLength = 0;
    UniChar unicodeString[maxStringLength];

    OSStatus status = UCKeyTranslate(keyboardLayout,
                                     keyCode, kUCKeyActionDown, mods,
                                     LMGetKbdType(), 0,
                                     &deadKeyState,
                                     maxStringLength,
                                     &actualStringLength, unicodeString);

    if (actualStringLength == 0 && deadKeyState)
    {
      status = UCKeyTranslate(keyboardLayout,
                                       kVK_Space, kUCKeyActionDown, mods,
                                       LMGetKbdType(), 0,
                                       &deadKeyState,
                                       maxStringLength,
                                       &actualStringLength, unicodeString);
    }
    if(actualStringLength > 0 && status == noErr)
      return [[NSString stringWithCharacters:unicodeString
                        length:(NSUInteger)actualStringLength] lowercaseString];
  }

  return nil;
}

@implementation View
{
    NSSharingService *airdrop_service;
}
- (std::string)clipboard_get_string
{
    NSString* ns = [[NSPasteboard generalPasteboard] stringForType:NSPasteboardTypeString];
    const char* ptr = [ns cStringUsingEncoding:NSUTF8StringEncoding];
    return std::string(ptr);
}
- (bool)clipboard_set_string:(const std::string &)s
{
    NSString* ns = [NSString stringWithUTF8String:s.c_str()];
    [[NSPasteboard generalPasteboard] setString:ns forType:NSPasteboardTypeString];
    return true;
}
- (void)hockeyapp_crash
{
    [MSCrashes generateTestCrash];
}
- (void)async_lock
{
    CGLLockContext([glctx CGLContextObj]);
    [glctx makeCurrentContext];
}
- (void)async_unlock
{
    CGLUnlockContext([glctx CGLContextObj]);
}
- (void)async_swap
{
    CGLFlushDrawable([glctx CGLContextObj]);
}
- (void)close
{
    [[NSApplication sharedApplication] terminate:nil];
}
- (void)show_cursor:(bool)visible
{
    if (cursor_visible == visible)
        return;
    cursor_visible = visible;
    if (cursor_inside && !cursor_ignore)
        visible ? [NSCursor unhide] : [NSCursor hide];
}
- (std::string)pick_file:(NSArray<NSString*>*)types
{
    NSOpenPanel *panel = [NSOpenPanel openPanel];
    [panel setCanChooseFiles:YES];
    [panel setCanChooseDirectories:NO];
    [panel setAllowsMultipleSelection:NO]; // yes if more than one dir is allowed
    
    //NSArray* fileTypes = [NSArray arrayWithObjects:@"png", @"PNG", @"jpg", @"JPG", @"jpeg", nil];
    [panel setAllowedFileTypes:types];

    [self removeTrackingArea:trackingArea];
    if (!cursor_visible)
        [NSCursor unhide];

    NSInteger clicked = [panel runModal];
    
    [self addTrackingArea:trackingArea];
    if (!cursor_visible)
        [NSCursor hide];

    std::string ret;
    if (clicked == NSFileHandlingPanelOKButton)
    {
        for (NSURL *url in [panel URLs])
        {
            LOG("selected file: %s", [[url path] cStringUsingEncoding:NSUTF8StringEncoding]);
            ret = [[url path] cStringUsingEncoding:NSUTF8StringEncoding];
        }
    }
    return ret;
}
- (std::string)pick_file_save:(NSArray<NSString*>*)types
{
    NSSavePanel *panel = [NSSavePanel savePanel];
    [panel setCanCreateDirectories:YES];
    
    [panel setAllowedFileTypes:types];

    [self removeTrackingArea:trackingArea];
    if (!cursor_visible)
        [NSCursor unhide];

    NSInteger clicked = [panel runModal];
    
    [self addTrackingArea:trackingArea];
    if (!cursor_visible)
        [NSCursor hide];

    std::string ret;
    if (clicked == NSFileHandlingPanelOKButton)
    {
        NSURL *url = [panel URL];
        LOG("selected file: %s", [[url path] cStringUsingEncoding:NSUTF8StringEncoding]);
        ret = [[url path] cStringUsingEncoding:NSUTF8StringEncoding];
    }
    return ret;
}
- (std::string)pick_dir
{
    NSOpenPanel *panel = [NSOpenPanel openPanel];
    [panel setCanChooseFiles:NO];
    [panel setCanChooseDirectories:YES];
    [panel setAllowsMultipleSelection:NO]; // yes if more than one dir is allowed
    
    [self removeTrackingArea:trackingArea];
    if (!cursor_visible)
        [NSCursor unhide];

    NSInteger clicked = [panel runModal];

    [self addTrackingArea:trackingArea];
    if (!cursor_visible)
        [NSCursor hide];

    std::string ret;
    if (clicked == NSFileHandlingPanelOKButton)
    {
        for (NSURL *url in [panel URLs])
        {
            LOG("selected path: %s", [[url path] cStringUsingEncoding:NSUTF8StringEncoding]);
            ret = [[url path] cStringUsingEncoding:NSUTF8StringEncoding];
        }
    }
    return ret;
}
- (void)share_file:(NSString*)file_path
{
    NSURL *url = [NSURL fileURLWithPath:file_path];
    NSArray *objectsToShare = @[url];
    
    [self removeTrackingArea:trackingArea];
    cursor_ignore = true;
    if (!cursor_visible)
        [NSCursor unhide];

    [airdrop_service performWithItems:objectsToShare];
}
- (NSWindow *)sharingService:(NSSharingService *)sharingService sourceWindowForShareItems:(NSArray *)items sharingContentScope:(NSSharingContentScope *)sharingContentScope
{
    return self.window;
}
- (NSView *)anchoringViewForSharingService:(NSSharingService *)sharingService showRelativeToRect:(NSRect *)positioningRect preferredEdge:(NSRectEdge *)preferredEdge
{
    return self;
}
-(void)sharingService:(NSSharingService *)sharingService willShareItems:(NSArray *)items
{
}
- (void)sharingService:(NSSharingService *)sharingService didFailToShareItems:(NSArray *)items error:(NSError *)error
{
    [self addTrackingArea:trackingArea];
    cursor_ignore = false;
    if (!cursor_visible)
        [NSCursor hide];
}
- (void)sharingService:(NSSharingService *)sharingService didShareItems:(NSArray *)items
{
    [self addTrackingArea:trackingArea];
    cursor_ignore = false;
    if (!cursor_visible)
        [NSCursor hide];
}
- (instancetype)initWithFrame:(NSRect)frameRect
{
    airdrop_service = [NSSharingService  sharingServiceNamed:NSSharingServiceNameSendViaAirDrop];
    airdrop_service.delegate = self;
    cursor_visible = CGCursorIsVisible();
    cursor_inside = false;
    cursor_ignore = false;
    NSOpenGLPixelFormatAttribute attrs[] =
    {
        NSOpenGLPFADoubleBuffer,
        NSOpenGLPFADepthSize, 24,
        // Must specify the 3.2 Core Profile to use OpenGL 3.2
        NSOpenGLPFAOpenGLProfile, NSOpenGLProfileVersion3_2Core,
        // Multisample
        NSOpenGLPFAMultisample,
        NSOpenGLPFASamples, 2,
        NSOpenGLPFASampleBuffers, 1,
        0
    };
    NSOpenGLPixelFormat *pf = [[NSOpenGLPixelFormat alloc] initWithAttributes:attrs];
    NSOpenGLContext* context = [[NSOpenGLContext alloc] initWithFormat:pf shareContext:nil];
    self = [super initWithFrame:frameRect pixelFormat:pf];
    [self setPixelFormat:pf];
    [self setOpenGLContext:context];

    [self updateTrackingAreas];
    return self;
}
- (void)prepareOpenGL
{
    [super prepareOpenGL];
    
    NSLog(@"prepare");
    
    // Synchronize buffer swaps with vertical refresh rate
    GLint swapInt = 1;
    [[self openGLContext] setValues:&swapInt forParameter:NSOpenGLCPSwapInterval];
    
    glctx = [self openGLContext];
    
    App::I->render_thread_start();
    App::I->render_sync();
}

- (void)terminateGL
{
    App::I->ui_thread_stop();
    App::I->render_thread_stop();
    App::I->terminate();
    delete App::I;
    App::I = nullptr;
}

- (void)drawRect:(NSRect)dirtyRect
{
    return;
}
- (void)updateTrackingAreas
{
    if(trackingArea != nil)
        [self removeTrackingArea:trackingArea];
    trackingArea = [[NSTrackingArea alloc] initWithRect:[self bounds]
            options: (NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveInKeyWindow)
            owner:self userInfo:nil];
    [self addTrackingArea:trackingArea];
}
- (void)reshape
{
    [super reshape];
    
    // Get the view size in Points
    NSRect viewRectPoints = [self bounds];

    // We draw on a secondary thread through the display link. However, when
    // resizing the view, -drawRect is called on the main thread.
    // Add a mutex around to avoid the threads accessing the context
    // simultaneously when resizing.
    
#if SUPPORT_RETINA_RESOLUTION
    
    // Rendering at retina resolutions will reduce aliasing, but at the potential
    // cost of framerate and battery life due to the GPU needing to render more
    // pixels.
    
    // Any calculations the renderer does which use pixel dimentions, must be
    // in "retina" space.  [NSView convertRectToBacking] converts point sizes
    // to pixel sizes.  Thus the renderer gets the size in pixels, not points,
    // so that it can set it's viewport and perform and other pixel based
    // calculations appropriately.
    // viewRectPixels will be larger than viewRectPoints for retina displays.
    // viewRectPixels will be the same as viewRectPoints for non-retina displays
    NSRect viewRectPixels = [self convertRectToBacking:viewRectPoints];
    
#else //if !SUPPORT_RETINA_RESOLUTION
    
    // App will typically render faster and use less power rendering at
    // non-retina resolutions since the GPU needs to render less pixels.
    // There is the cost of more aliasing, but it will be no-worse than
    // on a Mac without a retina display.
    
    // Points:Pixels is always 1:1 when not supporting retina resolutions
    NSRect viewRectPixels = viewRectPoints;
    
#endif // !SUPPORT_RETINA_RESOLUTION
    App::I->ui_task_async([=]
    {
        App::I->resize(viewRectPixels.size.width, viewRectPixels.size.height);
    });
}
- (void)renewGState
{
    // Called whenever graphics state updated (such as window resize)
    
    // OpenGL rendering is not synchronous with other rendering on the OSX.
    // Therefore, call disableScreenUpdatesUntilFlush so the window server
    // doesn't render non-OpenGL content in the window asynchronously from
    // OpenGL content, which could cause flickering.  (non-OpenGL content
    // includes the title bar and drawing done by the app with other APIs)
    [[self window] disableScreenUpdatesUntilFlush];
    
    [super renewGState];
}
- (void)mouseDown:(NSEvent *)theEvent
{
    auto mouseLoc = [self convertPoint:[theEvent locationInWindow] fromView:nil];
    App::I->ui_task_async([mouseLoc,p=theEvent.pressure] {
        App::I->mouse_down(0, mouseLoc.x, App::I->height - mouseLoc.y - 1, p, kEventSource::Mouse, 0);
    });
}
- (void)rightMouseDown:(NSEvent *)theEvent
{
    auto mouseLoc = [self convertPoint:[theEvent locationInWindow] fromView:nil];
    App::I->ui_task_async([mouseLoc,p=theEvent.pressure] {
        App::I->mouse_down(1, mouseLoc.x, App::I->height - mouseLoc.y - 1, p, kEventSource::Mouse, 0);
    });
}
- (void)mouseUp:(NSEvent *)theEvent
{
    auto mouseLoc = [self convertPoint:[theEvent locationInWindow] fromView:nil];
    App::I->ui_task_async([mouseLoc] {
        App::I->mouse_up(0, mouseLoc.x, App::I->height - mouseLoc.y - 1, kEventSource::Mouse, 0);
    });
}
- (void)rightMouseUp:(NSEvent *)theEvent
{
    auto mouseLoc = [self convertPoint:[theEvent locationInWindow] fromView:nil];
    App::I->ui_task_async([mouseLoc] {
        App::I->mouse_up(1, mouseLoc.x, App::I->height - mouseLoc.y - 1, kEventSource::Mouse, 0);
    });
}
- (void)mouseMoved:(NSEvent *)theEvent
{
    auto mouseLoc = [self convertPoint:[theEvent locationInWindow] fromView:nil];
    App::I->ui_task_async([mouseLoc,p=theEvent.pressure] {
        App::I->mouse_move(mouseLoc.x, App::I->height - mouseLoc.y - 1, p, kEventSource::Mouse, 0);
    });
}
-(void)mouseDragged:(NSEvent *)theEvent
{
    auto mouseLoc = [self convertPoint:[theEvent locationInWindow] fromView:nil];
    App::I->ui_task_async([mouseLoc,p=theEvent.pressure] {
        App::I->mouse_move(mouseLoc.x, App::I->height - mouseLoc.y - 1, p, kEventSource::Mouse, 0);
    });
}
- (void)rightMouseDragged:(NSEvent *)theEvent
{
    auto mouseLoc = [self convertPoint:[theEvent locationInWindow] fromView:nil];
    App::I->ui_task_async([mouseLoc,p=theEvent.pressure] {
        App::I->mouse_move(mouseLoc.x, App::I->height - mouseLoc.y - 1, p, kEventSource::Mouse, 0);
    });
}
-(void)mouseExited:(NSEvent *)event
{
    if (!cursor_visible)
        [NSCursor unhide];
    cursor_inside = false;
}
-(void)mouseEntered:(NSEvent *)event
{
    if (!cursor_visible)
        [NSCursor hide];
    cursor_inside = true;
}
- (void)scrollWheel:(NSEvent *)theEvent
{
    auto mouseLoc = [self convertPoint:[theEvent locationInWindow] fromView:nil];
    App::I->ui_task_async([mouseLoc,d=[theEvent deltaY]] {
        App::I->mouse_scroll(mouseLoc.x, App::I->height - mouseLoc.y - 1, d);
    });
}
- (void)keyDown:(NSEvent *)theEvent
{
    auto keyCode = [theEvent keyCode];
    auto chars = [theEvent characters];
    NSString* nss = keyCodeToString(keyCode, [theEvent modifierFlags]);
    std::string s = [nss cStringUsingEncoding:NSUTF8StringEncoding];
    App::I->ui_task_async([keyCode, s] {
        if (App::I->keys[(int)kKey::KeyCtrl])
        {
            App::I->key_down(convert_key(keyCode));
            App::I->key_up(convert_key(keyCode));
        }
        else
        {
            App::I->key_down(convert_key(keyCode));
            if (!s.empty())
                App::I->key_char(s[0]);
        }
    });
}
- (void)keyUp:(NSEvent *)theEvent
{
    auto keyCode = [theEvent keyCode];
    App::I->ui_task_async([keyCode] {
        App::I->key_up(convert_key(keyCode));
    });
}
- (void)flagsChanged:(NSEvent *)event
{
    [super flagsChanged:event];

    auto flags = [event modifierFlags];
    App::I->ui_task_async([flags] {
        static bool pressed_shift = false;
        if (pressed_shift != (flags & NSShiftKeyMask))
        {
            pressed_shift = (flags & NSShiftKeyMask);
            pressed_shift ? App::I->key_down(kKey::KeyShift) : App::I->key_up(kKey::KeyShift);
        }
        static bool pressed_alt = false;
        if (pressed_alt != (flags & NSAlternateKeyMask))
        {
            pressed_alt = (flags & NSAlternateKeyMask);
            pressed_alt ? App::I->key_down(kKey::KeyAlt) : App::I->key_up(kKey::KeyAlt);
        }
        static bool pressed_cmd = false;
        if (pressed_cmd != (flags & NSCommandKeyMask))
        {
            pressed_cmd = (flags & NSCommandKeyMask);
            pressed_cmd ? App::I->key_down(kKey::KeyCtrl) : App::I->key_up(kKey::KeyCtrl);
        }
    });
}
@end

@implementation Window
@end

@implementation Controller
- (void)windowWillClose:(NSNotification *)notification
{
    [[NSApplication sharedApplication] terminate:nil];
}
-(BOOL)windowShouldClose:(NSWindow *)sender
{
    return App::I->request_close();
}
@end

@implementation AppOSX
- (instancetype)init
{
    self = [super init];
    [self setActivationPolicy:NSApplicationActivationPolicyRegular]; // make it to the front
    [self setDelegate:self];
    return self;
}
- (BOOL)application:(NSApplication *)sender openFile:(NSString *)filename
{
    LOG("open file %s", [filename UTF8String]);
    if (App::I)
    {
        std::string path = [filename UTF8String];
        App::I->ui_task_async([=]
        {
            App::I->open_document(path);
        });
    }
    return YES;
}
- (void)applicationWillTerminate:(NSNotification *)notification
{
    [view terminateGL];
    [window close];
}
- (void)applicationDidFinishLaunching:(NSNotification *)notification
{
    [MSAppCenter start:@"1500ff4a-32eb-4076-be3b-5147e2d8e27e" withServices:@[
      [MSAnalytics class],
      [MSCrashes class]
    ]];

    g_platform_services = pp::platform::apple::create_apple_platform_services();
    App::I->set_platform_services(g_platform_services.get());
    App::I->initLog();
    App::I->create();
    NSRect r = NSMakeRect(0, 0, App::I->width, App::I->height);
    if (Settings::has("window-rect"))
    {
        glm::ivec4 wr = Settings::value<Serializer::IVec4>("window-rect");
        r.origin = CGPointMake(wr.x, wr.y);
        r.size = CGSizeMake(wr.z, wr.w);
    }
    
    view = [[View alloc] initWithFrame:r];
    controller = [[Controller alloc] initWithWindow:window];
    pp::platform::apple::set_legacy_apple_state(view, nullptr);
    
    float z = (float)window.backingScaleFactor;
    App::I->zoom = Settings::value_or<Serializer::Float>("ui-scale", (z > 0.f) ? z : 1.f);

    auto style = NSTitledWindowMask|NSMiniaturizableWindowMask|NSResizableWindowMask|NSClosableWindowMask;
    window = [[Window alloc] initWithContentRect:r styleMask:style backing:NSBackingStoreBuffered defer:NO];
    [window setDelegate:controller];
    [window setTitle:[NSString stringWithUTF8String:g_window_title]];
    [window center];
    [window makeKeyAndOrderFront:controller];
    [window setContentView:view];
    [window setAcceptsMouseMovedEvents:true];
    [window makeFirstResponder:view];
    view->wnd = window;

    auto menubar = [NSMenu new];
    auto appMenuItem = [NSMenuItem new];
    [menubar addItem:appMenuItem];
    [self setMainMenu:menubar];
    
    auto appMenu = [NSMenu new];
    auto appName = [[NSProcessInfo processInfo] processName];
    auto quitTitle = [@"Quit " stringByAppendingString:appName];
    auto quitMenuItem = [[NSMenuItem alloc] initWithTitle:quitTitle
                                                   action:@selector(terminate:) keyEquivalent:@"q"];
    [appMenu addItem:quitMenuItem];
    [appMenuItem setSubmenu:appMenu];
    
    LOG("app launched");
    App::I->ui_thread_start();
}
- (void)save_ui_state
{
    Settings::set("window-rect", Serializer::IVec4({window.frame.origin.x,
        window.frame.origin.y, window.frame.size.width, window.frame.size.height}));
}
- (void)init_dirs
{
    NSArray* paths = NSSearchPathForDirectoriesInDomains(NSPicturesDirectory, NSUserDomainMask, YES);
    NSString* docpath = [(NSString*)[paths objectAtIndex:0] stringByAppendingString:@"/PanoPainter"];

    NSError* err = nil;

    if (![[NSFileManager defaultManager] createDirectoryAtPath:docpath withIntermediateDirectories:YES attributes:nil error:&err])
    {
        LOG("error creating rec path: %s", [[err localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]);
    }
    const std::string data_path = [docpath cStringUsingEncoding:NSASCIIStringEncoding];
    
    NSString* recpath = [docpath stringByAppendingString:@"/rec"];
    const std::string recording_path = [recpath cStringUsingEncoding:NSASCIIStringEncoding];
    if (![[NSFileManager defaultManager] createDirectoryAtPath:recpath withIntermediateDirectories:YES attributes:nil error:&err])
    {
        LOG("error creating rec path: %s", [[err localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]);
    }
    // brushes
    if (![[NSFileManager defaultManager] createDirectoryAtPath:[docpath stringByAppendingString:@"/brushes"] withIntermediateDirectories:YES attributes:nil error:&err])
    {
        LOG("error creating brushes path: %s", [[err localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]);
    }
    if (![[NSFileManager defaultManager] createDirectoryAtPath:[docpath stringByAppendingString:@"/brushes/thumbs"] withIntermediateDirectories:YES attributes:nil error:&err])
    {
        LOG("error creating brushes thumbs path: %s", [[err localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]);
    }
    // textures
    if (![[NSFileManager defaultManager] createDirectoryAtPath:[docpath stringByAppendingString:@"/patterns"] withIntermediateDirectories:YES attributes:nil error:&err])
    {
        LOG("error creating patterns path: %s", [[err localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]);
    }
    if (![[NSFileManager defaultManager] createDirectoryAtPath:[docpath stringByAppendingString:@"/patterns/thumbs"] withIntermediateDirectories:YES attributes:nil error:&err])
    {
        LOG("error creating patterns thumbs path: %s", [[err localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]);
    }
    // settings
    if (![[NSFileManager defaultManager] createDirectoryAtPath:[docpath stringByAppendingString:@"/settings"] withIntermediateDirectories:YES attributes:nil error:&err])
    {
        LOG("error creating settings path: %s", [[err localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]);
    }

    pp::platform::apple::set_legacy_apple_storage_paths({
        data_path,
        data_path,
        recording_path,
        {},
    });
}
@end

int main(int argc, const char * argv[])
{
    install_global_handlers();

    App::I = new App;

    if (!App::I->check_license())
        return 0;
    
    AppOSX* app = [AppOSX sharedApplication];
    pp::platform::apple::set_legacy_apple_state(nullptr, app);
    [app run];
    
    return 0;
}
