//
//  Controller.m
//  SnoopX
//
//  Created by tzhuan on 2008/7/21.
//  Copyright 2008 NTU CSIE CMLAB. All rights reserved.
//

#import "Controller.h"
#import "Snooper.h"

#define KEYCODE_LEFT_ARROW 123
#define KEYCODE_RIGHT_ARROW 124
#define KEYCODE_DOWN_ARROW 125
#define KEYCODE_UP_ARROW 126

@implementation Controller

+(void) initialize 
{
#ifndef NDEBUG
	NSLog(@"[Controller initialize]");
#endif
	
	// default preference
	NSArray *keys = [
		NSArray arrayWithObjects:
		@"Zoom", @"Time", @"AlwaysOnTop", @"Graph", @"Highlight", @"LockPosition", @"DisplayMode", @"EDRMode", @"Statistics", @"Swatch", nil
	];
	NSArray *objects = [
		NSArray arrayWithObjects:
		@"1", @"50", @"YES", @"0", @"YES", @"NO", @"0", @"0", @"NO", @"NO", nil
	];
	NSDictionary *appDefaults = [NSDictionary dictionaryWithObjects:objects
															forKeys:keys];
	// register the default preference
	[[NSUserDefaults standardUserDefaults] registerDefaults:appDefaults];
	
}

-(id)init
{
#ifndef NDEBUG
	NSLog(@"[Controller init]");
#endif
	if ((self = [super init])) {
		snooper = [[Snooper alloc] init];
		timer = nil;
		currentGraph = nil;
		currentZoom = nil;
		currentTimer = nil;
		maxBackingScaleFactor = 2.0; // safe default until first updateView
		currentZoomLevel = 1;
		precisionCursorX = 0.0;
		precisionCursorY = 0.0;
		isNudging = NO;
		nudgeMouseBaseline = NSZeroPoint;

		keydownHandler = ^NSEvent * (NSEvent * event) {
			// "0" key toggles 16x zoom
			if ([event keyCode] == 29) { // key "0"
				int newZoom = (self->currentZoomLevel == 16) ? 8 : 16;
				[self updateZoom:newZoom Sync:YES];
				[self actUpdate:nil];
				return nil;
			}
			// Arrow-key nudge: drive precisionCursor directly in AppKit coords
			// (no CGWarpMouseCursorPosition — its sub-point target gets floored,
			// which on Retina makes positive +0.5-point nudges (Right/Down) appear
			// to no-op and fall back on the next updateView tick).
			CGFloat scale = (self->maxBackingScaleFactor > 0) ? self->maxBackingScaleFactor : 1.0;
			CGFloat step = 1.0 / scale;
			CGFloat dx = 0;
			CGFloat dy = 0;
			switch ([event keyCode]) {
			case KEYCODE_LEFT_ARROW:  dx = -step; break;
			case KEYCODE_RIGHT_ARROW: dx = +step; break;
			case KEYCODE_DOWN_ARROW:  dy = -step; break;  // AppKit Y down = decrease
			case KEYCODE_UP_ARROW:    dy = +step; break;  // AppKit Y up   = increase
			default:
				return event;
			}
			if ([event modifierFlags] & NSEventModifierFlagShift) {
				dx *= 10;
				dy *= 10;
			}
			self->precisionCursorX += dx;
			self->precisionCursorY += dy;
			self->isNudging = YES;
			self->nudgeMouseBaseline = [NSEvent mouseLocation];
			return nil;
		};
		keydownMonitorId = [NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskKeyDown handler:keydownHandler];
	}
	return self;
}

