/**
 ** Supermodel
 ** A Sega Model 3 Arcade Emulator.
 ** Copyright 2011 Bart Trzynadlowski 
 **
 ** This file is part of Supermodel.
 **
 ** Supermodel is free software: you can redistribute it and/or modify it under
 ** the terms of the GNU General Public License as published by the Free 
 ** Software Foundation, either version 3 of the License, or (at your option)
 ** any later version.
 **
 ** Supermodel is distributed in the hope that it will be useful, but WITHOUT
 ** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 ** FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
 ** more details.
 **
 ** You should have received a copy of the GNU General Public License along
 ** with Supermodel.  If not, see <http://www.gnu.org/licenses/>.
 **/
 
/*
 * Main.cpp
 * 
 * Main program driver for the SDL port.
 *
 * Compile-Time Options
 * --------------------
 * - SUPERMODEL_SOUND: Enables experimental sound code. The 68K core (Turbo68K)
 *   only works on x86 (32-bit) systems, so this cannot be enabled on 64-bit
 *   builds.
 * - SUPERMODEL_WIN32: Define this if compiling on Windows.
 * - SUPERMODEL_OSX: Define this if compiling on Mac OS X.
 *
 * TO-DO List
 * ----------
 * - A lot of this code is actually OS-independent! Should it be moved into the
 *   root of the source tree? Might not be worth it; eventually, OS-dependent
 *	 UIs will be introduced.
 */
 
#include <new>
#include <math.h>
#include <stdio.h>
#include <string.h>
#include <stdarg.h>
#include "Pkgs/glew.h"
#ifdef SUPERMODEL_OSX
#include <SDL/SDL.h>
#else
#include <SDL.h>
#endif

#include "Supermodel.h"
#include "SDLInputSystem.h"
#ifdef SUPERMODEL_WIN32
#include "DirectInputSystem.h"
#endif

CLogger *GetLogger();
void SetLogger(CLogger *logger);

/******************************************************************************
 Display Management
******************************************************************************/

/*
 * Position and size of rectangular region within OpenGL display to render to
 */
unsigned	xOffset, yOffset;	// offset of renderer output within OpenGL viewport
unsigned 	xRes, yRes;			// renderer output resolution (can be smaller than GL viewport)

/*
 * CreateGLScreen():
 *
 * Creates an OpenGL display surface of the requested size. xOffset and yOffset
 * are used to return a display surface offset (for OpenGL viewport commands)
 * because the actual drawing area may need to be adjusted to preserve the 
 * Model 3 aspect ratio. The new resolution will be passed back as well.
 */
static BOOL CreateGLScreen(const char *caption, unsigned *xOffsetPtr, unsigned *yOffsetPtr, unsigned *xResPtr, unsigned *yResPtr,
						   BOOL keepAspectRatio, BOOL fullScreen)
{
	const SDL_VideoInfo	*VideoInfo;
	GLenum				err;
	float				model3Ratio, ratio;
	float				xRes, yRes;
	
	// Initialize video subsystem
	if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0)
    	return ErrorLog("Unable to initialize SDL video subsystem: %s\n", SDL_GetError());
    
    // Important GL attributes
    SDL_GL_SetAttribute(SDL_GL_RED_SIZE,5);	// need at least RGB555 for Model 3 textures
    SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE,5);
    SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE,5);
    SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE,16);
    SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER,1);

  	// Set video mode
  	if (SDL_SetVideoMode(*xResPtr,*yResPtr,0,SDL_OPENGL|(fullScreen?SDL_FULLSCREEN|SDL_HWSURFACE:0)) == NULL)
  	{
    	ErrorLog("Unable to create an OpenGL display: %s\n", SDL_GetError());
    	return FAIL;
  	}
  	
  	VideoInfo = SDL_GetVideoInfo();	// what resolution did we actually get?
  	
  	// If required, fix the aspect ratio of the resolution that the user passed to match Model 3 ratio
  	xRes = (float) *xResPtr;
  	yRes = (float) *yResPtr;
  	if (keepAspectRatio)
	{
		model3Ratio = 496.0f/384.0f;
  		ratio = xRes/yRes;
  		if (yRes < (xRes/model3Ratio))
  			xRes = yRes*model3Ratio;
  		if (xRes < (yRes*model3Ratio))
			yRes = xRes/model3Ratio;
	}
		
	// Center the visible area 
  	*xOffsetPtr = (*xResPtr - (unsigned) xRes)/2;
  	*yOffsetPtr = (*yResPtr - (unsigned) yRes)/2;
  	
  	// If the desired resolution is smaller than what we got, re-center again
  	if (*xResPtr < VideoInfo->current_w)
  		*xOffsetPtr += (VideoInfo->current_w - *xResPtr)/2;
  	if (*yResPtr < VideoInfo->current_h)
  		*yOffsetPtr += (VideoInfo->current_h - *yResPtr)/2;
  	
  	// Create window caption
  	SDL_WM_SetCaption(caption,NULL);
  	
  	// Initialize GLEW, allowing us to use features beyond OpenGL 1.2
	err = glewInit();
	if (GLEW_OK != err)
	{
		ErrorLog("OpenGL initialization failed: %s\n", glewGetErrorString(err));
		return FAIL;
	}
  	
  	// OpenGL initialization
 	glViewport(0,0,*xResPtr,*yResPtr);
 	glClearColor(0.0,0.0,0.0,0.0);
 	glClearDepth(1.0);
 	glDepthFunc(GL_LESS);
 	glEnable(GL_DEPTH_TEST);
 	glShadeModel(GL_SMOOTH);
 	glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
 	glDisable(GL_CULL_FACE);
 	glMatrixMode(GL_PROJECTION);
 	glLoadIdentity();
 	gluPerspective(90.0,(GLfloat)xRes/(GLfloat)yRes,0.1,1e5);
 	glMatrixMode(GL_MODELVIEW);
 	glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);	// clear at least once to ensure black border
 	
 	// Write back resolution parameters
 	*xResPtr = (unsigned) xRes;
 	*yResPtr = (unsigned) yRes;
 	 	
	return 0;
}

