/*****************************************************************************\
     Snes9x - Portable Super Nintendo Entertainment System (TM) emulator.
                This file is licensed under the Snes9x License.
   For further information, consult the LICENSE file in the root directory.
\*****************************************************************************/

/***********************************************************************************
  SNES9X for Mac OS (c) Copyright John Stiles

  Snes9x for Mac OS X

  (c) Copyright 2001 - 2011  zones
  (c) Copyright 2002 - 2005  107
  (c) Copyright 2002         PB1400c
  (c) Copyright 2004         Alexander and Sander
  (c) Copyright 2004 - 2005  Steven Seeger
  (c) Copyright 2005         Ryan Vogt
  (c) Copyright 2019         Michael Donald Buckley
 ***********************************************************************************/


#import "snes9x.h"
#import "memmap.h"
#import "apu.h"
#import "snapshot.h"
#import "snes.hpp"

#import <Cocoa/Cocoa.h>
#import <sys/time.h>
#import <pthread.h>

#import "mac-prefix.h"
#import "mac-audio.h"
#import "mac-file.h"
#import "mac-os.h"
#import "mac-musicbox.h"

volatile bool8			mboxPause = false;

static volatile bool8	stopNow, showIndicator, headPressed;
static int32			oldCPUCycles;
static uint16			stereo_switch;
static uint8			storedSoundSnapshot[SPC_SAVE_STATE_BLOCK_SIZE];

static void SPCPlayExec (void);
static void SPCPlayFreeze (void);
static void SPCPlayDefrost (void);
static void MusicBoxForceFreeze (void);
static void MusicBoxForceDefrost (void);
static void * SoundTask (void *);


@implementation MusicBoxController

- (id) init
{
	NSUserDefaults	*defaults;
	NSString		*s;
	NSRect			rect;
	NSSize			size;
	BOOL			apuonly, r;

	self = [super init];
	if (!self)
		return (self);

	r = [NSBundle loadNibNamed: @"musicbox" owner: self ];
	if (!r)
		return (self);

	apuonly = (musicboxmode == kMBXSoundEmulation);

	if (apuonly)
		SPCPlayFreeze();
	else
		MusicBoxForceFreeze();

	auto path = splitpath(Memory.ROMFilename);
	[gametitle setStringValue: [NSString stringWithUTF8String: path.stem.c_str()]];

	[led setImage: [NSImage imageNamed: (apuonly ? @"musicbox_ledoff.icns" : @"musicbox_ledon.icns")]];

	if (!apuonly)
	{
		[rewind setState: NSOffState];
		[rewind setEnabled: NO];
	}

	defaults = [NSUserDefaults standardUserDefaults];
	s = [defaults stringForKey: @"frame_musicbox"];
	if (s)
	{
		rect = NSRectFromString(s);
		[window setFrame: rect display: NO];
	}

	if (!savewindowpos)
		[window center];

	size = [window minSize];
	mbxClosedHeight = size.height;
	size = [window maxSize];
	mbxOpenedHeight = size.height;

	rect = [window frame];
	showIndicator = (rect.size.height > mbxClosedHeight) ? true : false;

	[disclosure setState: (showIndicator ? NSOnState : NSOffState)];

	[window makeKeyAndOrderFront: self];

	mboxPause   = false;
	headPressed = false;

	stereo_switch = ~0;
	SNES::dsp.spc_dsp.set_stereo_switch(stereo_switch);

	for (int i = 0; i < MAC_MAX_PLAYERS; i++)
		controlPad[i] = 0;

	stopNow = false;
	MacStartSound();
	pthread_create(&mbxThread, NULL, SoundTask, NULL);

	timer = [NSTimer scheduledTimerWithTimeInterval: (2.0 / (double) Memory.ROMFramesPerSecond) target: self selector: @selector(updateIndicator:) userInfo: nil repeats: YES];
//
	return (self);
}

- (void) windowWillClose: (NSNotification *) aNotification
{
	NSUserDefaults	*defaults;
	NSString		*s;
	BOOL			r;

	[timer invalidate];
    timer = nil;

	showIndicator = false;

	stopNow = true;
	pthread_join(mbxThread, NULL);
	MacStopSound();

	defaults = [NSUserDefaults standardUserDefaults];
	s = NSStringFromRect([window frame]);
	[defaults setObject: s forKey: @"frame_musicbox"];
	r = [defaults synchronize];

	[NSApp stopModal];
}

- (void) dealloc
{
	stereo_switch = ~0;
	SNES::dsp.spc_dsp.set_stereo_switch(stereo_switch);

	if (musicboxmode == kMBXSoundEmulation)
		SPCPlayDefrost();
	else
		MusicBoxForceDefrost();
}

- (NSWindow *) window
{
	return (window);
}

- (IBAction) handlePauseButton: (id) sender
{
	mboxPause = !mboxPause;
	S9xSetSoundMute(mboxPause);
}

- (IBAction) handleRewindButton: (id) sender
{
	headPressed = true;
}

