#import <Cocoa/Cocoa.h>
#import <ApplicationServices/ApplicationServices.h>
#import <Carbon/Carbon.h>
#import <stdatomic.h>
#import <pthread.h>
#import "darwin.h"

// Go exports
extern void chr_onSelectionTrigger(int kind);
extern void chr_onEvent(int kind, double x, double y);

@interface CHRPanel : NSPanel
@end

@implementation CHRPanel
- (BOOL)canBecomeKeyWindow {
    return NO;
}
- (BOOL)canBecomeMainWindow {
    return NO;
}
@end

@interface CHRView : NSView
@property (nonatomic, assign) NSRect hitRect;
@property (nonatomic, assign) BOOL hasEntered;
@property (nonatomic, strong) NSTrackingArea *trackingArea;

- (void)updateHit:(NSRect)hit;
@end

@implementation CHRView

- (BOOL)isFlipped {
    return YES;
}

- (NSView *)hitTest:(NSPoint)point {
    NSPoint local = [self convertPoint:point fromView:self.superview];
    if (NSPointInRect(local, self.hitRect)) {
        return self;
    }
    return nil;
}

- (void)updateHit:(NSRect)hit {
    self.hitRect = hit;
    if (self.trackingArea) {
        [self removeTrackingArea:self.trackingArea];
        self.trackingArea = nil;
    }
    NSTrackingAreaOptions opts = NSTrackingMouseEnteredAndExited |
                                 NSTrackingMouseMoved |
                                 NSTrackingActiveAlways;
    self.trackingArea = [[NSTrackingArea alloc] initWithRect:self.hitRect
                                                     options:opts
                                                       owner:self
                                                    userInfo:nil];
    [self addTrackingArea:self.trackingArea];
}

- (void)mouseUp:(NSEvent *)event {
    NSPoint loc = [self convertPoint:[event locationInWindow] fromView:nil];
    if (NSPointInRect(loc, self.hitRect)) {
        chr_onEvent(0, loc.x, loc.y); // Click
    }
}

- (void)mouseMoved:(NSEvent *)event {
    NSPoint loc = [self convertPoint:[event locationInWindow] fromView:nil];
    if (NSPointInRect(loc, self.hitRect)) {
        chr_onEvent(1, loc.x, loc.y); // Motion
    }
}

- (void)mouseEntered:(NSEvent *)event {
    self.hasEntered = YES;
}

- (void)mouseExited:(NSEvent *)event {
    if (self.hasEntered) {
        self.hasEntered = NO;
        chr_onEvent(2, 0, 0); // Leave
    }
}

- (void)mouseDown:(NSEvent *)event {
    NSPoint loc = [self convertPoint:[event locationInWindow] fromView:nil];
    if (!NSPointInRect(loc, self.hitRect)) {
        chr_onEvent(3, 0, 0); // Dismiss if clicked on shadow
    }
}

@end

static CHRPanel *gPanel = nil;
static CHRView *gView = nil;
static NSRect gHitOnScreen = {{0, 0}, {0, 0}};

static atomic_bool gAppIsRunning = false;

typedef struct {
    NSRect frame;
    NSRect visibleFrame;
    double scale;
} CHRScreenInfo;

#define MAX_SCREENS 16
static CHRScreenInfo gScreens[MAX_SCREENS];
static int gScreenCount = 0;
static double gPrimaryScreenHeight = 1080.0;
static pthread_mutex_t gScreenLock = PTHREAD_MUTEX_INITIALIZER;

static id gMouseUpMonitor = nil;
static id gMouseDownMonitor = nil;
static id gKeyUpMonitor = nil;
static id gGlobalClickMonitor = nil;
static id gLocalClickMonitor = nil;

static NSPoint gMouseDownLoc = {0, 0};
static NSInteger gMouseDownClickCount = 0;

// pids we have already asked to build their accessibility tree
static NSMutableSet<NSNumber *> *gWoken = nil;

static void updateScreenCache(void) {
    NSArray<NSScreen *> *screens = [NSScreen screens];
    pthread_mutex_lock(&gScreenLock);
    gScreenCount = (int)MIN(screens.count, MAX_SCREENS);
    if (screens.count > 0) {
        gPrimaryScreenHeight = screens[0].frame.size.height;
    }
    for (int i = 0; i < gScreenCount; i++) {
        NSScreen *s = screens[i];
        gScreens[i].frame = s.frame;
        gScreens[i].visibleFrame = s.visibleFrame;
        gScreens[i].scale = s.backingScaleFactor;
    }
    pthread_mutex_unlock(&gScreenLock);
}

