/*
** main.cpp
**
** This file is part of mkxp.
**
** Copyright (C) 2013 Jonas Kulla <Nyocurio@gmail.com>
**
** mkxp 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.
**
** mkxp 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 mkxp.  If not, see <http://www.gnu.org/licenses/>.
*/

#include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h>
#include <SDL3_ttf/SDL_ttf.h>
#include <SDL3_mixer/SDL_mixer.h>
#include <physfs.h>

#ifdef _MSC_VER
#include <direct.h>
#define _chdir chdir
#else
#include <unistd.h>
#endif
#include <SDL3/SDL_stdinc.h>
#include <assert.h>
#include <string>
#include <iostream>
#include <fstream>
#include <cstdlib>
#include <filesystem>
#include <boost/chrono.hpp>

#include "sharedstate.h"
#include "eventthread.h"
#include "gl-debug.h"
#include "debugwriter.h"
#include "exception.h"
#include "gl-fun.h"
#include "i18n.h"
#include "security.h"

#include "sunshine.h"

#include "define.h"
#include "meow.h"

#include "binding.h"

#include "icon.png.xxd"
#include <SDL3/SDL_system.h>
#ifdef STEAM
	#include "steamshim/steamshim_child.h"
#else
	#include "gamecontrollerdb.txt.xxd"
#endif

#ifdef ps2
	#ifdef DEBUG
		SDL_PS2_SKIP_IOP_RESET();
	#endif
#endif

#ifndef VERSION_STRING
	#define VERSION_STRING ">w<"
#endif

static void rgssThreadError(RGSSThreadData *rtData, const std::string &msg){
	rtData->rgssErrorMsg = msg;
	rtData->ethread->requestTerminate();
	rtData->rqTermAck.set();
}

static inline const char* glGetStringInt(GLenum name){
	return (const char*) gl.GetString(name);
}

int rgssThreadFun(void *userdata){
	RGSSThreadData *threadData = static_cast<RGSSThreadData*>(userdata);
	const Config &conf = threadData->config;
	SDL_Window *win = threadData->window;
	static char msg[512];
	SDL_GLContext glCtx;

	/* Setup GL context */
	SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);

#ifndef NDEBUG
	SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_DEBUG_FLAG);
#endif

	glCtx = SDL_GL_CreateContext(win);

	if (!glCtx){
		rgssThreadError(threadData, std::string(msg));
		return 0;
	}

	try{
		initGLFunctions();
	}
	catch (const Exception &exc){
		rgssThreadError(threadData, exc.msg);
		SDL_GL_DestroyContext(glCtx);
		return 0;
	}

	if (!conf.enableBlitting)
		gl.BlitFramebuffer = 0;

	gl.ClearColor(0, 0, 0, 1);
	gl.Clear(GL_COLOR_BUFFER_BIT);
	SDL_GL_SwapWindow(win);

	Debug() << "[main] GL Vendor    :" << glGetStringInt(GL_VENDOR);
    Debug() << "[main] GL Renderer  :" << glGetStringInt(GL_RENDERER);
    Debug() << "[main] GL Version   :" << glGetStringInt(GL_VERSION);
    Debug() << "[main] GLSL Version :" << glGetStringInt(GL_SHADING_LANGUAGE_VERSION);
#ifndef NDEBUG
	GLDebugLogger dLogger;
#endif

	try{
		SharedState::initInstance(threadData);
	}catch (const Exception &exc){
		rgssThreadError(threadData, exc.msg);
		SDL_GL_DestroyContext(glCtx);
		return 0;
	}

	/* Start script execution */
	scriptBinding->execute();

	threadData->rqTermAck.set();
	threadData->ethread->requestTerminate();

	SharedState::finiInstance();

	SDL_GL_DestroyContext(glCtx);
	return 0;
}

static void setupWindowIcon(const Config &conf, SDL_Window *win){
	SDL_IOStream *iconSrc;

	if (conf.iconPath.empty())
		iconSrc = SDL_IOFromConstMem(assets_icon_png, assets_icon_png_len);
	else
		iconSrc = SDL_IOFromFile(conf.iconPath.c_str(), "rb");

	SDL_Surface *iconImg = IMG_Load_IO(iconSrc, true);

	if (iconImg){
		SDL_SetWindowIcon(win, iconImg);
		SDL_DestroySurface(iconImg);
	}
}

