/*
 *
 *  Singe 2
 *  Copyright (C) 2006-2020 Scott Duensing <scott@kangaroopunch.com>
 *
 * 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.
 *
 */


// -c -x 720 -y 480 -d data/maddog_dvd -v maddog_dvd/frame_maddog_dvd.txt maddog_dvd/maddog_dvd.singe
// -c -x 640 -y 480 -v ActionMax/frame_SonicFury.txt ActionMax/SonicFury.singe
// -x 640 -y 480 -d data/ActionMax ActionMax/BlueThunder.singe
// -d data/ActionMax -v ActionMax/BlueThunder.mp4 test.singe


#include <stdio.h>

#include <SDL2/SDL.h>
#include <SDL2/SDL_image.h>
#include <SDL2/SDL_mixer.h>
#include <SDL2/SDL_ttf.h>
#include <carg_parser.h>
#include <manymouse.h>

#include "stddclmr.h"
#include "common.h"
#include "util.h"
#include "frameFile.h"
#include "videoPlayer.h"
#include "singe.h"
#include "extensions.h"
#include "embedded.h"


typedef struct RatioS {
	int32_t aspectNum;
	int32_t aspectDom;
} RatioT;

typedef struct ResolutionS {
	int32_t width;
	int32_t height;
} ResolutionT;

typedef struct ModeS {
	RatioT      ratio;
	ResolutionT resolution;
} ModeT;


bool extractFile(char *filename, unsigned char *data, int32_t length);
void showUsage(char *name, char *message);


bool extractFile(char *filename, unsigned char *data, int32_t length) {
	FILE *out;

	if (!utilFileExists(filename)) {
		out = fopen(filename, "wb");
		if (!out) utilDie("Unable to create %s", filename);
		fwrite(data, length, 1, out);
		fclose(out);
		utilSay("Created File:  %s", filename);
		return true;
	}

	return false;
}


__attribute__((noreturn))
void showUsage(char *name, char *message) {
	char    *temp    = NULL;
	bool     created = false;
	int32_t  result  = 0;

	utilRedirectConsole();

	//       00000000011111111112222222222333333333344444444445555555555666666666677777777778
	//       12345678901234567890123456789012345678901234567890123456789012345678901234567890
	utilSay(" ___ ___ _  _  ___ ___");
	utilSay("/ __|_ _| \\| |/ __| __|   Somewhat Interactive Nostalgic Game Engine %s", VERSION_STRING);
	utilSay("\\__ \\| || .` | (_ | _|    Copyright (c) 2006-%s Scott C. Duensing", COPYRIGHT_END_YEAR);
	utilSay("|___/___|_|\\_|\\___|___|   https://kangaroopunch.com");
	utilSay("");
	utilSay("Usage:  %s [OPTIONS] scriptName{.singe}", utilGetLastPathComponent(name));
	utilSay("");
	utilSay("  -a, --aspect=N:D               force aspect ratio");
	utilSay("  -c, --showcalculated           show calculated framefile values for debugging");
	utilSay("  -d, --datadir=PATHNAME         alternate location for written files");
	utilSay("  -e, --volume_nonvldp=PERCENT   specify sound effects volume in percent");
	utilSay("  -f, --fullscreen               run in full screen mode");
	utilSay("  -g, --sindengun=PARAMS         enable Sinden Light Gun support");
	utilSay("  -h, --help                     this display");
	utilSay("  -k, --nologos                  kill the splash screens");
	utilSay("  -l, --volume_vldp=PERCENT      specify laserdisc volume in percent");
	utilSay("  -m, --nomouse                  disable mouse");
	utilSay("  -o, --scalefactor=PERCENT      reduce screen size for overscan compensation");
	utilSay("  -s, --nosound, --mutesound     mutes all sound");
	utilSay("  -t, --trace                    trace script execution to screen and file");
	utilSay("  -u, --stretch                  use ugly stretched video");
	utilSay("  -v, --framefile=FILENAME       use an alternate video file");
	utilSay("  -w, --fullscreen_window        run in windowed full screen mode");
	utilSay("  -x, --xresolution=VALUE        specify horizontal resolution");
	utilSay("  -y, --yresolution=VALUE        specify vertical resolution");
	utilSay("  -z, --noconsole                zero console output");
	utilSay("");

	// Extract any missing support files.  We do this here so they're not generated if launched from a front end.
	if (!utilPathExists("Singe")) utilMkDirP("Singe", 0777);
	// Singe/Framework.singe
	temp = utilCreateString("Singe%cFramework.singe", utilGetPathSeparator());
	created |= extractFile(temp, Framework_singe, Framework_singe_len);
	free(temp);
	// Singe/controls.cfg.example
	temp = utilCreateString("Singe%ccontrols.cfg.example", utilGetPathSeparator());
	created |= extractFile(temp, controls_cfg, controls_cfg_len);
	free(temp);
	temp = NULL;

	if (created) utilSay("");

	if (message) {
		utilSay("Error:  %s", message);
		utilSay("");
		result = 1;
	}

#ifdef _WIN32
	getchar();
#endif
	exit(result);
}