void chr_init(void) {
    @autoreleasepool {
        [NSApplication sharedApplication];
        [NSApp setActivationPolicy:NSApplicationActivationPolicyAccessory];

        updateScreenCache();

        [[NSNotificationCenter defaultCenter] addObserverForName:NSApplicationDidChangeScreenParametersNotification
                                                          object:nil
                                                           queue:[NSOperationQueue mainQueue]
                                                      usingBlock:^(NSNotification * _Nonnull note) {
            updateScreenCache();
        }];

        gPanel = [[CHRPanel alloc] initWithContentRect:NSZeroRect
                                             styleMask:NSWindowStyleMaskBorderless | NSWindowStyleMaskNonactivatingPanel
                                               backing:NSBackingStoreBuffered
                                                 defer:NO];
        [gPanel setLevel:NSPopUpMenuWindowLevel];
        [gPanel setOpaque:NO];
        [gPanel setBackgroundColor:[NSColor clearColor]];
        [gPanel setHasShadow:NO];
        [gPanel setCollectionBehavior:NSWindowCollectionBehaviorCanJoinAllSpaces |
                                      NSWindowCollectionBehaviorFullScreenAuxiliary |
                                      NSWindowCollectionBehaviorStationary];
        [gPanel setBecomesKeyOnlyIfNeeded:YES];
        [gPanel setHidesOnDeactivate:NO];
        [gPanel setAcceptsMouseMovedEvents:YES];
        [gPanel setIgnoresMouseEvents:NO];

        gView = [[CHRView alloc] initWithFrame:NSZeroRect];
        [gView setWantsLayer:YES];
        [gPanel setContentView:gView];

        gWoken = [[NSMutableSet alloc] init];
    }
}

void chr_run(void) {
    gAppIsRunning = true;
    [NSApp run];
    gAppIsRunning = false;
}

void chr_stop(void) {
    dispatch_async(dispatch_get_main_queue(), ^{
        gAppIsRunning = false;
        [NSApp stop:nil];
        NSEvent *event = [NSEvent otherEventWithType:NSEventTypeApplicationDefined
                                           location:NSMakePoint(0, 0)
                                      modifierFlags:0
                                          timestamp:0
                                       windowNumber:0
                                            context:nil
                                            subtype:0
                                              data1:0
                                              data2:0];
        [NSApp postEvent:event atStart:YES];
    });
}

void chr_installMonitors(void) {
    @autoreleasepool {
        gMouseDownMonitor = [NSEvent addGlobalMonitorForEventsMatchingMask:NSEventMaskLeftMouseDown
                                                                   handler:^(NSEvent *e) {
            gMouseDownLoc = [NSEvent mouseLocation];
            gMouseDownClickCount = [e clickCount];
        }];

        gMouseUpMonitor = [NSEvent addGlobalMonitorForEventsMatchingMask:NSEventMaskLeftMouseUp
                                                                 handler:^(NSEvent *e) {
            NSPoint mouseUpLoc = [NSEvent mouseLocation];
            double dist = hypot(mouseUpLoc.x - gMouseDownLoc.x, mouseUpLoc.y - gMouseDownLoc.y);
            bool isDragOrMulti = (dist > 3.0) || (gMouseDownClickCount >= 2);
            chr_onSelectionTrigger(isDragOrMulti ? 1 : 0);
        }];

        gKeyUpMonitor = [NSEvent addGlobalMonitorForEventsMatchingMask:NSEventMaskKeyUp
                                                               handler:^(NSEvent *e) {
            NSEventModifierFlags flags = [e modifierFlags];
            unsigned short kc = [e keyCode];
            bool isShiftNav = ((flags & NSEventModifierFlagShift) != 0) ||
                              (kc >= 123 && kc <= 126) ||
                              kc == 115 || kc == 119 || kc == 116 || kc == 121;
            bool isCmdA = ((flags & NSEventModifierFlagCommand) != 0) && (kc == 0);
            if (isShiftNav || isCmdA) {
                chr_onSelectionTrigger(2);
            }
        }];

        gGlobalClickMonitor = [NSEvent addGlobalMonitorForEventsMatchingMask:NSEventMaskLeftMouseDown | NSEventMaskKeyDown
                                                                     handler:^(NSEvent *e) {
            if (e.type == NSEventTypeLeftMouseDown) {
                if (gPanel && [gPanel isVisible]) {
                    NSPoint loc = [NSEvent mouseLocation];
                    if (!NSPointInRect(loc, gHitOnScreen)) {
                        chr_onEvent(3, 0, 0); // Dismiss
                    }
                }
            } else if (e.type == NSEventTypeKeyDown) {
                if ([e keyCode] == 53) { // Escape
                    if (gPanel && [gPanel isVisible]) {
                        chr_onEvent(3, 0, 0); // Dismiss
                    }
                }
            }
        }];

        gLocalClickMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskKeyDown
                                                                   handler:^NSEvent *(NSEvent *e) {
            if ([e keyCode] == 53) { // Escape
                if (gPanel && [gPanel isVisible]) {
                    chr_onEvent(3, 0, 0); // Dismiss
                }
            }
            return e;
        }];
    }
}

