// Copyright (c) 2023 Alex Diener. All rights reserved.

#include "3dmodelio/TextureAtlasData.h"
#include "audioplayer/AudioPlayer.h"
#include "binaryserialization/BinaryDeserializationContext.h"
#include "dynamictypes/DataDeserializationContext.h"
#include "dynamictypes/DataSerializationContext.h"
#include "dynamictypes/DataSerializer.h"
#include "gamepad/Gamepad.h"
#include "imageio/ImageIO.h"
#include "jsonserialization/JSONDeserializationContext.h"
#include "jsonserialization/JSONSerializationContext.h"
#include "PROJECT_NAME/Atoms.h"
#include "PROJECT_NAME/GameSession.h"
#include "PROJECT_NAME/Globals.h"
#include "PROJECT_NAME/InputMapDefaults.h"
#include "PROJECT_NAME/SharedDefinitions.h"
#include "PROJECT_NAME/Sounds.h"
#include "shell/Shell.h"
#include "shell/ShellKeyCodes.h"
#include "uitoolkit/UIDrawingInterface2DTexture.h"
#include "uitoolkit/UIToolkitAppearance.h"
#include "uitoolkit/UITypeface_BitmapFont.h"
#include "utilities/IOUtilities.h"
#include "stem_core.h"
#include <errno.h>
#include <limits.h>
#include <stdio.h>

#define stemobject_implementation GameSession

v_begin();
v_func(dispose);
v_end();

#define PREFERENCE_KEY_PLAY_SOUND "play_sound"
#define PREFERENCE_KEY_PLAY_MUSIC "play_music"
#define PREFERENCE_KEY_SOUND_VOLUME "sound_volume"
#define PREFERENCE_KEY_MUSIC_VOLUME "music_volume"
#define PREFERENCE_KEY_FULL_SCREEN "full_screen"
#define PREFERENCE_KEY_INPUT_MAP "input_map"
#define PREFERENCE_KEY_REPEAT_DELAY "repeat_delay"
#define PREFERENCE_KEY_REPEAT_INTERVAL "repeat_interval"

#define REPEAT_DELAY_MIN 1
#define REPEAT_DELAY_MAX 5
#define REPEAT_INTERVAL_MIN 1
#define REPEAT_INTERVAL_MAX 5

GameSession * GameSession_create(void) {
	stemobject_create_implementation(init)
}

static void updateRepeatRate(GameSession * self) {
	double repeatDelays[REPEAT_DELAY_MAX - REPEAT_DELAY_MIN + 1] = {
		0.0,
		0.06,
		0.12,
		0.2,
		0.3,
	};
	double repeatIntervals[REPEAT_INTERVAL_MAX - REPEAT_INTERVAL_MIN + 1] = {
		0.06,
		0.08,
		0.1,
		0.2,
		0.3
	};
	unsigned int delayIndex = GameSession_getRepeatDelay(self) - REPEAT_DELAY_MIN;
	unsigned int intervalIndex = GameSession_getRepeatInterval(self) - REPEAT_INTERVAL_MIN;
	InputController_setRepeatRate(self->inputController,
	                              repeatDelays[delayIndex] + repeatIntervals[intervalIndex],
	                              repeatIntervals[intervalIndex],
	                              repeatIntervals[intervalIndex]);
}

static void writeInputMapToPreferences(GameSession * self) {
	DataSerializationContext * serializationContext = DataSerializationContext_create();
	InputMap_serialize(self->inputMap, serializationContext);
	Preferences_set(self->preferences, PREFERENCE_KEY_INPUT_MAP, *DataSerializationContext_result(serializationContext));
	call_virtual(dispose, serializationContext);
}