/******************************************************************************
 Configuration
******************************************************************************/

#define CONFIG_FILE_PATH	"Config/Supermodel.ini"
#define CONFIG_FILE_COMMENT	";\n" \
							"; Supermodel Configuration File\n" \
							";\n"

// Create and configure inputs
static CInputs *CreateInputs(CInputSystem *InputSystem, BOOL configure)
{
	// Create and initialize inputs
	CInputs* Inputs = new CInputs(InputSystem);
	if (!Inputs->Initialize())
	{
		ErrorLog("Unable to initalize inputs.\n");
		return NULL;
	}
	
	// Open and parse configuration file
	CINIFile INI;
	INI.Open(CONFIG_FILE_PATH);	// doesn't matter if it exists or not, will get overwritten
	INI.Parse();
	
	Inputs->ReadFromINIFile(&INI, "Global");
		
	// If the user wants to configure the inputs, do that now
	if (configure)
	{
		// Open an SDL window 
		unsigned xOffset, yOffset, xRes=496, yRes=384;
		if (OKAY != CreateGLScreen("Supermodel - Configuring Inputs...",&xOffset,&yOffset,&xRes,&yRes,FALSE,FALSE))
		{
			ErrorLog("Unable to start SDL to configure inputs.\n");
			return NULL;
		}
		
		// Configure the inputs
		if (Inputs->ConfigureInputs(NULL, xOffset, yOffset, xRes, yRes))
		{
			// Write input configuration and input system settings to config file
			Inputs->WriteToINIFile(&INI, "Global");
		
			if (OKAY != INI.Write(CONFIG_FILE_COMMENT))
				ErrorLog("Unable to save configuration to %s.", CONFIG_FILE_PATH);
			else
				printf("Configuration successfully saved to %s.\n", CONFIG_FILE_PATH);
		}
		else
			puts("Configuration aborted...");
		puts("");
	}
	
	return Inputs;
}
	
/******************************************************************************
 Save States and NVRAM
 
 Save states and NVRAM use the same basic format.
 
 Header block name: "Supermodel Save State" or "Supermodel NVRAM State"
 Data: Save state file version (4-byte integer), ROM set ID (up to 9 bytes, 
 including terminating \0).
 
 Different subsystems output their own blocks.
******************************************************************************/

#define STATE_FILE_VERSION	0	// save state file version

static unsigned	saveSlot = 0;	// save state slot #

static void SaveState(CModel3 *Model3)
{
	CBlockFile	SaveState;
	char		filePath[24];
	int			fileVersion = STATE_FILE_VERSION;
	
	sprintf(filePath, "Saves/%s.st%d", Model3->GetGameInfo()->id, saveSlot);
	if (OKAY != SaveState.Create(filePath, "Supermodel Save State", "Supermodel Version " SUPERMODEL_VERSION))
	{
		ErrorLog("Unable to save state to %s.", filePath);
		return;
	}
	
	// Write file format version and ROM set ID to header block 
	SaveState.Write(&fileVersion, sizeof(fileVersion));
	SaveState.Write(Model3->GetGameInfo()->id, strlen(Model3->GetGameInfo()->id)+1);
	
	// Save state
	Model3->SaveState(&SaveState);
	SaveState.Close();
	printf("Saved state to %s.\n", filePath);
	DebugLog("Saved state to %s.\n", filePath);
}

static void LoadState(CModel3 *Model3)
{
	CBlockFile	SaveState;
	char		filePath[24];
	int			fileVersion;
	
	// Generate file path
	sprintf(filePath, "Saves/%s.st%d", Model3->GetGameInfo()->id, saveSlot);
	
	// Open and check to make sure format is correct
	if (OKAY != SaveState.Load(filePath))
	{
		ErrorLog("Unable to load state from %s.", filePath);
		return;
	}
	
	if (OKAY != SaveState.FindBlock("Supermodel Save State"))
	{
		ErrorLog("%s does not appear to be a valid save state file.", filePath);
		return;
	}
	
	SaveState.Read(&fileVersion, sizeof(fileVersion));
	if (fileVersion != STATE_FILE_VERSION)
	{
		ErrorLog("Format of %s is incompatible with this version of Supermodel.", filePath);
		return;
	}
	
	// Load
	Model3->LoadState(&SaveState);
	SaveState.Close();
	printf("Loaded state from %s.\n", filePath);
	DebugLog("Loaded state from %s.\n", filePath);
}

static void SaveNVRAM(CModel3 *Model3)
{
	CBlockFile	NVRAM;
	char		filePath[24];
	int			fileVersion = STATE_FILE_VERSION;
	
	sprintf(filePath, "NVRAM/%s.nv", Model3->GetGameInfo()->id);
	if (OKAY != NVRAM.Create(filePath, "Supermodel NVRAM State", "Supermodel Version " SUPERMODEL_VERSION))
	{
		ErrorLog("Unable to save NVRAM to %s. Make sure directory exists!", filePath);
		return;
	}
	
	// Write file format version and ROM set ID to header block 
	NVRAM.Write(&fileVersion, sizeof(fileVersion));
	NVRAM.Write(Model3->GetGameInfo()->id, strlen(Model3->GetGameInfo()->id)+1);
	
	// Save NVRAM
	Model3->SaveNVRAM(&NVRAM);
	NVRAM.Close();
	DebugLog("Saved NVRAM to %s.\n", filePath);
}

