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

#include "PROJECT_NAME/Atoms.h"
#include "PROJECT_NAME/FileUtilities.h"
#include "PROJECT_NAME/GameplayScreen.h"
#include "PROJECT_NAME/Globals.h"
#include "PROJECT_NAME/RoomStateUndoStateDelta.h"
#include "PROJECT_NAME/SharedDefinitions.h"
#include "PROJECT_NAME/Sounds.h"
#include "shell/ShellKeyCodes.h"

#define stemobject_implementation GameplayScreen

v_begin();
v_func(dispose);
v_func(update);
v_func(mouseMoved);
v_func(mouseLeave);
v_func(keyDown);
v_func(listRenderables);
v_func(needsRedraw);
v_func(activate);
v_func(deactivate);
v_end();

GameplayScreen * GameplayScreen_create(void) {
	stemobject_create_implementation(init)
}

static void resetUndoStack(GameplayScreen * self) {
	UndoStack_dispose(self->undoStack);
	self->undoStack = UndoStack_create(self->gameState->rooms[self->gameState->currentRoomIndex]);
	disposeRoomStateUndoData(&self->roomStateUndoData);
	self->roomStateUndoData = RoomState_createUndoData(self->gameState->rooms[self->gameState->currentRoomIndex]);
}

static bool roomChanged(Atom eventID, void * eventData, void * context) {
	GameplayScreen * self = context;
	struct GameState_roomChangedEvent * event = eventData;
	GameView_beginRoomTransition(self->gameView, event->oldRoomIndex);
	resetUndoStack(self);
	disposeRoomStateUndoData(&self->roomStateUndoData);
	self->roomStateUndoData = RoomState_createUndoData(self->gameState->rooms[self->gameState->currentRoomIndex]);
	return true;
}

static void addUndoNode(GameplayScreen * self) {
	RoomStateUndoStateDelta * undoStateDelta = RoomStateUndoStateDelta_create(self->gameState->rooms[self->gameState->currentRoomIndex], &self->roomStateUndoData);
	UndoStack_appendNode(self->undoStack, undoStateDelta);
	disposeRoomStateUndoData(&self->roomStateUndoData);
	self->roomStateUndoData = RoomState_createUndoData(self->gameState->rooms[self->gameState->currentRoomIndex]);
}

static bool turnEnded(Atom eventID, void * eventData, void * context) {
	GameplayScreen * self = context;
	addUndoNode(self);
	return true;
}

static void initGameState(GameplayScreen * self, GameSavedState * savedState) {
	self->gameState = GameState_create(self->gameData, savedState);
	self->gameView = GameView_create(self->gameState);
	self->undoStack = UndoStack_create(self->gameState->rooms[self->gameState->currentRoomIndex]);
	self->roomStateUndoData = RoomState_createUndoData(self->gameState->rooms[self->gameState->currentRoomIndex]);
	EventDispatcher_registerForEvent(self->gameState->eventDispatcher, ATOM_event_room_changed, roomChanged, self);
	EventDispatcher_registerForEvent(self->gameState->eventDispatcher, ATOM_event_turn_ended, turnEnded, self);
}

static void resetGameState(GameplayScreen * self, GameSavedState * savedState) {
	GameView_dispose(self->gameView);
	GameState_dispose(self->gameState);
	UndoStack_dispose(self->undoStack);
	disposeRoomStateUndoData(&self->roomStateUndoData);
	initGameState(self, savedState);
	playSoundEffect(SOUND_RESET);
}

bool GameplayScreen_init(GameplayScreen * self) {
	call_super(init, self);
	self->paused = false;
	
	self->gameData = GameData_create();
	self->savedGameState = NULL;
	initGameState(self, NULL);
	
	return true;
}

void GameplayScreen_dispose(GameplayScreen * self) {
	GameView_dispose(self->gameView);
	GameState_dispose(self->gameState);
	GameData_dispose(self->gameData);
	if (self->savedGameState != NULL) {
		GameSavedState_dispose(self->savedGameState);
	}
	call_super_virtual(dispose, self);
}

static void pauseGame(GameplayScreen * self) {
	self->paused = true;
	pauseMusic();
}

static void unpauseGame(GameplayScreen * self) {
	self->paused = false;
	unpauseMusic();
	Shell_redisplay();
}

void GameplayScreen_update(GameplayScreen * self, double deltaTime) {
	if (!self->paused && !self->backgrounded) {
		GameView_update(self->gameView, deltaTime);
	}
}