bool GameSession_init(GameSession * self) {
	call_super(init, self);
	
	self->preferences = Preferences_create();
	Preferences_set(self->preferences, PREFERENCE_KEY_PLAY_SOUND,      valueCreateBoolean(true));
	Preferences_set(self->preferences, PREFERENCE_KEY_PLAY_MUSIC,      valueCreateBoolean(true));
	Preferences_set(self->preferences, PREFERENCE_KEY_SOUND_VOLUME,    valueCreateFloat(1.0f));
	Preferences_set(self->preferences, PREFERENCE_KEY_MUSIC_VOLUME,    valueCreateFloat(1.0f));
	Preferences_set(self->preferences, PREFERENCE_KEY_FULL_SCREEN,     valueCreateBoolean(false));
	Preferences_set(self->preferences, PREFERENCE_KEY_REPEAT_DELAY,    valueCreateUInt8(3));
	Preferences_set(self->preferences, PREFERENCE_KEY_REPEAT_INTERVAL, valueCreateUInt8(3));
	Preferences_set(self->preferences, PREFERENCE_KEY_INPUT_MAP,       valueCreateHashTable(hashCreate(), true, false));
	
	extern const char EMBEDDATA_InputMapSchema_bin[];
	extern unsigned int EMBEDSIZE_InputMapSchema_bin;
	BinaryDeserializationContext * binaryDeserializationContext = BinaryDeserializationContext_createWithBytes(EMBEDDATA_InputMapSchema_bin, EMBEDSIZE_InputMapSchema_bin);
	DataValueSchema * inputMapSchema = DataValueSchema_deserialize(binaryDeserializationContext);
	BinaryDeserializationContext_dispose(binaryDeserializationContext);
	DataValueSchemaField preferencesSchemaFields[] = {
		{DATA_TYPE_BOOLEAN,    PREFERENCE_KEY_PLAY_SOUND,      valueCreateBoolean(true), 0, NULL, NULL},
		{DATA_TYPE_BOOLEAN,    PREFERENCE_KEY_PLAY_MUSIC,      valueCreateBoolean(true), 0, NULL, NULL},
		{DATA_TYPE_FLOAT,      PREFERENCE_KEY_SOUND_VOLUME,    valueCreateFloat(1.0f), 0, NULL, NULL},
		{DATA_TYPE_FLOAT,      PREFERENCE_KEY_MUSIC_VOLUME,    valueCreateFloat(1.0f), 0, NULL, NULL},
		{DATA_TYPE_BOOLEAN,    PREFERENCE_KEY_FULL_SCREEN,     valueCreateBoolean(false), 0, NULL, NULL},
		{DATA_TYPE_UINT8,      PREFERENCE_KEY_REPEAT_DELAY,    valueCreateUInt8(3), 0, NULL, NULL},
		{DATA_TYPE_UINT8,      PREFERENCE_KEY_REPEAT_INTERVAL, valueCreateUInt8(3), 0, NULL, NULL},
		{DATA_TYPE_HASH_TABLE, PREFERENCE_KEY_INPUT_MAP,       valueCreateBoolean(false), 0, NULL, inputMapSchema}
	};
	self->preferencesSchema = DataValueSchema_create(sizeof_count(preferencesSchemaFields), preferencesSchemaFields);
	DataValueSchema_dispose(inputMapSchema);
	
	char path[PATH_MAX];
	Preferences_getFilePath(PREFERENCES_FILE_NAME, path, sizeof(path));
	JSONDeserializationContext * context = JSONDeserializationContext_createWithFile(path);
	if (context->status == SERIALIZATION_ERROR_OK) {
		call_virtual(beginStructure, context, "preferences");
		uint16_t preferencesVersion = call_virtual(readUInt16, context, "version");
		if (preferencesVersion == PREFERENCES_LATEST_VERSION) {
			DataValue preferencesData = DataValue_deserializeWithSchema(self->preferencesSchema, "preferences", DATA_TYPE_HASH_TABLE, context, true);
			call_virtual(endStructure, context);
			if (preferencesData.type == DATA_TYPE_HASH_TABLE) {
				Preferences_import(self->preferences, preferencesData.value.hashTable);
			}
			valueDispose(&preferencesData);
		}
	}
	JSONDeserializationContext_dispose(context);
	
	self->inputMap = NULL;
	DataDeserializationContext * dataDeserializationContext = DataDeserializationContext_create(*Preferences_get(self->preferences, PREFERENCE_KEY_INPUT_MAP));
	if (dataDeserializationContext->status == SERIALIZATION_ERROR_OK) {
		self->inputMap = InputMap_deserialize(dataDeserializationContext, true);
	}
	DataDeserializationContext_dispose(dataDeserializationContext);
	
	if (self->inputMap == NULL) {
		self->inputMap = InputMap_create();
		bindDefaultActions(self->inputMap);
		writeInputMapToPreferences(self);
	}
	self->gamepadMap = GamepadMap_create();
	GamepadMap_registerGlobalDefaults(self->gamepadMap);
	self->inputController = InputController_create(self->gamepadMap,
	                                               self->inputMap,
	                                               ATOM_input_left,
	                                               ATOM_input_right,
	                                               ATOM_input_up,
	                                               ATOM_input_down,
	                                               ATOM_input_undo,
	                                               ATOM_input_redo,
	                                               ATOM_input_reset,
	                                               ATOM_input_wait,
	                                               ATOM_input_interact,
	                                               ATOM_input_pause,
	                                               ATOM_input_save,
	                                               ATOM_input_load,
	                                               ATOM_input_menu_left,
	                                               ATOM_input_menu_right,
	                                               ATOM_input_menu_up,
	                                               ATOM_input_menu_down,
	                                               ATOM_input_menu_next,
	                                               ATOM_input_menu_previous,
	                                               ATOM_input_menu_accept,
	                                               ATOM_input_menu_cancel,
	                                               ATOM_input_menu_alternate,
	                                               ATOM_input_menu_scroll_up,
	                                               ATOM_input_menu_scroll_down,
	                                               NULL);
	updateRepeatRate(self);
	
	self->eventDispatcher = EventDispatcher_create();
	
	return true;
}

