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

#include "gamemath/PCGRandom.h"
#include "gamemath/Scalar.h"
#include "gamemath/VectorConversions.h"
#include "PROJECT_NAME/Atoms.h"
#include "PROJECT_NAME/Drawing.h"
#include "PROJECT_NAME/EntityComponent_position.h"
#include "PROJECT_NAME/EntityComponent_sprite.h"
#include "PROJECT_NAME/GameView.h"
#include "PROJECT_NAME/Globals.h"
#include "PROJECT_NAME/Music.h"
#include "PROJECT_NAME/Shaders.h"
#include "PROJECT_NAME/Sprites.h"
#include "PROJECT_NAME/Sounds.h"
#include "PROJECT_NAME/VisualEffect_sparkle.h"
#include "stem_core.h"
#include <limits.h>
#include <math.h>
#include <stdio.h>

#define stemobject_implementation GameView

stemobject_vtable_begin();
stemobject_vtable_entry(dispose);
stemobject_vtable_end();

#define TURN_INTERPOLATION_TIME (3 / 60.0)
#define BUMP_OFFSET 4.0f

GameView * GameView_create(GameState * gameState) {
	stemobject_create_implementation(init, gameState)
}

static Vector2f getCameraOffset(GameView * self) {
	Vector2f cameraOffset = VECTOR2f(-self->roomCameraPosition.x, -self->roomCameraPosition.y);
	if (self->roomTransition != NULL) {
		cameraOffset = RoomTransition_getCameraOffset(self->roomTransition);
	}
	cameraOffset.x += self->screenShakeOffset.x;
	cameraOffset.y += self->screenShakeOffset.y;
	return cameraOffset;
}

struct spriteDrawCommand {
	SpriteID spriteID;
	unsigned int animationTime;
	Vector2f screenPosition;
	int drawLayer;
	int drawPriority;
};

static bool getSpriteDrawCommandForEntity(GameEntity * entity, Vector2f offset, float interpolation, double animationReferenceTime, struct spriteDrawCommand * outDrawCommand) {
	EntityComponent_position * positionComponent = call_virtual(getComponent, entity, COMPONENT_POSITION);
	EntityComponent_sprite * spriteComponent = call_virtual(getComponent, entity, COMPONENT_SPRITE);
	if (positionComponent != NULL && spriteComponent != NULL) {
		Vector2f position = worldToScreenPosition(positionComponent->position);
		Vector2f lastPosition;
		if (positionComponent->lastPosition.x == positionComponent->position.x && positionComponent->lastPosition.y == positionComponent->position.y) {
			lastPosition = VECTOR2f(position.x + positionComponent->lastBumpDirection.x * BUMP_OFFSET, position.y - positionComponent->lastBumpDirection.y * BUMP_OFFSET);
		} else {
			lastPosition = worldToScreenPosition(positionComponent->lastPosition);
		}
		Vector2f drawnPosition = Vector2f_interpolate(lastPosition, position, interpolation);
		outDrawCommand->spriteID = spriteComponent->spriteID;
		outDrawCommand->animationTime = call_virtual(getAnimationTime, spriteComponent, animationReferenceTime);
		outDrawCommand->screenPosition = Vector2f_add(drawnPosition, offset);
		outDrawCommand->drawLayer = spriteComponent->drawLayer;
		outDrawCommand->drawPriority = spriteComponent->drawPriority;
		return true;
	}
	return false;
}

static int compareSpriteDrawCommands(const void * lhsUntyped, const void * rhsUntyped) {
	const struct spriteDrawCommand * lhs = lhsUntyped, * rhs = rhsUntyped;
	if (lhs->drawLayer != rhs->drawLayer) {
		return (lhs->drawLayer > rhs->drawLayer) * 2 - 1;
	}
	if (lhs->screenPosition.y != rhs->screenPosition.y) {
		return (lhs->screenPosition.y < rhs->screenPosition.y) * 2 - 1;
	}
	if (lhs->screenPosition.x != rhs->screenPosition.x) {
		return (lhs->screenPosition.x > rhs->screenPosition.x) * 2 - 1;
	}
	return (lhs->drawPriority > rhs->drawPriority) * 2 - 1;
}