- (void)awakeFromNib
{
	[super awakeFromNib];

	[window setTitle:@"SnoopX"];

	// Configure window color space for HDR
	if (@available(macOS 10.12, *)) {
		// Use Extended Generic RGB color space for proper HDR rendering
		// This allows values > 1.0 without clamping
		CGColorSpaceRef cgColorSpace = CGColorSpaceCreateWithName(kCGColorSpaceExtendedLinearSRGB);
		NSColorSpace *extendedColorSpace = [[NSColorSpace alloc] initWithCGColorSpace:cgColorSpace];
		CGColorSpaceRelease(cgColorSpace);
		[window setColorSpace:extendedColorSpace];
		[extendedColorSpace release];
	}

	// Replace NSImageView with HDRImageView programmatically
	[self setupHDRView];

	// Disable problematic zoom levels (keep only 1, 2, 4, 8, and repurpose 10→16)
	[zoom3 setEnabled:NO];
	[zoom3 setHidden:YES];
	[zoom5 setEnabled:NO];
	[zoom5 setHidden:YES];
	[zoom6 setEnabled:NO];
	[zoom6 setHidden:YES];
	[zoom7 setEnabled:NO];
	[zoom7 setHidden:YES];
	[zoom9 setEnabled:NO];
	[zoom9 setHidden:YES];
	// Repurpose zoom10 as the 16x zoom level
	[zoom10 setTitle:@"16"];
	[zoom10 setTag:16];
	[zoom10 setEnabled:YES];
	[zoom10 setHidden:NO];

	// Add a small left margin to the pixel readout text field
	NSRect statusFrame = [status frame];
	statusFrame.origin.x += 6;
	statusFrame.size.width -= 6;
	[status setFrame:statusFrame];

	// Hotkeys for Display Mode
	[displayModeAuto setKeyEquivalent:@"a"];
	[displayModeAuto setKeyEquivalentModifierMask:NSEventModifierFlagControl];
	[displayModeSDR setKeyEquivalent:@"s"];
	[displayModeSDR setKeyEquivalentModifierMask:NSEventModifierFlagControl];
	[displayModeHDR setKeyEquivalent:@"h"];
	[displayModeHDR setKeyEquivalentModifierMask:NSEventModifierFlagControl];
	[displayModeGammaEDR setKeyEquivalent:@"g"];
	[displayModeGammaEDR setKeyEquivalentModifierMask:NSEventModifierFlagControl];
	[displayModeLinearEDR setKeyEquivalent:@"l"];
	[displayModeLinearEDR setKeyEquivalentModifierMask:NSEventModifierFlagControl];

	[self loadUserDefaults];

	// Reposition the color-plot popup whenever the snoop window moves or resizes,
	// so the waveform window stays width-matched and aligned.
	NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
	[nc addObserver:self selector:@selector(snoopWindowGeometryChanged:)
	           name:NSWindowDidResizeNotification object:window];
	[nc addObserver:self selector:@selector(snoopWindowGeometryChanged:)
	           name:NSWindowDidMoveNotification object:window];
	// Coordinate snoop window's foreground/minimize/restore with the plot popup.
	[nc addObserver:self selector:@selector(snoopWindowDidBecomeMain:)
	           name:NSWindowDidBecomeMainNotification object:window];
	[nc addObserver:self selector:@selector(snoopWindowDidMiniaturize:)
	           name:NSWindowDidMiniaturizeNotification object:window];
	[nc addObserver:self selector:@selector(snoopWindowDidDeminiaturize:)
	           name:NSWindowDidDeminiaturizeNotification object:window];
}

- (void) snoopWindowGeometryChanged:(NSNotification*)note
{
	if (colorPlotWC) {
		[colorPlotWC repositionRelativeToWindow:window];
	}
}

- (void) snoopWindowDidBecomeMain:(NSNotification*)note
{
	// Bring the plot popup to the foreground along with the snoop window.
	if (colorPlotWC && ![[colorPlotWC window] isMiniaturized]) {
		[[colorPlotWC window] orderFront:nil];
	}
}

- (void) snoopWindowDidMiniaturize:(NSNotification*)note
{
	NSWindow *plot = [colorPlotWC window];
	if (plot && ![plot isMiniaturized]) {
		[plot miniaturize:nil];
	}
}

- (void) snoopWindowDidDeminiaturize:(NSNotification*)note
{
	NSWindow *plot = [colorPlotWC window];
	if (plot && [plot isMiniaturized]) {
		[plot deminiaturize:nil];
	}
}

- (void) plotWindowDidMiniaturize:(NSNotification*)note
{
	if (![window isMiniaturized]) {
		[window miniaturize:nil];
	}
}

- (void) plotWindowDidDeminiaturize:(NSNotification*)note
{
	if ([window isMiniaturized]) {
		[window deminiaturize:nil];
	}
}

- (void) attachPlotWindowObservers:(NSWindow*)plotWin
{
	if (!plotWin) return;
	NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
	[nc addObserver:self selector:@selector(plotWindowDidMiniaturize:)
	           name:NSWindowDidMiniaturizeNotification object:plotWin];
	[nc addObserver:self selector:@selector(plotWindowDidDeminiaturize:)
	           name:NSWindowDidDeminiaturizeNotification object:plotWin];
}