static void LoadNVRAM(CModel3 *Model3)
{
	CBlockFile	NVRAM;
	char		filePath[24];
	int			fileVersion;
	
	// Generate file path
	sprintf(filePath, "NVRAM/%s.nv", Model3->GetGameInfo()->id);
	
	// Open and check to make sure format is correct
	if (OKAY != NVRAM.Load(filePath))
	{
		//ErrorLog("Unable to restore NVRAM from %s.", filePath);
		return;
	}
	
	if (OKAY != NVRAM.FindBlock("Supermodel NVRAM State"))
	{
		ErrorLog("%s does not appear to be a valid NVRAM file.", filePath);
		return;
	}
	
	NVRAM.Read(&fileVersion, sizeof(fileVersion));
	if (fileVersion != STATE_FILE_VERSION)
	{
		ErrorLog("Format of %s is incompatible with this version of Supermodel.", filePath);
		return;
	}
	
	// Load
	Model3->LoadNVRAM(&NVRAM);
	NVRAM.Close();
	DebugLog("Loaded NVRAM from %s.\n", filePath);
}


/******************************************************************************
 Main Program Driver
 
 All configuration management is done prior to calling Supermodel().
******************************************************************************/

#ifdef SUPERMODEL_DEBUGGER
int Supermodel(const char *zipFile, CModel3 *Model3, CInputs *Inputs, Debugger::CDebugger *Debugger, unsigned ppcFrequency, BOOL multiThreaded, 
			   unsigned xResParam, unsigned yResParam, BOOL keepAspectRatio, BOOL fullScreen, BOOL noThrottle, BOOL showFPS, 
			   const char *vsFile, const char *fsFile)
{
#else
int Supermodel(const char *zipFile, CInputs *Inputs, unsigned ppcFrequency, BOOL multiThreaded, unsigned xResParam, unsigned yResParam,
			   BOOL keepAspectRatio, BOOL fullScreen, BOOL noThrottle, BOOL showFPS, const char *vsFile, const char *fsFile)
{				  
	CModel3			*Model3 = new CModel3();
#endif // SUPERMODEL_DEBUGGER
	char			titleStr[128], titleFPSStr[128];
	CRender2D		*Render2D = new CRender2D();
	CRender3D		*Render3D = new CRender3D();
	unsigned		prevFPSTicks, currentFPSTicks, currentTicks, targetTicks, startTicks;
	unsigned		fpsFramesElapsed, framesElapsed;
	BOOL			showCursor = FALSE;	// show cursor in full screen mode?
	BOOL			quit = 0;
	BOOL            paused = 0;
	
	// Info log user options
	InfoLog("PowerPC frequency: %d Hz", ppcFrequency); 	
	InfoLog("Resolution: %dx%d (%s)", xResParam, yResParam, fullScreen?"full screen":"windowed");
	InfoLog("Frame rate limiting: %s", noThrottle?"Disabled":"Enabled");

	// Initialize and load ROMs
	Model3->Init(ppcFrequency, multiThreaded);
	if (OKAY != Model3->LoadROMSet(Model3GameList, zipFile))
		return 1;
		
	// Load NVRAM
	LoadNVRAM(Model3);
		
	// Start up SDL and open a GL window
  	xRes = xResParam;
  	yRes = yResParam;
  	sprintf(titleStr, "Supermodel - %s", Model3->GetGameInfo()->title);
	if (OKAY != CreateGLScreen(titleStr,&xOffset,&yOffset,&xRes,&yRes,keepAspectRatio,fullScreen))
		return 1;
	
	// Initialize audio system
	if (OKAY != OpenAudio())
		return 1;

	// Hide mouse if fullscreen
	Inputs->GetInputSystem()->SetMouseVisibility(!fullScreen);

	// Attach the inputs to the emulator
	Model3->AttachInputs(Inputs);
	
	// Initialize the renderer
	if (OKAY != Render2D->Init(xOffset, yOffset, xRes, yRes))
		goto QuitError;
	if (OKAY != Render3D->Init(xOffset, yOffset, xRes, yRes, vsFile, fsFile))
		goto QuitError;
	Model3->AttachRenderers(Render2D,Render3D);

	// Reset emulator
	Model3->Reset();
	
#ifdef SUPERMODEL_DEBUGGER
	// If debugger was supplied, set it as logger and attach it to system
	CLogger *oldLogger = GetLogger();
	if (Debugger != NULL)
	{
		SetLogger(Debugger);
		Debugger->Attach();
	}
#endif // SUPERMODEL_DEBUGGER

	// Emulate!
	fpsFramesElapsed = 0;
	framesElapsed = 0;
	prevFPSTicks = SDL_GetTicks();
	startTicks = prevFPSTicks;
	while (!quit)
	{
		// Check if paused
		if (!paused)
		{
			// If not, run one frame
			Model3->RunFrame();
		
			// Swap the buffers
			SDL_GL_SwapBuffers();
		}
		
		// Poll the inputs
		if (!Inputs->Poll(Model3->GetGameInfo(), xOffset, yOffset, xRes, yRes))
			quit = 1;
		
#ifdef SUPERMODEL_DEBUGGER
		if (Debugger != NULL)
		{
			Debugger->Poll();

			// Check if debugger requests exit or pause
			if (Debugger->CheckExit())
				quit = 1;
			else if (Debugger->CheckPause())	
				paused = 1;
			else
			{
#endif // SUPERMODEL_DEBUGGER

		// Check UI controls
		if (Inputs->uiExit->Pressed())
 		{
			// Quit emulator
 			quit = 1; 			
		}
		else if (Inputs->uiReset->Pressed())
 		{
			// Reset emulator
			Model3->Reset();
			
#ifdef SUPERMODEL_DEBUGGER
			// If debugger was supplied, reset it too
			if (Debugger != NULL)
				Debugger->Reset();
#endif // SUPERMODEL_DEBUGGER

			printf("Model 3 reset.\n");
		}
		else if (Inputs->uiPause->Pressed())
		{
			// Toggle emulator paused flag
			paused = !paused;
		}
		else if (Inputs->uiSaveState->Pressed())
		{
			// Save game state
 			SaveState(Model3);
		}
		else if (Inputs->uiChangeSlot->Pressed())
		{
			// Change save slot
 			++saveSlot;
 			saveSlot %= 10;	// clamp to [0,9]
 			printf("Save slot: %d\n", saveSlot);
		}
		else if (Inputs->uiLoadState->Pressed())
		{
			// Load game state
			LoadState(Model3);
						
#ifdef SUPERMODEL_DEBUGGER
			// If debugger was supplied, reset it after loading state
			if (Debugger != NULL)
				Debugger->Reset();
#endif // SUPERMODEL_DEBUGGER
		}
		else if (Inputs->uiDumpInpState->Pressed())
		{
			// Dump input states
			Inputs->DumpState(Model3->GetGameInfo());
		}
		else if (Inputs->uiToggleCursor->Pressed() && fullScreen)
 		{
			// Toggle cursor in full screen mode
			showCursor = !showCursor;
			Inputs->GetInputSystem()->SetMouseVisibility(!!showCursor);
 		}
		else if (Inputs->uiClearNVRAM->Pressed())
 		{
			// Clear NVRAM
 			Model3->ClearNVRAM();
 			printf("NVRAM cleared.\n");
 		}
		else if (Inputs->uiToggleFrLimit->Pressed())
 		{
			// Toggle frame limiting
 			noThrottle = !noThrottle;
 			printf("Frame limiting: %s\n", noThrottle?"Off":"On");
 		}
#ifdef SUPERMODEL_DEBUGGER
				else if (Inputs->uiEnterDebugger->Pressed())
				{
					// Break execution and enter debugger
					Debugger->ForceBreak(true);
				}
			}
		}
#endif // SUPERMODEL_DEBUGGER
 		
 		// FPS and frame rate
 		currentFPSTicks = SDL_GetTicks();
 		currentTicks = currentFPSTicks;
 		
 		// FPS
 		if (showFPS)
 		{
 			++fpsFramesElapsed;
			if((currentFPSTicks-prevFPSTicks) >= 1000)	// update FPS every 1 second (each tick is 1 ms)
			{
				sprintf(titleFPSStr, "%s - %1.1f FPS", titleStr, (float)fpsFramesElapsed*(float)(currentFPSTicks-prevFPSTicks)/1000.0f);
				SDL_WM_SetCaption(titleFPSStr,NULL);
				prevFPSTicks = currentFPSTicks;			// reset tick count
				fpsFramesElapsed = 0;					// reset frame count
			}
		}
		
		// Frame limiting/paused
		if (paused || !noThrottle)
		{
			++framesElapsed;
			targetTicks = startTicks + (unsigned) ((float)framesElapsed * 1000.0f/60.0f);
			if (currentTicks <= targetTicks)	// add a delay until we reach the next (target) frame time
				SDL_Delay(targetTicks-currentTicks);
			else								// begin a new frame
			{
				framesElapsed = 0;
				startTicks = currentTicks;
			}
		}
	}

#ifdef SUPERMODEL_DEBUGGER
	// If debugger was supplied, detach it from system and restore old logger
	if (Debugger != NULL)
	{
		Debugger->Detach();
		SetLogger(oldLogger);
	}
#endif // SUPERMODEL_DEBUGGER
	
	// Save NVRAM
	SaveNVRAM(Model3);
	
	// Close audio
	CloseAudio();

	// Shut down
#ifndef SUPERMODEL_DEBUGGER
	delete Model3;
#endif // SUPERMODEL_DEBUGGER
	delete Render2D;
	delete Render3D;
	
	// Dump PowerPC registers
#ifdef DEBUG
	for (int i = 0; i < 32; i += 4)
        printf("R%d=%08X\tR%d=%08X\tR%d=%08X\tR%d=%08X\n",
        i + 0, ppc_get_gpr(i + 0),
        i + 1, ppc_get_gpr(i + 1),
        i + 2, ppc_get_gpr(i + 2),
        i + 3, ppc_get_gpr(i + 3));
	printf("PC =%08X\n", ppc_get_pc());
  	printf("LR =%08X\n", ppc_get_lr());
	/*
	printf("DBAT0U=%08X\tIBAT0U=%08X\n", ppc_read_spr(SPR603E_DBAT0U), ppc_read_spr(SPR603E_IBAT0U));
	printf("DBAT0L=%08X\tIBAT0L=%08X\n", ppc_read_spr(SPR603E_DBAT0L), ppc_read_spr(SPR603E_IBAT0L));
	printf("DBAT1U=%08X\tIBAT1U=%08X\n", ppc_read_spr(SPR603E_DBAT1U), ppc_read_spr(SPR603E_IBAT1U));
	printf("DBAT1L=%08X\tIBAT1L=%08X\n", ppc_read_spr(SPR603E_DBAT1L), ppc_read_spr(SPR603E_IBAT1L));
	printf("DBAT2U=%08X\tIBAT2U=%08X\n", ppc_read_spr(SPR603E_DBAT2U), ppc_read_spr(SPR603E_IBAT2U));
	printf("DBAT2L=%08X\tIBAT2L=%08X\n", ppc_read_spr(SPR603E_DBAT2L), ppc_read_spr(SPR603E_IBAT2L));
	printf("DBAT3U=%08X\tIBAT3U=%08X\n", ppc_read_spr(SPR603E_DBAT3U), ppc_read_spr(SPR603E_IBAT3U));
	printf("DBAT3L=%08X\tIBAT3L=%08X\n", ppc_read_spr(SPR603E_DBAT3L), ppc_read_spr(SPR603E_IBAT3L));
	for (int i = 0; i < 10; i++)
		printf("SR%d =%08X\n", i, ppc_read_sr(i));
	for (int i = 10; i < 16; i++)
		printf("SR%d=%08X\n", i, ppc_read_sr(i));
	printf("SDR1=%08X\n", ppc_read_spr(SPR603E_SDR1));
	*/
#endif
	
	return 0;

	// Quit with an error
QuitError:
#ifndef SUPERMODEL_DEBUGGER
	delete Model3;
#endif // SUPERMODEL_DEBUGGER
	delete Render2D;
	delete Render3D;
	return 1;
}


/******************************************************************************
 Error and Debug Logging
******************************************************************************/

static CLogger *s_logger = NULL;

/*
 * Returns the current logger.
 */ 
CLogger *GetLogger()
{
	return s_logger;
}

/*
 * Sets the current logger.
 */
void SetLogger(CLogger *logger)
{
	s_logger = logger;
}

/*
 * DebugLog(fmt, ...):
 *
 * Logs a debug message with the logger.
 *
 * Parameters:
 *		fmt		Format string (same as printf()).
 *		...		Variable number of arguments as required by format string.
 */
void DebugLog(const char *fmt, ...)
{
	if (s_logger == NULL)
		return;
	va_list vl;
	va_start(vl, fmt);
	s_logger->DebugLog(fmt, vl);
	va_end(vl);
}

/*
 * InfoLog(fmt, ...);
 *
 * Logs an info message with the logger.
 *
 * Parameters:
 *		fmt		Format string (same as printf()).
 *		...		Variable number of arguments as required by format string.
 */
void InfoLog(const char *fmt, ...)
{
	if (s_logger == NULL)
		return;
	va_list vl;
	va_start(vl, fmt);
	s_logger->InfoLog(fmt, vl);
	va_end(vl);
}

/*
 * ErrorLog(fmt, ...):
 *
 * Logs an error message with the logger.
 *
 * Parameters:
 *		fmt		Format string (same as printf()).
 *		...		Variable number of arguments as required by format string.
 *
 * Returns:
 *		Always returns FAIL.
 */
BOOL ErrorLog(const char *fmt, ...)
{
	if (s_logger == NULL)
		return FAIL;
	va_list vl;
	va_start(vl, fmt);
	s_logger->ErrorLog(fmt, vl);
	va_end(vl);
	return FAIL;
}

#define DEBUG_LOG_FILE	"debug.log"
#define ERROR_LOG_FILE	"error.log"

/*
 * Default logger that logs to debug and error log files.
 */ 
class CFileLogger : public CLogger
{
private:
	const char *m_debugLogFile;
	const char *m_errorLogFile;

public:
	CFileLogger(const char *debugLogFile, const char *errorLogFile) : 
		m_debugLogFile(debugLogFile), m_errorLogFile(errorLogFile)
	{
		//
	}

	/*
	 * DebugLog(fmt, ...):
	 *
	 * Prints to debug log. The file is opened and closed each time so that its 
	 * contents are preserved even if the program crashes.
	 */
	void DebugLog(const char *fmt, va_list vl)
	{	
#ifdef DEBUG
		char	string[1024];
		FILE 	*fp;

		fp = fopen(m_debugLogFile, "ab");
		if (NULL != fp)
		{
			vsprintf(string, fmt, vl);
			fprintf(fp, string);
			fclose(fp);
		}
#endif // DEBUG
	}

	/*
	 * InfoLog(fmt, ...);
	 *
	 * Prints information to the error log file but does not print to stderr. This
	 * is useful for logging non-error information.
	 */
	void InfoLog(const char *fmt, va_list vl)
	{
		char	string[4096];
		FILE 	*fp;

		vsprintf(string, fmt, vl);
		
		fp = fopen(m_errorLogFile, "ab");
		if (NULL != fp)
		{
			fprintf(fp, "%s\n", string);
			fclose(fp);
		}
	
		CLogger::DebugLog("Info: ");
		CLogger::DebugLog(string);
		CLogger::DebugLog("\n");
	}

	/*
	 * ErrorLog(fmt, ...):
	 *
	 * Prints an error to stderr and the error log file.
	 */
	void ErrorLog(const char *fmt, va_list vl)
	{
		char	string[4096];
		FILE 	*fp;

		vsprintf(string, fmt, vl);
		fprintf(stderr, "Error: %s\n", string);
		
		fp = fopen(m_errorLogFile, "ab");
		if (NULL != fp)
		{
			fprintf(fp, "%s\n", string);
			fclose(fp);
		}
		
		CLogger::DebugLog("Error: ");
		CLogger::DebugLog(string);
		CLogger::DebugLog("\n");
	}

	void ClearLogs()
	{
#ifdef DEBUG
		ClearLog(DEBUG_LOG_FILE, "Supermodel v"SUPERMODEL_VERSION" Debug Log");
#endif // DEBUG
		ClearLog(ERROR_LOG_FILE, "Supermodel v"SUPERMODEL_VERSION" Error Log");	
	}
	
	// Clear log file
	void ClearLog(const char *file, const char *title)
	{
		FILE *fp = fopen(file, "w");
		if (NULL != fp)
		{
			unsigned	i;
			fprintf(fp, "%s\n", title);
			for (i = 0; i < strlen(title); i++)
				fputc('-', fp);
			fprintf(fp, "\n\n");
			fclose(fp);
		}
	}
};

/******************************************************************************
 Diagnostic Commands
******************************************************************************/

// Disassemble instructions from CROM
static int DisassembleCROM(const char *zipFile, UINT32 addr, unsigned n)
{
	const struct GameInfo	*Game;
	UINT8					*crom;
	struct ROMMap Map[] =
	{
		{ "CROM", 	NULL },
		{ "CROMxx",	NULL },
		{ NULL, 	NULL }
	};
	char	mnem[16], oprs[48];
	UINT32	op;
	
	// Do we have a valid CROM address?
	if (addr < 0xFF800000)
		return ErrorLog("Valid CROM address range is FF800000-FFFFFFFF.");
		
	// Allocate memory and set ROM region
	crom = new(std::nothrow) UINT8[0x8800000];
	if (NULL == crom)
		return ErrorLog("Insufficient memory to load CROM (need %d MB).", (0x8800000/8));
	Map[0].ptr = crom;
	Map[1].ptr = &crom[0x800000];
	
	// Load ROM set
	Game = LoadROMSetFromZIPFile(Map, Model3GameList, zipFile, FALSE);
	if (NULL == Game)
		return ErrorLog("Failed to load ROM set.");	
		
	// Mirror CROM if necessary
	if (Game->cromSize < 0x800000)	// high part of fixed CROM region contains CROM0
		CopyRegion(crom, 0, 0x800000-0x200000, &crom[0x800000], 0x800000);
		
	// Disassemble!
	addr -= 0xFF800000;
	while ((n > 0) && ((addr+4) <= 0x800000))
	{
		op = (crom[addr+0]<<24) | (crom[addr+1]<<16) | (crom[addr+2]<<8) | crom[addr+3];
        
        printf("%08X: ", addr+0xFF800000);
        if (DisassemblePowerPC(op, addr+0xFF800000, mnem, oprs, 1))
        {
            if (mnem[0] != '\0')    // invalid form
                printf("%08X %s*\t%s\n", op, mnem, oprs);
            else
                printf("%08X ?\n", op);
        }
        else
            printf("%08X %s\t%s\n", op, mnem, oprs);
		
		addr += 4;
		--n;
	}
	
	delete [] crom;
	return OKAY;
}

/*
 * PrintGLInfo():
 *
 * Queries and prints OpenGL information. A full list of extensions can
 * optionally be printed.
 */
static void PrintGLInfo(BOOL printExtensions)
{
	const GLubyte	*str;
	char			*strLocal;
	GLint			value;
	unsigned		xOffset, yOffset, xRes=496, yRes=384;
	
	if (OKAY != CreateGLScreen("Supermodel - Querying OpenGL Information...",&xOffset,&yOffset,&xRes,&yRes,FALSE,FALSE))
	{
		ErrorLog("Unable to query OpenGL.\n");
		return;
	}
	
	puts("OpenGL information:\n");

	str = glGetString(GL_VENDOR);
	printf("                   Vendor: %s\n", str);
	
	str = glGetString(GL_RENDERER);
	printf("                 Renderer: %s\n", str);
	
	str = glGetString(GL_VERSION);
	printf("                  Version: %s\n", str);
	
	str = glGetString(GL_SHADING_LANGUAGE_VERSION);
	printf(" Shading Language Version: %s\n", str);
	
	glGetIntegerv(GL_MAX_ELEMENTS_VERTICES, &value);
	printf("Maximum Vertex Array Size: %d vertices\n", value);
	
	glGetIntegerv(GL_MAX_TEXTURE_SIZE, &value);
	printf("     Maximum Texture Size: %d texels\n", value);
	
	glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &value);
	printf("Maximum Vertex Attributes: %d\n", value);
	
	glGetIntegerv(GL_MAX_VERTEX_UNIFORM_COMPONENTS, &value);
	printf("  Maximum Vertex Uniforms: %d\n", value);
	
	if (printExtensions)
	{
		str = glGetString(GL_EXTENSIONS);
		strLocal = (char *) malloc((strlen((char *) str)+1)*sizeof(char));
		if (NULL == strLocal)
			printf("     Supported Extensions: %s\n", str);
		else
		{
			strcpy(strLocal, (char *) str);
			printf("     Supported Extensions: %s\n", (strLocal = strtok(strLocal, " \t\n")));
			while ((strLocal = strtok(NULL, " \t\n")) != NULL)
				printf("                           %s\n", strLocal);
		}
	}
	
	printf("\n");
}


