#include "PowderToySDL.h"
#include "Format.h"
#include "X86KillDenormals.h"
#include "prefs/GlobalPrefs.h"
#include "client/Client.h"
#include "client/GameSave.h"
#include "client/SaveFile.h"
#include "client/SaveInfo.h"
#include "client/http/requestmanager/RequestManager.h"
#include "client/http/GetSaveRequest.h"
#include "client/http/GetSaveDataRequest.h"
#include "common/platform/Platform.h"
#include "graphics/Graphics.h"
#include "simulation/SaveRenderer.h"
#include "common/tpt-rand.h"
#include "gui/game/Favorite.h"
#include "gui/Style.h"
#include "gui/game/GameController.h"
#include "gui/game/GameView.h"
#include "gui/dialogues/ConfirmPrompt.h"
#include "gui/dialogues/ErrorMessage.h"
#include "gui/interface/Engine.h"
#include "Config.h"
#include "SimulationConfig.h"
#include <optional>
#include <climits>
#include <iostream>
#include <csignal>
#include <SDL.h>

void LoadWindowPosition()
{
	if (Client::Ref().IsFirstRun())
	{
		return;
	}

	auto &prefs = GlobalPrefs::Ref();
	int savedWindowX = prefs.Get("WindowX", INT_MAX);
	int savedWindowY = prefs.Get("WindowY", INT_MAX);

	int borderTop, borderLeft;
	SDL_GetWindowBordersSize(sdl_window, &borderTop, &borderLeft, nullptr, nullptr);
	// Sometimes (Windows), the border size may not be reported for 200+ frames
	// So just have a default of 5 to ensure the window doesn't get stuck where it can't be moved
	if (borderTop == 0)
		borderTop = 5;

	int numDisplays = SDL_GetNumVideoDisplays();
	SDL_Rect displayBounds;
	bool ok = false;
	for (int i = 0; i < numDisplays; i++)
	{
		SDL_GetDisplayBounds(i, &displayBounds);
		if (savedWindowX + borderTop > displayBounds.x && savedWindowY + borderLeft > displayBounds.y &&
				savedWindowX + borderTop < displayBounds.x + displayBounds.w &&
				savedWindowY + borderLeft < displayBounds.y + displayBounds.h)
		{
			ok = true;
			break;
		}
	}
	if (ok)
		SDL_SetWindowPosition(sdl_window, savedWindowX + borderLeft, savedWindowY + borderTop);
}

void SaveWindowPosition()
{
	int x, y;
	SDL_GetWindowPosition(sdl_window, &x, &y);

	int borderTop, borderLeft;
	SDL_GetWindowBordersSize(sdl_window, &borderTop, &borderLeft, nullptr, nullptr);

	auto &prefs = GlobalPrefs::Ref();
	prefs.Set("WindowX", x - borderLeft);
	prefs.Set("WindowY", y - borderTop);
}

void LargeScreenDialog()
{
	StringBuilder message;
	message << "Switching to " << scale << "x size mode since your screen was determined to be large enough: ";
	message << desktopWidth << "x" << desktopHeight << " detected, " << WINDOWW*scale << "x" << WINDOWH*scale << " required";
	message << "\nTo undo this, hit Cancel. You can change this in settings at any time.";
	if (!ConfirmPrompt::Blocking("Large screen detected", message.Build()))
	{
		GlobalPrefs::Ref().Set("Scale", 1);
		ui::Engine::Ref().SetScale(1);
	}
}

void TickClient()
{
	Client::Ref().Tick();
}

void BlueScreen(String detailMessage)
{
	auto &engine = ui::Engine::Ref();
	engine.g->BlendFilledRect(engine.g->Size().OriginRect(), 0x1172A9_rgb .WithAlpha(0xD2));

	String errorTitle = "ERROR";
	String errorDetails = "Details: " + detailMessage;
	String errorHelp = String("An unrecoverable fault has occurred, please report the error by visiting the website below\n") + SCHEME + SERVER;

	// We use the width of errorHelp to center, but heights of the individual texts for vertical spacing
	auto pos = engine.g->Size() / 2 - Vec2(Graphics::TextSize(errorHelp).X / 2, 100);
	engine.g->BlendText(pos, errorTitle, 0xFFFFFF_rgb .WithAlpha(0xFF));
	pos.Y += 4 + Graphics::TextSize(errorTitle).Y;
	engine.g->BlendText(pos, errorDetails, 0xFFFFFF_rgb .WithAlpha(0xFF));
	pos.Y += 4 + Graphics::TextSize(errorDetails).Y;
	engine.g->BlendText(pos, errorHelp, 0xFFFFFF_rgb .WithAlpha(0xFF));

	//Death loop
	SDL_Event event;
	while(true)
	{
		while (SDL_PollEvent(&event))
			if(event.type == SDL_QUIT)
				exit(-1); // Don't use Platform::Exit, we're practically zombies at this point anyway.
		blit(engine.g->Data());
	}
}