- (void) detachPlotWindowObservers:(NSWindow*)plotWin
{
	if (!plotWin) return;
	NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
	[nc removeObserver:self name:NSWindowDidMiniaturizeNotification   object:plotWin];
	[nc removeObserver:self name:NSWindowDidDeminiaturizeNotification object:plotWin];
}

- (void)setupHDRView
{
	// HDRImageView is now MTKView-based and supports true HDR display
	// Enable HDR mode by default - will be dynamically enabled/disabled based on capture
	[view setEnableHDR:YES];
}

- (BOOL) applicationShouldTerminateAfterLastWindowClosed:(NSApplication*)app
{
	return YES;
}

- (BOOL) applicationShouldHandleReopen:(NSApplication *)app hasVisibleWindows:(BOOL)flag
{
	if (flag) {
		return NO;
	} else {
		[window makeKeyAndOrderFront:self];
		return YES;	
	}
}

-(void) loadUserDefaults
{
#ifndef NDEBUG
	NSLog(@"[Controller loadUserDefaults]");
#endif
	NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
	[self updateZoom:(int)[defaults integerForKey:@"Zoom"] Sync:NO];
	[self updateAlwaysOnTop:[defaults boolForKey:@"AlwaysOnTop"] Sync:NO];
	[self updateHighlight:[defaults boolForKey:@"Highlight"] Sync:NO];
	[self updateLockPosition:[defaults boolForKey:@"LockPosition"] Sync:NO];
	if ([defaults boolForKey:@"LockPosition"]) {
		cursor.x = [defaults doubleForKey:@"CursorX"];
		cursor.y = [defaults doubleForKey:@"CursorY"];
		precisionCursorX = cursor.x;
		precisionCursorY = cursor.y;
	} else {
		// Initialize cursor from current mouse position so status bar shows
		// the correct coordinates immediately, before the first timer tick.
		cursor = [NSEvent mouseLocation];
		precisionCursorX = cursor.x;
		precisionCursorY = cursor.y;
	}
	[self updateGraph:(int)[defaults integerForKey:@"Graph"] Sync:NO];
	[self updateStatistics:[defaults boolForKey:@"Statistics"] Sync:NO];
	[self updateSwatch:[defaults boolForKey:@"Swatch"] Sync:NO];
	[self updateTimer:(int)[defaults integerForKey:@"Time"] Sync:NO];
	[self updateDisplayMode:(int)[defaults integerForKey:@"DisplayMode"] Sync:NO];
	[self updateEDRMode:(int)[defaults integerForKey:@"EDRMode"] Sync:NO];
}

-(void) dealloc 
{
#ifndef NDEBUG
	NSLog(@"[Controller dealloc]\n");
#endif
	[snooper release];
	[super dealloc];
}

-(void)updateZoom:(int)zoom Sync:(BOOL)sync
{
#ifndef NDEBUG
	NSLog(@"[Controller updateZoom:%d]", zoom);
#endif

	// Map invalid zoom levels to nearest valid level (only 1, 2, 4, 8, 16 supported)
	int validZoom = zoom;
	if (zoom == 3) validZoom = 4;
	else if (zoom == 5) validZoom = 4;
	else if (zoom == 6 || zoom == 7) validZoom = 8;
	else if (zoom >= 9 && zoom != 16) validZoom = 8;
	else if (zoom < 1) validZoom = 1;
	// Valid levels: 1, 2, 4, 8, 16 pass through unchanged

	NSMenuItem *items[] = {
		zoom1, zoom2, zoom3, zoom4, zoom5,
		zoom6, zoom7, zoom8, zoom9, zoom10
	};

	if (currentZoom)
		[currentZoom setState:NSControlStateValueOff];
	if (validZoom == 16) {
		[zoom10 setState:NSControlStateValueOn];
		currentZoom = zoom10;
	} else {
		[items[validZoom-1] setState:NSControlStateValueOn];
		currentZoom = items[validZoom-1];
	}
	currentZoomLevel = validZoom;
	[snooper setZoom:validZoom];

	if (sync) {
		[[NSUserDefaults standardUserDefaults] setInteger:validZoom forKey:@"Zoom"];
		[[NSUserDefaults standardUserDefaults] synchronize];
	}
}