/******************************************************************************
 Entry Point and Command Line Procesing
******************************************************************************/

// Print Supermodel title and copyright information
static void Title(void)
{
	puts("Supermodel: A Sega Model 3 Arcade Emulator (Version "SUPERMODEL_VERSION")");
	puts("Copyright (C) 2011 by Bart Trzynadlowski");
	puts("");
}

// Print usage information
static void Help(void)
{
	puts("Usage: Supermodel <romset> [options]");
	puts("ROM set must be a valid ZIP file containing a single game.");
	puts("");
	puts("General Options:");
	puts("    -?, -h                 Print this help text");
	puts("    -print-games           List supported games");
	puts("");
	puts("Emulation Options:");
	puts("    -ppc-frequency=<f>     Set PowerPC frequency in MHz [Default: 25]");
	puts("    -multi-threaded        Enable multi-threading");
#ifdef SUPERMODEL_DEBUGGER
	puts("    -disable-debugger	     Completely disable debugger functionality");
	puts("    -enter-debugger        Enter debugger at start of emulation");
#endif // SUPERMODEL_DEBUGGER
	puts("");
	puts("Video Options:");
	puts("    -res=<x>,<y>           Resolution");
	puts("    -fullscreen            Full screen mode");
	puts("    -no-throttle           Disable 60 Hz frame rate limit");
	puts("    -show-fps              Display frame rate in window title bar");
#ifdef DEBUG	// ordinary users do not need to know about these, but they are always available
	puts("    -vert-shader=<file>    Load 3D vertex shader from external file");
	puts("    -frag-shader=<file>    Load 3D fragment shader from external file");
#endif
	puts("");
	puts("Input Options:");
	puts("    -input-system=<s>      Set input system [Default: SDL]");
	puts("    -print-inputs          Prints current input configuration");
	puts("    -config-inputs         Configure inputs for keyboards, mice and joysticks");
	puts("");
	puts("Diagnostic Options:");
#ifdef DEBUG
	puts("    -dis=<addr>[,n]        Disassemble PowerPC code from CROM");
#endif
	puts("    -print-gl-info         Print extensive OpenGL information\n");
}

