//
//  JuicePluginController.m
//  Juice
//
//  Created by John Holdsworth on 01/05/2014.
//  Copyright (c) 2014 John Holdsworth. All rights reserved.
//

#import <Cocoa/Cocoa.h>
#import "JuicePluginController.h"

typedef NS_ENUM(int, DBGState) {
    DBGStateIdle,
    DBGStatePaused,
    DBGStateRunning
};

@interface DBGLLDBSession : NSObject
- (DBGState)state;
- (void)requestPause;
- (void)requestContinue;
- (void)evaluateExpression:(id)a0 threadID:(unsigned long)a1 stackFrameID:(unsigned long)a2 queue:(id)a3 completionHandler:(id)a4;
- (void)executeConsoleCommand:(id)a0 threadID:(unsigned long)a1 stackFrameID:(unsigned long)a2 ;
@end

@interface JuicePluginController() <JuicePlugin>

@property NSWindowController *lastWindowController;
@property Class IDEWorkspaceWindowController;
@property NSConnection *theConnection;
@property id<JuiceService> juiceEngine;
@property NSString *juicePath;
@property NSString *lastFile;
@property BOOL hasSaved;

@end

@implementation JuicePluginController

static JuicePluginController *juicePlugin;

+ (BOOL)loadBundleForPlugin:(NSString *)resourcePath;
{
    @try {
        return[juicePlugin.juiceEngine loadBundleForPlugin:resourcePath];
    }
    @catch ( NSException *e ) {
        NSLog( @"loadBundleForPlugin: %@", e );
        return FALSE;
    }
}

+ (NSString *)sourceForClass:(NSString *)className;
{
    @try {
        return [juicePlugin.juiceEngine sourceForClass:className];
    }
    @catch ( NSException *e ) {
        NSLog( @"sourceForClass: %@", e );
        return nil;
    }
}

+ (void)evalCode:(NSString *)code;
{
    [juicePlugin communicateWithJuiceEngine:^{
        [juicePlugin.juiceEngine evalCode:code];
    }];
}

+ (void)showParams;
{
    [juicePlugin communicateWithJuiceEngine:^{
        [juicePlugin.juiceEngine displayParameters];
    }];
}

+ (void)pluginDidLoad:(NSBundle *)plugin {
    if ([[NSBundle mainBundle].infoDictionary[@"CFBundleName"] isEqual:@"Xcode"]) {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            juicePlugin = [[self alloc] init];
            [[NSNotificationCenter defaultCenter] addObserver:juicePlugin
                                                     selector:@selector(applicationDidFinishLaunching:)
                                                         name:NSApplicationDidFinishLaunchingNotification object:nil];
        });
    }
}

#define COULD_NOT_COMMUNICATE @"Could not communicate"

- (NSModalResponse)error:(NSString *)format, ... {
    va_list argp;
    va_start(argp, format);
    NSString *msg = [[NSString alloc] initWithFormat:format arguments:argp],
       *alternate = [msg hasPrefix:COULD_NOT_COMMUNICATE] ? @"Run it now" : nil;
    return [[NSAlert alertWithMessageText:@"Juice Plugin:"
                            defaultButton:@"OK" alternateButton:alternate otherButton:nil
                informativeTextWithFormat:@"%@", msg] runModal];
}

- (void)applicationDidFinishLaunching:(NSNotification *)notification {
    NSMenu *productMenu = [[[NSApp mainMenu] itemWithTitle:@"Product"] submenu];
    if (productMenu) {
        struct { const char *item,  *key; SEL action; } items[] = {
            {"Inject Source", "=", @selector(injectSource:)},
            {"Inject & Reset", "+", @selector(injectWithReset:)},
            {"Patch Project", "", @selector(patchProject:)},
            {"Unpatch Project", "", @selector(unpatchProject:)},
            {"Show Parameters", "", @selector(displayParameters:)}
        };

        NSMenu *juiceMenu = [NSMenu new];
        for ( int i=0 ; i<sizeof items/sizeof items[0] ; i++ ) {
            NSMenuItem *menuItem = [[NSMenuItem alloc] initWithTitle:[NSString stringWithUTF8String:items[i].item]
                                                              action:items[i].action
                                                       keyEquivalent:[NSString stringWithUTF8String:items[i].key]];
            [menuItem setKeyEquivalentModifierMask:NSControlKeyMask];
            [menuItem setTarget:self];
            [juiceMenu addItem:menuItem];
        }

        NSMenuItem *menuItem = [[NSMenuItem alloc] initWithTitle:@"Juice Plugin"
                                                          action:NULL
                                                   keyEquivalent:@""];

        [productMenu addItem:[NSMenuItem separatorItem]];
        [menuItem setSubmenu:juiceMenu];
        [productMenu addItem:menuItem];
    }
    else
        NSLog( @"Could not locate proudct menu" );

    self.IDEWorkspaceWindowController = NSClassFromString(@"IDEWorkspaceWindowController");
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(workspaceDidChange:)
                                                 name:NSWindowDidBecomeKeyNotification object:nil];

    self.juicePath = @"/Applications/Juice.app";
    [self connectToJuice:NO];
}