-(void)updateHighlight:(BOOL)flag Sync:(BOOL)sync
{
#ifndef NDEBUG
	NSLog(@"[Controller updateHighlight:%d]", flag);
#endif
	[highlight setState:(flag ? NSControlStateValueOn: NSControlStateValueOff)];
	[snooper setHighlight:flag];
	
	if (sync) {
		[[NSUserDefaults standardUserDefaults] setBool:flag forKey:@"Highlight"];
		[[NSUserDefaults standardUserDefaults] synchronize];
	}
}


-(void)updateLockPosition:(BOOL)flag Sync:(BOOL)sync
{
#ifndef NDEBUG
	NSLog(@"[Controller updateLockPosition:%d]", flag);
#endif
	[lockPosition setState:(flag ? NSControlStateValueOn: NSControlStateValueOff)];
	
	if (sync) {
		[[NSUserDefaults standardUserDefaults] setBool:flag forKey:@"LockPosition"];
		if (flag) {
			// Save the current cursor position so it can be restored on next startup
			[[NSUserDefaults standardUserDefaults] setDouble:cursor.x forKey:@"CursorX"];
			[[NSUserDefaults standardUserDefaults] setDouble:cursor.y forKey:@"CursorY"];
		}
		// Use CFPreferencesSynchronize to force an immediate write to disk.
		// NSUserDefaults synchronize is a no-op on modern macOS; pending writes are
		// lost when the process is killed with ^C (SIGINT).
		CFPreferencesSynchronize(
			(__bridge CFStringRef)[[NSBundle mainBundle] bundleIdentifier],
			kCFPreferencesCurrentUser,
			kCFPreferencesCurrentHost
		);
	}
}

-(void)updateAlwaysOnTop:(BOOL)flag Sync:(BOOL)sync
{
#ifndef NDEBUG
	NSLog(@"[Controller updateAlwaysOnTop:%d]", flag);
#endif
	if (flag) {
		[alwaysOnTop setState:NSControlStateValueOn];
		[[NSApp mainWindow] setLevel:NSScreenSaverWindowLevel];
	} else {
		[alwaysOnTop setState:NSControlStateValueOff];
		[[NSApp mainWindow] setLevel:NSNormalWindowLevel];
	}

	if (sync) {
		[[NSUserDefaults standardUserDefaults] setBool:flag forKey:@"AlwaysOnTop"];
		[[NSUserDefaults standardUserDefaults] synchronize];
	}
}

-(void)updateTimer:(int)time Sync:(BOOL)sync
{
#ifndef NDEBUG
	NSLog(@"[Controller updateTimer:%d]", time);
#endif
	NSMenuItem *items[] = {
		timerNone, timer50, timer100, timer200, timer500, timer1000
	};
	int times[] = {0, 50, 100, 200, 500, 1000};
	int size = sizeof(times)/sizeof(int);
	int index = -1;
	for (int i = 0; i < size; ++i)
		if (time == times[i]) {
			index = i;
			break;
		}
	if (index >= size) index = 0;

	if (currentTimer)
		[currentTimer setState:NSControlStateValueOff];
	[items[index] setState:NSControlStateValueOn];
	currentTimer = items[index];
	[self stopTimer];
	if (time)
		[self startTimer:time/1000.0f];

	if (sync) {
		[[NSUserDefaults standardUserDefaults] setInteger:time forKey:@"Time"];
		[[NSUserDefaults standardUserDefaults] synchronize];
	}
}
	