static void writeEntityVertices(Renderable * renderable, VertexIO * vertexIO, void * context) {
	GameView * self = context;
	RoomState * roomState = self->gameState->rooms[self->gameState->currentRoomIndex];
	unsigned int totalEntityCount = roomState->entityCount;
	if (self->roomTransition != NULL) {
		totalEntityCount += self->gameState->rooms[self->previousRoomIndex]->entityCount;
	}
	struct spriteDrawCommand spriteDrawCommands[totalEntityCount];
	
	Vector2f transitionCameraOffset = VECTOR2f_ZERO;
	if (self->roomTransition != NULL) {
		transitionCameraOffset = RoomTransition_getCameraOffset(self->roomTransition);
	}
	unsigned int drawnEntityCount = 0;
	float interpolation = sinf((1.0f - self->turnInterpolationRemainingTime / TURN_INTERPOLATION_TIME) * M_PI * 0.5f);
	for (unsigned int entityIndex = 0; entityIndex < roomState->entityCount; entityIndex++) {
		drawnEntityCount += getSpriteDrawCommandForEntity(roomState->entities[entityIndex].entity, transitionCameraOffset, interpolation, self->animationReferenceTime, &spriteDrawCommands[drawnEntityCount]);
	}
	if (self->roomTransition != NULL) {
		Vector2f cameraOffset = Vector2f_add(Vector2f_inverted(self->roomTransition->offsetFromOldToNew), transitionCameraOffset);
		RoomState * previousRoomState = self->gameState->rooms[self->previousRoomIndex];
		for (unsigned int entityIndex = 0; entityIndex < previousRoomState->entityCount; entityIndex++) {
			drawnEntityCount += getSpriteDrawCommandForEntity(previousRoomState->entities[entityIndex].entity, cameraOffset, interpolation, self->animationReferenceTime, &spriteDrawCommands[drawnEntityCount]);
		}
	}
	
	qsort(spriteDrawCommands, drawnEntityCount, sizeof(spriteDrawCommands[0]), compareSpriteDrawCommands);
	for (unsigned int entityIndex = 0; entityIndex < drawnEntityCount; entityIndex++) {
		drawSprite(spriteDrawCommands[entityIndex].spriteID, spriteDrawCommands[entityIndex].animationTime, true, spriteDrawCommands[entityIndex].screenPosition, COLOR4f(1.0f, 1.0f, 1.0f, 1.0f), vertexIO);
	}
	
	if (self->mouseoverTile.x != INT_MIN) {
		drawSprite(SPRITE_ID_MOUSEOVER, 0, true, worldToScreenPosition(self->mouseoverTile), COLOR4f(1.0f, 1.0f, 1.0f, 1.0f), vertexIO);
	}
}

static void writeVisualEffectVertices(Renderable * renderable, VertexIO * vertexIO, void * context) {
	GameView * self = context;
	for (unsigned int visualEffectIndex = 0; visualEffectIndex < self->visualEffectCount; visualEffectIndex++) {
		call_virtual(writeVertices, self->visualEffects[visualEffectIndex], VECTOR2f_ZERO, vertexIO);
	}
}

static void writeRoomVisualToVertexBuffer(GameView * self) {
	VertexIO * vertexIO = VertexIO_create(self->roomShaderConfiguration->shader->vertexFormat, 0.0, 0.0);
	if (self->roomTransition != NULL) {
		RoomTransition_draw(self->roomTransition, VECTOR2f_ZERO, vertexIO);
	} else {
		RoomVisual_draw(self->roomVisual, VECTOR2f_ZERO, vertexIO);
	}
	VertexBuffer_bufferData(self->roomVertexBuffer, vertexIO->vertexCount, vertexIO->vertices, vertexIO->indexCount, vertexIO->indexes, BUFFER_USAGE_STREAM);
	VertexIO_dispose(vertexIO);
}

