//
//  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;
		didNudge = NO;

		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;
			}
			CGEventRef getEvent = CGEventCreate(NULL);
			CGPoint mouse = CGEventGetLocation(getEvent);
			CFRelease(getEvent);
			// Step by 1 physical pixel: divide by backing scale factor
			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;
			case KEYCODE_UP_ARROW:
				dy = -step;
				break;
			default:
				return event;
			}
			if ([event modifierFlags] & NSEventModifierFlagShift) {
				dx *= 10;
				dy *= 10;
			}
			mouse.x += dx;
			mouse.y += dy;
			CGWarpMouseCursorPosition(mouse);
			// Track sub-point position independently of [NSEvent mouseLocation] quantization
			// AppKit X == CG X; AppKit Y is opposite of CG Y
			self->precisionCursorX += dx;
			self->precisionCursorY += -dy;
			self->didNudge = YES;
			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];
}

- (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
	};

	// Bounds check: if invalid graph value (e.g., old GSStatistics=5), map to GSNone
	if (graph < 0 || graph >= 5) {
		graph = GSNone;
	}

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

	// Highlight is only relevant for plotting modes (not off)
	if (graph == GSNone) {
		[highlight setEnabled:FALSE];
	} else {
		[highlight setEnabled:TRUE];
	}

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

-(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;
	}

	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
{
	// 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->didNudge) {
			// Arrow-key nudge: use precision-tracked position, not OS-quantized mouseLocation
			self->didNudge = NO;
			pixelPerfectX = self->precisionCursorX;
			pixelPerfectY = self->precisionCursorY;
		} else {
			// Normal mouse movement: sync precision vars from OS position
			pixelPerfectX = round(cursor.x * maxFactor) / maxFactor;
			pixelPerfectY = round(cursor.y * maxFactor) / maxFactor;
			self->precisionCursorX = pixelPerfectX;
			self->precisionCursorY = pixelPerfectY;
		}
	} 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 CGImage API for HDR to preserve Float16 data
	if ([snooper isHDRActive]) {
		CGImageRef cgImage = [snooper snoopCGImageFrom:screens]; // Retained

		// Enable HDR display mode
		[view setEnableHDR:YES];
		
		// Set the CGImage directly (releases it)
		[view setCGImage:cgImage]; // Takes ownership and releases
	} else {
		// SDR path - use NSImage
		NSImage *image = [snooper snoopFrom:screens];

		// Enable HDR display mode based on capture status
		[view setEnableHDR:NO];

		[view setImage:image];
		[image autorelease];
	}
	
	// Display color values based on HDR status
	if ([snooper isHDRActive]) {
		// HDR mode: format values based on range
		// <= 1.0: show as 8-bit (0-255)
		// > 1.0: show as float (f-stops above menu white)
		CGFloat r = [snooper cursorRedHDR];
		CGFloat g = [snooper cursorGreenHDR];
		CGFloat b = [snooper cursorBlueHDR];

		// In HDR mode, always show as floating point
		NSString *rStr = [NSString stringWithFormat:@"%.3f", r];
		NSString *gStr = [NSString stringWithFormat:@"%.3f", g];
		NSString *bStr = [NSString stringWithFormat:@"%.3f", 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];
		NSString *rgb = [NSString stringWithFormat:@"(%@, %@, %@)", rStr, gStr, bStr];
		NSMutableAttributedString *attrStr = [[[NSMutableAttributedString alloc] initWithString:[prefix stringByAppendingString:rgb]] autorelease];
		[attrStr addAttribute:NSForegroundColorAttributeName value:[NSColor controlTextColor] range:NSMakeRange(0, [prefix length])];
		[attrStr addAttribute:NSForegroundColorAttributeName value:orangeColor range:NSMakeRange([prefix length], [rgb length])];
		[status setAttributedStringValue:attrStr];
		NSString *edrStr = (currentEDRMode == displayModeLinearEDR) ? @", Linear EDR" : @", Gamma EDR";
		[window setTitle:[NSString stringWithFormat:@"SnoopX (HDR Display mode%@)", edrStr]];
	} else {
		// SDR mode: show values in 0-255 range
		[status setTextColor:[NSColor controlTextColor]];
		[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
{
 	[self updateGraph:(int)[sender tag] 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;
}

@end
