#include "floatingnumbers.h"

#include <cstdint>
#include <ctime>
#include <deque>
#include <fmt/format.h>
#include <string>

#include "engine/render/text_render.hpp"
#include "options.h"
#include "utils/str_cat.hpp"

namespace devilution {

namespace {

struct FloatingNumber {
	Point startPos;
	Displacement startOffset;
	Displacement endOffset;
	std::string text;
	uint32_t time;
	uint32_t lastMerge;
	UiFlags style;
	DamageType type;
	int value;
	int index;
	bool reverseDirection;
};

std::deque<FloatingNumber> FloatingQueue;

void ClearExpiredNumbers()
{
	while (!FloatingQueue.empty()) {
		FloatingNumber &num = FloatingQueue.front();
		if (num.time > SDL_GetTicks())
			break;

		FloatingQueue.pop_front();
	}
}

GameFontTables GetGameFontSizeByDamage(int value)
{
	value >>= 6;
	if (value >= 300)
		return GameFont30;
	if (value >= 100)
		return GameFont24;
	return GameFont12;
}

UiFlags GetFontSizeByDamage(int value)
{
	value >>= 6;
	if (value >= 300)
		return UiFlags::FontSize30;
	if (value >= 100)
		return UiFlags::FontSize24;
	return UiFlags::FontSize12;
}

void UpdateFloatingData(FloatingNumber &num)
{
	if (num.value > 0 && num.value < 64) {
		num.text = fmt::format("{:.2f}", num.value / 64.0);
	} else {
		num.text = StrCat(num.value >> 6);
	}

	num.style &= ~(UiFlags::FontSize12 | UiFlags::FontSize24 | UiFlags::FontSize30);
	num.style |= GetFontSizeByDamage(num.value);

	switch (num.type) {
	case DamageType::Physical:
		num.style |= UiFlags::ColorGold;
		break;
	case DamageType::Fire:
		num.style |= UiFlags::ColorUiSilver; // UiSilver appears dark red ingame
		break;
	case DamageType::Lightning:
		num.style |= UiFlags::ColorBlue;
		break;
	case DamageType::Magic:
		num.style |= UiFlags::ColorOrange;
		break;
	case DamageType::Acid:
		num.style |= UiFlags::ColorYellow;
		break;
	}
}

void AddFloatingNumber(Point pos, Displacement offset, DamageType type, int value, int index, bool damageToPlayer)
{
	// 45 deg angles to avoid jitter caused by px alignment
	Displacement goodAngles[] = {
		{ 0, -140 },
		{ 100, -100 },
		{ -100, -100 },
	};

	Displacement endOffset;
	if (*sgOptions.Gameplay.enableFloatingNumbers == FloatingNumbers::Random) {
		endOffset = goodAngles[rand() % 3];
	} else if (*sgOptions.Gameplay.enableFloatingNumbers == FloatingNumbers::Vertical) {
		endOffset = goodAngles[0];
	}

	if (damageToPlayer)
		endOffset = -endOffset;

	for (auto &num : FloatingQueue) {
		if (num.reverseDirection == damageToPlayer && num.type == type && num.index == index && (SDL_GetTicks() - static_cast<int>(num.lastMerge)) <= 100) {
			num.value += value;
			num.lastMerge = SDL_GetTicks();
			UpdateFloatingData(num);
			return;
		}
	}
	FloatingNumber num {
		pos, offset, endOffset, "", SDL_GetTicks() + 2500, SDL_GetTicks(), UiFlags::Outlined, type, value, index, damageToPlayer
	};
	UpdateFloatingData(num);
	FloatingQueue.push_back(num);
}

} // namespace

void AddFloatingNumber(DamageType damageType, const Monster &monster, int damage)
{
	if (*sgOptions.Gameplay.enableFloatingNumbers == FloatingNumbers::Off)
		return;

	Displacement offset = {};
	if (monster.isWalking()) {
		offset = GetOffsetForWalking(monster.animInfo, monster.direction);
		if (monster.mode == MonsterMode::MoveSideways) {
			if (monster.direction == Direction::West)
				offset -= Displacement { 64, 0 };
			else
				offset += Displacement { 64, 0 };
		}
	}
	if (monster.animInfo.sprites) {
		const ClxSprite sprite = monster.animInfo.currentSprite();
		offset.deltaY -= sprite.height() / 2;
	}

	AddFloatingNumber(monster.position.tile, offset, damageType, damage, monster.getId(), false);
}

void AddFloatingNumber(DamageType damageType, const Player &player, int damage)
{
	if (*sgOptions.Gameplay.enableFloatingNumbers == FloatingNumbers::Off)
		return;

	Displacement offset = {};
	if (player.isWalking()) {
		offset = GetOffsetForWalking(player.AnimInfo, player._pdir);
		if (player._pmode == PM_WALK_SIDEWAYS) {
			if (player._pdir == Direction::West)
				offset -= Displacement { 64, 0 };
			else
				offset += Displacement { 64, 0 };
		}
	}

	AddFloatingNumber(player.position.tile, offset, damageType, damage, player.getId(), true);
}

void DrawFloatingNumbers(const Surface &out, Point viewPosition, Displacement offset)
{
	if (*sgOptions.Gameplay.enableFloatingNumbers == FloatingNumbers::Off)
		return;

	for (auto &floatingNum : FloatingQueue) {
		Displacement worldOffset = viewPosition - floatingNum.startPos;
		worldOffset = worldOffset.worldToScreen() + offset + Displacement { TILE_WIDTH / 2, -TILE_HEIGHT / 2 } + floatingNum.startOffset;

		if (*sgOptions.Graphics.zoom) {
			worldOffset *= 2;
		}

		Point screenPosition { worldOffset.deltaX, worldOffset.deltaY };

		int lineWidth = GetLineWidth(floatingNum.text, GetGameFontSizeByDamage(floatingNum.value));
		screenPosition.x -= lineWidth / 2;
		uint32_t timeLeft = floatingNum.time - SDL_GetTicks();
		float mul = 1 - (timeLeft / 2500.0f);
		screenPosition += floatingNum.endOffset * mul;

		DrawString(out, floatingNum.text, Rectangle { screenPosition, { lineWidth, 0 } }, floatingNum.style);
	}

	ClearExpiredNumbers();
}

void ClearFloatingNumbers()
{
	srand(time(nullptr));

	FloatingQueue.clear();
}

} // namespace devilution
