/* FCE Ultra - NES/Famicom Emulator
 *
 * Copyright notice for this file:
 *  Copyright (C) 2020 mjbudd77
 *
 * This program 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 2 of the License, or
 * (at your option) any later version.
 *
 * This program 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 this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */
// fceuWrapper.cpp
//
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <limits.h>
#include <unzip.h>

#include <QFileInfo>
#include <QStyleFactory>
#include "Qt/main.h"
#include "Qt/throttle.h"
#include "Qt/config.h"
#include "Qt/dface.h"
#include "Qt/fceuWrapper.h"
#include "Qt/input.h"
#include "Qt/sdl.h"
#include "Qt/sdl-video.h"
#include "Qt/nes_shm.h"
#include "Qt/unix-netplay.h"
#include "Qt/NetPlay.h"
#include "Qt/AviRecord.h"
#include "Qt/HexEditor.h"
#include "Qt/CheatsConf.h"
#include "Qt/SymbolicDebug.h"
#include "Qt/CodeDataLogger.h"
#include "Qt/QtScriptManager.h"
#include "Qt/ConsoleDebugger.h"
#include "Qt/ConsoleWindow.h"
#include "Qt/ConsoleUtilities.h"
#include "Qt/TasEditor/TasEditorWindow.h"
#include "Qt/fceux_git_info.h"

#include "common/cheat.h"
#include "../../fceu.h"
#include "../../cheat.h"
#include "../../movie.h"
#include "../../state.h"
#include "../../profiler.h"
#include "../../version.h"

#ifdef _S9XLUA_H
#include "../../fceulua.h"
#endif

#include "common/os_utils.h"
#include "common/configSys.h"
#include "utils/timeStamp.h"
#include "utils/StringUtils.h"
#include "../../oldmovie.h"
#include "../../types.h"

#ifdef CREATE_AVI
#include "../videolog/nesvideos-piece.h"
#endif

#ifdef _MSC_VER 
//not #if defined(_WIN32) || defined(_WIN64) because we have strncasecmp in mingw
#define strncasecmp _strnicmp
#define strcasecmp _stricmp
#endif
//*****************************************************************
// Define Global Variables to be shared with FCEU Core
//*****************************************************************
int  dendy = 0;
int eoptions=0;
int isloaded=0;
int pal_emulation=0;
int gametype = 0;
int closeFinishedMovie = 0;
int KillFCEUXonFrame = 0;

bool swapDuty = 0;
bool turbo = false;
bool pauseAfterPlayback = false;
bool suggestReadOnlyReplay = true;
bool showStatusIconOpt = true;
bool drawInputAidsEnable = true;
bool usePaletteForVideoBg = false;
unsigned int gui_draw_area_width   = 256;
unsigned int gui_draw_area_height  = 256;

// global configuration object
Config *g_config = NULL;

static int inited = 0;
static int noconfig=0;
static int frameskip=0;
static int periodic_saves = 0;
static int   mutexLocks = 0;
static int   mutexPending = 0;
static bool  emulatorHasMutex = 0;
unsigned int emulatorCycleCount = 0;
static int archiveFileLoadIndex = -1;

extern double g_fpsScale;

#ifdef CREATE_AVI
int mutecapture = 0;
#endif
//*****************************************************************
// Define Global Functions to be shared with FCEU Core
//*****************************************************************
//

// Message functions defined in MsgLogViewer.cpp
//void FCEUD_Message(const char *text)
//void FCEUD_PrintError(const char *errormsg)

/**
 * Opens a file, C++ style, to be read a byte at a time.
 */
FILE *FCEUD_UTF8fopen(const char *fn, const char *mode)
{
   FILE *fp = ::fopen(fn,mode);
	return(fp);
}

/**
 * Opens a file to be read a byte at a time.
 */
EMUFILE_FILE* FCEUD_UTF8_fstream(const char *fn, const char *m)
{
	std::ios_base::openmode mode = std::ios_base::binary;
	if(!strcmp(m,"r") || !strcmp(m,"rb"))
		mode |= std::ios_base::in;
	else if(!strcmp(m,"w") || !strcmp(m,"wb"))
		mode |= std::ios_base::out | std::ios_base::trunc;
	else if(!strcmp(m,"a") || !strcmp(m,"ab"))
		mode |= std::ios_base::out | std::ios_base::app;
	else if(!strcmp(m,"r+") || !strcmp(m,"r+b"))
		mode |= std::ios_base::in | std::ios_base::out;
	else if(!strcmp(m,"w+") || !strcmp(m,"w+b"))
		mode |= std::ios_base::in | std::ios_base::out | std::ios_base::trunc;
	else if(!strcmp(m,"a+") || !strcmp(m,"a+b"))
		mode |= std::ios_base::in | std::ios_base::out | std::ios_base::app;
    return new EMUFILE_FILE(fn, m);
	//return new std::fstream(fn,mode);
}

#if defined(MSVC)
 #ifdef _M_X64
   #define _MSVC_ARCH "x64"
 #else
   #define _MSVC_ARCH "x86"
 #endif
 #ifdef _DEBUG
  #define _MSVC_BUILD "debug"
 #else
  #define _MSVC_BUILD "release"
 #endif
 #define __COMPILER__STRING__ "msvc " _Py_STRINGIZE(_MSC_VER) " " _MSVC_ARCH " " _MSVC_BUILD
 #define _Py_STRINGIZE(X) _Py_STRINGIZE1((X))
 #define _Py_STRINGIZE1(X) _Py_STRINGIZE2 ## X
 #define _Py_STRINGIZE2(X) #X
 //re: http://72.14.203.104/search?q=cache:HG-okth5NGkJ:mail.python.org/pipermail/python-checkins/2002-November/030704.html+_msc_ver+compiler+version+string&hl=en&gl=us&ct=clnk&cd=5
#elif defined(__GNUC__)
 #define __COMPILER__STRING__ "gcc " __VERSION__
#elif defined(__clang__)
 #define __COMPILER__STRING__ "clang " __VERSION__
#else
 #define __COMPILER__STRING__ "unknown"
#endif

static const char *s_CompilerString = __COMPILER__STRING__;
/**
 * Returns the compiler string.
 */
const char *FCEUD_GetCompilerString(void)
{
	return s_CompilerString;
}

/**
 * Get the time in ticks.
 */
uint64
FCEUD_GetTime(void)
{
	uint64 t;

	if (FCEU::timeStampModuleInitialized())
	{
		FCEU::timeStampRecord ts;

		ts.readNew();

		t = ts.toCounts();
	}
	else
	{
		t = (double)SDL_GetTicks();

		t = t * 1e-3;
	}
	return t;
}

/**
 * Get the tick frequency in Hz.
 */
uint64
FCEUD_GetTimeFreq(void)
{
	// SDL_GetTicks() is in milliseconds
	uint64 f = 1000;

	if (FCEU::timeStampModuleInitialized())
	{
		f = FCEU::timeStampRecord::countFreq();
	}
	return f;
}

/**
 * Initialize all of the subsystem drivers: video, audio, and joystick.
 */
static int
DriverInitialize(FCEUGI *gi)
{
	if (InitVideo(gi) < 0)
	{
		return 0;
	}
	inited|=4;

	if (InitSound())
	{
		inited|=1;
	}

	if (InitJoysticks())
	{
		inited|=2;
	}

	int fourscore=0;
	g_config->getOption("SDL.FourScore", &fourscore);
	eoptions &= ~EO_FOURSCORE;
	if (fourscore)
	{
		eoptions |= EO_FOURSCORE;
	}

	InitInputInterface();
	return 1;
}

/**
 * Shut down all of the subsystem drivers: video, audio, and joystick.
 */
