#pragma once

#ifdef GAMEQC
#include <common/mutators/base.qh>

REGISTER_MUTATOR(status_effects, true);
#endif

#include "all.qh"

#ifdef GAMEQC
/** Entity statuseffects */
.StatusEffect statuseffects;
/** Player statuseffects storage (holds previous state) */
.StatusEffect statuseffects_store;

REGISTER_NET_LINKED(ENT_CLIENT_STATUSEFFECTS)

const int StatusEffects_groups_minor = 8; // must be a multiple of 8 (one byte) to optimize bandwidth usage
const int StatusEffects_groups_major = 4; // must be >= ceil(REGISTRY_COUNT(StatusEffects) / StatusEffects_groups_minor)
#endif

// no need to perform these checks on both server and client
#ifdef CSQC
STATIC_INIT(StatusEffects)
{
	if (StatusEffects_groups_minor / 8 != floor(StatusEffects_groups_minor / 8))
		error("StatusEffects_groups_minor is not a multiple of 8.");
	int min_major_value = ceil(REGISTRY_COUNT(StatusEffects) / StatusEffects_groups_minor);
	if (StatusEffects_groups_major < min_major_value)
		error(sprintf("StatusEffects_groups_major can not be < %d.", min_major_value));
}
#endif

#ifdef SVQC
#define G_MAJOR(id) (floor((id) / StatusEffects_groups_minor))
#define G_MINOR(id) ((id) % StatusEffects_groups_minor)
#endif

#ifdef CSQC
StatusEffect g_statuseffects;
void StatusEffects_entremove(entity this)
{
    if(g_statuseffects == this)
        g_statuseffects = NULL;
}

NET_HANDLE(ENT_CLIENT_STATUSEFFECTS, bool isnew)
{
    make_pure(this);
    g_statuseffects = this;
    this.entremove = StatusEffects_entremove;
    const int majorBits = Readbits(StatusEffects_groups_major);
    for (int i = 0; i < StatusEffects_groups_major; ++i) {
        if (!(majorBits & BIT(i))) {
            continue;
        }
        const int minorBits = Readbits(StatusEffects_groups_minor);
        for (int j = 0; j < StatusEffects_groups_minor; ++j) {
            if (!(minorBits & BIT(j))) {
                continue;
            }
            const StatusEffect it = REGISTRY_GET(StatusEffects, StatusEffects_groups_minor * i + j);
            this.statuseffect_time[it.m_id] = ReadFloat();
            this.statuseffect_flags[it.m_id] = ReadByte();
        }
    }
    return true;
}
#endif

#ifdef SVQC
int SEFminorBitsArr[StatusEffects_groups_major];
void StatusEffects_Write(StatusEffect data, StatusEffect store)
{
    if (!data) {
        WriteShort(MSG_ENTITY, 0);
        return;
    }
    TC(StatusEffect, data);

	for (int i = 0; i < StatusEffects_groups_major; ++i)
		SEFminorBitsArr[i] = 0;

    int majorBits = 0;
    FOREACH(StatusEffects, true, {
        .float fld = statuseffect_time[it.m_id];
        .int flg = statuseffect_flags[it.m_id];
        const bool changed = (store.(fld) != data.(fld) || store.(flg) != data.(flg));
        store.(fld) = data.(fld);
        store.(flg) = data.(flg);
        if (changed) {
			int maj = G_MAJOR(it.m_id);
			majorBits = BITSET(majorBits, BIT(maj), true);
			SEFminorBitsArr[maj] = BITSET(SEFminorBitsArr[maj], BIT(G_MINOR(it.m_id)), true);
        }
    });

	Writebits(MSG_ENTITY, majorBits, StatusEffects_groups_major);
	for (int i = 0; i < StatusEffects_groups_major; ++i)
	{
		if (!(majorBits & BIT(i)))
			continue;

		const int minorBits = SEFminorBitsArr[i];
		Writebits(MSG_ENTITY, minorBits, StatusEffects_groups_minor);
		for (int j = 0; j < StatusEffects_groups_minor; ++j)
		{
			if (!(minorBits & BIT(j)))
				continue;

			const entity it = REGISTRY_GET(StatusEffects, StatusEffects_groups_minor * i + j);
            WriteFloat(MSG_ENTITY, data.statuseffect_time[it.m_id]);
			WriteByte(MSG_ENTITY, data.statuseffect_flags[it.m_id]);
		}
	}
}
#endif

#undef G_MAJOR
#undef G_MINOR

#ifdef SVQC
bool StatusEffects_Send(StatusEffect this, entity to, int sf)
{
    TC(StatusEffect, this);
    WriteHeader(MSG_ENTITY, ENT_CLIENT_STATUSEFFECTS);
    StatusEffects_Write(this, to.statuseffects_store);
    return true;
}

bool StatusEffects_customize(entity this, entity client)
{
    // sends to spectators too!
    return (client.statuseffects == this);
}

void StatusEffects_new(entity this)
{
    StatusEffect eff = NEW(StatusEffect);
	this.statuseffects = eff;
	eff.owner = this;
    if(this.statuseffects_store)
    {
        setcefc(eff, StatusEffects_customize);
        Net_LinkEntity(eff, false, 0, StatusEffects_Send);
    }
}

void StatusEffects_delete(entity e) { delete(e.statuseffects); e.statuseffects = NULL; }
// may be called on non-player entities, should be harmless!

void StatusEffects_update(entity e) { e.statuseffects.SendFlags = 0xFFFFFF; }

// this clears the storage entity instead of the statuseffects object, useful for map resets and such
void StatusEffects_clearall(entity store)
{
    if(!store)
        return; // safety net
    // NOTE: you will need to perform StatusEffects_update after this to update the storage entity
    // (unless store is the storage entity)
    FOREACH(StatusEffects, true, {
        store.statuseffect_time[it.m_id] = 0;
        store.statuseffect_flags[it.m_id] = 0;
    });
}

void StatusEffectsStorage_attach(entity e) { e.statuseffects_store = NEW(StatusEffect); e.statuseffects_store.drawonlytoclient = e; }
void StatusEffectsStorage_delete(entity e) { delete(e.statuseffects_store); e.statuseffects_store = NULL; }

// called when an entity is deleted with delete() / remove()
// or when a player disconnects
void ONREMOVE(entity this)
{
    // remove statuseffects object attached to 'this'
    if(this.statuseffects && this.statuseffects.owner == this)
        StatusEffects_delete(this);
}
#endif

#ifdef GAMEQC
bool StatusEffects_active(StatusEffect this, entity actor);

// runs every SV_StartFrame on the server
// called by HUD_Powerups_add on the client
void StatusEffects_tick(entity actor);

// accesses the status effect timer, returns 0 if the effect isn't active
// pass g_statuseffects as the actor on client side
// pass the entity with a .statuseffects on server side
float StatusEffects_gettime(StatusEffect this, entity actor);
#endif
#ifdef SVQC
// call when applying the effect to an entity
void StatusEffects_apply(StatusEffect this, entity actor, float eff_time, int eff_flags);

// copies all the status effect fields to the specified storage entity
// does not perform an update
void StatusEffects_copy(StatusEffect this, entity store, float time_offset);

// call when removing the effect
void StatusEffects_remove(StatusEffect this, entity actor, int removal_type);

void StatusEffects_removeall(entity actor, int removal_type);
#endif
