#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 <CoreFoundation/CoreFoundation.h>
#include <Cocoa/Cocoa.h>
#include <CoreVideo/CoreVideo.h>
#include <OpenGL/OpenGL.h>
#import "objc_utils.h"
#import <HockeySDK/HockeySDK.h>

#include <deque>
#include <chrono>

std::deque<std::packaged_task<void()>> tasklist;
std::mutex task_mutex;

@implementation View
{
    NSSharingService *airdrop_service;
}
- (void)hockeyapp_crash
{
    [[[BITHockeyManager sharedHockeyManager] crashManager] 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];
}
- (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];

    NSInteger clicked = [panel runModal];

    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_dir
{
    NSOpenPanel *panel = [NSOpenPanel openPanel];
    [panel setCanChooseFiles:NO];
    [panel setCanChooseDirectories:YES];
    [panel setAllowsMultipleSelection:NO]; // yes if more than one dir is allowed
    
    NSInteger clicked = [panel runModal];

    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];
    [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;
}
- (instancetype)initWithFrame:(NSRect)frameRect
{
    airdrop_service = [NSSharingService  sharingServiceNamed:NSSharingServiceNameSendViaAirDrop];
    airdrop_service.delegate = self;
    gl_ready = 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];
    return self;
}
- (void)prepareOpenGL
{
    NSLog(@"prepare");
    
    // Synchronize buffer swaps with vertical refresh rate
    GLint swapInt = 1;
    [[self openGLContext] setValues:&swapInt forParameter:NSOpenGLCPSwapInterval];
    
    // Create a display link capable of being used with all active displays
    CVDisplayLinkCreateWithActiveCGDisplays(&dl);
    
    // Set the renderer output callback function
    CVDisplayLinkSetOutputCallback(dl, &MyDisplayLinkCallback, (__bridge void*)self);
    
    // Set the display link for the current renderer
    CGLContextObj cglContext = [[self openGLContext] CGLContextObj];
    CGLPixelFormatObj cglPixelFormat = [[self pixelFormat] CGLPixelFormatObj];
    CVDisplayLinkSetCurrentCGDisplayFromOpenGLContext(dl, cglContext, cglPixelFormat);
    
    CGLEnable([self.openGLContext CGLContextObj], kCGLCECrashOnRemovedFunctions);

    // Activate the display link
    CVDisplayLinkStart(dl);

    cgl = [[self openGLContext] CGLContextObj];
    glctx = [self openGLContext];
    
    CGLLockContext([[self openGLContext] CGLContextObj]);
    App::I.init();
    [self.window setTitle:[NSString stringWithFormat:@"%s - %s", g_window_title, glGetString(GL_RENDERER)]];
    CGLUnlockContext([[self openGLContext] CGLContextObj]);
    gl_ready = true;

    if ([file2open length] > 0)
    {
        LOG("open file %s", [file2open UTF8String]);
        App::I.open_document([file2open UTF8String]);
    }
}

- (void)terminateGL
{
    CGLLockContext([[self openGLContext] CGLContextObj]);
    CVDisplayLinkRelease(dl);
    CGLUnlockContext([[self openGLContext] CGLContextObj]);
}

// This is the renderer output callback function
static CVReturn MyDisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeStamp* now,
                                      const CVTimeStamp* outputTime, CVOptionFlags flagsIn, CVOptionFlags* flagsOut, void* displayLinkContext)
{
    CVReturn result = [(__bridge View*)displayLinkContext getFrameForTime:outputTime];
    return result;
}

- (CVReturn)getFrameForTime:(const CVTimeStamp*)outputTime
{
    static double _timeFreq = CVGetHostClockFrequency();
    static double _prevTime = (double)outputTime->hostTime / _timeFreq;
    static double elapsed = 0;
    double hostTime = (double)outputTime->hostTime;
    double now = hostTime / _timeFreq;
    double dt = now - _prevTime;
    _prevTime = now;

    // this will not update unless 1/30th of a second has passed since the last update
    //if (now < _prevTime + (1.0 / 30.0))
    {
        std::deque<std::packaged_task<void()>> working_list;
        {
            std::lock_guard<std::mutex> lock(task_mutex);
            working_list = std::move(tasklist);
        }

        // We draw on a secondary thread through the display link
        // When resizing the view, -reshape is called automatically on the main
        // thread. Add a mutex around to avoid the threads accessing the context
        // simultaneously when resizing
        CGLLockContext([glctx CGLContextObj]);
        [glctx makeCurrentContext];

        while (!working_list.empty())
        {
            working_list.front()();
            working_list.pop_front();
        }
        
        App::I.tick(dt);
        
        if (App::I.redraw)
        {
            App::I.clear();
            App::I.update(elapsed);
            elapsed = 0;
            CGLFlushDrawable([glctx CGLContextObj]);
        }
        
        //[[self openGLContext] flushBuffer];
        // returning NO will cause the layer to NOT be redrawn
        
        CGLUnlockContext([glctx CGLContextObj]);
        return NO;
    }

    return kCVReturnSuccess;
}