static bool turnStartedCallback(Atom eventID, void * eventData, void * context) {
	beginSoundEffectGroup();
	return true;
}

static bool turnEndedCallback(Atom eventID, void * eventData, void * context) {
	GameView * self = context;
	endSoundEffectGroup();
	self->turnInterpolationRemainingTime = TURN_INTERPOLATION_TIME;
	self->mouseoverTile = VECTOR2i(INT_MIN, INT_MIN);
	return true;
}

static bool playSoundCallback(Atom eventID, void * eventData, void * context) {
	SoundID soundID = (uintptr_t) context;
	playSoundEffect(soundID);
	return true;
}

static bool pickupCollectedCallback(Atom eventID, void * eventData, void * context) {
	GameView * self = context;
	GameEntity * entity = eventData;
	playSoundEffect(SOUND_PICKUP);
	EntityComponent_position * positionComponent = call_virtual(getComponent, entity, COMPONENT_POSITION);
	if (positionComponent != NULL) {
		GameView_addVisualEffect(self, VisualEffect_sparkle_create(positionComponent->position));
	}
	return true;
}

bool GameView_init(GameView * self, GameState * gameState) {
	call_super(init, self);
	self->gameState = gameState;
	self->roomVisual = RoomVisual_create(self->gameState->rooms[self->gameState->currentRoomIndex]);
	self->visualEffectCount = 0;
	self->visualEffectAllocatedCount = 8;
	self->visualEffects = malloc(self->visualEffectAllocatedCount * sizeof(*self->visualEffects));
	self->screenShakeTime = 0.0;
	self->screenShakeRadius = 0.0f;
	self->screenShakeOffset = VECTOR2f_ZERO;
	self->roomCameraPosition = VECTOR2f_ZERO;
	self->animationReferenceTime = 0.0;
	self->turnInterpolationRemainingTime = 0.0;
	self->mouseoverTile = VECTOR2i(INT_MIN, INT_MIN);
	
	self->roomShaderConfiguration = ShaderConfigurationRoom_create(g_roomShader);
	call_virtual(setTexture, self->roomShaderConfiguration, 0, g_imageCollectionTexture, false);
	call_virtual(referenceProjectionMatrix, self->roomShaderConfiguration, &g_projectionUniform);
	
	self->roomVertexBuffer = VertexBuffer_create(g_roomShader->vertexFormat, 0, NULL, INDEX_SIZE_UINT32, 0, NULL, BUFFER_USAGE_STREAM);
	self->roomRenderable = Renderable_createWithVertexBuffer(PRIMITIVE_TRIANGLES, &g_uiContext.defaultRenderPipelineConfiguration, self->roomShaderConfiguration, self->roomVertexBuffer, false, NULL, self);
	self->entityRenderable = Renderable_createWithCallback(PRIMITIVE_TRIANGLES, &g_uiContext.defaultRenderPipelineConfiguration, g_shaderConfiguration, writeEntityVertices, NULL, self);
	self->visualEffectRenderable = Renderable_createWithCallback(PRIMITIVE_TRIANGLES, &g_uiContext.defaultRenderPipelineConfiguration, g_shaderConfiguration, writeVisualEffectVertices, NULL, self);
	
	self->roomTransition = NULL;
	writeRoomVisualToVertexBuffer(self);
	
	EventDispatcher_registerForEvent(self->gameState->eventDispatcher, ATOM_event_turn_started,       turnStartedCallback, self);
	EventDispatcher_registerForEvent(self->gameState->eventDispatcher, ATOM_event_turn_ended,         turnEndedCallback, self);
	EventDispatcher_registerForEvent(self->gameState->eventDispatcher, ATOM_event_player_moved,       playSoundCallback, (void *) (uintptr_t) SOUND_PLAYER_MOVE);
	EventDispatcher_registerForEvent(self->gameState->eventDispatcher, ATOM_event_player_move_failed, playSoundCallback, (void *) (uintptr_t) SOUND_PLAYER_MOVE_FAILED);
	EventDispatcher_registerForEvent(self->gameState->eventDispatcher, ATOM_event_player_bumped_wall, playSoundCallback, (void *) (uintptr_t) SOUND_PLAYER_BUMP);
	EventDispatcher_registerForEvent(self->gameState->eventDispatcher, ATOM_event_tile_destroyed,     playSoundCallback, (void *) (uintptr_t) SOUND_WALL_BREAK);
	EventDispatcher_registerForEvent(self->gameState->eventDispatcher, ATOM_event_pickup_collected,   pickupCollectedCallback, self);
	EventDispatcher_registerForEvent(self->gameState->eventDispatcher, ATOM_event_game_state_saved,   playSoundCallback, (void *) (uintptr_t) SOUND_CHECKPOINT);
	EventDispatcher_registerForEvent(self->gameState->eventDispatcher, ATOM_event_block_pushed,       playSoundCallback, (void *) (uintptr_t) SOUND_PUSH);
	EventDispatcher_registerForEvent(self->gameState->eventDispatcher, ATOM_event_enemy_killed,       playSoundCallback, (void *) (uintptr_t) SOUND_ENEMY_DEATH);
	EventDispatcher_registerForEvent(self->gameState->eventDispatcher, ATOM_event_player_killed,      playSoundCallback, (void *) (uintptr_t) SOUND_PLAYER_DEATH);
	
	playMusic(self->gameState->rooms[self->gameState->currentRoomIndex]->roomData->musicID, true);
	return true;
}