- (void)connectToJuice:(BOOL)prompt {
    @try {
        self.theConnection = [NSConnection connectionWithRegisteredName:JUICE_ENGINE_DO
                                                                   host:nil];//@"localhost"];
        self.juiceEngine = (id<JuiceService>)[self.theConnection rootProxy];
        [(id)self.juiceEngine setProtocolForProxy:@protocol(JuiceService)];

        if ( !self.juiceEngine ) {
            NSLog( @"JuicePlugin: Could not connect to Juice %@ %@", JUICE_ENGINE_DO, self.theConnection );

            if ( prompt && [self error:COULD_NOT_COMMUNICATE" with Juice App"] == NSAlertAlternateReturn ) {

                [[NSWorkspace sharedWorkspace] openURL:[NSURL fileURLWithPath:self.juicePath]];
                sleep( 2 );

                [self connectToJuice:prompt];
            }
            return;
        }

        self.juicePath = [self.juiceEngine xcodeStarted:[[NSBundle mainBundle] bundlePath] plugin:self];
        NSString *workspacePath = [self workspacePath];
        if ( workspacePath )
            [self.juiceEngine workspaceChanged:workspacePath logDirectory:[self logDirectory]];
    }
    @catch ( NSException *e ) {
        NSLog( @"JuicePlugin: Could not connect to juice, is Juice App running? %@", e );
    }
}

- (void)communicateWithJuiceEngine:(void(^)())block prompt:(BOOL)prompt {
    if ( !self.juiceEngine )
        [self connectToJuice:prompt];
    dispatch_async( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        @try {
            [self.juiceEngine workspaceChanged:[self workspacePath] logDirectory:[self logDirectory]];
            block();
        }
        @catch ( NSException *e ) {
            @try {
                dispatch_sync( dispatch_get_main_queue(), ^{
                    [self connectToJuice:prompt];
                } );
                block();
            }
            @catch ( NSException *e ) {
                NSLog( @"JuicePlugin: Could not communicate with Juice App: %@", e );
            }
        }
    } );
}

- (void)communicateWithJuiceEngine:(void(^)())block {
    [self communicateWithJuiceEngine:block prompt:YES];
}

- (BOOL)validateMenuItem:(NSMenuItem *)menuItem {
    return self.lastWindowController != nil;
}

- (IBAction)displayParameters:(id)sender {
    [self communicateWithJuiceEngine:^{
        [self.juiceEngine displayParameters];
    }];
}

- (IBAction)patchProject:(id)sender {
    [self communicateWithJuiceEngine:^{
        if ( ![self.juiceEngine patchProject] )
            dispatch_async( dispatch_get_main_queue(), ^{
                [self error:@"Unable to locate project's main.m to patch "
                 "(or project is already patched.) "
                 "To inject Swift projects on a device or in AppCode add a dummy main.m."];
            } );
    }];
}

- (IBAction)unpatchProject:(id)sender {
    [self communicateWithJuiceEngine:^{
        [self.juiceEngine unpatchProject];
    }];
}

- (void)inject:(id)sender actualCmd:(SEL)actualCmd {
    if ( [sender isKindOfClass:[NSMenuItem class]] )
        self.lastFile = [self lastFileSaving:YES];

    if ( !self.hasSaved )
        [self performSelector:actualCmd withObject:self afterDelay:.05];
    else {
        if ( [self.lastFile rangeOfString:@"/main\\.mm?$"
                                  options:NSRegularExpressionSearch].location != NSNotFound )
            [self error:@"Injecting main.m is not permitted."];
        else if ( [self.lastFile rangeOfString:JUICABLE_PATTERN
                                       options:NSRegularExpressionSearch].location == NSNotFound )
            [self error:@"Only class implementations (.m, .mm, .swift), or .storyboard files can be injected."];
        else {
            NSArray *sources = @[self.lastFile];
            [self communicateWithJuiceEngine:^{
                [self.juiceEngine injectSources:sources
                                       resetApp:actualCmd == @selector(injectWithReset:)];
            }];
        }

        self.lastFile = nil;
    }
}

