// Emacs style mode select   -*- C++ -*-
//-----------------------------------------------------------------------------
//
// $Id: c2ef3515321bae440ce05ac896c6d750c72224e9 $
//
// Copyright (C) 1998-2006 by Randy Heit (ZDoom).
// Copyright (C) 2006-2026 by The Odamex Team.
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// DESCRIPTION:
//   Handle the loading of multi kill data from SPREEDEF,
//   as well as static functions to handle players'
//   multikills events.
//
//-----------------------------------------------------------------------------

#include "odamex.h"

#include "p_mobj.h"
#include "p_local.h"
#include "s_sound.h"

#include "g_multikill.h"
#include "m_ostring.h"

MultiKillManager& MultiKillManager::getInstance()
{
	static MultiKillManager instance;
	return instance;
}

MultiKillManager::~MultiKillManager()
{
	reset();
}

MultiKillManager::MultiKillManager()
{
	multiTimeInterval = 4 * TICRATE;
	emptyLevel = MultiKillLevel_s();
	emptyTics = MultiKillTics_s();
}

void MultiKillManager::reset()
{
	multiKillLevels.clear();
	mutliKillPlayerDict.clear();
	multiTimeInterval = 4 * TICRATE;
}

void MultiKillManager::loadMultiKillDefaults()
{
	multiKillLevels.clear();
	MultiKillLevel_s emptylevel = {"", "", CR_GRAY};
	// First 2 levels (0 and 1), we insert empty levels as they're not multi kills.
	multiKillLevels.push_back(emptylevel); // 0
	multiKillLevels.push_back(emptylevel); // 1

	// Next, we input the next 9 levels with the default text. We don't use LANGUAGE tokens
	// since some people won't have an updated WAD.
	multiKillLevels.push_back({"Double Kill!", "", CR_WHITE});      // 2
	multiKillLevels.push_back({"Triple Kill!", "", CR_TAN});        // 3
	multiKillLevels.push_back({"Multi Kill!",  "", CR_BLUE});       // 4
	multiKillLevels.push_back({"Ultra Kill!",  "", CR_BRICK});      // 5
	multiKillLevels.push_back({"Overkill!",    "", CR_CYAN});       // 6
	multiKillLevels.push_back({"Mega Kill!",   "", CR_CREAM});      // 7
	multiKillLevels.push_back({"Monster Kill!","", CR_ORANGE});     // 8
	multiKillLevels.push_back({"Mythic Kill!", "", CR_PURPLE});     // 9
	multiKillLevels.push_back({"Killionaire!", "", CR_DARKGREEN});  // 10
	multiKillLevels.push_back({"Terminator!",  "", CR_GOLD});       // 11

	multiTimeInterval = 4 * TICRATE;
}

int MultiKillManager::getMultiKillInterval()
{
	return multiTimeInterval;
}

int MultiKillManager::getHighestMultiKillLevel()
{
	return multiKillLevels.size() - 1;
}

const MultiKillLevel_s& MultiKillManager::getMultiKillLevel(const int level)
{
	int newlevel = level;
	if (getHighestMultiKillLevel() <= 0)
		return emptyLevel;


	if (level >= multiKillLevels.size())
		newlevel = multiKillLevels.size() - 1;

	return multiKillLevels.at(newlevel);
}

void MultiKillManager::setMultiKillLevels(const std::vector<MultiKillLevel_s> multikills,
                                          const int newinterval)
{
	multiKillLevels = multikills;
	multiTimeInterval = newinterval * TICRATE;
}

// ==========================================================
// Multi kill bookkeeping functions start here.
// ==========================================================

const MultiKillTics_s& MultiKillManager::getMultiKills(const int playerid)
{
	if (mutliKillPlayerDict.find(playerid) == mutliKillPlayerDict.end())
	{
		return emptyTics;
	}
	else
	{
		return mutliKillPlayerDict[playerid];
	}
}

void MultiKillManager::addKill(const int playerid)
{
	MultiKillTics_s status = MultiKillTics_s();
	status.lastKillTime = ::gametic;
	status.ticsRemaining = multiTimeInterval;

	if (mutliKillPlayerDict.find(playerid) == mutliKillPlayerDict.end())
	{
		status.multiKills = 1;
		mutliKillPlayerDict[playerid] = status;
	}
	else
	{
		status.multiKills = mutliKillPlayerDict[playerid].multiKills + 1;
		mutliKillPlayerDict[playerid] = status;
	}
}

void MultiKillManager::ticPlayerMultiKill(const int playerid)
{
	if (mutliKillPlayerDict.find(playerid) == mutliKillPlayerDict.end())
	{
		return;
	}

	MultiKillTics_s& status = mutliKillPlayerDict[playerid];

	if (status.lastKillTime > ::gametic)
	{
		// We've rewinded in a demo and this multi kill
		// doesn't exist yet. Remove it.
		eraseMultiKills(playerid);
		return;
	
	}

	if (status.ticsRemaining)
	{
		status.ticsRemaining--;

		// We out of tics?
		if (status.ticsRemaining == 0)
		{
			// Remove the current multi kill as well.
			status.multiKills = 0;
		}
	}
}

void MultiKillManager::eraseMultiKills(const int playerid)
{
	if (mutliKillPlayerDict.find(playerid) == mutliKillPlayerDict.end())
	{
		return;
	}

	mutliKillPlayerDict.erase(playerid);
}

void MultiKillManager::clearMultiTics()
{
	mutliKillPlayerDict.clear();
}

// ==========================================================
// Static functions start here.
// ==========================================================

#ifdef CLIENT_APP
EXTERN_CVAR(cl_showmultikills)
EXTERN_CVAR(cl_showofflinemultikills)
#endif

EXTERN_CVAR(sv_showmultikills)

void P_ProcessMultiKills(const AActor* source, const player_t* target)
{
	MultiKillManager& manager = MultiKillManager::getInstance();

	if (target)
	{
		manager.eraseMultiKills(target->id);
	}

	if (!source || !source->player)
		return;

	manager.addKill(source->player->id);

	// Now get the player's new multi kill total
	// To see what sound we should play, if any.
	const MultiKillTics_s& status = manager.getMultiKills(source->player->id);

	#ifdef CLIENT_APP
	// Don't announce multi kills if the client has showing them disabled
	if (!cl_showmultikills || (!cl_showofflinemultikills && !network_game) || (!sv_showmultikills && network_game))
		return;
	#endif

	if (displayplayer_id == source->player->id && status.multiKills > 1)
	{
		const MultiKillLevel_s& level = manager.getMultiKillLevel(status.multiKills);
		// Play the gamesfx sound from the multi kill first.
		if (!level.gameSfxToken.empty() && S_FindSound(level.gameSfxToken.c_str()) != -1)
			S_Sound(CHAN_GAMEINFO, level.gameSfxToken.c_str(), 1, ATTN_NONE);

		// Play the announcer sound for the new multi kill
		//S_Sound(CHAN_ANNOUNCER, '', 1, ATTN_NONE);
	}
}

void P_TicMultiKill(const player_t& player)
{
	if (!validplayer(player))
		return;

	MultiKillManager::getInstance().ticPlayerMultiKill(player.id);
}