-(void)updateGraph:(int)graph Sync:(BOOL)sync
{
#ifndef NDEBUG
	NSLog(@"[Controller updateGraph:%d]", graph);
#endif
	NSMenuItem *items[] = {
		graphOff,
		graphHoriStep, graphHoriLinear,
		graphVertStep, graphVertLinear,
		graphColorVectorscope, graphColorWaveform, graphColorHistogram
	};

	// Bounds check: if invalid graph value (e.g., a stale value from older defaults), map to GSNone
	if (graph < 0 || graph >= GS_COUNT) {
		graph = GSNone;
	}

	if (currentGraph)
		[currentGraph setState:NSControlStateValueOff];
	[items[graph] setState:NSControlStateValueOn];
	currentGraph = items[graph];
	[snooper setGraph:graph];

	// Highlight applies to in-window scanline plots; not the color popup plots or off.
	BOOL isScanlinePlot = (graph == GSHoriStep || graph == GSHoriLinear ||
	                      graph == GSVertStep || graph == GSVertLinear);
	[highlight setEnabled:isScanlinePlot];

	// Manage the popup color-plot window
	BOOL isColorPlot = (graph == GSColorVectorscope ||
	                    graph == GSColorWaveform ||
	                    graph == GSColorHistogram);
	if (isColorPlot) {
		ColorPlotMode mode;
		if (graph == GSColorVectorscope)      mode = ColorPlotModeVectorscope;
		else if (graph == GSColorWaveform)    mode = ColorPlotModeWaveform;
		else                                  mode = ColorPlotModeHistogram;
		if (!colorPlotWC) {
			colorPlotWC = [[ColorPlotWindowController alloc] initWithMode:mode];
			[colorPlotWC setPlotDelegate:self];
			[self attachPlotWindowObservers:[colorPlotWC window]];
		} else {
			[colorPlotWC setMode:mode];
		}
		[colorPlotWC repositionRelativeToWindow:window];
		[[colorPlotWC window] orderFront:nil];
		[self pushColorPlotSnapshot];
	} else if (colorPlotWC) {
		[self detachPlotWindowObservers:[colorPlotWC window]];
		[colorPlotWC closeProgrammatically];
		[colorPlotWC release];
		colorPlotWC = nil;
	}

	if (sync) {
		[[NSUserDefaults standardUserDefaults] setInteger:graph forKey:@"Graph"];
		[[NSUserDefaults standardUserDefaults] synchronize];
	}
}

-(void)pushColorPlotSnapshot
{
	if (!colorPlotWC) return;
	SnoopxFrameSnapshot snap;
	if ([snooper copyFrameSnapshot:&snap]) {
		[colorPlotWC updateSnapshot:snap];
	}
	// Push the snoop window's center-pixel color so the vectorscope can show
	// a red-square marker at the corresponding HSV position.
	float r, g, b;
	if ([snooper isHDRActive]) {
		r = (float)[snooper cursorRedHDR];
		g = (float)[snooper cursorGreenHDR];
		b = (float)[snooper cursorBlueHDR];
	} else {
		r = (float)[snooper cursorRed]   / 255.0f;
		g = (float)[snooper cursorGreen] / 255.0f;
		b = (float)[snooper cursorBlue]  / 255.0f;
	}
	[colorPlotWC updateCursorR:r G:g B:b];
}

#pragma mark - ColorPlotWindowControllerDelegate

- (void)colorPlotWindowDidUserClose:(ColorPlotWindowController*)wc
{
	if (wc != colorPlotWC) return;
	[self detachPlotWindowObservers:[wc window]];
	[colorPlotWC autorelease];
	colorPlotWC = nil;
	// Closing the popup is equivalent to selecting Graph Off.
	[self updateGraph:GSNone Sync:YES];
}

-(void)updateStatistics:(BOOL)flag Sync:(BOOL)sync
{
#ifndef NDEBUG
	NSLog(@"[Controller updateStatistics:%d]", flag);
#endif
	[statistics setState:(flag ? NSControlStateValueOn : NSControlStateValueOff)];
	[snooper setStatistics:flag];

	if (sync) {
		[[NSUserDefaults standardUserDefaults] setBool:flag forKey:@"Statistics"];
		[[NSUserDefaults standardUserDefaults] synchronize];
	}
}

-(void)updateSwatch:(BOOL)flag Sync:(BOOL)sync
{
#ifndef NDEBUG
	NSLog(@"[Controller updateSwatch:%d]", flag);
#endif
	[swatch setState:(flag ? NSControlStateValueOn : NSControlStateValueOff)];
	[snooper setSwatch:flag];

	if (sync) {
		[[NSUserDefaults standardUserDefaults] setBool:flag forKey:@"Swatch"];
		[[NSUserDefaults standardUserDefaults] synchronize];
	}
}