void chr_uninstallMonitors(void) {
    @autoreleasepool {
        if (gMouseDownMonitor) {
            [NSEvent removeMonitor:gMouseDownMonitor];
            gMouseDownMonitor = nil;
        }
        if (gMouseUpMonitor) {
            [NSEvent removeMonitor:gMouseUpMonitor];
            gMouseUpMonitor = nil;
        }
        if (gKeyUpMonitor) {
            [NSEvent removeMonitor:gKeyUpMonitor];
            gKeyUpMonitor = nil;
        }
        if (gGlobalClickMonitor) {
            [NSEvent removeMonitor:gGlobalClickMonitor];
            gGlobalClickMonitor = nil;
        }
        if (gLocalClickMonitor) {
            [NSEvent removeMonitor:gLocalClickMonitor];
            gLocalClickMonitor = nil;
        }
    }
}

bool chr_checkAccessibility(bool prompt) {
    @autoreleasepool {
        NSDictionary *options = @{(__bridge id)kAXTrustedCheckOptionPrompt: @(prompt)};
        return AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)options);
    }
}

// wakeAccessibility asks an app to build its accessibility tree. Chromium
// browsers (AXEnhancedUserInterface) and Electron apps (AXManualAccessibility)
// only build it when an assistive app asks; before that, Chromium returns the
// AXApplication itself as the focused element with no selected text.
static void wakeAccessibility(pid_t pid, AXUIElementRef app) {
    if (!AXIsProcessTrusted()) return;
    NSNumber *n = @(pid);
    @synchronized (gWoken) {
        if ([gWoken containsObject:n]) return;
        if (gWoken.count > 64) [gWoken removeAllObjects];
        [gWoken addObject:n];
    }
    AXUIElementSetAttributeValue(app, CFSTR("AXManualAccessibility"), kCFBooleanTrue);
    AXUIElementSetAttributeValue(app, CFSTR("AXEnhancedUserInterface"), kCFBooleanTrue);
}

static NSString *stringValue(CFTypeRef v) {
    if (!v) return nil;
    if (CFGetTypeID(v) == CFStringGetTypeID()) return [(__bridge NSString *)v copy];
    if (CFGetTypeID(v) == CFAttributedStringGetTypeID()) return [[(__bridge NSAttributedString *)v string] copy];
    return nil;
}

// selectedText reads the selection of a focused element: AXSelectedText for
// text views and fields, else the selected text marker range that WebKit and
// Chromium use for page content.
static NSString *selectedText(AXUIElementRef el) {
    CFTypeRef v = NULL;
    NSString *s = nil;
    if (AXUIElementCopyAttributeValue(el, kAXSelectedTextAttribute, &v) == kAXErrorSuccess) {
        s = stringValue(v);
    }
    if (v) CFRelease(v);
    if (s.length > 0) return s;

    CFTypeRef range = NULL;
    if (AXUIElementCopyAttributeValue(el, CFSTR("AXSelectedTextMarkerRange"), &range) != kAXErrorSuccess || !range) {
        return s;
    }
    v = NULL;
    if (AXUIElementCopyParameterizedAttributeValue(el, CFSTR("AXStringForTextMarkerRange"), range, &v) == kAXErrorSuccess) {
        s = stringValue(v);
    }
    if (v) CFRelease(v);
    CFRelease(range);
    return s;
}