static void
DriverKill()
{
	if (!noconfig)
		g_config->save();

	KillJoysticks();

	if(inited&4)
		KillVideo();
	if(inited&1)
		KillSound();
	inited=0;
}

int LoadGameFromLua( const char *path )
{
	//printf("Load From Lua: '%s'\n", path);
	fceuWrapperUnLock();

	consoleWindow->emulatorThread->signalRomLoad(path);

	fceuWrapperLock();
	return 0;
}

/**
 * Reloads last game
 */
int reloadLastGame(void)
{
	std::string lastRom;
	g_config->getOption(std::string("SDL.LastOpenFile"), &lastRom);
	return LoadGame(lastRom.c_str(), false);
}

/**
 * Loads a game, given a full path/filename.  The driver code must be
 * initialized after the game is loaded, because the emulator code
 * provides data necessary for the driver code(number of scanlines to
 * render, what virtual input devices to use, etc.).
 */
int LoadGame(const char *path, bool silent, bool netPlayRequested)
{
	std::string fullpath;
	int gg_enabled, autoLoadDebug, autoOpenDebugger, autoInputPreset;

	// Check if this application instance has joined a net play session, 
	// NetPlay clients can only load ROMs retrieved from the host server.
	// However, clients can request that a host load their ROM for all players.
	auto* netPlayClient = NetPlayClient::GetInstance();

	if (!netPlayRequested && (netPlayClient != nullptr))
	{
		netPlayClient->requestRomLoad( path );
		return 0;
	}

	if (isloaded){
		CloseGame();
	}

	QFileInfo fi( path );

	// Resolve absolute path to file
	if ( fi.exists() )
	{
		//printf("FI: '%s'\n", fi.absoluteFilePath().toLocal8Bit().constData() );
		//printf("FI: '%s'\n", fi.canonicalFilePath().toLocal8Bit().constData() );
		fullpath = fi.canonicalFilePath().toLocal8Bit().constData();
	}
	else
	{
		fullpath.assign( path );
	}

	//printf("Fullpath: %zi '%s'\n", sizeof(fullpath), fullpath );

	// For some reason, the core of the emulator clears the state of 
	// the game genie option selection. So check the config each time
	// and re-enable the core game genie state if needed.
	g_config->getOption ("SDL.GameGenie", &gg_enabled);

	FCEUI_SetGameGenie (gg_enabled);

	// Set RAM Init Method Prior to Loading New Game
	g_config->getOption ("SDL.RamInitMethod", &RAMInitOption);

	// Load the game
	if(!FCEUI_LoadGame(fullpath.c_str(), 1, silent)) {
		return 0;
	}

	if ( consoleWindow )
	{
		consoleWindow->addRecentRom( fullpath.c_str() );
	}

	hexEditorLoadBookmarks();

	g_config->getOption( "SDL.AutoLoadDebugFiles", &autoLoadDebug );

	if ( autoLoadDebug )
	{
		loadGameDebugBreakpoints();
	}

	g_config->getOption( "SDL.AutoOpenDebugger", &autoOpenDebugger );

	if ( autoOpenDebugger && !debuggerWindowIsOpen() )
	{
		consoleWindow->openDebugWindow();
	}

	debugSymbolTable.loadGameSymbols();

	updateCheatDialog();

	CDLoggerROMChanged();

	int state_to_load;
	g_config->getOption("SDL.AutoLoadState", &state_to_load);
	if (state_to_load >= 0 && state_to_load < 10){
	    FCEUI_SelectState(state_to_load, 0);
	    FCEUI_LoadState(NULL, false);
	}

	g_config->getOption( "SDL.AutoInputPreset", &autoInputPreset );

	if ( autoInputPreset )
	{
		loadInputSettingsFromFile();
	}

	ParseGIInput(GameInfo);
	RefreshThrottleFPS();

	if(!DriverInitialize(GameInfo)) {
		return(0);
	}
	
	// set pal/ntsc
	int id, region, autoDetectPAL;
	g_config->getOption("SDL.PAL", &region);
	g_config->getOption("SDL.AutoDetectPAL", &autoDetectPAL);

	if ( autoDetectPAL )
	{
		id = FCEUI_GetCurrentVidSystem(NULL, NULL);

		if ( region == 2 )
		{	// Dendy mode:
			//   Run PAL Games as PAL
			//   Run NTSC Games as Dendy
			if ( id == 1 )
			{
				g_config->setOption("SDL.PAL", id);
				FCEUI_SetRegion(id);
			}
			else
			{
				FCEUI_SetRegion(region);
			}
		}
		else
		{	// Run NTSC games as NTSC and PAL games as PAL
			g_config->setOption("SDL.PAL", id);
			FCEUI_SetRegion(id);
		}
	}
	else
	{
		// If not Auto-detection of region,
		// Strictly enforce region GUI selection
		// Does not matter what type of game is 
		// loaded, the current region selection is used 
		FCEUI_SetRegion(region);
	}

	// Always re-calculate video dimensions after setting region.
	CalcVideoDimensions();

	// Force re-send of video settings to console viewer
	if ( consoleWindow )
	{
		consoleWindow->videoReset();
	}

	g_config->getOption("SDL.SwapDuty", &id);
	swapDuty = id;
	
	// Wave Recording done through menu or hotkeys
	//std::string filename;
	//g_config->getOption("SDL.Sound.RecordFile", &filename);
	//if (filename.size()) 
	//{
	//	if (!FCEUI_BeginWaveRecord(filename.c_str())) {
	//		g_config->setOption("SDL.Sound.RecordFile", "");
	//	}
	//}
	isloaded = 1;

	// Signal to listeners that a new ROM was loaded
	if ( consoleWindow )
	{
		emit consoleWindow->romLoaded();
	}
	return 1;
}

/**
 * Closes a game.  Frees memory, and deinitializes the drivers.
 */
int
CloseGame(void)
{
	std::string filename;

	if ( nes_shm )
	{	// Clear Screen on Game Close
		nes_shm->clear_pixbuf();
		nes_shm->blitUpdated = 1;
	}

	if (!isloaded) {
		return(0);
	}

	// Signal to listeners that current ROM is being unloaded
	if ( consoleWindow )
	{
		emit consoleWindow->romUnload();
	}

	// If the emulation thread is stuck hanging at a breakpoint,
	// disable breakpoint debugging and wait for the thread to 
	// complete its frame. So that it is idle with a minimal call
	// stack when we close the ROM. After thread has completed the
	// frame, it is then safe to re-enable breakpoint debugging.
	if ( debuggerWaitingAtBreakpoint() )
	{
		bpDebugSetEnable(false);

		if ( fceuWrapperIsLocked() )
		{
			fceuWrapperUnLock();
			msleep(100);
			fceuWrapperLock();
		}
		else
		{
			msleep(100);
		}
		bpDebugSetEnable(true);
	}

	hexEditorSaveBookmarks();
	saveGameDebugBreakpoints();
	debuggerClearAllBreakpoints();
	debuggerClearAllBookmarks();

	debugSymbolTable.save();
	debugSymbolTable.clear();
	CDLoggerROMClosed();

	int state_to_save;
	g_config->getOption("SDL.AutoSaveState", &state_to_save);
	if (state_to_save < 10 && state_to_save >= 0){
	    FCEUI_SelectState(state_to_save, 0);
	    FCEUI_SaveState(NULL, false);
	}

	int autoInputPreset;
	g_config->getOption( "SDL.AutoInputPreset", &autoInputPreset );

	if ( autoInputPreset )
	{
		saveInputSettingsToFile();
	}

	if ( tasWin != NULL )
	{
		tasWin->requestWindowClose();
	}

	FCEUI_CloseGame();

	DriverKill();
	isloaded = 0;
	GameInfo = 0;

	g_config->getOption("SDL.Sound.RecordFile", &filename);
	if(filename.size()) {
		FCEUI_EndWaveRecord();
	}

	InputUserActiveFix();
	return(1);
}