-(void)updateDisplayMode:(int)mode Sync:(BOOL)sync
{
#ifndef NDEBUG
	NSLog(@"[Controller updateDisplayMode:%d]", mode);
#endif
	NSMenuItem *items[] = {
		displayModeAuto, displayModeSDR, displayModeHDR
	};

	// Bounds check: if invalid mode, default to Auto
	if (mode < 0 || mode >= 3) {
		mode = DisplayModeAuto;
	}

	// HDR display requires Float16 capture (Apple Silicon GPU).
	// Only check on manual switches (sync=YES) when the stream is already running.
	// At startup (sync=NO), trust the saved preference — ScreenCaptureKit
	// hasn't delivered frames yet, so isHDRActive would always return NO.
	if (mode == DisplayModeHDR && sync) {
		[snooper setDisplayMode:DisplayModeHDR];
		BOOL hdrAvailable = [snooper isHDRActive];
		[snooper setDisplayMode:DisplayModeAuto]; // temporary, set properly below
		if (!hdrAvailable) {
			return; // Keep current mode
		}
	}

	if (currentDisplayMode)
		[currentDisplayMode setState:NSControlStateValueOff];
	[items[mode] setState:NSControlStateValueOn];
	currentDisplayMode = items[mode];
	[snooper setDisplayMode:(DisplayMode)mode];

	if (sync) {
		[[NSUserDefaults standardUserDefaults] setInteger:mode forKey:@"DisplayMode"];
		[[NSUserDefaults standardUserDefaults] synchronize];
	}
}

-(void)updateEDRMode:(int)mode Sync:(BOOL)sync
{
#ifndef NDEBUG
	NSLog(@"[Controller updateEDRMode:%d]", mode);
#endif
	NSMenuItem *items[] = {
		displayModeGammaEDR, displayModeLinearEDR
	};

	// Bounds check: default to Gamma
	if (mode < 0 || mode >= 2) {
		mode = 0;
	}

	if (currentEDRMode)
		[currentEDRMode setState:NSControlStateValueOff];
	[items[mode] setState:NSControlStateValueOn];
	currentEDRMode = items[mode];
	[view setUseLinearDisplayColorSpace:(mode == 1)];

	if (sync) {
		[[NSUserDefaults standardUserDefaults] setInteger:mode forKey:@"EDRMode"];
		[[NSUserDefaults standardUserDefaults] synchronize];
	}
}