void GameSession_dispose(GameSession * self) {
	InputController_dispose(self->inputController);
	InputMap_dispose(self->inputMap);
	GamepadMap_dispose(self->gamepadMap);
	Preferences_dispose(self->preferences);
	DataValueSchema_dispose(self->preferencesSchema);
	EventDispatcher_dispose(self->eventDispatcher);
	call_super(dispose, self);
}

bool GameSession_saveControlBindings(GameSession * self) {
	writeInputMapToPreferences(self);
	return GameSession_savePreferences(self);
}

bool GameSession_savePreferences(GameSession * self) {
	char filePath[PATH_MAX];
	Preferences_getFilePath(PREFERENCES_FILE_NAME, filePath, sizeof(filePath));
	JSONSerializationContext * serializationContext = JSONSerializationContext_create();
	
	call_virtual(beginStructure, serializationContext, "preferences");
	call_virtual(writeUInt16, serializationContext, "version", PREFERENCES_LATEST_VERSION);
	DataValue preferencesData = valueCreateHashTable(Preferences_export(self->preferences), true, false);
	DataValue_serializeWithSchema(&preferencesData, self->preferencesSchema, "preferences", serializationContext);
	valueDispose(&preferencesData);
	call_virtual(endStructure, serializationContext);
	
	struct JSONEmissionError error = {.description = NULL};
	bool success = JSONSerializationContext_writeToFile(serializationContext, JSONEmitterFormat_multiLine, filePath, &error);
#ifdef DEBUG
	if (!success) {
		fprintf(stderr, "Error: Couldn't save preferences: %s (%d)\n", error.description, error.code);
	}
#endif
	call_virtual(dispose, serializationContext);
	return success;
}

bool GameSession_getSoundEnabled(GameSession * self) {
	return valueGetBoolean(Preferences_get(self->preferences, PREFERENCE_KEY_PLAY_SOUND));
}

void GameSession_setSoundEnabled(GameSession * self, bool value) {
	Preferences_set(self->preferences, PREFERENCE_KEY_PLAY_SOUND, valueCreateBoolean(value));
	GameSession_savePreferences(self);
	if (value) {
		AudioPlayer_setCategoryVolume(AUDIO_CATEGORY_SOUND, GameSession_getSoundVolume(self) / 10.0f);
	} else {
		AudioPlayer_setCategoryVolume(AUDIO_CATEGORY_SOUND, 0.0f);
	}
}

bool GameSession_getMusicEnabled(GameSession * self) {
	return valueGetBoolean(Preferences_get(self->preferences, PREFERENCE_KEY_PLAY_MUSIC));
}

void GameSession_setMusicEnabled(GameSession * self, bool value) {
	Preferences_set(self->preferences, PREFERENCE_KEY_PLAY_MUSIC, valueCreateBoolean(value));
	GameSession_savePreferences(self);
	if (value) {
		AudioPlayer_setCategoryVolume(AUDIO_CATEGORY_MUSIC, GameSession_getMusicVolume(self) / 10.0f);
	} else {
		AudioPlayer_setCategoryVolume(AUDIO_CATEGORY_MUSIC, 0.0f);
	}
}