// mainly doing this so journal app knows where to load images from
static void setGamePathInRegistry() {
#if defined WIN32
	// this logic is currently windows specific
	const char* dataDir = SDL_GetBasePath();
	if (dataDir){
		HKEY key;
		long keyOpenError = RegOpenKey(HKEY_CURRENT_USER, TEXT("Software\\OneShot\\"), &key);

		if (keyOpenError != ERROR_SUCCESS) {
			// try creating the key first
			long keyCreateError = RegCreateKeyEx(HKEY_CURRENT_USER, TEXT("Software\\OneShot\\"), 0L, NULL, REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, NULL, &key, NULL);

			if (keyCreateError != ERROR_SUCCESS){
				WarnMsg("Unable to create key in registry");
			}
			else {
				keyOpenError = ERROR_SUCCESS;
			}
		}

		if (keyOpenError != ERROR_SUCCESS){
			WarnMsg("Unable to open registry.");
		}
		else {
			DWORD dataSize = (SDL_strlen(dataDir) + 1) * sizeof(char);
			if (RegSetValueEx(key, TEXT("GameDirectory"), 0, REG_SZ, (LPBYTE)dataDir, dataSize) != ERROR_SUCCESS){
				WarnMsg("Unable to set GameDirectory registry value");
			}
			RegCloseKey(key);
		}
	}
#endif
	//TODO handle this for Linux/Mac
}
int main(int argc, char *argv[]){
    startTime = boost::chrono::high_resolution_clock::now();
	loadLanguageMetadata(); //there will be a segfault on fclose if I don't move it here
	SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0");
	SDL_SetAppMetadata("Oneshot: Sunshine", "0.1.2", "meow.catwindowteam.sunshine");
	//X11 work on *BSD,Solaris too!
	#if unix_like
		SDL_SetHint(SDL_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR, "0");
		#if SDL_VERSION_ATLEAST(3, 4, 10)
			SDL_SetHint(SDL_HINT_VIDEO_X11_ENABLE_XSYNC_EXT, "1");
		#endif
	#elif windows
		SDL_SetHint(SDL_HINT_WINDOWS_RAW_KEYBOARD, "1");
	#elif android
		SDL_SetHint(SDL_HINT_ANDROID_ALLOW_PERSISTENT_FOLDER_ACCESS, "1");
	#elif vita
		SDL_SetHint(SDL_HINT_VITA_RESOLUTION, "1080");
	#elif ps2
		SFL_SetHint(SDL_HINT_PS2_GS_MODE, "NTSC");
	#endif
	/* initialize SDL first */
	if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_GAMEPAD) == false){
		WarnMsg("Error initializing SDL: %s", SDL_GetError());
		return 0;
	}

#ifdef STEAM
	if (!STEAMSHIM_init()){
		WarnMsg("Could not initialize Steamworks API");
		return 0;
	}
#endif

	if (!EventThread::allocUserEvents()){
		WarnMsg("Error allocating SDL user events");
		return 0;
	}

#ifndef WORKDIR_CURRENT
	/* set working directory */
	const char *dataDir = SDL_GetBasePath();
	if (dataDir) {
		int result = chdir(dataDir);
		(void)result;
	}