-(void)updateView
{
	// Skip all work when the window is minimized or not visible
	if ([window isMiniaturized] || !([window occlusionState] & NSWindowOcclusionStateVisible)) {
		return;
	}

	// get the mouse location
	if ([lockPosition state] == NSControlStateValueOff) {
		cursor = [NSEvent mouseLocation];
		// DON'T round here - we'll round to physical pixels later after scaling
	}

#ifdef SNOOPX_VERBOSE
	NSLog(@"cursor: %@", NSStringFromPoint(cursor));
#endif

	BOOL first = YES;
	size_t mainScreenHeight = 0;
	CGFloat maxFactor = 0.0;
	BOOL hasHDRScreen = NO;
	NSArray* screens = [NSScreen screens];
	for (id screen in screens) {
		if (first) {
			// first screen is the main screen
			NSRect screenRect = [screen frame];
			mainScreenHeight = screenRect.size.height;
			first = NO;
		}
		CGFloat factor = [screen backingScaleFactor];
		if (factor > maxFactor)
			maxFactor = factor;
		
		// Check for HDR capability (macOS 10.15+)
		if (@available(macOS 10.15, *)) {
			if ([screen maximumPotentialExtendedDynamicRangeColorComponentValue] > 1.0) {
				hasHDRScreen = YES;
			}
		}
	}

	// Store for use by the keydown handler
	self->maxBackingScaleFactor = maxFactor;

	// Determine pixel-perfect position (rounded to 1-physical-pixel boundaries)
	CGFloat pixelPerfectX, pixelPerfectY;
	if ([lockPosition state] == NSControlStateValueOff) {
		if (self->isNudging) {
			// Sticky nudge mode. The OS mouse hasn't been warped, so any change
			// in [NSEvent mouseLocation] vs. the baseline means the user physically
			// moved the mouse — exit nudge and resync from the OS.
			CGFloat ddx = cursor.x - self->nudgeMouseBaseline.x;
			CGFloat ddy = cursor.y - self->nudgeMouseBaseline.y;
			if (fabs(ddx) > 0.5 || fabs(ddy) > 0.5) {
				self->isNudging = NO;
				self->precisionCursorX = round(cursor.x * maxFactor) / maxFactor;
				self->precisionCursorY = round(cursor.y * maxFactor) / maxFactor;
			}
		} else {
			// Normal mouse movement: sync precision vars from OS position
			self->precisionCursorX = round(cursor.x * maxFactor) / maxFactor;
			self->precisionCursorY = round(cursor.y * maxFactor) / maxFactor;
		}
		pixelPerfectX = self->precisionCursorX;
		pixelPerfectY = self->precisionCursorY;
	} else {
		// Locked position: use stored cursor
		pixelPerfectX = round(cursor.x * maxFactor) / maxFactor;
		pixelPerfectY = round(cursor.y * maxFactor) / maxFactor;
	}

	// Get display-relative coordinates for display (in native physical pixels)
	NSRect displayFrame = [snooper getCurrentDisplayFrame];
	int displayRelativeX = (int)round((pixelPerfectX - displayFrame.origin.x) * maxFactor);
	int displayRelativeY = (int)round((pixelPerfectY - displayFrame.origin.y) * maxFactor);

	// Flip Y within the display (display coordinates are bottom-origin, native pixels)
	int flippedCursorX = displayRelativeX;
	int flippedCursorY = (int)round(displayFrame.size.height * maxFactor) - displayRelativeY;

	NSRect rect = [view bounds];

	[snooper setMainScreenHeight:mainScreenHeight];
	[snooper setMaxBackingScaleFactor:maxFactor];
	[snooper setHDRCapable:hasHDRScreen];
	[snooper setCursorPointX:pixelPerfectX Y:pixelPerfectY];
	[snooper setViewPointWidth:rect.size.width Height:rect.size.height];

	// Get the actual sample position (may be clamped at screen edges)
	CGFloat actualSampleX = [snooper getActualSamplePointX];
	CGFloat actualSampleY = [snooper getActualSamplePointY];

	// Use actual sample position for display coordinates (in native physical pixels)
	displayRelativeX = (int)round((actualSampleX - displayFrame.origin.x) * maxFactor);
	displayRelativeY = (int)round((actualSampleY - displayFrame.origin.y) * maxFactor);
	flippedCursorX = displayRelativeX;
	flippedCursorY = (int)round(displayFrame.size.height * maxFactor) - displayRelativeY;

	// Use ScreenCaptureKit pipeline whenever available (correct zoom/red square/coordinates)
	if ([snooper useModernPipeline]) {
		CGImageRef cgImage = [snooper snoopCGImageFrom:screens]; // Retained

		// Only enable EDR rendering when we have actual HDR content
		[view setEnableHDR:[snooper isHDRActive]];

		// Set the CGImage directly (releases it)
		[view setCGImage:cgImage]; // Takes ownership and releases
	} else {
		// Legacy path - only used on macOS < 12.3 or if ScreenCaptureKit fails
		NSImage *image = [snooper snoopFrom:screens];

		[view setEnableHDR:NO];

		[view setImage:image];
		[image autorelease];
	}

	// Refresh the color-plot popup, if any, with the latest captured frame
	if (colorPlotWC) {
		[self pushColorPlotSnapshot];
	}
	
	// Display color values based on HDR status, with HSV instead of RGB while
	// the vectorscope is active.
	BOOL useHSV = ([snooper graph] == GSColorVectorscope);
	if ([snooper isHDRActive]) {
		CGFloat r = [snooper cursorRedHDR];
		CGFloat g = [snooper cursorGreenHDR];
		CGFloat b = [snooper cursorBlueHDR];

		NSString *valStr;
		if (useHSV) {
			float h, s, v;
			SnoopxRGBToHSV((float)r, (float)g, (float)b, &h, &s, &v);
			valStr = [NSString stringWithFormat:@"HSV(%5.1f°, %.3f, %.3f)", h, s, v];
		} else {
			valStr = [NSString stringWithFormat:@"(%.3f, %.3f, %.3f)", r, g, b];
		}

		NSColor *orangeColor = [NSColor colorWithCalibratedRed:0.56 green:0.17 blue:0.015 alpha:1.0];
		NSString *prefix = [NSString stringWithFormat:@"Pixel at (% 05d,% 05d) = ", flippedCursorX, flippedCursorY];
		NSMutableAttributedString *attrStr = [[[NSMutableAttributedString alloc] initWithString:[prefix stringByAppendingString:valStr]] autorelease];
		[attrStr addAttribute:NSForegroundColorAttributeName value:[NSColor controlTextColor] range:NSMakeRange(0, [prefix length])];
		[attrStr addAttribute:NSForegroundColorAttributeName value:orangeColor range:NSMakeRange([prefix length], [valStr length])];
		[status setAttributedStringValue:attrStr];
		NSString *edrStr = (currentEDRMode == displayModeLinearEDR) ? @", Linear EDR" : @", Gamma EDR";
		[window setTitle:[NSString stringWithFormat:@"SnoopX (HDR Display mode%@)", edrStr]];
	} else {
		// SDR mode
		[status setTextColor:[NSColor controlTextColor]];
		if (useHSV) {
			float r = (float)[snooper cursorRed]   / 255.0f;
			float g = (float)[snooper cursorGreen] / 255.0f;
			float b = (float)[snooper cursorBlue]  / 255.0f;
			float h, s, v;
			SnoopxRGBToHSV(r, g, b, &h, &s, &v);
			[status setStringValue:[NSString stringWithFormat:
				@"Pixel at (% 05d,% 05d) = HSV(%5.1f°, %.3f, %.3f)",
				flippedCursorX, flippedCursorY, h, s, v]];
		} else {
			[status setStringValue:[NSString stringWithFormat:
				@"Pixel at (% 05d,% 05d) = (%3zu, %3zu, %3zu)",
				flippedCursorX, flippedCursorY,
				[snooper cursorRed], [snooper cursorGreen], [snooper cursorBlue]]];
		}
		NSString *edrStr = (currentEDRMode == displayModeLinearEDR) ? @", Linear EDR" : @", Gamma EDR";
		[window setTitle:[NSString stringWithFormat:@"SnoopX (SDR Display mode%@)", edrStr]];
	}
}