int main(int argc, char *argv[]) {

	int32_t                     x              =  0;
	int32_t                     err            =  0;
	int32_t                     flags          =  0;
	int32_t                     bestResIndex   = -1;
	int32_t                     bestRatioIndex = -1;
	int32_t                     aspectNum      = -1;
	int32_t                     aspectDom      = -1;
	int32_t                     argCount       =  0;
	int32_t                     argIndex       =  0;
	int32_t                     code           =  0;
	char                       *exeName        =  NULL;
	char                       *temp           =  NULL;
	char                       *aspectString   =  NULL;
	char                       *sindenString   =  NULL;
	float                       thisRatio      =  0;
	float                       bestRatio      =  9999;
	bool                        tracing        =  false;
	SDL_Window                 *window         =  NULL;
	SDL_Renderer               *renderer       =  NULL;
	SDL_Surface                *icon           =  NULL;
	SDL_DisplayMode             mode;
	const char                 *arg            =  NULL;
	const char                **cargv          =  (const char **)argv;
	struct Arg_parser           parser;
	static struct ap_Option     options[]      = {
		{ 'a', "aspect",            ap_yes },
		{ 'c', "showcalculated",    ap_no  },
		{ 'd', "datadir",           ap_yes },
		{ 'e', "volume_nonlvdp",    ap_yes },
		{ 'f', "fullscreen",        ap_no  },
		{ 'g', "sindengun",         ap_yes },
		{ 'h', "help",              ap_no  },
		{ 'k', "nologos",           ap_no  },
		{ 'l', "volume_vldp",       ap_yes },
		{ 'm', "nomouse",           ap_no  },
		{ 'o', "scalefactor",       ap_yes },
		{ 's', "nosound",           ap_no  },
		{ 't', "trace",             ap_no  },
		{ 'u', "stretch",           ap_no  },
		{ 'v', "framefile",         ap_yes },
		{ 'w', "fullscreenwindow",  ap_no  },
		{ 'x', "xresolution",       ap_yes },
		{ 'y', "yresolution",       ap_yes },
		{ 'z', "noconsole",         ap_no  },
		{   0, 0,                   ap_no  }
	};
	static ModeT                modes[]        = {
		{ {  4,  3 }, {  640,  480 } },
		{ {  4,  3 }, {  800,  600 } },
		{ {  4,  3 }, {  960,  720 } },
		{ {  4,  3 }, { 1024,  768 } },
		{ {  4,  3 }, { 1280,  960 } },
		{ {  4,  3 }, { 1400, 1050 } },
		{ {  4,  3 }, { 1440, 1080 } },
		{ {  4,  3 }, { 1600, 1200 } },
		{ {  4,  3 }, { 1856, 1392 } },
		{ {  4,  3 }, { 1920, 1440 } },
		{ {  4,  3 }, { 2048, 1536 } },
		{ { 16,  9 }, { 1024,  576 } },
		{ { 16,  9 }, { 1152,  648 } },
		{ { 16,  9 }, { 1280,  720 } },
		{ { 16,  9 }, { 1366,  768 } },
		{ { 16,  9 }, { 1600,  900 } },
		{ { 16,  9 }, { 1920, 1080 } },
		{ { 16,  9 }, { 2560, 1440 } },
		{ { 16,  9 }, { 3840, 2160 } },
		{ { 16,  9 }, { 7680, 4320 } },
		{ { 16, 10 }, { 1280,  800 } },
		{ { 16, 10 }, { 1440,  900 } },
		{ { 16, 10 }, { 1680, 1050 } },
		{ { 16, 10 }, { 1920, 1200 } },
		{ { 16, 10 }, { 2560, 1600 } },
		{ {  0,  0 }, {    0,    0 } }
	};

	exeName = (char *)argv[0];

	if (!ap_init(&parser, argc, cargv, options, 0)) {
		utilDie("Out of memory parsing arguments.");
	}
	if (ap_error(&parser)) {
		utilDie("%s", ap_error(&parser));
	}

	// Parse command line
	argCount = ap_arguments(&parser);
	for (argIndex=0; argIndex < ap_arguments(&parser); ++argIndex) {

		code = ap_code(&parser, argIndex);
		if (!code) break;
		arg = ap_argument(&parser, argIndex);

		// Handle options
		switch (code) {

			// Aspect
			case 'a':
				if (aspectString) free(aspectString);
				aspectString = strdup(arg);
				argCount++;
				break;

			// Show Calculated Frame File Values
			case 'c':
				_confShowCalculated = true;
				break;

			// Data Dir
			case 'd':
				if (_confDataDir) free(_confDataDir);
				_confDataDir = strdup(arg);
				argCount++;
				break;

			// Effects Volume
			case 'e':
				_confVolumeNonVldp = atoi(arg);
				argCount++;
				break;

			// Full Screen
			case 'f':
				_confFullScreen = true;
				break;

			// Sinden Light Gun
			case 'g':
				if (sindenString) free(sindenString);
				sindenString = strdup(arg);
				argCount++;
				break;

			// Help
			case 'h':
				showUsage(exeName, NULL);
				break;

			// No Logos
			case 'k':
				_confNoLogos = true;
				break;

			// Video Volume
			case 'l':
				_confVolumeVldp = atoi(arg);
				argCount++;
				break;

			// No Mouse
			case 'm':
				_confNoMouse = true;
				break;

			// Overscan Zoom
			case 'o':
				_confScaleFactor = atoi(arg);
				argCount++;
				break;

			// No Sound
			case 's':
				_confNoSound = true;
				break;

			// Trace
			case 't':
				tracing = true;
				break;

			// Ugly Stretched Video
			case 'u':
				_confStretchVideo = true;
				break;

			// Video File
			case 'v':
				if (_confVideoFile) free(_confVideoFile);
				_confVideoFile = strdup(arg);
				argCount++;
				break;

			// Full Screen Windowed
			case 'w':
				_confFullScreenWindow = true;
				break;

			// X Resolution
			case 'x':
				_confXResolution = atoi(arg);
				argCount++;
				break;

			// Y Resolution
			case 'y':
				_confYResolution = atoi(arg);
				argCount++;
				break;

			// No console output or splash screens
			case 'z':
				_confNoConsole = true;
				break;

			default:
				abort();  // Something bad happened
				break;
		}
	}

	// For that dumb OS
	if (!_confNoConsole) utilRedirectConsole();

	// Did we get a filename or path to open?
	if ((argc - argCount) != 1) showUsage(exeName, "No script file specified.");
	_confScriptFile = strdup(argv[argCount]);
	utilFixPathSeparators(&_confScriptFile, false);
	// Exists?
	if (!utilFileExists(_confScriptFile)) {
		// Missing.  Is a path?
		if (utilPathExists(_confScriptFile)) {
			// See if the script exists in the path.
			temp = utilCreateString("%s%c%s.singe", _confScriptFile, utilGetPathSeparator(), utilGetLastPathComponent(_confScriptFile));
			if (utilFileExists(temp)) {
				// Found script named for path inside path.
				free(_confScriptFile);
				_confScriptFile = temp;
				temp = NULL;
			} else {
				// Not in the path either.
				free(_confScriptFile);
				_confScriptFile = NULL;
			}
		} else {
			// Not a path either.
			free(_confScriptFile);
			_confScriptFile = NULL;
		}
	}
	if (!_confScriptFile) showUsage(exeName, "Unable to locate script.");

	// Do we need to generate a video name?
	if (_confVideoFile) {
		utilFixPathSeparators(&_confVideoFile, false);
		if (!utilFileExists(_confVideoFile)) {
			free(_confVideoFile);
			_confVideoFile = NULL;
		}
	} else {
		x = (int32_t)(strlen(_confScriptFile) - strlen(utilGetFileExtension(_confScriptFile))) - 1;
		if (x < 0) {
			x = 0;
		}
		temp = strdup(_confScriptFile);
		temp[x] = 0;
		// Check all known extensions - lower case only, Windows users!
		x = 0;
		while (ffmpegExtensions[x]) {
			_confVideoFile = utilCreateString("%s.%s", temp, ffmpegExtensions[x]);
			if (utilFileExists(_confVideoFile)) {
				break;
			}
			free(_confVideoFile);
			_confVideoFile = NULL;
			x++;
		}
		free(temp);
		// If we still don't have one, try a framefile
		if (!_confVideoFile) {
			_confVideoFile = utilCreateString("%s.txt", temp);
			if (!utilFileExists(_confVideoFile)) {
				free(_confVideoFile);
				_confVideoFile = NULL;
			}
		}
	}
	if (!_confVideoFile) showUsage(exeName, "Unable to locate video.");
	// Is it a framefile?
	if (strncmp(utilGetFileExtension(_confVideoFile), "txt", 3) == 0) {
		_confIsFrameFile = true;
	}

	// Do we need to generate a data directory name?
	if (_confDataDir) {
		utilFixPathSeparators(&_confDataDir, false);
		// Try to create data directory to ensure it exists.
		utilMkDirP(_confDataDir, 0777);
		// Does it exist?
		if (!utilPathExists(_confDataDir)) {
			free(_confDataDir);
			_confDataDir = NULL;
		}
	} else {
		// Put it in the game folder.
		x = (int32_t)(strlen(_confScriptFile) - strlen(utilGetLastPathComponent(_confScriptFile))) - 1;
		if (x < 0) {
			x = 0;
		}
		temp = strdup(_confScriptFile);
		temp[x] = 0;
		_confDataDir = strdup(temp);
		free(temp);
		temp = NULL;
	}
	if (!_confDataDir) showUsage(exeName, "Unable to locate data directory.");
	utilFixPathSeparators(&_confDataDir, true);

	// Do they want tracing?
	if (tracing) {
		temp = utilCreateString("%strace.txt", _confDataDir);
		utilTraceStart(temp);
		free(temp);
		temp = NULL;
	}

	// Do the full screen options make sense?
	if (_confFullScreen && _confFullScreenWindow) showUsage(exeName, "Full Screen or Full Screen Windowed.  Pick one.");

	// Sane volume values?
	if ((_confVolumeVldp < 0) || (_confVolumeVldp > 100)) showUsage(exeName, "Laserdisc volume must be between 0 and 100 percent.");
	if ((_confVolumeNonVldp < 0) || (_confVolumeNonVldp > 100)) showUsage(exeName, "Effects volume must be between 0 and 100 percent.");

	// Sane scale factor?
	if ((_confScaleFactor < 50) || (_confScaleFactor > 100)) showUsage(exeName, "Display scale must be between 50 and 100 percent.");

	// Sinden light gun?
	if (sindenString) {
		if (_confScaleFactor != 100) showUsage(exeName, "Cannot use --sindengun and --scalefactor together.");
		// Was it wrapped in quotes?
		if ((sindenString[0] == '"') || (sindenString[0] == '\'')) {
			sindenString[0] = ' ';
		}
		// Ok, this thing can have a mess of different arguments:
		//   WW - Just the width of the white border
		//   WW WB - Width of white border and then black border
		//   RW GW BW WW - Custom color "white" border and width
		//   RW GW BW WW WB - Custom color "white" border and width then width of black border
		//   RW GW BW WW RB GB BB WB - Custom color "white" border and width then custom color "black" border and width
		temp = strtok(sindenString, " ");
		while (temp != NULL) {
			_confSindenArgv[_confSindenArgc++] = atoi(temp);
			temp = strtok(NULL, " ");
			if ((temp != NULL) && (_confSindenArgc > SINDEN_OPTION_COUNT)) showUsage(exeName, "Too many arguments to --sindengun.");
		}
		// Did we get a sane number of arguments?
		if ((_confSindenArgc != SINDEN_WHITE) && (_confSindenArgc != SINDEN_WHITE_BLACK) && (_confSindenArgc != SINDEN_CUSTOM_WHITE) && (_confSindenArgc != SINDEN_CUSTOM_WHITE_BLACK) && (_confSindenArgc != SINDEN_CUSTOM_WHITE_CUSTOM_BLACK)) showUsage(exeName, "Bad argument count to --sindengun.");
		free(sindenString);
	}

	// Init SDL
	if (SDL_Init(SDL_INIT_EVERYTHING) != 0) utilDie("%s", SDL_GetError());
	if (SDL_GetCurrentDisplayMode(0, &mode) < 0) utilDie("%s", SDL_GetError());

	// Determine resolution if not specified
	if ((_confXResolution <= 0) || (_confYResolution <= 0)) {
		// Did they specify an aspect ratio?
		if (aspectString) {
			aspectNum = atoi(aspectString);
			temp = strstr(aspectString, ":");
			if (temp) {
				temp++;
				aspectDom = atoi(temp);
				temp = NULL;
			}
			if ((aspectNum > 0) && (aspectDom > 0)) {
				// Do we understand what they asked for?
				x = 0;
				while (modes[x].ratio.aspectNum != 0) {
					if ((modes[x].ratio.aspectNum == aspectNum) && (modes[x].ratio.aspectDom == aspectDom)) {
						bestRatioIndex = x;
						break;
					}
					x++;
				}
			}
			free(aspectString);
		} else {
			// Find our current aspect ratio
			x = 0;
			while (modes[x].ratio.aspectNum != 0) {
				thisRatio = fabsf(((float)modes[x].ratio.aspectNum / (float)modes[x].ratio.aspectDom) - ((float)mode.w / (float)mode.h));
				if (thisRatio < bestRatio) {
					bestRatio = thisRatio;
					bestRatioIndex = x;
				}
				x++;
			}
		}
		if (bestRatioIndex < 0) showUsage(exeName, "Unknown aspect ratio.");
		x = 0;
		// Were both resolutions not specified?
		if ((_confXResolution <= 0) && (_confYResolution <= 0)) {
			// Are we full screen?
			if (_confFullScreen || _confFullScreenWindow) {
				// Use desktop resolution
				_confXResolution = mode.w;
				_confYResolution = mode.h;
			} else {
				// Find largest window that will fit on the screen but not fill it
				while (modes[x].ratio.aspectNum != 0) {
					if ((modes[x].ratio.aspectNum == modes[bestRatioIndex].ratio.aspectNum) && (modes[x].ratio.aspectDom == modes[bestRatioIndex].ratio.aspectDom)) {
						if ((modes[x].resolution.width < mode.w) && (modes[x].resolution.height < mode.h)) {
							bestResIndex = x;
						}
					}
					x++;
				}
				_confXResolution = modes[bestResIndex].resolution.width;
				_confYResolution = modes[bestResIndex].resolution.height;
			}
		} else {
			// Find unprovided width/height using provided value
			while (modes[x].ratio.aspectNum != 0) {
				// Is this the aspect ratio we're using?
				if ((modes[bestRatioIndex].ratio.aspectNum == modes[x].ratio.aspectNum) && (modes[bestRatioIndex].ratio.aspectDom == modes[x].ratio.aspectDom)) {
					// Do we have the width or height?
					if (_confXResolution > 0) {
						// We have the width.  Is this the matching entry?
						if (modes[x].resolution.width == _confXResolution) {
							bestResIndex = x;
							_confYResolution = modes[x].resolution.height;
							break;
						}
					} else {
						// We have the height.  Is this the matching entry?
						if (modes[x].resolution.height == _confYResolution) {
							bestResIndex = x;
							_confXResolution = modes[x].resolution.width;
							break;
						}
					}
				}
				x++;
			}
		}
	}
	// Did we end up with a valid resolution?
	if (_confXResolution <= 0) showUsage(exeName, "Unable to determine X resolution.  (Is the Y value sane?)");
	if (_confYResolution <= 0) showUsage(exeName, "Unable to determine Y resolution.  (Is the X value sane?)");
	if ((_confXResolution > mode.w) || (_confYResolution > mode.h)) showUsage(exeName, "Specified resolution is larger than the display.");

	// Init SDL_mixer  ***FIX***
	flags = /* MIX_INIT_FLAC | */ MIX_INIT_MOD | /* MIX_INIT_MP3 | */ MIX_INIT_OGG | MIX_INIT_MID | MIX_INIT_OPUS;
	err = Mix_Init(flags);
	if (err != flags) utilDie("%s", Mix_GetError());

	// Init SDL_image  ***FIX***
	flags = IMG_INIT_JPG | IMG_INIT_PNG |  /* IMG_INIT_TIF | */ IMG_INIT_WEBP;
	err = IMG_Init(flags);
	if (err != flags) utilDie("%s", IMG_GetError());

	// Init SDL_ttf
	if (TTF_Init() < 0) utilDie("%s", TTF_GetError());

	// Create Resizable Window
	window = SDL_CreateWindow("SINGE", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, _confXResolution, _confYResolution, SDL_WINDOW_RESIZABLE);
	if (window == NULL) utilDie("%s", SDL_GetError());

	// Window Icon
	icon = IMG_LoadPNG_RW(SDL_RWFromMem(icon_png, icon_png_len));
	if (icon == NULL) utilDie("%s", SDL_GetError());
	SDL_SetWindowIcon(window, icon);
	SDL_FreeSurface(icon);
	icon = NULL;

	// Do we want full screen of some kind?
	if (_confFullScreen || _confFullScreenWindow) {
		flags = _confFullScreen ? SDL_WINDOW_FULLSCREEN : SDL_WINDOW_FULLSCREEN_DESKTOP;
		SDL_SetWindowFullscreen(window, (Uint32)flags);
	}

	// Create an accelerated renderer.
	renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
	if (renderer == NULL) utilDie("%s", SDL_GetError());

	// Clear screen with black
	SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
	SDL_RenderClear(renderer);

	// Create audio mixer device
	err = Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 44100 /* freq */ * 16  /* bits */ * 2 /* channels */ * 2 /* seconds */);
	if (err != 0) utilDie("%s", Mix_GetError());
	Mix_AllocateChannels(16);

	// Start our video playback system
	if (frameFileInit()) utilDie("Unable to initialize framefile handler.");
	if (videoInit()) utilDie("Unable to initialize video player.");

	// Finish our setup
	SDL_DisableScreenSaver();

	// Run Singe!
	singe(window, renderer);

	// Shutdown
	videoQuit();
	frameFileQuit();
	Mix_CloseAudio();
	SDL_DestroyRenderer(renderer);
	SDL_DestroyWindow(window);
	SDL_EnableScreenSaver();
	TTF_Quit();
	IMG_Quit();
	Mix_Quit();
	SDL_Quit();
	if (_confDataDir) free(_confDataDir);
	if (_confVideoFile) free(_confVideoFile);
	if (_confScriptFile) free(_confScriptFile);
	utilTraceEnd();
	ap_free(&parser);

#ifdef _WIN32
	getchar();
#endif

	return 0;
}