void GameView_dispose(GameView * self) {
	EventDispatcher_unregisterAllForContext(self->gameState->eventDispatcher, self);
	EventDispatcher_unregisterAllForCallback(self->gameState->eventDispatcher, playSoundCallback);
	Renderable_dispose(self->roomRenderable);
	Renderable_dispose(self->entityRenderable);
	Renderable_dispose(self->visualEffectRenderable);
	VertexBuffer_dispose(self->roomVertexBuffer);
	call_virtual(dispose, self->roomShaderConfiguration);
	RoomVisual_dispose(self->roomVisual);
	if (self->roomTransition != NULL) {
		call_virtual(dispose, self->roomTransition);
	}
	for (unsigned int visualEffectIndex = 0; visualEffectIndex < self->visualEffectCount; visualEffectIndex++) {
		call_virtual(dispose, self->visualEffects[visualEffectIndex]);
	}
	free(self->visualEffects);
	call_super_virtual(dispose, self);
}

static Vector2f getRoomCameraPosition(GameView * self, unsigned int roomIndex) {
	RoomData * roomData = self->gameState->rooms[roomIndex]->roomData;
	Rect4f roomCameraBounds = {0.0f, roomData->size.x * TILE_WIDTH - DISPLAY_WIDTH, -((int) roomData->size.y - TILE_MAP_HEIGHT) * TILE_HEIGHT, roomData->size.y * TILE_HEIGHT - DISPLAY_HEIGHT - ((int) roomData->size.y - TILE_MAP_HEIGHT) * TILE_HEIGHT};
	if (roomCameraBounds.xMin > roomCameraBounds.xMax) {
		roomCameraBounds.xMin = roomCameraBounds.xMax = roomCameraBounds.xMin + (roomCameraBounds.xMax - roomCameraBounds.xMin) * 0.5f;
	}
	if (roomCameraBounds.yMin > roomCameraBounds.yMax) {
		roomCameraBounds.yMin = roomCameraBounds.yMax = roomCameraBounds.yMin + (roomCameraBounds.yMax - roomCameraBounds.yMin) * 0.5f;
	}
	Vector2f roomCameraPosition = VECTOR2f(self->gameState->player->positionComponent.position.x * TILE_WIDTH - DISPLAY_WIDTH / 2, self->gameState->player->positionComponent.position.y * TILE_HEIGHT + DISPLAY_HEIGHT / 2);
	return Rect4f_clampVector2f(roomCameraBounds, roomCameraPosition);
}