static NSString *chr_copyFallback(void) {
    if (!gAppIsRunning || ![NSApp isRunning]) return nil;

    __block NSString *result = nil;
    void (^task)(void) = ^{
        @autoreleasepool {
            NSPasteboard *pb = [NSPasteboard generalPasteboard];
            NSInteger oldCount = [pb changeCount];
            NSString *prevString = [pb stringForType:NSPasteboardTypeString];

            CGEventSourceRef src = CGEventSourceCreate(kCGEventSourceStateHIDSystemState);
            if (src) {
                CGEventRef cDown = CGEventCreateKeyboardEvent(src, (CGKeyCode)8, true);
                CGEventRef cUp = CGEventCreateKeyboardEvent(src, (CGKeyCode)8, false);
                if (cDown && cUp) {
                    CGEventSetFlags(cDown, kCGEventFlagMaskCommand);
                    CGEventSetFlags(cUp, kCGEventFlagMaskCommand);
                    CGEventPost(kCGHIDEventTap, cDown);
                    CGEventPost(kCGHIDEventTap, cUp);
                }
                if (cDown) CFRelease(cDown);
                if (cUp) CFRelease(cUp);
                CFRelease(src);
            }

            usleep(80000); // 80 ms

            if ([pb changeCount] != oldCount) {
                result = [[pb stringForType:NSPasteboardTypeString] copy];
                [pb clearContents];
                if (prevString && prevString.length > 0) {
                    [pb setString:prevString forType:NSPasteboardTypeString];
                }
            }
        }
    };

    if ([NSThread isMainThread]) {
        task();
    } else {
        dispatch_sync(dispatch_get_main_queue(), task);
    }
    return result;
}

char *chr_getSelectedText(bool allowCopyFallback) {
    @autoreleasepool {
        AXUIElementRef focused = NULL;
        AXUIElementRef app = NULL;
        pid_t pid = 0;
        NSRunningApplication *front = [[NSWorkspace sharedWorkspace] frontmostApplication];
        if (front) {
            pid = [front processIdentifier];
            app = AXUIElementCreateApplication(pid);
            if (app) {
                AXUIElementCopyAttributeValue(app, kAXFocusedUIElementAttribute, (CFTypeRef *)&focused);
            }
        }
        if (!focused) {
            AXUIElementRef sys = AXUIElementCreateSystemWide();
            if (sys) {
                AXUIElementCopyAttributeValue(sys, kAXFocusedUIElementAttribute, (CFTypeRef *)&focused);
                CFRelease(sys);
            }
        }

        NSString *s = nil;
        if (focused) {
            s = selectedText(focused);
            CFRelease(focused);
        }
        if (s.length == 0 && app) {
            wakeAccessibility(pid, app); // the next read will find the tree
        }
        if (app) {
            CFRelease(app);
        }
        if (s.length == 0 && allowCopyFallback) {
            s = chr_copyFallback();
        }
        return s ? strdup([s UTF8String]) : NULL;
    }
}

CHRAnchor chr_getAnchor(void) {
    @autoreleasepool {
        NSPoint mouseLoc = [NSEvent mouseLocation];
        CHRAnchor a = {0};

        pthread_mutex_lock(&gScreenLock);
        double primaryH = gPrimaryScreenHeight > 0 ? gPrimaryScreenHeight : 1080.0;
        a.x = mouseLoc.x;
        a.y = primaryH - mouseLoc.y;

        int targetIdx = -1;
        for (int i = 0; i < gScreenCount; i++) {
            if (NSPointInRect(mouseLoc, gScreens[i].frame)) {
                targetIdx = i;
                break;
            }
        }
        if (targetIdx < 0 && gScreenCount > 0) {
            targetIdx = 0;
        }

        if (targetIdx >= 0) {
            NSRect vf = gScreens[targetIdx].visibleFrame;
            a.screenX = vf.origin.x;
            a.screenY = primaryH - (vf.origin.y + vf.size.height);
            a.screenW = vf.size.width;
            a.screenH = vf.size.height;
            a.scale = gScreens[targetIdx].scale;
        } else {
            a.screenX = 0; a.screenY = 0;
            a.screenW = 1920; a.screenH = 1080;
            a.scale = 2.0;
        }
        pthread_mutex_unlock(&gScreenLock);
        return a;
    }
}