int  fceuWrapperSoftReset(void)
{
	if ( isloaded )
	{
		ResetNES();

		if (consoleWindow != nullptr)
		{
			emit consoleWindow->nesResetOccurred();
		}
	}
	return 0;
}

int  fceuWrapperHardReset(void)
{
	if ( isloaded && GameInfo )
	{
		char romPath[2048];

		romPath[0] = 0;

		if ( GameInfo->archiveFilename )
		{
			Strlcpy( romPath, GameInfo->archiveFilename, sizeof(romPath) );
		}
		else if ( GameInfo->filename )
		{
			Strlcpy( romPath, GameInfo->filename, sizeof(romPath) );
		}
		fceuWrapperSetArchiveFileLoadIndex( GameInfo->archiveIndex );

		if ( romPath[0] != 0 )
		{
			CloseGame();
			//printf("Loading: '%s'\n", romPath );
			LoadGame ( romPath );
		}
		fceuWrapperClearArchiveFileLoadIndex();
	}
	return 0;
}

int  fceuWrapperTogglePause(void)
{
	if ( isloaded )
	{
		FCEUI_ToggleEmulationPause();
	}
	return 0;
}

bool fceuWrapperGameLoaded(void)
{
	return (isloaded ? true : false);
}

void fceuWrapperRequestAppExit(void)
{
	if ( consoleWindow )
	{
		consoleWindow->requestClose();
	}
}

static const char *DriverUsage =
"Option         Value   Description\n"
"--pal          {0|1}   Use PAL timing.\n"
"--newppu       {0|1}   Enable the new PPU core. (WARNING: May break savestates)\n"
"--input(1,2)   d       Set which input device to emulate for input 1 or 2.\n"
"                         Devices:  gamepad zapper powerpad.0 powerpad.1\n"
"                         arkanoid\n"
"--input(3,4)   d       Set the famicom expansion device to emulate for\n"
"                       input(3, 4)\n"
"                          Devices: quizking hypershot mahjong toprider ftrainer\n"
"                          familykeyboard oekakids arkanoid shadow bworld\n"
"                          4player\n"
"--gamegenie    {0|1}   Enable emulated Game Genie.\n"
"--frameskip    x       Set # of frames to skip per emulated frame.\n"
"--xres         x       Set horizontal resolution for full screen mode.\n"
"--yres         x       Set vertical resolution for full screen mode.\n"
"--autoscale    {0|1}   Enable autoscaling in fullscreen. \n"
"--keepratio    {0|1}   Keep native NES aspect ratio when autoscaling. \n"
"--(x/y)scale   x       Multiply width/height by x. \n"
"                         (Real numbers >0 with OpenGL, otherwise integers >0).\n"
"--(x/y)stretch {0|1}   Stretch to fill surface on x/y axis (OpenGL only).\n"
"--fullscreen   {0|1}   Enable full screen mode.\n"
"--noframe      {0|1}   Hide title bar and window decorations.\n"
"--special      {1-4}   Use special video scaling filters\n"
"                         (1 = hq2x; 2 = Scale2x; 3 = NTSC 2x; 4 = hq3x;\n"
"                         5 = Scale3x; 6 = Prescale2x; 7 = Prescale3x; 8=Precale4x; 9=PAL)\n"
"--palette      f       Load custom global palette from file f.\n"
"--sound        {0|1}   Enable sound.\n"
"--soundrate    x       Set sound playback rate to x Hz.\n"
"--soundq      {0|1|2}  Set sound quality. (0 = Low 1 = High 2 = Very High)\n"
"--soundbufsize x       Set sound buffer size to x ms.\n"
"--volume      {0-256}  Set volume to x.\n"
"--soundrecord  f       Record sound to file f.\n"
"--playmov      f       Play back a recorded FCM/FM2/FM3 movie from filename f.\n"
"--pauseframe   x       Pause movie playback at frame x.\n"
"--fcmconvert   f       Convert fcm movie file f to fm2.\n"
"--ripsubs      f       Convert movie's subtitles to srt\n"
"--subtitles    {0|1}   Enable subtitle display\n"
"--fourscore    {0|1}   Enable fourscore emulation\n"
"--no-config    {0|1}   Use default config file and do not save\n"
"--net          s       Connect to server 's' for TCP/IP network play.\n"
"--port         x       Use TCP/IP port x for network play.\n"
"--user         x       Set the nickname to use in network play.\n"
"--pass         x       Set password to use for connecting to the server.\n"
"--netkey       s       Use string 's' to create a unique session for the\n"
"                       game loaded.\n"
"--players      x       Set the number of local players in a network play\n"
"                       session.\n"
"--rp2mic       {0|1}   Replace Port 2 Start with microphone (Famicom).\n"
"--4buttonexit {0|1}    exit the emulator when A+B+Select+Start is pressed\n"
"--loadstate {0-9|>9}   load from the given state when the game is loaded\n"
"--savestate {0-9|>9}   save to the given state when the game is closed\n"
"                         to not save/load automatically provide a number\n"
"                         greater than 9\n"
"--periodicsaves {0|1}  enable automatic periodic saving.  This will save to\n"
"                         the state passed to --savestate\n";

static void ShowUsage(const char *prog)
{
	int i,j;
	FCEUD_Message("Starting " FCEU_NAME_AND_VERSION "...\n");
	printf("\nUsage is as follows:\n%s <options> filename\n\n",prog);
	puts(DriverUsage);
#ifdef _S9XLUA_H
	puts ("--loadlua      f       Loads lua script from filename f.");
#endif
#ifdef CREATE_AVI
	puts ("--videolog     c       Calls mencoder to grab the video and audio streams to\n                         encode them. Check the documentation for more on this.");
	puts ("--mute        {0|1}    Mutes FCEUX while still passing the audio stream to\n                         mencoder during avi creation.");
#endif
	puts ("--style=KEY            Use Qt GUI Style based on supplied key. Available system style keys are:\n");

	QStringList styleList = QStyleFactory::keys();

	j=0;
	for (i=0; i<styleList.size(); i++)
	{
		printf("  %16s  ", styleList[i].toLocal8Bit().constData() ); j++;

		if ( j >= 4 )
		{
			printf("\n"); j=0;
		}
	}
	printf("\n\n");
	printf(" Custom Qt stylesheets (.qss files) may be used by setting an\n");
	printf(" environment variable named FCEUX_QT_STYLESHEET equal to the \n");
	printf(" full (absolute) path to the qss file.\n");

	puts("");
	printf("Compiled with SDL version %d.%d.%d\n", SDL_MAJOR_VERSION, SDL_MINOR_VERSION, SDL_PATCHLEVEL );
	SDL_version v; 
	SDL_GetVersion(&v);
	printf("Linked with SDL version %d.%d.%d\n", v.major, v.minor, v.patch);
  	printf("Compiled with QT version %d.%d.%d\n", QT_VERSION_MAJOR, QT_VERSION_MINOR, QT_VERSION_PATCH );
	printf("git URL: %s\n", fceu_get_git_url() );
	printf("git Rev: %s\n", fceu_get_git_rev() );
	
}

// Pre-GUI initialization.
int  fceuWrapperPreInit( int argc, char *argv[] )
{
	for (int i=0; i<argc; i++)
	{
		if ( (strcmp(argv[i], "--help") == 0) || (strcmp(argv[i],"-h") == 0) )
		{
			ShowUsage(argv[0]);
			exit(0);
		}
		else if ( strcmp(argv[i], "--no-gui") == 0)
		{
			printf("Error: Qt/SDL version does not support --no-gui option.\n");
			exit(1);
		}
		else if ( strcmp(argv[i], "--version") == 0)
		{
			printf("%i.%i.%i\n", FCEU_VERSION_MAJOR, FCEU_VERSION_MINOR, FCEU_VERSION_PATCH);
			exit(0);
		}
	}
	return 0;
}