struct
{
	int sig;
	const char *message;
} signalMessages[] = {
	{ SIGSEGV, "Memory read/write error" },
	{ SIGFPE, "Floating point exception" },
	{ SIGILL, "Program execution exception" },
	{ SIGABRT, "Unexpected program abort" },
	{ 0, nullptr },
};

void SigHandler(int signal)
{
	const char *message = "Unknown signal";
	for (auto *msg = signalMessages; msg->message; ++msg)
	{
		if (msg->sig == signal)
		{
			message = msg->message;
			break;
		}
	}
	BlueScreen(ByteString(message).FromUtf8());
}

constexpr int SCALE_MAXIMUM = 10;
constexpr int SCALE_MARGIN = 30;

int GuessBestScale()
{
	const int widthNoMargin = desktopWidth - SCALE_MARGIN;
	const int widthGuess = widthNoMargin / WINDOWW;

	const int heightNoMargin = desktopHeight - SCALE_MARGIN;
	const int heightGuess = heightNoMargin / WINDOWH;

	int guess = std::min(widthGuess, heightGuess);
	if(guess < 1 || guess > SCALE_MAXIMUM)
		guess = 1;

	return guess;
}

struct ExplicitSingletons
{
	// These need to be listed in the order they are populated in main.
	std::unique_ptr<GlobalPrefs> globalPrefs;
	http::RequestManagerPtr requestManager;
	std::unique_ptr<Client> client;
	std::unique_ptr<SaveRenderer> saveRenderer;
	std::unique_ptr<Favorite> favorite;
	std::unique_ptr<ui::Engine> engine;
	std::unique_ptr<GameController> gameController;
};
static std::unique_ptr<ExplicitSingletons> explicitSingletons;