bool GameplayScreen_mouseMoved(GameplayScreen * self, float x, float y, float deltaX, float deltaY, unsigned int modifiers, double timestamp) {
	bool handled = false;
	if (!self->paused && !self->backgrounded) {
		handled = GameView_mouseMoved(self->gameView, x, y, deltaX, deltaY, modifiers, timestamp);
		if (handled) {
			Shell_redisplay();
		}
	}
	return handled || call_super_virtual(mouseMoved, self, x, y, deltaX, deltaY, modifiers, timestamp);
}

bool GameplayScreen_mouseLeave(GameplayScreen * self, unsigned int modifiers, double timestamp) {
	GameView_hideMouseover(self->gameView);
	Shell_redisplay();
	return true;
}

bool GameplayScreen_keyDown(GameplayScreen * self, unsigned int charCode, unsigned int keyCode, unsigned int modifiers, bool isRepeat, double timestamp) {
	if (keyCode == KEY_CODE_R && (modifiers & MODIFIER_ALT_BIT)) {
		resetGameState(self, NULL);
		Shell_redisplay();
		return true;
	}
	return call_super_virtual(keyDown, self, charCode, keyCode, modifiers, isRepeat, timestamp);
}

void GameplayScreen_listRenderables(GameplayScreen * self, RenderableIO * renderableIO, int drawOrderOffset, Rect4i clipBounds) {
	GameView_listRenderables(self->gameView, renderableIO, drawOrderOffset, clipBounds);
	call_super_virtual(listRenderables, self, renderableIO, drawOrderOffset, clipBounds);
}

bool GameplayScreen_needsRedraw(GameplayScreen * self) {
	return !self->paused && !self->backgrounded;
}

static bool isMoveDirectionInput(Atom actionID, MoveInput_axis * outAxis, MoveInput_direction * outDirection) {
	if (actionID == ATOM_input_left) {
		*outAxis = MOVE_AXIS_X;
		*outDirection = MOVE_DIRECTION_NEGATIVE;
		return true;
	}
	if (actionID == ATOM_input_right) {
		*outAxis = MOVE_AXIS_X;
		*outDirection = MOVE_DIRECTION_POSITIVE;
		return true;
	}
	if (actionID == ATOM_input_up) {
		*outAxis = MOVE_AXIS_Y;
		*outDirection = MOVE_DIRECTION_NEGATIVE;
		return true;
	}
	if (actionID == ATOM_input_down) {
		*outAxis = MOVE_AXIS_Y;
		*outDirection = MOVE_DIRECTION_POSITIVE;
		return true;
	}
	return false;
}

static bool isActionInput(Atom actionID, ActionInput_action * outAction) {
	if (actionID == ATOM_input_wait) {
		*outAction = ACTION_WAIT;
		return true;
	}
	if (actionID == ATOM_input_interact) {
		*outAction = ACTION_INTERACT;
		return true;
	}
	return false;
}

static bool isMenuDirectionInput(Atom actionID, UINavigationDirection * outDirection) {
	if (actionID == ATOM_input_menu_left) {
		*outDirection = UI_LEFT;
		return true;
	}
	if (actionID == ATOM_input_menu_right) {
		*outDirection = UI_RIGHT;
		return true;
	}
	if (actionID == ATOM_input_menu_up) {
		*outDirection = UI_UP;
		return true;
	}
	if (actionID == ATOM_input_menu_down) {
		*outDirection = UI_DOWN;
		return true;
	}
	if (actionID == ATOM_input_menu_next) {
		*outDirection = UI_NEXT;
		return true;
	}
	if (actionID == ATOM_input_menu_previous) {
		*outDirection = UI_PREVIOUS;
		return true;
	}
	return false;
}

static bool isMenuActionInput(Atom actionID, unsigned int * outMenuAction) {
	if (actionID == ATOM_input_menu_accept) {
		*outMenuAction = MENU_ACTION_ACCEPT;
		return true;
	}
	if (actionID == ATOM_input_menu_alternate) {
		*outMenuAction = MENU_ACTION_ALTERNATE;
		return true;
	}
	if (actionID == ATOM_input_menu_cancel) {
		*outMenuAction = MENU_ACTION_CANCEL;
		return true;
	}
	return false;
}

static bool isMenuScrollInput(Atom actionID, int * outDeltaY) {
	if (actionID == ATOM_input_menu_scroll_up) {
		*outDeltaY = -1;
		return true;
	}
	if (actionID == ATOM_input_menu_scroll_down) {
		*outDeltaY = 1;
		return true;
	}
	return false;
}