void GameView_update(GameView * self, double deltaTime) {
	self->animationReferenceTime += deltaTime;
	self->turnInterpolationRemainingTime = fmax(0.0, self->turnInterpolationRemainingTime - deltaTime);
	self->roomCameraPosition = Vector2f_round(getRoomCameraPosition(self, self->gameState->currentRoomIndex));
	
	Matrix4x4f matrix = MATRIX4x4f_IDENTITY;
	if (self->roomTransition != NULL) {
		RoomTransition_advance(self->roomTransition, deltaTime);
		if (RoomTransition_isDone(self->roomTransition)) {
			RoomTransition_dispose(self->roomTransition);
			self->roomTransition = NULL;
			writeRoomVisualToVertexBuffer(self);
			Matrix4x4f_translate(&matrix, V3f(-self->roomCameraPosition.x, -self->roomCameraPosition.y, 0.0f));
		} else {
			Vector2f cameraOffset = RoomTransition_getCameraOffset(self->roomTransition);
			Matrix4x4f_translate(&matrix, V3f(cameraOffset.x, cameraOffset.y, 0.0f));
		}
	} else {
		Matrix4x4f_translate(&matrix, V3f(-self->roomCameraPosition.x, -self->roomCameraPosition.y, 0.0f));
	}
	if (self->screenShakeTime > 0.0) {
		float radius = self->screenShakeTime / self->screenShakeMaxTime * self->screenShakeRadius;
		float angle = pcg32_frand(&g_pcgState, M_PI);
		self->screenShakeOffset = VECTOR2f(roundpositivef(cosf(angle) * radius), roundpositivef(sinf(angle) * radius));
		self->screenShakeTime -= deltaTime;
		if (self->screenShakeTime < 0.0) {
			self->screenShakeTime = 0.0;
		}
		Matrix4x4f_translate(&matrix, V3f(self->screenShakeOffset.x, self->screenShakeOffset.y, 0.0f));
	} else {
		self->screenShakeOffset = VECTOR2f_ZERO;
	}
	ShaderConfigurationRoom_setViewMatrix(self->roomShaderConfiguration, matrix);
	
	unsigned int visualEffectOffset = 0;
	for (unsigned int visualEffectIndex = 0; visualEffectIndex < self->visualEffectCount; visualEffectIndex++) {
		self->visualEffects[visualEffectIndex] = self->visualEffects[visualEffectIndex + visualEffectOffset];
		call_virtual(update, self->visualEffects[visualEffectIndex], deltaTime);
		if (call_virtual(isFinished, self->visualEffects[visualEffectIndex])) {
			call_virtual(dispose, self->visualEffects[visualEffectIndex]);
			visualEffectOffset++;
			self->visualEffectCount--;
			visualEffectIndex--;
		}
	}
}

void GameView_listRenderables(GameView * self, RenderableIO * renderableIO, int drawOrderOffset, Rect4i clipBounds) {
	RenderableIO_addRenderable(renderableIO, self->roomRenderable, 0, RECT4i_EMPTY);
	RenderableIO_addRenderable(renderableIO, self->entityRenderable, 0, RECT4i_EMPTY);
	RenderableIO_addRenderable(renderableIO, self->visualEffectRenderable, 0, RECT4i_EMPTY);
	for (unsigned int visualEffectIndex = 0; visualEffectIndex < self->visualEffectCount; visualEffectIndex++) {
		call_virtual(listRenderables, self->visualEffects[visualEffectIndex], renderableIO, drawOrderOffset, clipBounds);
	}
}