-(IBAction)actUpdate:(id)sender
{
	[self updateView];
}

-(IBAction)actGraph:(id)sender
{
	int newGraph = (int)[sender tag];
	// Pressing a graph hotkey while that graph is already active toggles it off.
	if (newGraph != GSNone && [snooper graph] == newGraph) {
		newGraph = GSNone;
	}
	[self updateGraph:newGraph Sync:YES];
	[self updateView];
}

-(IBAction)actZoom:(id)sender
{
	[self updateZoom:(int)[sender tag] Sync:YES];
	[self updateView];
}

-(IBAction)actTimer:(id)sender
{	
	[self updateTimer:(int)[sender tag] Sync:YES];
}

-(IBAction)actAlwaysOnTop:(id)sender
{
	[self updateAlwaysOnTop:([sender state] == NSControlStateValueOff) Sync:YES];
}

-(IBAction)actHighlight:(id)sender
{
	[self updateHighlight:([sender state] == NSControlStateValueOff) Sync:YES];
}

-(IBAction)actStatistics:(id)sender
{
	[self updateStatistics:([sender state] == NSControlStateValueOff) Sync:YES];
	[self updateView];
}

-(IBAction)actSwatch:(id)sender
{
	[self updateSwatch:([sender state] == NSControlStateValueOff) Sync:YES];
	[self updateView];
}

-(IBAction)actLockPosition:(id)sender
{
	[self updateLockPosition:([sender state] == NSControlStateValueOff) Sync:YES];
}

-(IBAction)actDisplayMode:(id)sender
{
	int tag = (int)[sender tag];
	if (tag >= 3) {
		// EDR mode: tags 3=Gamma, 4=Linear → map to 0, 1
		[self updateEDRMode:tag - 3 Sync:YES];
	} else {
		// Display mode: tags 0=Auto, 1=SDR, 2=HDR
		[self updateDisplayMode:tag Sync:YES];
	}
	[self updateView];
}

-(void)startTimer:(float)time
{
#ifndef NDEBUG
	NSLog(@"[Controller startTimer:%f]", time);
#endif
	timer = [
		NSTimer scheduledTimerWithTimeInterval:time
										target:self
									  selector:@selector(updateView)
									  userInfo:NULL
									   repeats:YES];
}

-(void)stopTimer
{
	[timer invalidate];
	timer = nil;
}

-(IBAction)openHelpPage:(id)sender
{
	NSString *helpPath = [[NSBundle mainBundle] pathForResource:@"index"
														ofType:@"html"
												   inDirectory:@"SnoopXHelp.help/Contents/Resources/English.lproj"];
	if (helpPath) {
		[[NSWorkspace sharedWorkspace] openURL:[NSURL fileURLWithPath:helpPath]];
	}
}

@end