static bool scrollFocusedElement(GameplayScreen * self, int deltaY, double timestamp) {
	bool handled = false;
	UIElement * focusedElement = call_virtual(getFocusedElement, self->rootContainer);
	if (focusedElement != NULL) {
		Rect4f bounds = call_virtual(getAbsoluteBounds, focusedElement);
		handled = call_virtual(scrollWheel, focusedElement, bounds.xMin, bounds.yMin, 0, deltaY, 0, 0, false, timestamp);
	}
	if (!handled) {
		Rect4f bounds = call_virtual(getAbsoluteBounds, self->rootContainer);
		handled = call_virtual(scrollWheel, self->rootContainer, bounds.xMin, bounds.yMin, 0, deltaY, 0, 0, false, timestamp);
	}
	return handled;
}

static void undo(GameplayScreen * self) {
	if (UndoStack_canUndo(self->undoStack)) {
		playSoundEffect(SOUND_UNDO);
		UndoStack_undo(self->undoStack);
		RoomState_undoActionPerformed(self->gameState->rooms[self->gameState->currentRoomIndex]);
		GameView_hideMouseover(self->gameView);
		disposeRoomStateUndoData(&self->roomStateUndoData);
		self->roomStateUndoData = RoomState_createUndoData(self->gameState->rooms[self->gameState->currentRoomIndex]);
	}
}

static void redo(GameplayScreen * self) {
	if (UndoStack_canRedo(self->undoStack)) {
		playSoundEffect(SOUND_REDO);
		UndoStack_redo(self->undoStack);
		RoomState_undoActionPerformed(self->gameState->rooms[self->gameState->currentRoomIndex]);
		GameView_hideMouseover(self->gameView);
		disposeRoomStateUndoData(&self->roomStateUndoData);
		self->roomStateUndoData = RoomState_createUndoData(self->gameState->rooms[self->gameState->currentRoomIndex]);
	}
}

static void saveGameState(GameplayScreen * self) {
	if (self->savedGameState != NULL) {
		GameSavedState_dispose(self->savedGameState);
	}
	self->savedGameState = GameState_createSave(self->gameState);
	GameSavedState_encodeUndoStack(self->savedGameState, self->undoStack);
	EventDispatcher_dispatchEvent(self->gameState->eventDispatcher, ATOM_event_game_state_saved, NULL);
}

static void loadGameState(GameplayScreen * self) {
	if (self->savedGameState != NULL) {
		resetGameState(self, self->savedGameState);
		GameSavedState_decodeUndoStack(self->savedGameState, self->undoStack);
	}
}

static bool actionDown(Atom eventID, void * eventData, void * context) {
	GameplayScreen * self = context;
	struct InputController_event * event = eventData;
	MoveInput_axis moveAxis;
	MoveInput_direction moveDirection;
	ActionInput_action action;
	UINavigationDirection menuDirection;
	unsigned int menuAction;
	int scrollDeltaY;
	bool handled = false;
	
	if (event->actionID == ATOM_input_pause) {
		if (self->paused) {
			unpauseGame(self);
		} else {
			pauseGame(self);
		}
		handled = true;
		
	} else if (isMoveDirectionInput(event->actionID, &moveAxis, &moveDirection)) {
		if (!self->paused) {
			if (self->gameView->roomTransition != NULL) {
				self->gameView->roomTransition->speedMultiplier = 2.0f;
			}
			handled = EntityPlayer_startMoveInput(self->gameState->player, moveAxis, moveDirection, false);
		}
		
	} else if (isActionInput(event->actionID, &action)) {
		if (!self->paused) {
			if (self->gameView->roomTransition != NULL) {
				self->gameView->roomTransition->speedMultiplier = 2.0f;
			}
			handled = EntityPlayer_startActionInput(self->gameState->player, action, false);
		}
		
	} else if (event->actionID == ATOM_input_reset) {
		GameState_resetCurrentRoom(self->gameState);
		self->undoStack->target = self->gameState->rooms[self->gameState->currentRoomIndex];
		addUndoNode(self);
		playSoundEffect(SOUND_RESET);
		handled = true;
		
	} else if (event->actionID == ATOM_input_undo) {
		undo(self);
		handled = true;
		
	} else if (event->actionID == ATOM_input_redo) {
		redo(self);
		handled = true;
		
	} else if (event->actionID == ATOM_input_save) {
		saveGameState(self);
		handled = true;
		
	} else if (event->actionID == ATOM_input_load) {
		loadGameState(self);
		handled = true;
		
	} else if (isMenuDirectionInput(event->actionID, &menuDirection)) {
		handled = call_virtual(menuDirectionDown, self->rootContainer, menuDirection, false, event->timestamp);
		
	} else if (isMenuActionInput(event->actionID, &menuAction)) {
		handled = call_virtual(menuActionDown, self->rootContainer, menuAction, false, event->timestamp);
		
	} else if (isMenuScrollInput(event->actionID, &scrollDeltaY)) {
		handled = scrollFocusedElement(self, scrollDeltaY, event->timestamp);
	}
	
	if (handled) {
		Shell_redisplay();
	}
	return handled;
}