#endif

	setGamePathInRegistry();

	/* Initialize physfs here so that config can call PHYSFS_getPrefDir */
	PHYSFS_init(argv[0]);

	/* now we load the config */
	Config conf;
	conf.read(argc, argv);
	if(conf.SecurityEngine){
		SecurityManagerInit();	
	}
	#if windows
		if(conf.Windows_AllocConsole == true){
    			AllocConsole();
    			freopen("CONOUT$", "w", stdout);
    			freopen("CONOUT$", "w", stderr);
		}
	#endif

	if (!conf.gameFolder.empty()){
		if (chdir(conf.gameFolder.c_str()) != 0){
			WarnMsg("Unable to switch into gameFolder %s", conf.gameFolder.c_str());
			return 0;
		}
	}
	
	extern int screenMain(Config &conf);
	if (conf.screenMode)
		return screenMain(conf);

	if (conf.windowTitle.empty())
		conf.windowTitle = conf.game.title;

	if (TTF_Init() == false){
		WarnMsg("Error initializing SDL_ttf: %s", SDL_GetError());
		SDL_Quit();
	}

	if (MIX_Init() == false){
		WarnMsg("Error initializing SDL_mixer: %s", SDL_GetError());
		TTF_Quit();
		SDL_Quit();

		return 0;
	}

	SDL_Window *win;
	Uint32 winFlags = SDL_WINDOW_OPENGL | SDL_WINDOW_INPUT_FOCUS;

	win = SDL_CreateWindow(conf.windowTitle.c_str(), conf.defScreenW, conf.defScreenH, winFlags);
	if (conf.fullscreen)
		SDL_SetWindowFullscreen(win, true);

	if (!win){
		WarnMsg("Error creating window: %s", SDL_GetError());
		MIX_Quit();
		TTF_Quit();
		SDL_Quit();

		return 0;
	}

	/* OSX and Windows have their own native ways of
	 * dealing with icons; don't interfere with them */
#ifdef unix_like
	setupWindowIcon(conf, win);
#else
	(void) setupWindowIcon;
#endif

	SDL_AudioSpec spec{};
	spec.format = SDL_AUDIO_F32;
	spec.channels = 2;
	spec.freq = 44100;

	MIX_Mixer* mixer = MIX_CreateMixerDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec);

	if (!mixer){
		SDL_DestroyWindow(win);
		WarnMsg("Error creating Mixer Device, check your system audio configuration");
		MIX_Quit();
		TTF_Quit();
		SDL_Quit();
		
		return 0;
	}

	SDL_DisplayMode mode;
	EventThread eventThread;
	RGSSThreadData rtData(&eventThread, win, mixer, mode.refresh_rate, conf);

#ifndef STEAM
	/* Add controller bindings from embedded controller DB */
	SDL_IOStream *controllerDB = SDL_IOFromConstMem(assets_gamecontrollerdb_txt, assets_gamecontrollerdb_txt_len);
	SDL_AddGamepadMappingsFromIO(controllerDB, 1);
#endif

	int winW, winH;
	SDL_GetWindowSize(win, &winW, &winH); // SDL_GL_GetDrawableSize(win, &winW, &winH);
	rtData.windowSizeMsg.post(Vec2i(winW, winH));

	/* Load and post key bindings */
	rtData.bindingUpdateMsg.post(loadBindings(conf));
	/* Start RGSS thread */
	SDL_Thread *rgssThread = SDL_CreateThread(rgssThreadFun, "rgss", &rtData);

	/* Start event processing */
	eventThread.process(rtData);

	/* Request RGSS thread to stop */
	rtData.rqTerm.set();

	/* Wait for RGSS thread response */
	for (int i = 0; i < 1000; ++i){
		/* We can stop waiting when the request was ack'd */
		if (rtData.rqTermAck){
			Debug() << "[main] RGSS thread ack'd request after" << i*10 << "ms";
			break;
		}

		/* Give RGSS thread some time to respond */
		SDL_Delay(10);
	}

	/* If RGSS thread ack'd request, wait for it to shutdown,
	 * otherwise abandon hope and just end the process as is. */
	if (rtData.rqTermAck)
		SDL_WaitThread(rgssThread, 0);
	else
		SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, conf.windowTitle.c_str(), "The RGSS script seems to be stuck and Sunshine will now force quit", win);

	if (!rtData.rgssErrorMsg.empty())
		ErrorMsg(rtData.rgssErrorMsg.c_str());
	
	/* Clean up any remainin events */
	eventThread.cleanup();

	unloadLocale();
	unloadLanguageMetadata();

	if(show_crash_sceen){
		crash_screen(win);
	}
	
	MIX_DestroyMixer(mixer);
	SDL_DestroyWindow(win);

	MIX_Quit();
	TTF_Quit();
	SDL_Quit();

#ifdef STEAM
	STEAMSHIM_deinit();
#endif

	return 0;
}