// Print game list
static void PrintGameList(void)
{
	int	i, j;
	
	puts("Supported games:");
	puts("");
	puts("    ROM Set        Title");
	puts("    -------        -----");
	for (i = 0; Model3GameList[i].title != NULL; i++)
	{
		printf("    %s", Model3GameList[i].id);
		for (j = strlen(Model3GameList[i].id); j < 8; j++)	// pad for alignment (no game ID is more than 8 letters)
			printf(" ");
		printf("       %s\n", Model3GameList[i].title);
	}
}

/*
 * main(argc, argv):
 *
 * Program entry point.
 */
int main(int argc, char **argv)
{
	int			i, ret;
	int			cmd=0, fileIdx=0, cmdMultiThreaded=0, cmdFullScreen=0, cmdNoThrottle=0, cmdShowFPS=0, cmdPrintInputs=0, cmdConfigInputs=0, cmdPrintGames=0, cmdDis=0, cmdPrintGLInfo=0;
#ifdef SUPERMODEL_DEBUGGER
	int			cmdDisableDebugger = 0, cmdEnterDebugger=0;
#endif // SUPERMODEL_DEBUGGER
	unsigned	n, xRes=496, yRes=384, ppcFrequency=25000000;
	char		*vsFile = NULL, *fsFile = NULL, *inpSysName = NULL;
	UINT32		addr;

	Title();
	if (argc <= 1)
	{
		Help();
		return 0;
	}

	// Create default logger
	CFileLogger Logger(DEBUG_LOG_FILE, ERROR_LOG_FILE);
	Logger.ClearLogs();
	SetLogger(&Logger);

	// Parse command line
	for (i = 1; i < argc; i++)
	{
		if (!strcmp(argv[i],"-h") || !strcmp(argv[i],"-?"))
		{
			Help();
			return 0;
		}
		else if (!strcmp(argv[i],"-print-games"))
			cmd = cmdPrintGames = 1;
		else if (!strncmp(argv[i],"-ppc-frequency",14))
		{
			int	f;
			ret = sscanf(&argv[i][14],"=%d",&f);
			if (ret != 1)
				ErrorLog("-ppc-frequency requires a frequency.");
			else
			{
				if ((f<1) || (f>1000))	// limit to 1-1000MHz
					ErrorLog("PowerPC frequency must be between 1 and 1000 MHz. Ignoring.");
				else
					ppcFrequency = f*1000000;
			}
		}
		else if (!strncmp(argv[i],"-multi-threaded", 16))
			cmd = cmdMultiThreaded = 1;
#ifdef SUPERMODEL_DEBUGGER
		else if (!strncmp(argv[i],"-disable-debugger",17))
			cmd = cmdDisableDebugger = 1;
		else if (!strncmp(argv[i],"-enter-debugger",15))
			cmd = cmdEnterDebugger = 1;
#endif // SUPERMODEL_DEBUGGER
		else if (!strncmp(argv[i],"-res",4))
		{
			unsigned	x, y;
			
			ret = sscanf(&argv[i][4],"=%d,%d",&x,&y);
			if (ret != 2)
				ErrorLog("-res requires both a width and a height.");
			else
			{
				xRes = x;
				yRes = y;
			}
		}
		else if (!strcmp(argv[i],"-fullscreen"))
			cmd = cmdFullScreen = 1;
		else if (!strcmp(argv[i],"-no-throttle"))
			cmd = cmdNoThrottle = 1;
		else if (!strcmp(argv[i],"-show-fps"))
			cmd = cmdShowFPS = 1;
		else if (!strncmp(argv[i],"-vert-shader=",13))
		{
			if (argv[i][13] == '\0')
				ErrorLog("-vert-shader requires a file path.");
			else
				vsFile = &argv[i][13];
		}
		else if (!strncmp(argv[i],"-frag-shader=",13))
		{
			if (argv[i][13] == '\0')
				ErrorLog("-frag-shader requires a file path.");
			else
				fsFile = &argv[i][13];
		}
		else if (!strncmp(argv[i],"-input-system=", 14))
		{
			if (argv[i][14] == '\0')
				ErrorLog("-input-system requires an input system name.");
			else
				inpSysName = &argv[i][14];
		}
		else if (!strcmp(argv[i],"-print-inputs"))
			cmd = cmdPrintInputs = 1;
		else if (!strcmp(argv[i],"-config-inputs"))
			cmd = cmdConfigInputs = 1;
		else if (!strncmp(argv[i],"-dis",4))
		{
			ret = sscanf(&argv[i][4],"=%X,%X",&addr,&n);
			if (ret == 1)
			{
				n = 16;
				cmd = cmdDis = 1;
			}
			else if (ret == 2)
				cmd = cmdDis = 1;
			else
				ErrorLog("-dis requires address and, optionally, number of instructions.");
		}
		else if (!strcmp(argv[i],"-print-gl-info"))
			cmd = cmdPrintGLInfo = 1;
		else if (argv[i][0] == '-')
			ErrorLog("Ignoring invalid option: %s.", argv[i]);
		else
		{
			if (fileIdx)		// already specified a file
 				ErrorLog("Multiple files specified. Using %s, ignoring %s.", argv[i], argv[fileIdx]);
 			else
 				fileIdx = i;
 		}
	}
	
	// Initialize SDL (individual subsystems get initialized later)
	if (SDL_Init(0) != 0)
	{
		ErrorLog("Unable to initialize SDL: %s\n", SDL_GetError());
		return 1;
	}
	
	CInputSystem *InputSystem = NULL;
	CInputs *Inputs = NULL;
	int exitCode = 0;
#ifdef SUPERMODEL_DEBUGGER
	CModel3 *Model3 = NULL;
	Debugger::CSupermodelDebugger *Debugger = NULL;
#endif // SUPERMODEL_DEBUGGER

	// Create input system (default is SDL)
	if (inpSysName == NULL || stricmp(inpSysName, "sdl") == 0)
		InputSystem = new CSDLInputSystem();
#ifdef SUPERMODEL_WIN32
	else if (stricmp(inpSysName, "dinput") == 0)
		InputSystem = new CDirectInputSystem(false, false, false);
	else if (stricmp(inpSysName, "xinput") == 0)
		InputSystem = new CDirectInputSystem(false, true, false);
	else if (stricmp(inpSysName, "rawinput") == 0)
		InputSystem = new CDirectInputSystem(true, false, false);
#endif // SUPERMODEL_WIN32
	else
	{
		ErrorLog("Unknown input system: '%s'.\n", inpSysName);
		exitCode = 1;
		goto Exit;
	}

	// Create inputs from input system (configuring them if required)
	Inputs = CreateInputs(InputSystem, cmdConfigInputs);
	if (Inputs == NULL)
	{
		exitCode = 1;
		goto Exit;
	}

	if (cmdPrintInputs)
	{
		Inputs->PrintInputs(NULL);
		InputSystem->PrintSettings();
	}
	
	// Process commands that don't require ROM set
	if (cmd)
	{	
		if (cmdPrintGames)
		{
			PrintGameList();
			goto Exit;
		}
		
		if (cmdPrintGLInfo)
		{
			PrintGLInfo(FALSE);
			goto Exit;
		}
	}
	
	if (fileIdx == 0)
	{
		ErrorLog("No ROM set specified.");
		exitCode = 1;
		goto Exit;
	}
	
	// Process commands that require ROMs
	if (cmd)
	{				
		if (cmdDis)
		{
			if (OKAY != DisassembleCROM(argv[fileIdx], addr, n))
				exitCode = 1;
			goto Exit;
		}
	}

#ifdef SUPERMODEL_DEBUGGER
	// Create Model3
	Model3 = new CModel3();
	// Create Supermodel debugger unless debugging is disabled
	if (!cmdDisableDebugger)
	{
		Debugger = new Debugger::CSupermodelDebugger(Model3, Inputs, &Logger);
		// If -enter-debugger option was set force debugger to break straightaway
		if (cmdEnterDebugger)
			Debugger->ForceBreak(true);
	}
	// Fire up Supermodel with debugger
	exitCode = Supermodel(argv[fileIdx],Model3,Inputs,Debugger,ppcFrequency,cmdMultiThreaded,xRes,yRes,TRUE,cmdFullScreen,cmdNoThrottle,cmdShowFPS,vsFile,fsFile);
	if (Debugger != NULL)
		delete Debugger;
	delete Model3;
#else
	// Fire up Supermodel
	exitCode = Supermodel(argv[fileIdx],Inputs,ppcFrequency,cmdMultiThreaded,xRes,yRes,TRUE,cmdFullScreen,cmdNoThrottle,cmdShowFPS,vsFile,fsFile);
#endif // SUPERMODEL_DEBUGGER

Exit:
	if (Inputs != NULL)
		delete Inputs;
	if (InputSystem != NULL)
		delete InputSystem;
	SDL_Quit();
	return exitCode;
}