static bool actionRepeat(Atom eventID, void * eventData, void * context) {
	GameplayScreen * self = context;
	struct InputController_event * event = eventData;
	MoveInput_axis moveAxis;
	MoveInput_direction moveDirection;
	ActionInput_action action;
	UINavigationDirection menuDirection;
	unsigned int menuAction;
	int scrollDeltaY;
	bool handled = false;
	
	if (isMoveDirectionInput(event->actionID, &moveAxis, &moveDirection)) {
		if (!self->paused) {
			if (self->gameView->roomTransition != NULL) {
				self->gameView->roomTransition->speedMultiplier = 2.0f;
			}
			handled = EntityPlayer_startMoveInput(self->gameState->player, moveAxis, moveDirection, true);
		}
		
	} else if (isActionInput(event->actionID, &action)) {
		if (!self->paused) {
			if (self->gameView->roomTransition != NULL) {
				self->gameView->roomTransition->speedMultiplier = 2.0f;
			}
			handled = EntityPlayer_startActionInput(self->gameState->player, action, true);
		}
		
	} else if (event->actionID == ATOM_input_undo) {
		undo(self);
		handled = true;
		
	} else if (event->actionID == ATOM_input_redo) {
		redo(self);
		handled = true;
		
	} else if (isMenuDirectionInput(event->actionID, &menuDirection)) {
		handled = call_virtual(menuDirectionDown, self->rootContainer, menuDirection, true, event->timestamp);
		
	} else if (isMenuActionInput(event->actionID, &menuAction)) {
		handled = call_virtual(menuActionDown, self->rootContainer, menuAction, true, event->timestamp);
		
	} else if (isMenuScrollInput(event->actionID, &scrollDeltaY)) {
		handled = scrollFocusedElement(self, scrollDeltaY, event->timestamp);
	}
	if (handled) {
		Shell_redisplay();
	}
	return handled;
}

static bool actionUp(Atom eventID, void * eventData, void * context) {
	GameplayScreen * self = context;
	struct InputController_event * event = eventData;
	MoveInput_axis moveAxis;
	MoveInput_direction moveDirection;
	ActionInput_action action;
	UINavigationDirection menuDirection;
	unsigned int menuAction;
	bool handled = false;
	
	if (isMoveDirectionInput(event->actionID, &moveAxis, &moveDirection)) {
		handled = EntityPlayer_stopMoveInput(self->gameState->player, moveAxis, moveDirection);
		
	} else if (isActionInput(event->actionID, &action)) {
		handled = EntityPlayer_stopActionInput(self->gameState->player, action);
		
	} else if (isMenuDirectionInput(event->actionID, &menuDirection)) {
		handled = call_virtual(menuDirectionUp, self->rootContainer, menuDirection, event->timestamp);
		
	} else if (isMenuActionInput(event->actionID, &menuAction)) {
		handled = call_virtual(menuActionUp, self->rootContainer, menuAction, event->timestamp);
	}
	
	if (handled) {
		Shell_redisplay();
	}
	return false;
}

void GameplayScreen_activate(GameplayScreen * self, Screen * lastScreen, const char * transitionName) {
	call_super_virtual(activate, self, lastScreen, transitionName);
	EventDispatcher_registerForEvent(g_gameSession->inputController->eventDispatcher, ATOM(INPUT_CONTROLLER_EVENT_ACTION_DOWN), actionDown, self);
	EventDispatcher_registerForEvent(g_gameSession->inputController->eventDispatcher, ATOM(INPUT_CONTROLLER_EVENT_ACTION_REPEAT), actionRepeat, self);
	EventDispatcher_registerForEvent(g_gameSession->inputController->eventDispatcher, ATOM(INPUT_CONTROLLER_EVENT_ACTION_UP), actionUp, self);
	self->paused = false;
}

void GameplayScreen_deactivate(GameplayScreen * self, Screen * lastScreen, const char * transitionName) {
	EventDispatcher_unregisterForEvent(g_gameSession->inputController->eventDispatcher, ATOM(INPUT_CONTROLLER_EVENT_ACTION_DOWN), actionDown, self);
	EventDispatcher_unregisterForEvent(g_gameSession->inputController->eventDispatcher, ATOM(INPUT_CONTROLLER_EVENT_ACTION_REPEAT), actionRepeat, self);
	EventDispatcher_unregisterForEvent(g_gameSession->inputController->eventDispatcher, ATOM(INPUT_CONTROLLER_EVENT_ACTION_UP), actionUp, self);
	call_super_virtual(deactivate, self, lastScreen, transitionName);
}