- (void)dealloc
{
    // Release the display link
    CVDisplayLinkRelease(dl);
}

- (void)drawRect:(NSRect)dirtyRect
{
    NSLog(@"drawRect");
    
    // We draw on a secondary thread through the display link
    // When resizing the view, -reshape is called automatically on the main
    // thread. Add a mutex around to avoid the threads accessing the context
    // simultaneously when resizing
    CGLLockContext(cgl);
    [glctx makeCurrentContext];

    App::I.redraw = true;
    App::I.clear();
    App::I.update(0);
    
    //[[self openGLContext] flushBuffer];
    // returning NO will cause the layer to NOT be redrawn
    
    CGLFlushDrawable(cgl);
    CGLUnlockContext(cgl);
}

- (void)reshape
{
    [super reshape];
    
    // 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.
    CGLLockContext([[self openGLContext] CGLContextObj]);
    
    // Get the view size in Points
    NSRect viewRectPoints = [self bounds];
    
#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.resize(viewRectPixels.size.width, viewRectPixels.size.height);
    
    CGLUnlockContext([[self openGLContext] CGLContextObj]);
}
- (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];
    std::lock_guard<std::mutex> lock(task_mutex);
    tasklist.emplace_back([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];
    std::lock_guard<std::mutex> lock(task_mutex);
    tasklist.emplace_back([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];
    std::lock_guard<std::mutex> lock(task_mutex);
    tasklist.emplace_back([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];
    std::lock_guard<std::mutex> lock(task_mutex);
    tasklist.emplace_back([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];
    std::lock_guard<std::mutex> lock(task_mutex);
    tasklist.emplace_back([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];
    std::lock_guard<std::mutex> lock(task_mutex);
    tasklist.emplace_back([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];
    std::lock_guard<std::mutex> lock(task_mutex);
    tasklist.emplace_back([mouseLoc,p=theEvent.pressure] {
        App::I.mouse_move(mouseLoc.x, App::I.height - mouseLoc.y - 1, p, kEventSource::Mouse, 0);
    });
}
- (void)scrollWheel:(NSEvent *)theEvent
{
    auto mouseLoc = [self convertPoint:[theEvent locationInWindow] fromView:nil];
    std::lock_guard<std::mutex> lock(task_mutex);
    tasklist.emplace_back([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];
    const char* c_str = [chars cStringUsingEncoding:NSASCIIStringEncoding];
    std::string s = c_str ? c_str : "";
    std::lock_guard<std::mutex> lock(task_mutex);
    tasklist.emplace_back([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];
    std::lock_guard<std::mutex> lock(task_mutex);
    tasklist.emplace_back([keyCode] {
        App::I.key_up(convert_key(keyCode));
    });
}
- (void)flagsChanged:(NSEvent *)event
{
    [super flagsChanged:event];

    auto flags = [event modifierFlags];
    std::lock_guard<std::mutex> lock(task_mutex);
    tasklist.emplace_back([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
{
    App::I.terminate();
    [[NSApplication sharedApplication] terminate:nil];
}
-(BOOL)windowShouldClose:(NSWindow *)sender
{
    return App::I.request_close();
}
@end

@interface AppOSX : NSApplication<NSApplicationDelegate>
{
    Window* window;
    Controller* controller;
    View* view;
    NSString* file2open;
}
@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]);
    //App::I.open_document([filename UTF8String]);
    file2open = filename;
    return YES;
}
- (void)applicationWillTerminate:(NSNotification *)notification
{
    [view terminateGL];
    [window close];
}
- (void)applicationDidFinishLaunching:(NSNotification *)notification
{
    //[[NSUserDefaults standardUserDefaults] registerDefaults:@{ @"NSApplicationCrashOnExceptions": @YES }];
    
    [[BITHockeyManager sharedHockeyManager] configureWithIdentifier:@"1500ff4a32eb4076be3b5147e2d8e27e"];
    // Do some additional configuration if needed here
    [[BITHockeyManager sharedHockeyManager] startManager];

    if (!App::I.check_license())
        return;
    
    App::I.initLog();
    App::I.create();
    NSRect r = NSMakeRect(0, 0, App::I.width, App::I.height);
    
    view = [[View alloc] initWithFrame:r];
    view->file2open = file2open;
    controller = [[Controller alloc] initWithWindow:window];
    App::I.osx_view = view;
    
    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];
    
    NSLog(@"app launched");
}
@end

int main(int argc, const char * argv[])
{
    install_global_handlers();
    AppOSX* app = [AppOSX sharedApplication];
    [app run];
    return 0;
}