int  fceuWrapperInit( int argc, char *argv[] )
{
	int opt, error;
	std::string s;

	FCEUD_Message("Starting " FCEU_NAME_AND_VERSION "...\n");

	/* SDL_INIT_VIDEO Needed for (joystick config) event processing? */
	if (SDL_Init(SDL_INIT_VIDEO)) 
	{
		printf("Could not initialize SDL: %s.\n", SDL_GetError());
		exit(-1);
	}
	if ( SDL_SetHint( SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1" ) == SDL_FALSE )
	{
		printf("Error setting SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS\n");
	}

	// Initialize the configuration system
	g_config = InitConfig();

	if ( !g_config )
	{
		printf("Error: Could not initialize configuration system\n");
		exit(-1);
	}

	// initialize the infrastructure
	error = FCEUI_Initialize();

	if (error != 1) 
	{
		printf("Error: Initializing FCEUI\n");
		ShowUsage(argv[0]);
		//SDL_Quit();
		exit(-1);
	}

	int romIndex = g_config->parse(argc, argv);

	// This is here so that a default fceux.cfg will be created on first
	// run, even without a valid ROM to play.
	// Unless, of course, there's actually --no-config given
	// mbg 8/23/2008 - this is also here so that the inputcfg routines can have 
    // a chance to dump the new inputcfg to the fceux.cfg  in case you didnt 
    // specify a rom  filename
	g_config->getOption("SDL.NoConfig", &noconfig);

	if (!noconfig)
	{
		g_config->save();
	}

	// update the input devices
	UpdateInput(g_config);

	// check for a .fcm file to convert to .fm2
	g_config->getOption ("SDL.FCMConvert", &s);
	g_config->setOption ("SDL.FCMConvert", "");
	if (!s.empty())
	{
		int okcount = 0;
		std::string infname = s.c_str();
		// produce output filename
		std::string outname;
		size_t dot = infname.find_last_of (".");
		if (dot == std::string::npos)
			outname = infname + ".fm2";
		else
			outname = infname.substr(0,dot) + ".fm2";
	  
		MovieData md;
		EFCM_CONVERTRESULT result = convert_fcm (md, infname);

		if (result == FCM_CONVERTRESULT_SUCCESS) {
			okcount++;
        // *outf = new EMUFILE;
		EMUFILE_FILE* outf = FCEUD_UTF8_fstream (outname, "wb");
		md.dump (outf,false);
		delete outf;
		FCEUD_Message ("Your file has been converted to FM2.\n");
	}
	else {
		FCEUD_Message ("Something went wrong while converting your file...\n");
	}
	  
		DriverKill();
	  SDL_Quit();
	  return 0;
	}
	// If x/y res set to 0, store current display res in SDL.LastX/YRes
	int yres, xres;
	g_config->getOption("SDL.XResolution", &xres);
	g_config->getOption("SDL.YResolution", &yres);
	
	int autoResume;
	g_config->getOption("SDL.AutoResume", &autoResume);
	if(autoResume)
	{
		AutoResumePlay = true;
	}
	else
	{
		AutoResumePlay = false;
	}

	// Cheats
	g_config->getOption ("SDL.CheatsDisabled"     , &globalCheatDisabled);
	g_config->getOption ("SDL.CheatsDisableAutoLS", &disableAutoLSCheats);

	g_config->getOption ("SDL.DrawInputAids", &drawInputAidsEnable);

	// Initialize Autofire Pattern
	int autofireOnFrames=1, autofireOffFrames=1;
	g_config->getOption ("SDL.AutofireOnFrames" , &autofireOnFrames );
	g_config->getOption ("SDL.AutofireOffFrames", &autofireOffFrames);

	SetAutoFirePattern( autofireOnFrames, autofireOffFrames );

	// check to see if recording HUD to AVI is enabled
	int rh;
	g_config->getOption("SDL.RecordHUD", &rh);
	if( rh )
		FCEUI_SetAviEnableHUDrecording(true);
	else
		FCEUI_SetAviEnableHUDrecording(false);

	g_config->getOption("SDL.SuggestReadOnlyReplay"  , &suggestReadOnlyReplay);
	g_config->getOption("SDL.PauseAfterMoviePlayback", &pauseAfterPlayback);
	g_config->getOption("SDL.CloseFinishedMovie"     , &closeFinishedMovie);
	g_config->getOption("SDL.MovieBindSavestate"     , &bindSavestate);
	g_config->getOption("SDL.SubtitlesOnAVI"         , &subtitlesOnAVI);
	g_config->getOption("SDL.AutoMovieBackup"        , &autoMovieBackup);
	g_config->getOption("SDL.MovieFullSaveStateLoads", &fullSaveStateLoads);

	// check to see if movie messages are disabled
	int mm;
	g_config->getOption("SDL.MovieMsg", &mm);
	if( mm == 0)
		FCEUI_SetAviDisableMovieMessages(true);
	else
		FCEUI_SetAviDisableMovieMessages(false);
  
	g_config->getOption("SDL.AviVideoFormat", &opt);
	aviSetSelVideoFormat(opt);

	// check for a .fm2 file to rip the subtitles
	g_config->getOption("SDL.RipSubs", &s);
	g_config->setOption("SDL.RipSubs", "");
	if (!s.empty())
	{
		MovieData md;
		std::string infname;
		infname = s.c_str();
		FCEUFILE *fp = FCEU_fopen(s.c_str(), 0, "rb", 0);
		
		// load the movie and and subtitles
		extern bool LoadFM2(MovieData&, EMUFILE*, int, bool);
		LoadFM2(md, fp->stream, INT_MAX, false);
		LoadSubtitles(md); // fill subtitleFrames and subtitleMessages
		delete fp;
		
		// produce .srt file's name and open it for writing
		std::string outname;
		size_t dot = infname.find_last_of (".");
		if (dot == std::string::npos)
			outname = infname + ".srt";
		else
			outname = infname.substr(0,dot) + ".srt";
		FILE *srtfile;
		srtfile = fopen(outname.c_str(), "w");
		
		if (srtfile != NULL)
		{
			extern std::vector<int> subtitleFrames;
			extern std::vector<std::string> subtitleMessages;
			float fps = (md.palFlag == 0 ? 60.0988 : 50.0069); // NTSC vs PAL
			float subduration = 3; // seconds for the subtitles to be displayed
			for (size_t i = 0; i < subtitleFrames.size(); i++)
			{
				fprintf(srtfile, "%zi\n", i+1); // starts with 1, not 0
				double seconds, ms, endseconds, endms;
				seconds = subtitleFrames[i]/fps;
				if (i+1 < subtitleFrames.size()) // there's another subtitle coming after this one
				{
					if (subtitleFrames[i+1]-subtitleFrames[i] < subduration*fps) // avoid two subtitles at the same time
					{
						endseconds = (subtitleFrames[i+1]-1)/fps; // frame x: subtitle1; frame x+1 subtitle2
					} else {
						endseconds = seconds+subduration;
							}
				} else {
					endseconds = seconds+subduration;
				}
				ms = modf(seconds, &seconds);
				endms = modf(endseconds, &endseconds);
				// this is just beyond ugly, don't show it to your kids
				fprintf(srtfile,
				"%02.0f:%02d:%02d,%03d --> %02.0f:%02d:%02d,%03d\n", // hh:mm:ss,ms --> hh:mm:ss,ms
				floor(seconds/3600),	(int)floor(seconds/60   ) % 60, (int)floor(seconds)	% 60, (int)(ms*1000),
				floor(endseconds/3600), (int)floor(endseconds/60) % 60, (int)floor(endseconds) % 60, (int)(endms*1000));
				fprintf(srtfile, "%s\n\n", subtitleMessages[i].c_str()); // new line for every subtitle
			}
		fclose(srtfile);
		printf("%d subtitles have been ripped.\n", (int)subtitleFrames.size());
		} else {
		FCEUD_Message("Couldn't create output srt file...\n");
		}
	  
		DriverKill();
		SDL_Quit();
		return 0;
	}

	nes_shm = open_nes_shm();

	if ( nes_shm == NULL )
	{
		printf("Error: Failed to open NES Shared memory\n");
		return -1;
	}

	// update the emu core
	UpdateEMUCore(g_config);

	CalcVideoDimensions();

	#ifdef CREATE_AVI
	g_config->getOption("SDL.VideoLog", &s);
	g_config->setOption("SDL.VideoLog", "");
	if(!s.empty())
	{
		NESVideoSetVideoCmd(s.c_str());
		LoggingEnabled = 1;
		g_config->getOption("SDL.MuteCapture", &mutecapture);
	} else {
		mutecapture = 0;
	}
	#endif

	{
		int id;
		g_config->getOption("SDL.InputDisplay", &id);
		extern int input_display;
		input_display = id;
		// not exactly an id as an true/false switch; still better than creating another int for that
		g_config->getOption("SDL.SubtitleDisplay", &id); 
		movieSubtitles = id ? true : false;
	}

	// Emulation Timing Mechanism
	{
		int timingMode;

		g_config->getOption("SDL.EmuTimingMech", &timingMode);

		setTimingMode( timingMode );
	}
	
	// load the hotkeys from the config life
	setHotKeys();

	// Initialize the State Recorder
	{
		bool srEnable = false;
		bool srUseTimeMode = false;
		int srHistDurMin = 15;
		int srFramesBtwSnaps = 60;
		int srTimeBtwSnapsMin = 0;
		int srTimeBtwSnapsSec = 3;
		int srCompressionLevel = 0;
		int pauseOnLoadTime = 3;
		int pauseOnLoad = StateRecorderConfigData::TEMPORARY_PAUSE;

		g_config->getOption("SDL.StateRecorderEnable", &srEnable);
		g_config->getOption("SDL.StateRecorderTimingMode", &srUseTimeMode);
		g_config->getOption("SDL.StateRecorderHistoryDurationMin", &srHistDurMin);
		g_config->getOption("SDL.StateRecorderFramesBetweenSnaps", &srFramesBtwSnaps);
		g_config->getOption("SDL.StateRecorderTimeBetweenSnapsMin", &srTimeBtwSnapsMin);
		g_config->getOption("SDL.StateRecorderTimeBetweenSnapsSec", &srTimeBtwSnapsSec);
		g_config->getOption("SDL.StateRecorderCompressionLevel", &srCompressionLevel);
		g_config->getOption("SDL.StateRecorderPauseOnLoad", &pauseOnLoad);
		g_config->getOption("SDL.StateRecorderPauseDuration", &pauseOnLoadTime);

		StateRecorderConfigData srConfig;

		srConfig.historyDurationMinutes = srHistDurMin;
		srConfig.timingMode = srUseTimeMode ?
					StateRecorderConfigData::TIME : StateRecorderConfigData::FRAMES;
		srConfig.timeBetweenSnapsMinutes = static_cast<float>( srTimeBtwSnapsMin ) +
			                          ( static_cast<float>( srTimeBtwSnapsSec ) / 60.0f );
		srConfig.framesBetweenSnaps = srFramesBtwSnaps;
		srConfig.compressionLevel = srCompressionLevel;
		srConfig.loadPauseTimeSeconds = pauseOnLoadTime;
		srConfig.pauseOnLoad = static_cast<StateRecorderConfigData::PauseType>(pauseOnLoad);

		FCEU_StateRecorderSetEnabled( srEnable );
		FCEU_StateRecorderSetConfigData( srConfig );
	}

	// Rom Load
	if (romIndex >= 0)
	{
		QFileInfo fi( argv[romIndex] );

		// Resolve absolute path to file
		if ( fi.exists() )
		{
			std::string fullpath = fi.canonicalFilePath().toLocal8Bit().constData();

			error = LoadGame( fullpath.c_str() );

			if (error != 1)
			{
				DriverKill();
				SDL_Quit();
				return -1;
			}
			g_config->setOption("SDL.LastOpenFile", fullpath.c_str() );
			g_config->save();
		}
		else
		{
			// File was not found
			return -1;
		}
	}

	aviRecordInit();

	// movie playback
	g_config->getOption("SDL.Movie", &s);
	g_config->setOption("SDL.Movie", "");
	if (s != "")
	{
		if(s.find(".fm2") != std::string::npos || s.find(".fm3") != std::string::npos)
		{
			static int pauseframe;
			char replayReadOnlySetting;
			g_config->getOption("SDL.PauseFrame", &pauseframe);
			g_config->setOption("SDL.PauseFrame", 0);

			if (suggestReadOnlyReplay)
			{
				replayReadOnlySetting = true;
			}
			else
			{
				replayReadOnlySetting = FCEUI_GetMovieToggleReadOnly();
			}
			FCEUI_printf("Playing back movie located at %s\n", s.c_str());
			FCEUI_LoadMovie(s.c_str(), replayReadOnlySetting, pauseframe ? pauseframe : false);
		}
		else
		{
		  FCEUI_printf("Sorry, I don't know how to play back %s\n", s.c_str());
		}
		g_config->getOption("SDL.MovieLength",&KillFCEUXonFrame);
		printf("KillFCEUXonFrame %d\n",KillFCEUXonFrame);
	}
	
    int save_state;
    g_config->getOption("SDL.PeriodicSaves", &periodic_saves);
    g_config->getOption("SDL.AutoSaveState", &save_state);
    if(periodic_saves && save_state < 10 && save_state >= 0){
        FCEUI_SelectState(save_state, 0);
    } else {
        periodic_saves = 0;
    }
	
#ifdef _S9XLUA_H
	// load lua script if option passed
	g_config->getOption("SDL.LuaScript", &s);
	g_config->setOption("SDL.LuaScript", "");
	if (s.size() > 0)
	{
		QFileInfo fi( s.c_str() );

		// Resolve absolute path to file
		if ( fi.exists() )
		{
			//printf("FI: '%s'\n", fi.absoluteFilePath().toLocal8Bit().constData() );
			//printf("FI: '%s'\n", fi.canonicalFilePath().toLocal8Bit().constData() );
			s = fi.canonicalFilePath().toLocal8Bit().constData();
		}
//#if defined(__linux__) || defined(__APPLE__) || defined(__unix__)
//
//		// Resolve absolute path to file
//		char fullpath[2048];
//		if ( realpath( s.c_str(), fullpath ) != NULL )
//		{
//			printf("Fullpath: '%s'\n", fullpath );
//			s.assign( fullpath );
//		}
//#endif
		FCEU_LoadLuaCode(s.c_str());
	}
#endif
	
	g_config->getOption("SDL.NewPPU", &newppu);
	g_config->getOption("SDL.Frameskip", &frameskip);

	return 0;
}

int  fceuWrapperClose( void )
{
	CloseGame();

	// exit the infrastructure
	FCEUI_Kill();
	SDL_Quit();

	return 0;
}

int  fceuWrapperMemoryCleanup(void)
{
	FreeCDLog();

	close_nes_shm();

	if ( g_config )
	{
		delete g_config; g_config = NULL;
	}

	return 0;
}

/**
 * Update the video, audio, and input subsystems with the provided
 * video (XBuf) and audio (Buffer) information.
 */
void
FCEUD_Update(uint8 *XBuf,
	 int32 *Buffer,
	 int Count)
{
	int blitDone = 0;
	//extern int FCEUDnetplay;

	//#ifdef CREATE_AVI
	//if (LoggingEnabled == 2 || (eoptions&EO_NOTHROTTLE))
	//{
	//	if(LoggingEnabled == 2)
	//	{
	//		int16* MonoBuf = new int16[Count];
	//		int n;
	//		for(n=0; n<Count; ++n)
	//		{
	//			MonoBuf[n] = Buffer[n] & 0xFFFF;
	//		}
	//		NESVideoLoggingAudio
	//		(
	//		  MonoBuf, 
	//		  FSettings.SndRate, 16, 1,
	//		  Count
	//		);
	//		delete [] MonoBuf;
	//	}
	//	Count /= 2;
	//	if (inited & 1)
	//	{
	//		if (Count > GetWriteSound()) Count = GetWriteSound();

	//		if (!mutecapture)
	//		{
	//			if(Count > 0 && Buffer) WriteSound(Buffer,Count);   
	//		}
	//	}
	//	//if (inited & 2)
	//	//	FCEUD_UpdateInput();
	//  	if(XBuf && (inited & 4)) BlitScreen(XBuf);
	//  
	//	return;
	//}
	//#endif
	aviRecordAddAudioFrame( Buffer, Count );
	
	WriteSound(Buffer,Count);

	//int ocount = Count;
	// apply frame scaling to Count
	//Count = (int)(Count / g_fpsScale);
	//if (Count) 
	//{
	//	int32 can=GetWriteSound();
	//	static int uflow=0;
	//	int32 tmpcan;

	//	// don't underflow when scaling fps
	//	if(can >= GetMaxSound() && g_fpsScale==1.0) uflow=1;	/* Go into massive underflow mode. */

	//	if(can > Count) can=Count;
	//	else uflow=0;

	//	#ifdef CREATE_AVI
	//	if (!mutecapture)
	//	#endif
	//	  WriteSound(Buffer,can);

	//	//if(uflow) puts("Underflow");
	//	tmpcan = GetWriteSound();
	//	// don't underflow when scaling fps
	//	if (g_fpsScale>1.0 || ((tmpcan < Count*0.90) && !uflow)) 
	//	{
	//		if (XBuf && (inited&4) && !(NoWaiting & 2))
	//		{
	//			BlitScreen(XBuf); blitDone = 1;
	//		}
	//		Buffer+=can;
	//		Count-=can;
	//		if(Count) 
	//		{
	//			if(NoWaiting) 
	//			{
	//				can=GetWriteSound();
	//				if(Count>can) Count=can;
	//				#ifdef CREATE_AVI
	//				if (!mutecapture)
	//				#endif
	//				  WriteSound(Buffer,Count);
	//			}
	//		  	else
	//		  	{
	//				while(Count>0) 
	//				{
	//					#ifdef CREATE_AVI
	//					if (!mutecapture)
	//					#endif
	//					  WriteSound(Buffer,(Count<ocount) ? Count : ocount);
	//					Count -= ocount;
	//				}
	//			}
	//		}
	//	} //else puts("Skipped");
	//	//else if (!NoWaiting && FCEUDnetplay && (uflow || tmpcan >= (Count * 1.8))) 
	//	//{
	//	//	if (Count > tmpcan) Count=tmpcan;
	//	//	while(tmpcan > 0) 
	//	//	{
	//	//		//	printf("Overwrite: %d\n", (Count <= tmpcan)?Count : tmpcan);
	//	//		#ifdef CREATE_AVI
	//	//		if (!mutecapture)
	//	//		#endif
	//	//		  WriteSound(Buffer, (Count <= tmpcan)?Count : tmpcan);
	//	//		tmpcan -= Count;
	//	//	}
	//	//}
	//}
  	//else 
	//{
	//	if (XBuf && (inited&4)) 
	//	{
	//		BlitScreen(XBuf); blitDone = 1;
	//	}
	//}
	if ( !blitDone )
	{
		if (XBuf && (inited&4)) 
		{
			BlitScreen(XBuf); blitDone = 1;
		}
	}
	//FCEUD_UpdateInput();
}

static void DoFun(int frameskip, int periodic_saves)
{
	uint8 *gfx = 0;
	int32 *sound = 0;
	int32 ssize = 0;
	static int fskipc = 0;
	//static int opause = 0;

	// If TAS editor is engaged, check whether a seek frame is set.
	// If a seek is in progress, don't emulate past target frame.
	if ( tasWindowIsOpen() )
	{
		int runToFrameTarget;
	
		runToFrameTarget = PLAYBACK::getPauseFrame();

		if ( runToFrameTarget >= 0)
		{
			if ( currFrameCounter >= runToFrameTarget )
			{
				FCEUI_SetEmulationPaused(EMULATIONPAUSED_PAUSED);
				return;
			}
		}
	}
    //TODO peroidic saves, working on it right now
    if (periodic_saves && FCEUD_GetTime() % PERIODIC_SAVE_INTERVAL < 30){
        FCEUI_SaveState(NULL, false);
    }
#ifdef FRAMESKIP
	fskipc = (fskipc + 1) % (frameskip + 1);
#endif

	if (NoWaiting || turbo) 
	{
		gfx = 0;
	}
	FCEUI_Emulate(&gfx, &sound, &ssize, fskipc);
	FCEUD_Update(gfx, sound, ssize);

	//if(opause!=FCEUI_EmulationPaused()) 
	//{
	//	opause=FCEUI_EmulationPaused();
	//	SilenceSound(opause);
	//}
	
	emulatorCycleCount++;
}

static std::string lockFile;
static bool debugMutexLock = false;

void fceuWrapperLock(const char *filename, int line, const char *func)
{
	fceuWrapperLock();

	if ( debugMutexLock )
	{
		char txt[32];

		if ( mutexLocks > 1 )
		{
			printf("Recursive Lock:%i\n", mutexLocks );
			printf("Already Locked By: %s\n", lockFile.c_str() );
			printf("Requested By: %s:%i - %s\n", filename, line, func );
		}
		snprintf( txt, sizeof(txt), ":%i - ", line );
		lockFile.assign(filename);
		lockFile.append(txt);
		lockFile.append(func);
	}
}

void fceuWrapperLock(void)
{
	mutexPending++;
	if ( consoleWindow != NULL )
	{
		consoleWindow->emulatorMutex.lock();
	}
	mutexPending--;
	mutexLocks++;
}

bool fceuWrapperTryLock(const char *filename, int line, const char *func, int timeout)
{
	bool lockAcq = false;

	lockAcq = fceuWrapperTryLock( timeout );

	if ( lockAcq && debugMutexLock)
	{
		char txt[32];
		snprintf( txt, sizeof(txt), ":%i - ", line );
		lockFile.assign(filename);
		lockFile.append(txt);
		lockFile.append(func);
	}
	return lockAcq;
}

bool fceuWrapperTryLock(int timeout)
{
	bool lockAcq = false;

	mutexPending++;
	if ( consoleWindow != NULL )
	{
		lockAcq = consoleWindow->emulatorMutex.tryLock( timeout );
	}
	mutexPending--;

	if ( lockAcq )
	{
		mutexLocks++;
	}
	return lockAcq;
}

void fceuWrapperUnLock(void)
{
	if ( mutexLocks > 0 )
	{
		mutexLocks--;
		if ( consoleWindow != NULL )
		{
			consoleWindow->emulatorMutex.unlock();
		}
	}
	else
	{
		printf("Error: Mutex is Already UnLocked\n");
		//abort(); // Uncomment to catch a stack trace
	}
}

bool fceuWrapperIsLocked(void)
{
	return mutexLocks > 0;
}

int  fceuWrapperUpdate( void )
{
	bool lock_acq;
	static bool mutexLockFail = false;

	// If a request is pending, 
	// sleep to allow request to be serviced.
	if ( mutexPending > 0 )
	{
		msleep( 16 );
	}

	lock_acq = fceuWrapperTryLock( __FILE__, __LINE__, __func__ );

	if ( !lock_acq )
	{
		if ( GameInfo )
		{
			if ( !mutexLockFail )
			{
				printf("Warning: Emulator Thread Failed to Acquire Mutex - GUI has Lock\n");
			}
			mutexLockFail = true;
		}
		msleep( 16 );

		return -1;
	}
	mutexLockFail = false;
	emulatorHasMutex = 1;

	// For netplay, set pause if we do not have input ready for all players
	if (NetPlayActive())
	{
		if (NetPlayFrameWait())
		{
			FCEUI_SetNetPlayPause(true);
		}
		else
		{
			FCEUI_SetNetPlayPause(false);
		}
	}
	else
	{
		FCEUI_SetNetPlayPause(false);
	}
 
	if ( GameInfo )
	{
#ifdef __FCEU_QSCRIPT_ENABLE__
		auto* qscriptMgr = QtScriptManager::getInstance();

		bool scriptsLoaded = (qscriptMgr != nullptr) && (qscriptMgr->numScriptsLoaded() > 0);

		if (scriptsLoaded)
		{
			qscriptMgr->frameBeginUpdate();
		}
#endif

		DoFun(frameskip, periodic_saves);
	
#ifdef __FCEU_QSCRIPT_ENABLE__
		if (scriptsLoaded)
		{
			qscriptMgr->frameFinishedUpdate();
		}
#endif

		hexEditorUpdateMemoryValues();

		fceuWrapperUnLock();

		emulatorHasMutex = 0;

		if ( consoleWindow )
		{
			consoleWindow->emulatorThread->signalFrameFinished();
		}

#ifdef __FCEU_PROFILER_ENABLE__
		FCEU_profiler_log_thread_activity();
#endif
		while ( SpeedThrottle() )
		{
			// Input device processing is in main thread
			// because to MAC OS X SDL2 requires it.
			//FCEUD_UpdateInput(); 
		}
	}
	else
	{
		fceuWrapperUnLock();

		emulatorHasMutex = 0;

#ifdef __FCEU_PROFILER_ENABLE__
		FCEU_profiler_log_thread_activity();
#endif
		msleep( 100 );
	}
	return 0;
}

static int minizip_ScanArchive( const char *filepath, ArchiveScanRecord &rec)
{
	int idx=0, ret;
	unzFile zf;
	unz_file_info fi;
	char filename[512];

	zf = unzOpen( filepath );

	if ( zf == NULL )
	{
		//printf("Error: Failed to open Zip File: '%s'\n", fname.c_str() );
		return -1;
	}
	rec.type = 0;

	ret = unzGoToFirstFile( zf );

	//printf("unzGoToFirstFile: %i \n", ret );

	while ( ret == 0 )
	{
		FCEUARCHIVEFILEINFO_ITEM item;

		unzGetCurrentFileInfo( zf, &fi, filename, sizeof(filename), NULL, 0, NULL, 0 );

		//printf("Filename: %u '%s' \n", fi.uncompressed_size, filename );

		item.name.assign( filename );
		item.size  = fi.uncompressed_size;
		item.index = idx; idx++;

		rec.files.push_back( item );

		ret = unzGoToNextFile( zf );

		//printf("unzGoToNextFile: %i \n", ret );
	}
	rec.numFilesInArchive = idx;

	unzClose( zf );

	return 0;
}

#ifdef _USE_LIBARCHIVE
#include <archive.h>
#include <archive_entry.h>

static int libarchive_ScanArchive( const char *filepath, ArchiveScanRecord &rec)
{
	int r, idx=0;
	struct archive *a;
	struct archive_entry *entry;

	a = archive_read_new();

	if (a == nullptr)
	{
		return -1;
	}

	// Initialize decoders
	r = archive_read_support_filter_all(a);
	if (r)
	{
		archive_read_free(a);
		return -1;
	}

	// Initialize formats
	r = archive_read_support_format_all(a);
	if (r)
	{
		archive_read_free(a);
		return -1;
	}

	r = archive_read_open_filename(a, filepath, 10240);

	if (r)
	{
		archive_read_free(a);
		return -1;
	}
	rec.type = 1;

	while (1)
	{
		r = archive_read_next_header(a, &entry);
		if (r == ARCHIVE_EOF)
		{
			break;
		}
		else if (r != ARCHIVE_OK)
		{
			printf("archive_read_next_header() %s\n", archive_error_string(a));
			break;
		}
		const char *filename = archive_entry_pathname(entry);

		FCEUARCHIVEFILEINFO_ITEM item;
		item.name.assign( filename );
		item.size  = archive_entry_size(entry);
		item.index = idx; idx++;

		rec.files.push_back( item );
	}
	rec.numFilesInArchive = idx;

	archive_read_free(a);

	return 0;
}
#endif

ArchiveScanRecord FCEUD_ScanArchive(std::string fname)
{
	int ret = -1;
	ArchiveScanRecord rec;
		
#ifdef _USE_LIBARCHIVE
	ret = libarchive_ScanArchive( fname.c_str(), rec );
#endif

	if (ret == -1)
	{
		minizip_ScanArchive( fname.c_str(), rec );
	}
	return rec;
}

static FCEUFILE* minizip_OpenArchive(ArchiveScanRecord& asr, std::string &fname, std::string *searchFile, int innerIndex )
{
	int ret, idx=0;
	FCEUFILE* fp = nullptr;
	void *tmpMem = nullptr;
	unzFile zf;
	unz_file_info fi;
	char filename[512];
	bool foundFile = false;

	zf = unzOpen( fname.c_str() );

	if ( zf == NULL )
	{
		//printf("Error: Failed to open Zip File: '%s'\n", fname.c_str() );
		return fp;
	}

	//printf("Searching for %s in %s \n", searchFile.c_str(), fname.c_str() );

	ret = unzGoToFirstFile( zf );

	//printf("unzGoToFirstFile: %i \n", ret );

	while ( ret == 0 )
	{
		unzGetCurrentFileInfo( zf, &fi, filename, sizeof(filename), NULL, 0, NULL, 0 );

		//printf("Filename: %u '%s' \n", fi.uncompressed_size, filename );

		if ( (searchFile != nullptr) && !searchFile->empty())
		{
			if ( strcmp( searchFile->c_str(), filename ) == 0 )
			{
			   //printf("Found Filename: %u '%s' \n", fi.uncompressed_size, filename );
				foundFile = true; break;
			}
		}
		else if ((innerIndex != -1) && (idx == innerIndex))
		{
			foundFile = true; break;
		}

		ret = unzGoToNextFile( zf );

		//printf("unzGoToNextFile: %i \n", ret );
		idx++;
	}

	if ( !foundFile )
	{
		unzClose( zf );
		return fp;
	}

	tmpMem = ::malloc( fi.uncompressed_size );

	if ( tmpMem == NULL )
	{
		unzClose( zf );
		return fp;
	}
	//printf("Loading via minizip\n");

	EMUFILE_MEMORY* ms = new EMUFILE_MEMORY(fi.uncompressed_size);

	unzOpenCurrentFile( zf );
	unzReadCurrentFile( zf, tmpMem, fi.uncompressed_size );
	unzCloseCurrentFile( zf );

	ms->fwrite( tmpMem, fi.uncompressed_size );

	free( tmpMem );

	//if we extracted the file correctly
	fp = new FCEUFILE();
	fp->archiveFilename = fname;
	fp->filename = filename;
	fp->fullFilename = fp->archiveFilename + "|" + fp->filename;
	fp->archiveIndex = idx;
	fp->mode = FCEUFILE::READ;
	fp->size = fi.uncompressed_size;
	fp->stream = ms;
	fp->archiveCount = (int)asr.numFilesInArchive;
	ms->fseek(0,SEEK_SET); //rewind so that the rom analyzer sees a freshly opened file

	unzClose( zf );

	return fp;
}

#ifdef _USE_LIBARCHIVE
static FCEUFILE* libarchive_OpenArchive( ArchiveScanRecord& asr, std::string& fname, std::string *searchFile, int innerIndex)
{
	int r, idx=0;
	struct archive *a;
	struct archive_entry *entry;
	const char *filename = nullptr;
	bool foundFile = false;
	int fileSize = 0;
	FCEUFILE* fp = nullptr;

	a = archive_read_new();

	if (a == nullptr)
	{
		archive_read_free(a);
		return nullptr;
	}

	// Initialize decoders
	r = archive_read_support_filter_all(a);
	if (r)
	{
		archive_read_free(a);
		return nullptr;
	}

	// Initialize formats
	r = archive_read_support_format_all(a);
	if (r)
	{
		archive_read_free(a);
		return nullptr;
	}

	r = archive_read_open_filename(a, fname.c_str(), 10240);

	if (r)
	{
		archive_read_free(a);
		return nullptr;
	}

	while (1)
	{
		r = archive_read_next_header(a, &entry);
		if (r == ARCHIVE_EOF)
		{
			break;
		}
		else if (r != ARCHIVE_OK)
		{
			printf("archive_read_next_header() %s\n", archive_error_string(a));
			break;
		}
		filename = archive_entry_pathname(entry);
		fileSize = archive_entry_size(entry);

		//printf("ArchiveFile:%i  %s\n", idx, filename);
		if ( (searchFile != nullptr) && !searchFile->empty())
		{
			if (strcmp( filename, searchFile->c_str() ) == 0)
			{
				foundFile = true; break;
			}
		}
		else if ((innerIndex != -1) && (idx == innerIndex))
		{
			foundFile = true; break;
		}
		idx++;
	}

	if (foundFile && (fileSize > 0))
	{
		const void *buff;
		size_t size, totalSize = 0;
		#if ARCHIVE_VERSION_NUMBER >= 3000000
			int64_t offset;
		#else
			off_t offset;
		#endif

		//printf("Loading via libarchive\n");

		EMUFILE_MEMORY* ms = new EMUFILE_MEMORY(fileSize);

		while (1)
		{
			r = archive_read_data_block(a, &buff, &size, &offset);

			if (r == ARCHIVE_EOF)
			{
				break;
			}
			if (r != ARCHIVE_OK)
			{
				break;
			}
			//printf("Read: %p   Size:%zu   Offset:%llu\n", buff, size, (long long int)offset);
			ms->fwrite( buff, size );
			totalSize += size;
		}

		//if we extracted the file correctly
		fp = new FCEUFILE();
		fp->archiveFilename = fname;
		fp->filename = filename;
		fp->fullFilename = fp->archiveFilename + "|" + fp->filename;
		fp->archiveIndex = idx;
		fp->mode = FCEUFILE::READ;
		fp->size = totalSize;
		fp->stream = ms;
		fp->archiveCount = (int)asr.numFilesInArchive;
		ms->fseek(0,SEEK_SET); //rewind so that the rom analyzer sees a freshly opened file
	}

	archive_read_free(a);

	return fp;
}

#endif

void fceuWrapperSetArchiveFileLoadIndex(int idx)
{
	archiveFileLoadIndex = idx;
}

void fceuWrapperClearArchiveFileLoadIndex()
{
	archiveFileLoadIndex = -1;
}

FCEUFILE* FCEUD_OpenArchive(ArchiveScanRecord& asr, std::string& fname, std::string* innerFilename, int* userCancel)
{
	FCEUFILE* fp = nullptr;
	std::string searchFile;

	if ( innerFilename != NULL )
	{
		searchFile = *innerFilename;
	}
	else
	{
		std::vector <std::string> fileList;

		for (size_t i=0; i<asr.files.size(); i++)
		{
			char base[512], suffix[128];

			//printf("File:%zi   %s\n", i, asr.files[i].name.c_str() );
			getFileBaseName( asr.files[i].name.c_str(), base, suffix );

			if ( (strcasecmp( suffix, ".nes" ) == 0) ||
			     (strcasecmp( suffix, ".nsf" ) == 0) ||
			     (strcasecmp( suffix, ".fds" ) == 0) ||
			     (strcasecmp( suffix, ".unf" ) == 0) ||
			     (strcasecmp( suffix, ".unif") == 0) )
			{
				fileList.push_back( asr.files[i].name );
			}
		}

		if ( fileList.size() > 1 )
		{
			if ( (archiveFileLoadIndex >= 0) && (static_cast<size_t>(archiveFileLoadIndex) < asr.files.size()))
			{
				searchFile.clear();
			}
			else if ( consoleWindow != NULL )
			{
				int sel = consoleWindow->showListSelectDialog( "Select ROM From Archive", fileList );

				if ( sel < 0 )
				{
					if ( userCancel )
					{
						*userCancel = 1;
					}
					return fp;
				}
				searchFile = fileList[sel];
			}
		}
		else if ( fileList.size() > 0 )
		{
			searchFile = fileList[0];
		}
		//printf("Archive Search File: %s\n", searchFile.c_str());
	}

#ifdef _USE_LIBARCHIVE
	fp = libarchive_OpenArchive(asr, fname, &searchFile, archiveFileLoadIndex );
#endif

	if (fp == nullptr)
	{
		fp = minizip_OpenArchive(asr, fname, &searchFile, archiveFileLoadIndex );
	}
	//printf("Archive File Index: %i\n", fp->archiveIndex);
	return fp;
}

FCEUFILE* FCEUD_OpenArchive(ArchiveScanRecord& asr, std::string& fname, std::string* innerFilename)
{
	int userCancel = 0;

	return FCEUD_OpenArchive( asr, fname, innerFilename, &userCancel );
}

FCEUFILE* FCEUD_OpenArchiveIndex(ArchiveScanRecord& asr, std::string &fname, int innerIndex, int* userCancel)
{
	FCEUFILE* fp = nullptr;

#ifdef _USE_LIBARCHIVE
	fp = libarchive_OpenArchive( asr, fname, nullptr, innerIndex );
#endif
	if (fp == nullptr)
	{
		fp = minizip_OpenArchive(asr, fname, nullptr, innerIndex);
	}

	return fp;
}

FCEUFILE* FCEUD_OpenArchiveIndex(ArchiveScanRecord& asr, std::string &fname, int innerIndex)
{
	int userCancel = 0;

	return FCEUD_OpenArchiveIndex( asr, fname, innerIndex, &userCancel );
}

// dummy functions

#define DUMMY(__f) \
    void __f(void) {\
        printf("%s\n", #__f);\
        FCEU_DispMessage("Not implemented.",0);\
    }
DUMMY(FCEUD_HideMenuToggle)
DUMMY(FCEUD_MovieReplayFrom)
void FCEUI_UseInputPreset(int preset) { }
bool FCEUD_PauseAfterPlayback() { return pauseAfterPlayback; }

int FCEUD_ShowStatusIcon(void)
{
	return showStatusIconOpt;
}
void FCEUD_ToggleStatusIcon(void)
{
	showStatusIconOpt = !showStatusIconOpt;
}

bool FCEUD_ShouldDrawInputAids(void)
{
	return drawInputAidsEnable;
}

void FCEUD_TurboOn (void) { turbo = true; };
void FCEUD_TurboOff   (void) { turbo = false; };
void FCEUD_TurboToggle(void) { turbo = !turbo; };