- (IBAction)injectSource:(id)sender {
    [self inject:sender actualCmd:_cmd];
}

- (IBAction)injectWithReset:(id)sender {
    [self inject:sender actualCmd:_cmd];
}

- (NSString *)lastFileSaving:(BOOL)save {
    NSDocument *doc = [[self lastEditor] document];
    if ( save ) {
        self.hasSaved = FALSE;
        [doc saveDocumentWithDelegate:self
                      didSaveSelector:@selector(document:didSave:contextInfo:)
                          contextInfo:NULL];
    }
    return [[doc fileURL] path];
}

- (void)document:(NSDocument *)doc didSave:(BOOL)didSave contextInfo:(void  *)contextInfo {
    self.hasSaved = TRUE;
}

- (id)lastEditor {
    return [self.lastWindowController valueForKeyPath:@"editorArea.lastActiveEditorContext.editor"];
}

- (void)workspaceDidChange:(NSNotification *)notification {
    NSWindow *object = [notification object];
    NSWindowController *currentWindowController = [object windowController];
    if ([currentWindowController isKindOfClass:self.IDEWorkspaceWindowController] &&
            [[[(id)currentWindowController document] fileURL] path] ) {
        self.lastWindowController = currentWindowController;
        [self communicateWithJuiceEngine:^{
            //[self.juiceEngine workspaceChanged:[self workspacePath] logDirectory:[self logDirectory]];
        } prompt:NO];
    }
}

- (NSString *)workspacePath {
    return [[[self.lastWindowController document] fileURL] path];
}

- (NSString *)logDirectory {
    return [self.lastWindowController valueForKeyPath:@"workspace.executionEnvironment.logStore.rootDirectoryPath"];
}

- (NSWindowController *)debugController {
    if ( [self sessionForController:self.lastWindowController] )
        return self.lastWindowController;

    for ( NSWindow *win in [NSApp windows] ) {
        NSWindowController *controller = [win windowController];
        if ( [controller isKindOfClass:self.IDEWorkspaceWindowController] &&
            [[[controller document] fileURL] path] &&
            [self sessionForController:controller] )
            return controller;
    }

    return self.lastWindowController;
}

- (DBGLLDBSession *)sessionForController:(NSWindowController *)controller {
    return [controller valueForKeyPath:@"workspace"
            ".executionEnvironment.selectedLaunchSession.currentDebugSession"];
}
 
 - (DBGLLDBSession *)session {
     return [self sessionForController:[self debugController]];
 }
 

- (BOOL)loadBundleInDebugger:(NSString *)bundlePath {
    DBGLLDBSession *session = [self session];

    if ( !session ) {
        [[NSAlert alertWithMessageText:@"Juice Plugin:"
                        defaultButton:@"OK" alternateButton:nil otherButton:nil
             informativeTextWithFormat:@"Program is not running."] runModal];
        return FALSE;
    }

    NSString *MacOS = [[self.lastWindowController valueForKeyPath:@"workspace.executionEnvironment.currentLaunchSession"
                       ".launchParameters.filePathToBinary.pathString"] stringByDeletingLastPathComponent];
    if ( [MacOS hasSuffix:@"/Contents/MacOS"] ) {
        NSString *newLocation = [[MacOS stringByAppendingPathComponent:@"../Resources"]
                                 stringByAppendingPathComponent:bundlePath.lastPathComponent];
        [[NSFileManager defaultManager] copyItemAtPath:bundlePath toPath:newLocation error:nil];
        bundlePath = newLocation;
    }

    if ( session.state != DBGStatePaused )
        [session requestPause];
    [self performSelector:@selector(loadBundle:) withObject:bundlePath afterDelay:.1];
    return TRUE;
}

- (void)loadBundle:(NSString *)bundlePath {
    DBGLLDBSession *session = [self session];

    if ( session.state != DBGStatePaused )
        [self performSelector:@selector(loadBundle:) withObject:bundlePath afterDelay:.1];
    else
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND,0), ^{
            NSString *lldbTemplate = @"p (void)[[NSBundle bundleWithPath:@\"%@\"] load]\r",
                *loader = [NSString stringWithFormat:lldbTemplate, bundlePath];
            [session executeConsoleCommand:loader threadID:1 stackFrameID:0];
            dispatch_async(dispatch_get_main_queue(), ^{
                [session requestContinue];
            });
        });
}

@end