void GameView_beginRoomTransition(GameView * self, unsigned int fromRoomIndex) {
	Vector2f oldRoomCameraPosition = self->roomCameraPosition;
	RoomVisual * oldRoomVisual = self->roomVisual;
	self->roomVisual = RoomVisual_create(self->gameState->rooms[self->gameState->currentRoomIndex]);
	self->roomCameraPosition = getRoomCameraPosition(self, self->gameState->currentRoomIndex);
	Vector2i oldRoomPosition = self->gameState->rooms[fromRoomIndex]->roomData->zonePosition;
	Vector2i newRoomPosition = self->gameState->rooms[self->gameState->currentRoomIndex]->roomData->zonePosition;
	float interpolationStart = 0.0f;
	if (self->roomTransition != NULL) {
		interpolationStart = 1.0f - self->roomTransition->interpolation.currentProgress;
		RoomTransition_dispose(self->roomTransition);
	}
	self->roomTransition = RoomTransition_create(oldRoomVisual, self->roomVisual, VECTOR2f((newRoomPosition.x - oldRoomPosition.x) * TILE_WIDTH, (oldRoomPosition.y - newRoomPosition.y) * TILE_HEIGHT), oldRoomCameraPosition, self->roomCameraPosition, interpolationStart);
	writeRoomVisualToVertexBuffer(self);
	
	for (unsigned int visualEffectIndex = 0; visualEffectIndex < self->visualEffectCount; visualEffectIndex++) {
		call_virtual(dispose, self->visualEffects[visualEffectIndex]);
	}
	self->visualEffectCount = 0;
	self->previousRoomIndex = fromRoomIndex;
	playMusic(self->gameState->rooms[self->gameState->currentRoomIndex]->roomData->musicID, true);
}

bool GameView_mouseMoved(GameView * self, float x, float y, float deltaX, float deltaY, unsigned int modifiers, double referenceTime) {
	Vector2i mouseoverTile = VECTOR2i(floorf(x / TILE_WIDTH), floorf((DISPLAY_HEIGHT - y) / TILE_HEIGHT));
	if (mouseoverTile.x < 0 || mouseoverTile.y < 0 || mouseoverTile.x >= TILE_MAP_WIDTH || mouseoverTile.y >= TILE_MAP_HEIGHT) {
		mouseoverTile = VECTOR2i(INT_MIN, INT_MIN);
	}
	if (mouseoverTile.x != self->mouseoverTile.x || mouseoverTile.y != self->mouseoverTile.y) {
		self->mouseoverTile = mouseoverTile;
		return true;
	}
	return false;
}

void GameView_hideMouseover(GameView * self) {
	self->mouseoverTile = VECTOR2i(INT_MIN, INT_MIN);
}

void GameView_addVisualEffect(GameView * self, compat_type(VisualEffect *) visualEffect) {
	if (self->visualEffectCount >= self->visualEffectAllocatedCount) {
		self->visualEffectAllocatedCount *= 2;
		self->visualEffects = realloc(self->visualEffects, self->visualEffectAllocatedCount * sizeof(*self->visualEffects));
	}
	self->visualEffects[self->visualEffectCount++] = visualEffect;
}

void GameView_shakeScreen(GameView * self, double duration, float radius) {
	if (self->screenShakeTime == 0.0) {
		self->screenShakeTime = duration;
		self->screenShakeMaxTime = duration;
		self->screenShakeRadius = radius;
	} else {
		if (self->screenShakeTime < duration) {
			self->screenShakeTime = duration;
			self->screenShakeMaxTime = duration;
		}
		if (radius > self->screenShakeRadius) {
			self->screenShakeRadius = radius;
		}
	}
}

Vector2f GameView_getPlayerScreenPosition(GameView * self) {
	Vector2f cameraOffset = getCameraOffset(self);
	Vector2f position = worldToScreenPosition(self->gameState->player->positionComponent.position);
	return VECTOR2f(position.x + cameraOffset.x, position.y + cameraOffset.y);
}

Rect4f GameView_getVisibleScreenBounds(GameView * self) {
	Vector2f cameraOffset = Vector2f_inverted(getCameraOffset(self));
	return Rect4f_fromPositionAndSize(VECTOR2f_TRUNCATE(cameraOffset), VECTOR2f(DISPLAY_WIDTH, DISPLAY_HEIGHT));
}