- (IBAction) handleEffectButton: (id) sender
{
	[window orderOut: self];
	showIndicator = false;
	ConfigureSoundEffects();
	showIndicator = true;
	[window makeKeyAndOrderFront: self];
}

- (IBAction) handleChannelButton: (id) sender
{
	stereo_switch ^= (1 << [sender tag]);
	SNES::dsp.spc_dsp.set_stereo_switch(stereo_switch);
}

- (IBAction) handleDisclosureButton: (id) sender
{
	NSRect	rect;
	float	h;

	showIndicator = !showIndicator;
	rect = [window frame];
	h = rect.size.height;
	rect.size.height = showIndicator ? mbxOpenedHeight : mbxClosedHeight;
	rect.origin.y += (h - rect.size.height);
	[window setFrame: rect display: YES animate: YES];
}

- (void) updateIndicator: (NSTimer *) aTimer
{
	if (showIndicator)
		[indicator setNeedsDisplay: YES];
}

@end

@implementation MusicBoxIndicatorView

- (id) initWithFrame: (NSRect) frame
{
	self = [super initWithFrame: frame];
	if (self)
	{
		NSRect			rect;
		long long		currentTime;
		struct timeval	tv;

		mbxOffsetX   =   0.0f;
		mbxOffsetY   =   0.0f;
		mbxBarWidth  =  12.0f;
		mbxBarHeight = 128.0f;
		mbxBarSpace  =   2.0f;
		mbxLRSpace   =  20.0f;
		mbxRightBarX = (mbxLRSpace + (mbxBarWidth * 8.0f + mbxBarSpace * 7.0f));
		yyscale      = (float) (128.0 / sqrt(64.0));

		rect = [self bounds];
		mbxViewWidth  = rect.size.width;
		mbxViewHeight = rect.size.height;
		mbxMarginX = (mbxViewWidth - ((mbxBarWidth * 8.0f + mbxBarSpace * 7.0f) * 2.0f + mbxLRSpace)) / 2.0f;
		mbxMarginY = (mbxViewHeight - mbxBarHeight) / 2.0f;

		gettimeofday(&tv, NULL);
		currentTime = tv.tv_sec * 1000000 + tv.tv_usec;

		for (int i = 0; i < 8; i++)
		{
			prevLMax[i] = prevRMax[i] = 0;
			prevLVol[i] = prevRVol[i] = 0;
			barTimeL[i] = barTimeR[i] = currentTime;
		}
	}

	return (self);
}