int main(int argc, char * argv[])
{
	Platform::SetupCrt();
	Platform::Atexit([]() {
		// Unregister dodgy error handlers so they don't try to show the blue screen when the window is closed
		for (auto *msg = signalMessages; msg->message; ++msg)
		{
			signal(msg->sig, SIG_DFL);
		}
		SDLClose();
		explicitSingletons.reset();
	});
	explicitSingletons = std::make_unique<ExplicitSingletons>();


	// https://bugzilla.libsdl.org/show_bug.cgi?id=3796
	if (SDL_Init(0) < 0)
	{
		fprintf(stderr, "Initializing SDL: %s\n", SDL_GetError());
		return 1;
	}

	Platform::originalCwd = Platform::GetCwd();

	using Argument = std::optional<ByteString>;
	std::map<ByteString, Argument> arguments;

	for (auto i = 1; i < argc; ++i)
	{
		auto str = ByteString(argv[i]);
		if (str.BeginsWith("file://"))
		{
			arguments.insert({ "open", format::URLDecode(str.substr(7 /* length of the "file://" prefix */)) });
		}
		else if (str.BeginsWith("ptsave:"))
		{
			arguments.insert({ "ptsave", str });
		}
		else if (auto split = str.SplitBy(':'))
		{
			arguments.insert({ split.Before(), split.After() });
		}
		else if (auto split = str.SplitBy('='))
		{
			arguments.insert({ split.Before(), split.After() });
		}
		else if (str == "open" || str == "ptsave" || str == "ddir")
		{
			if (i + 1 < argc)
			{
				arguments.insert({ str, argv[i + 1] });
				i += 1;
			}
			else
			{
				std::cerr << "no value provided for command line parameter " << str << std::endl;
			}
		}
		else
		{
			arguments.insert({ str, "" }); // so .has_value() is true
		}
	}

	auto ddirArg = arguments["ddir"];
	if (ddirArg.has_value())
	{
		if (Platform::ChangeDir(ddirArg.value()))
			Platform::sharedCwd = Platform::GetCwd();
		else
			perror("failed to chdir to requested ddir");
	}
	else
	{
		auto ddir = std::unique_ptr<char, decltype(&SDL_free)>(SDL_GetPrefPath(NULL, APPDATA), SDL_free);
		if (!Platform::FileExists("powder.pref"))
		{
			if (ddir)
			{
				if (!Platform::ChangeDir(ddir.get()))
				{
					perror("failed to chdir to default ddir");
					ddir.reset();
				}
			}
		}

		if (ddir)
		{
			Platform::sharedCwd = ddir.get();
		}
	}
	// We're now in the correct directory, time to get prefs.
	explicitSingletons->globalPrefs = std::make_unique<GlobalPrefs>();

	auto &prefs = GlobalPrefs::Ref();
	scale = prefs.Get("Scale", 1);
	auto graveExitsConsole = prefs.Get("GraveExitsConsole", true);
	resizable = prefs.Get("Resizable", false);
	fullscreen = prefs.Get("Fullscreen", false);
	altFullscreen = prefs.Get("AltFullscreen", false);
	forceIntegerScaling = prefs.Get("ForceIntegerScaling", true);
	momentumScroll = prefs.Get("MomentumScroll", true);
	showAvatars = prefs.Get("ShowAvatars", true);

	auto true_string = [](ByteString str) {
		str = str.ToLower();
		return str == "true" ||
		       str == "t" ||
		       str == "on" ||
		       str == "yes" ||
		       str == "y" ||
		       str == "1" ||
		       str == ""; // standalone "redirect" or "disable-bluescreen" or similar arguments
	};
	auto true_arg = [&true_string](Argument arg) {
		return arg.has_value() && true_string(arg.value());
	};

	auto kioskArg = arguments["kiosk"];
	if (kioskArg.has_value())
	{
		fullscreen = true_string(kioskArg.value());
		prefs.Set("Fullscreen", fullscreen);
	}

	if (true_arg(arguments["redirect"]))
	{
		FILE *new_stdout = freopen("stdout.log", "w", stdout);
		FILE *new_stderr = freopen("stderr.log", "w", stderr);
		if (!new_stdout || !new_stderr)
		{
			Platform::Exit(42);
		}
	}

	auto scaleArg = arguments["scale"];
	if (scaleArg.has_value())
	{
		try
		{
			scale = scaleArg.value().ToNumber<int>();
			prefs.Set("Scale", scale);
		}
		catch (const std::runtime_error &e)
		{
			std::cerr << "failed to set scale: " << e.what() << std::endl;
		}
	}

	auto clientConfig = [&prefs](Argument arg, ByteString name, ByteString defaultValue) {
		ByteString value;
		if (arg.has_value())
		{
			value = arg.value();
			if (value == "")
			{
				value = defaultValue;
			}
			prefs.Set(name, value);
		}
		else
		{
			value = prefs.Get(name, defaultValue);
		}
		return value;
	};
	ByteString proxyString = clientConfig(arguments["proxy"], "Proxy", "");
	ByteString cafileString = clientConfig(arguments["cafile"], "CAFile", "");
	ByteString capathString = clientConfig(arguments["capath"], "CAPath", "");
	bool disableNetwork = true_arg(arguments["disable-network"]);
	explicitSingletons->requestManager = http::RequestManager::Create(proxyString, cafileString, capathString, disableNetwork);

	explicitSingletons->client = std::make_unique<Client>();
	Client::Ref().Initialize();

	explicitSingletons->saveRenderer = std::make_unique<SaveRenderer>();
	explicitSingletons->favorite = std::make_unique<Favorite>();
	explicitSingletons->engine = std::make_unique<ui::Engine>();

	// TODO: maybe bind the maximum allowed scale to screen size somehow
	if(scale < 1 || scale > SCALE_MAXIMUM)
		scale = 1;

	SDLOpen();

	if (Client::Ref().IsFirstRun())
	{
		scale = GuessBestScale();
		if (scale > 1)
		{
			prefs.Set("Scale", scale);
			SDL_SetWindowSize(sdl_window, WINDOWW * scale, WINDOWH * scale);
			showLargeScreenDialog = true;
		}
	}

	StopTextInput();

	auto &engine = ui::Engine::Ref();
	engine.g = new Graphics();
	engine.Scale = scale;
	engine.GraveExitsConsole = graveExitsConsole;
	engine.SetResizable(resizable);
	engine.Fullscreen = fullscreen;
	engine.SetAltFullscreen(altFullscreen);
	engine.SetForceIntegerScaling(forceIntegerScaling);
	engine.MomentumScroll = momentumScroll;
	engine.ShowAvatars = showAvatars;
	engine.Begin();
	engine.SetFastQuit(prefs.Get("FastQuit", true));

	bool enableBluescreen = !DEBUG && !true_arg(arguments["disable-bluescreen"]);
	if (enableBluescreen)
	{
		//Get ready to catch any dodgy errors
		for (auto *msg = signalMessages; msg->message; ++msg)
		{
			signal(msg->sig, SigHandler);
		}
	}

	if constexpr (X86)
	{
		X86KillDenormals();
	}

	auto wrapWithBluescreen = [&]() {
		explicitSingletons->gameController = std::make_unique<GameController>();
		auto *gameController = explicitSingletons->gameController.get();
		engine.ShowWindow(gameController->GetView());

		auto openArg = arguments["open"];
		if (openArg.has_value())
		{
			if constexpr (DEBUG)
			{
				std::cout << "Loading " << openArg.value() << std::endl;
			}
			if (Platform::FileExists(openArg.value()))
			{
				try
				{
					std::vector<char> gameSaveData;
					if (!Platform::ReadFile(gameSaveData, openArg.value()))
					{
						new ErrorMessage("Error", "Could not read file");
					}
					else
					{
						auto newFile = std::make_unique<SaveFile>(openArg.value());
						auto newSave = std::make_unique<GameSave>(std::move(gameSaveData));
						newFile->SetGameSave(std::move(newSave));
						gameController->LoadSaveFile(std::move(newFile));
					}

				}
				catch (std::exception & e)
				{
					new ErrorMessage("Error", "Could not open save file:\n" + ByteString(e.what()).FromUtf8()) ;
				}
			}
			else
			{
				new ErrorMessage("Error", "Could not open file");
			}
		}

		auto ptsaveArg = arguments["ptsave"];
		if (ptsaveArg.has_value())
		{
			engine.g->Clear();
			engine.g->DrawRect(RectSized(engine.g->Size() / 2 - Vec2(100, 25), Vec2(200, 50)), 0xB4B4B4_rgb);
			String loadingText = "Loading save...";
			engine.g->BlendText(engine.g->Size() / 2 - Vec2((Graphics::TextSize(loadingText).X - 1) / 2, 5), loadingText, style::Colour::InformationTitle);

			blit(engine.g->Data());
			try
			{
				ByteString saveIdPart;
				if (ByteString::Split split = ptsaveArg.value().SplitBy(':'))
				{
					if (split.Before() != "ptsave")
						throw std::runtime_error("Not a ptsave link");
					saveIdPart = split.After().SplitBy('#').Before();
				}
				else
					throw std::runtime_error("Invalid save link");

				if (!saveIdPart.size())
					throw std::runtime_error("No Save ID");
				if constexpr (DEBUG)
				{
					std::cout << "Got Ptsave: id: " << saveIdPart << std::endl;
				}
				int saveId = saveIdPart.ToNumber<int>();

				auto getSave = std::make_unique<http::GetSaveRequest>(saveId, 0);
				getSave->Start();
				getSave->Wait();
				std::unique_ptr<SaveInfo> newSave;
				try
				{
					newSave = getSave->Finish();
				}
				catch (const http::RequestError &ex)
				{
					throw std::runtime_error("Could not load save info\n" + ByteString(ex.what()));
				}
				auto getSaveData = std::make_unique<http::GetSaveDataRequest>(saveId, 0);
				getSaveData->Start();
				getSaveData->Wait();
				std::unique_ptr<GameSave> saveData;
				try
				{
					saveData = std::make_unique<GameSave>(getSaveData->Finish());
				}
				catch (const http::RequestError &ex)
				{
					throw std::runtime_error("Could not load save\n" + ByteString(ex.what()));
				}
				newSave->SetGameSave(std::move(saveData));
				gameController->LoadSave(std::move(newSave));
			}
			catch (std::exception & e)
			{
				new ErrorMessage("Error", ByteString(e.what()).FromUtf8());
			}
		}

		EngineProcess();
		SaveWindowPosition();
	};

	if (enableBluescreen)
	{
		try
		{
			wrapWithBluescreen();
		}
		catch (const std::exception &e)
		{
			BlueScreen(ByteString(e.what()).FromUtf8());
		}
	}
	else
	{
		wrapWithBluescreen();
	}
	Platform::Exit(0);
	return 0;
}