void chr_getPointer(double *outX, double *outY) {
    @autoreleasepool {
        NSPoint mouseLoc = [NSEvent mouseLocation];
        pthread_mutex_lock(&gScreenLock);
        double primaryH = gPrimaryScreenHeight > 0 ? gPrimaryScreenHeight : 1080.0;
        pthread_mutex_unlock(&gScreenLock);
        *outX = mouseLoc.x;
        *outY = primaryH - mouseLoc.y;
    }
}

void chr_showPopup(const void *bytes, int stride, int width, int height,
                   double x, double y,
                   double hitX, double hitY, double hitW, double hitH,
                   double contentsScale) {
    @autoreleasepool {
        size_t byteCount = (size_t)stride * height;
        CFDataRef data = CFDataCreate(kCFAllocatorDefault, (const UInt8 *)bytes, byteCount);
        if (!data) return;

        dispatch_async(dispatch_get_main_queue(), ^{
            @autoreleasepool {
                CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB();
                CGDataProviderRef provider = CGDataProviderCreateWithCFData(data);
                CGImageRef img = CGImageCreate(
                    width, height, 8, 32, stride, cs,
                    kCGImageAlphaPremultipliedLast | kCGBitmapByteOrderDefault,
                    provider, NULL, false, kCGRenderingIntentDefault
                );
                CFRelease(provider);
                CFRelease(data);
                CGColorSpaceRelease(cs);

                if (!img) return;

                pthread_mutex_lock(&gScreenLock);
                double primaryH = gPrimaryScreenHeight > 0 ? gPrimaryScreenHeight : 1080.0;
                pthread_mutex_unlock(&gScreenLock);

                double logicalW = (double)width / contentsScale;
                double logicalH = (double)height / contentsScale;
                double appkitY = primaryH - (y + logicalH);

                NSRect frame = NSMakeRect(x, appkitY, logicalW, logicalH);
                [gPanel setFrame:frame display:YES];
                [gView setFrame:NSMakeRect(0, 0, logicalW, logicalH)];

                gView.layer.contentsScale = contentsScale;
                gView.layer.contents = (__bridge id)img;
                CGImageRelease(img);

                NSRect hit = NSMakeRect(hitX, hitY, hitW, hitH);
                [gView updateHit:hit];

                gHitOnScreen = NSMakeRect(frame.origin.x + hitX,
                                          frame.origin.y + (frame.size.height - (hitY + hitH)),
                                          hitW, hitH);

                NSPoint mouseLoc = [NSEvent mouseLocation];
                gView.hasEntered = NSPointInRect(mouseLoc, gHitOnScreen);

                if (![gPanel isVisible]) {
                    [gPanel setAlphaValue:0.0];
                    [gPanel orderFront:nil];
                    [NSAnimationContext runAnimationGroup:^(NSAnimationContext *context) {
                        [context setDuration:0.090];
                        [[gPanel animator] setAlphaValue:1.0];
                    } completionHandler:nil];
                }
            }
        });
    }
}

void chr_hidePopup(void) {
    dispatch_async(dispatch_get_main_queue(), ^{
        @autoreleasepool {
            if (gPanel) {
                [gPanel orderOut:nil];
            }
            if (gView) {
                gView.layer.contents = nil;
                gView.hasEntered = NO;
            }
            gHitOnScreen = NSMakeRect(0, 0, 0, 0);
        }
    });
}

bool chr_savePopupSnapshot(const char *path) {
    if (!gAppIsRunning || ![NSApp isRunning]) return false;

    __block bool ok = false;
    void (^task)(void) = ^{
        @autoreleasepool {
            if (!gView || !gView.layer) return;
            NSRect bounds = gView.bounds;
            if (bounds.size.width <= 0 || bounds.size.height <= 0) return;
            NSBitmapImageRep *rep = [gView bitmapImageRepForCachingDisplayInRect:bounds];
            [gView cacheDisplayInRect:bounds toBitmapImageRep:rep];
            NSData *png = [rep representationUsingType:NSBitmapImageFileTypePNG properties:@{}];
            if (png) {
                NSString *p = [NSString stringWithUTF8String:path];
                ok = [png writeToFile:p atomically:YES];
            }
        }
    };
    if ([NSThread isMainThread]) {
        task();
    } else {
        dispatch_sync(dispatch_get_main_queue(), task);
    }
    return ok;
}