- (void) drawRect: (NSRect) rect
{
	CGContextRef	mboxctx;

	mboxctx = (CGContextRef) [[NSGraphicsContext currentContext] graphicsPort];

	// Bar

	const float	length[] = { 1.0f, 1.0f };

	CGContextSetLineWidth(mboxctx, mbxBarWidth);
	//CGContextSetLineDash(mboxctx, 0, length, 2);
	CGContextSetLineJoin(mboxctx, kCGLineJoinMiter);

	CGContextBeginPath(mboxctx);

	float   x = mbxOffsetX + mbxMarginX + mbxBarWidth / 2.0f;

	for (int h = 0; h < 8; h++)
	{
		// Inactive

		CGContextSetRGBStrokeColor(mboxctx, (196.0f / 256.0f), (200.0f / 256.0f), (176.0f / 256.0f), 1.0f);

		CGContextMoveToPoint   (mboxctx, x,                mbxOffsetY + mbxMarginY);
		CGContextAddLineToPoint(mboxctx, x,                mbxOffsetY + mbxMarginY + mbxBarHeight);

		CGContextMoveToPoint   (mboxctx, x + mbxRightBarX, mbxOffsetY + mbxMarginY);
		CGContextAddLineToPoint(mboxctx, x + mbxRightBarX, mbxOffsetY + mbxMarginY + mbxBarHeight);

		CGContextStrokePath(mboxctx);

		// Max

		short			vl = (SNES::dsp.spc_dsp.reg_value(h, 0x00) * SNES::dsp.spc_dsp.envx_value(h)) >> 11;
		short			vr = (SNES::dsp.spc_dsp.reg_value(h, 0x01) * SNES::dsp.spc_dsp.envx_value(h)) >> 11;
		long long		currentTime;
		struct timeval	tv;

		if (vl <= 0) vl = 0; else if (vl > 64) vl = 64; else vl = (short) (yyscale * sqrt((double) vl)) & (~0 << 1);
		if (vr <= 0) vr = 0; else if (vr > 64) vr = 64; else vr = (short) (yyscale * sqrt((double) vr)) & (~0 << 1);

		if (vl < prevLVol[h]) vl = ((prevLVol[h] + vl) >> 1);
		if (vr < prevRVol[h]) vr = ((prevRVol[h] + vr) >> 1);

		gettimeofday(&tv, NULL);
		currentTime = tv.tv_sec * 1000000 + tv.tv_usec;

		// left

		if ((vl >= prevLMax[h]) && (vl > prevLVol[h]))
		{
			barTimeL[h] = currentTime;
			prevLMax[h] = vl;
		}
		else
		if ((prevLMax[h] > 0) && (barTimeL[h] + 1000000 > currentTime))
		{
			CGContextSetRGBStrokeColor(mboxctx, (22.0f / 256.0f), (156.0f / 256.0f), (20.0f / 256.0f), (float) (barTimeL[h] + 1000000 - currentTime) / 1000000.0f);

			CGContextMoveToPoint   (mboxctx, x, mbxOffsetY + mbxMarginY + (float) (prevLMax[h] - 2));
			CGContextAddLineToPoint(mboxctx, x, mbxOffsetY + mbxMarginY + (float) (prevLMax[h]    ));

			CGContextStrokePath(mboxctx);
		}
		else
			prevLMax[h] = 0;

		prevLVol[h] = vl;

		// right

		if ((vr >= prevRMax[h]) && (vr > prevRVol[h]))
		{
			barTimeR[h] = currentTime;
			prevRMax[h] = vr;
		}
		else
		if ((prevRMax[h] > 0) && (barTimeR[h] + 1000000 > currentTime))
		{
			CGContextSetRGBStrokeColor(mboxctx, (22.0f / 256.0f), (156.0f / 256.0f), (20.0f / 256.0f), (float) (barTimeR[h] + 1000000 - currentTime) / 1000000.0f);

			CGContextMoveToPoint   (mboxctx, x + mbxRightBarX, mbxOffsetY + mbxMarginY + (float) (prevRMax[h] - 2));
			CGContextAddLineToPoint(mboxctx, x + mbxRightBarX, mbxOffsetY + mbxMarginY + (float) (prevRMax[h]    ));

			CGContextStrokePath(mboxctx);
		}
		else
			prevRMax[h] = 0;

		prevRVol[h] = vr;

		// Active

		CGContextSetRGBStrokeColor(mboxctx, (22.0f / 256.0f), (22.0f / 256.0f), (20.0f / 256.0f), 1.0f);

		CGContextMoveToPoint   (mboxctx, x,                mbxOffsetY + mbxMarginY);
		CGContextAddLineToPoint(mboxctx, x,                mbxOffsetY + mbxMarginY + (float) vl);
		CGContextStrokePath(mboxctx);
		CGContextMoveToPoint   (mboxctx, x + mbxRightBarX, mbxOffsetY + mbxMarginY);
		CGContextAddLineToPoint(mboxctx, x + mbxRightBarX, mbxOffsetY + mbxMarginY + (float) vr);
		CGContextStrokePath(mboxctx);

		x += (mbxBarWidth + mbxBarSpace);
	}
}

@end

static void * SoundTask (void *)
{
	long long		last, curr;
	struct timeval	tv;

	gettimeofday(&tv, NULL);
	last = tv.tv_sec * 1000000 + tv.tv_usec;

	while (!stopNow)
	{
		if (!mboxPause)
		{
			if (musicboxmode == kMBXSoundEmulation)
				SPCPlayExec();
			else
				S9xMainLoop();
		}

		if (headPressed)
		{
			showIndicator = false;
			SPCPlayDefrost();
			showIndicator = true;

			headPressed = false;
		}

		last += (1000000 / Memory.ROMFramesPerSecond);
		gettimeofday(&tv, NULL);
		curr = tv.tv_sec * 1000000 + tv.tv_usec;

		if (last > curr)
			usleep((useconds_t) (last - curr));
	}

	return (NULL);
}

static void SPCPlayExec (void)
{
	for (int v = 0; v < Timings.V_Max; v++)
	{
		CPU.Cycles = Timings.H_Max;
		S9xAPUEndScanline();
		CPU.Cycles = 0;
		S9xAPUSetReferenceTime(0);
	}
}

static void MusicBoxForceFreeze (void)
{
	char filename[PATH_MAX + 1];

	strlcpy(filename, S9xGetFreezeFilename(999), sizeof(filename));
	strlcat(filename, ".tmp", sizeof(filename));

	S9xFreezeGame(filename);
}

static void MusicBoxForceDefrost (void)
{
	char filename[PATH_MAX + 1];

	strlcpy(filename, S9xGetFreezeFilename(999), sizeof(filename));
	strlcat(filename, ".tmp", sizeof(filename));

	S9xUnfreezeGame(filename);
	remove(filename);
}

static void SPCPlayFreeze (void)
{
	oldCPUCycles = CPU.Cycles;

	S9xSetSoundMute(true);
	S9xAPUSaveState(storedSoundSnapshot);
	S9xSetSoundMute(false);
}

static void SPCPlayDefrost (void)
{
	CPU.Cycles = oldCPUCycles;

	S9xSetSoundMute(true);
	S9xAPULoadState(storedSoundSnapshot);
	S9xSetSoundMute(false);
}

void MusicBoxDialog (void)
{
	MusicBoxController	*controller;

	if (!cartOpen)
		return;

	controller = [[MusicBoxController alloc] init];

	if (!controller)
		return;

	[NSApp runModalForWindow: [controller window]];
}