float GameSession_getSoundVolume(GameSession * self) {
	float value = valueGetFloat(Preferences_get(self->preferences, PREFERENCE_KEY_SOUND_VOLUME));
	if (value > 1.0f) {
		value = 1.0f;
	}
	if (value < 0.0f) {
		value = 0.0f;
	}
	return value;
}

void GameSession_setSoundVolume(GameSession * self, float value) {
	if (value > 1.0f) {
		value = 1.0f;
	}
	if (value < 0.0f) {
		value = 0.0f;
	}
	Preferences_set(self->preferences, PREFERENCE_KEY_SOUND_VOLUME, valueCreateFloat(value));
	GameSession_savePreferences(self);
	if (GameSession_getSoundEnabled(self)) {
		AudioPlayer_setCategoryVolume(AUDIO_CATEGORY_SOUND, value);
	}
}

float GameSession_getMusicVolume(GameSession * self) {
	float value = valueGetFloat(Preferences_get(self->preferences, PREFERENCE_KEY_MUSIC_VOLUME));
	if (value > 1.0f) {
		value = 1.0f;
	}
	if (value < 0.0f) {
		value = 0.0f;
	}
	return value;
}

void GameSession_setMusicVolume(GameSession * self, float value) {
	if (value > 1.0f) {
		value = 1.0f;
	}
	if (value < 0.0f) {
		value = 0.0f;
	}
	Preferences_set(self->preferences, PREFERENCE_KEY_MUSIC_VOLUME, valueCreateFloat(value));
	GameSession_savePreferences(self);
	if (GameSession_getMusicEnabled(self)) {
		AudioPlayer_setCategoryVolume(AUDIO_CATEGORY_MUSIC, value);
	}
}

bool GameSession_getFullScreen(GameSession * self) {
	return valueGetBoolean(Preferences_get(self->preferences, PREFERENCE_KEY_FULL_SCREEN));
}

void GameSession_setFullScreen(GameSession * self, bool value) {
	Preferences_set(self->preferences, PREFERENCE_KEY_FULL_SCREEN, valueCreateBoolean(value));
	GameSession_savePreferences(self);
	if (Shell_isFullScreen() != value) {
		if (value) {
			Shell_enterFullScreen(Shell_getDisplayIndexFromWindow(), false);
		} else {
			Shell_exitFullScreen();
		}
	}
}

unsigned int GameSession_getRepeatDelay(GameSession * self) {
	unsigned int value = valueGetUInt8(Preferences_get(self->preferences, PREFERENCE_KEY_REPEAT_DELAY));
	if (value < REPEAT_DELAY_MIN) {
		value = REPEAT_DELAY_MIN;
	} else if (value > REPEAT_DELAY_MAX) {
		value = REPEAT_DELAY_MAX;
	}
	return value;
}

void GameSession_setRepeatDelay(GameSession * self, unsigned int value) {
	if (value < REPEAT_DELAY_MIN) {
		value = REPEAT_DELAY_MIN;
	} else if (value > REPEAT_DELAY_MAX) {
		value = REPEAT_DELAY_MAX;
	}
	Preferences_set(self->preferences, PREFERENCE_KEY_REPEAT_DELAY, valueCreateUInt8(value));
	GameSession_savePreferences(self);
	updateRepeatRate(self);
}

unsigned int GameSession_getRepeatInterval(GameSession * self) {
	unsigned int value = valueGetUInt8(Preferences_get(self->preferences, PREFERENCE_KEY_REPEAT_INTERVAL));
	if (value < REPEAT_INTERVAL_MIN) {
		value = REPEAT_INTERVAL_MIN;
	} else if (value > REPEAT_INTERVAL_MAX) {
		value = REPEAT_INTERVAL_MAX;
	}
	return value;
}

void GameSession_setRepeatInterval(GameSession * self, unsigned int value) {
	if (value < REPEAT_INTERVAL_MIN) {
		value = REPEAT_INTERVAL_MIN;
	} else if (value > REPEAT_INTERVAL_MAX) {
		value = REPEAT_INTERVAL_MAX;
	}
	Preferences_set(self->preferences, PREFERENCE_KEY_REPEAT_INTERVAL, valueCreateUInt8(value));
	GameSession_savePreferences(self);
	updateRepeatRate(self);
}
