//-------------------------------------------------------------------------
/*
Copyright (C) 2010-2019 EDuke32 developers and contributors
Copyright (C) 2019 Nuke.YKT
Copyright (C) 2020 - Christoph Oelckers

This file is part of Raze

Raze is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License version 2
as published by the Free Software Foundation.

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.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
*/
//-------------------------------------------------------------------------

#include "ns.h"
#include "filesystem.h"
#include "tarray.h"

#include "build.h"

#include "blood.h"
#include "files.h"
#include "eventq.h"
#include "callback.h"


BEGIN_BLD_NS

static void (*seqClientCallback[])(int, DBloodActor*) = {
	FireballSeqCallback,
	Fx33Callback,
	NapalmSeqCallback,
	Fx32Callback,
	TreeToGibCallback,
	DudeToGibCallback1,
	DudeToGibCallback2,
	batBiteSeqCallback,
	SlashSeqCallback,
	StompSeqCallback,
	eelBiteSeqCallback,
	BurnSeqCallback,
	SeqAttackCallback,
	cerberusBiteSeqCallback,
	cerberusBurnSeqCallback,
	cerberusBurnSeqCallback2,
	TommySeqCallback,
	TeslaSeqCallback,
	ShotSeqCallback,
	cultThrowSeqCallback,
	cultThrowSeqCallback2,
	cultThrowSeqCallback3,
	SlashFSeqCallback,
	ThrowFSeqCallback,
	ThrowSSeqCallback,
	BlastSSeqCallback,
	ghostSlashSeqCallback,
	ghostThrowSeqCallback,
	ghostBlastSeqCallback,
	GillBiteSeqCallback,
	HandJumpSeqCallback,
	houndBiteSeqCallback,
	houndBurnSeqCallback,
	podAttack,
	sub_70284,
	sub_6FF08,
	sub_6FF54,
	ratBiteSeqCallback,
	SpidBiteSeqCallback,
	SpidJumpSeqCallback,
	SpidBirthSeqCallback,
	tchernobogBurnSeqCallback,
	tchernobogBurnSeqCallback2,
	sub_71A90,
	genDudeAttack1,
	punchCallback,
	ThrowCallback1,
	ThrowCallback2,
	HackSeqCallback,
	StandSeqCallback,
	zombfHackSeqCallback,
	PukeSeqCallback,
	ThrowSeqCallback,
	PlayerSurvive,
	PlayerKneelsOver,
	FireballTrapSeqCallback,
	MGunFireSeqCallback,
	MGunOpenSeqCallback,
};


//---------------------------------------------------------------------------
//
//
//
//---------------------------------------------------------------------------

void Seq::Precache(int palette)
{
	if (memcmp(signature, "SEQ\x1a", 4) != 0)
		I_Error("Invalid sequence");
	if ((version & 0xff00) != 0x300)
		I_Error("Obsolete sequence version");
	for (int i = 0; i < nFrames; i++)
		tilePrecacheTile(seqGetTile(&frames[i]), -1, palette);
}

void seqPrecacheId(int id, int palette)
{
	auto pSeq = getSequence(id);
	if (pSeq) pSeq->Precache(palette);
}

//---------------------------------------------------------------------------
//
//
//
//---------------------------------------------------------------------------

void UpdateCeiling(sectortype* pSector, SEQFRAME* pFrame)
{
	pSector->setceilingtexture(seqGetTexture(pFrame));
	pSector->ceilingshade = pFrame->shade;
	if (pFrame->palette)
		pSector->ceilingpal = pFrame->palette;
}

//---------------------------------------------------------------------------
//
//
//
//---------------------------------------------------------------------------

void UpdateFloor(sectortype* pSector, SEQFRAME* pFrame)
{
	pSector->setfloortexture(seqGetTexture(pFrame));
	pSector->floorshade = pFrame->shade;
	if (pFrame->palette)
		pSector->floorpal = pFrame->palette;
}

//---------------------------------------------------------------------------
//
//
//
//---------------------------------------------------------------------------

void UpdateWall(walltype* pWall, SEQFRAME* pFrame)
{
	assert(pWall->hasX());
	pWall->setwalltexture(seqGetTexture(pFrame));
	if (pFrame->palette)
		pWall->pal = pFrame->palette;
	if (pFrame->transparent)
		pWall->cstat |= CSTAT_WALL_TRANSLUCENT;
	else
		pWall->cstat &= ~CSTAT_WALL_TRANSLUCENT;
	if (pFrame->transparent2)
		pWall->cstat |= CSTAT_WALL_TRANS_FLIP;
	else
		pWall->cstat &= ~CSTAT_WALL_TRANS_FLIP;
	if (pFrame->blockable)
		pWall->cstat |= CSTAT_WALL_BLOCK;
	else
		pWall->cstat &= ~CSTAT_WALL_BLOCK;
	if (pFrame->hittable)
		pWall->cstat |= CSTAT_WALL_BLOCK_HITSCAN;
	else
		pWall->cstat &= ~CSTAT_WALL_BLOCK_HITSCAN;
}

//---------------------------------------------------------------------------
//
//
//
//---------------------------------------------------------------------------

void UpdateMasked(walltype* pWall, SEQFRAME* pFrame)
{
	assert(pWall->hasX());
	walltype* pWallNext = pWall->nextWall();
	auto texid = seqGetTexture(pFrame);
	pWall->setovertexture(texid);
	pWallNext->setovertexture(texid);
	if (pFrame->palette)
		pWall->pal = pWallNext->pal = pFrame->palette;
	if (pFrame->transparent)
	{
		pWall->cstat |= CSTAT_WALL_TRANSLUCENT;
		pWallNext->cstat |= CSTAT_WALL_TRANSLUCENT;
	}
	else
	{
		pWall->cstat &= ~CSTAT_WALL_TRANSLUCENT;
		pWallNext->cstat &= ~CSTAT_WALL_TRANSLUCENT;
	}
	if (pFrame->transparent2)
	{
		pWall->cstat |= CSTAT_WALL_TRANS_FLIP;
		pWallNext->cstat |= CSTAT_WALL_TRANS_FLIP;
	}
	else
	{
		pWall->cstat &= ~CSTAT_WALL_TRANS_FLIP;
		pWallNext->cstat &= ~CSTAT_WALL_TRANS_FLIP;
	}
	if (pFrame->blockable)
	{
		pWall->cstat |= CSTAT_WALL_BLOCK;
		pWallNext->cstat |= CSTAT_WALL_BLOCK;
	}
	else
	{
		pWall->cstat &= ~CSTAT_WALL_BLOCK;
		pWallNext->cstat &= ~CSTAT_WALL_BLOCK;
	}
	if (pFrame->hittable)
	{
		pWall->cstat |= CSTAT_WALL_BLOCK_HITSCAN;
		pWallNext->cstat |= CSTAT_WALL_BLOCK_HITSCAN;
	}
	else
	{
		pWall->cstat &= ~CSTAT_WALL_BLOCK_HITSCAN;
		pWallNext->cstat &= ~CSTAT_WALL_BLOCK_HITSCAN;
	}
}

//---------------------------------------------------------------------------
//
//
//
//---------------------------------------------------------------------------

void UpdateSprite(DBloodActor* actor, SEQFRAME* pFrame)
{
	assert(actor->hasX());
	if (actor->spr.flags & 2)
	{
		auto atex = TexMan.GetGameTexture(actor->spr.spritetexture());
		auto stex = TexMan.GetGameTexture(seqGetTexture(pFrame));
		if (atex->GetDisplayHeight() != stex->GetDisplayHeight() || atex->GetDisplayTopOffset() != stex->GetDisplayTopOffset()
			|| (pFrame->scaley && pFrame->scaley != int(actor->spr.scale.Y * INV_REPEAT_SCALE)))
			actor->spr.flags |= 4;
	}
	actor->spr.picnum = seqGetTile(pFrame);
	if (pFrame->palette)
		actor->spr.pal = pFrame->palette;
	actor->spr.shade = pFrame->shade;

	int scale = actor->xspr.scale; // SEQ size scaling
	if (pFrame->scalex) 
	{
		int s;
		if (scale) s = ClipRange(MulScale(pFrame->scalex, scale, 8), 0, 255);
		else s = pFrame->scalex;
		actor->spr.scale.X = (s * REPEAT_SCALE);
	}

	if (pFrame->scaley) {
		int s;
		if (scale) s = ClipRange(MulScale(pFrame->scaley, scale, 8), 0, 255);
		else s = pFrame->scaley;
		actor->spr.scale.Y = (s * REPEAT_SCALE);
	}

	if (pFrame->transparent)
		actor->spr.cstat |= CSTAT_SPRITE_TRANSLUCENT;
	else
		actor->spr.cstat &= ~CSTAT_SPRITE_TRANSLUCENT;
	if (pFrame->transparent2)
		actor->spr.cstat |= CSTAT_SPRITE_TRANS_FLIP;
	else
		actor->spr.cstat &= ~CSTAT_SPRITE_TRANS_FLIP;
	if (pFrame->blockable)
		actor->spr.cstat |= CSTAT_SPRITE_BLOCK;
	else
		actor->spr.cstat &= ~CSTAT_SPRITE_BLOCK;
	if (pFrame->hittable)
		actor->spr.cstat |= CSTAT_SPRITE_BLOCK_HITSCAN;
	else
		actor->spr.cstat &= ~CSTAT_SPRITE_BLOCK_HITSCAN;
	if (pFrame->invisible)
		actor->spr.cstat |= CSTAT_SPRITE_INVISIBLE;
	else
		actor->spr.cstat &= ~CSTAT_SPRITE_INVISIBLE;
	if (pFrame->pushable)
		actor->spr.cstat |= CSTAT_SPRITE_BLOOD_BIT1;
	else
		actor->spr.cstat &= ~CSTAT_SPRITE_BLOOD_BIT1;
	if (pFrame->smoke)
		actor->spr.flags |= 256;
	else
		actor->spr.flags &= ~256;
	if (pFrame->aiming)
		actor->spr.flags |= 8;
	else
		actor->spr.flags &= ~8;
	if (pFrame->flipx)
		actor->spr.flags |= 1024;
	else
		actor->spr.flags &= ~1024;
	if (pFrame->flipy)
		actor->spr.flags |= 2048;
	else
		actor->spr.flags &= ~2048;
}

//---------------------------------------------------------------------------
//
//
//
//---------------------------------------------------------------------------

void SEQINST::Update()
{
	assert(frameIndex < pSequence->nFrames);
	switch (type)
	{
	case SS_WALL:
		assert(target.isWall());
		UpdateWall(target.wall(), &pSequence->frames[frameIndex]);
		break;
	case SS_CEILING:
		assert(target.isSector());
		UpdateCeiling(target.sector(), &pSequence->frames[frameIndex]);
		break;
	case SS_FLOOR:
		assert(target.isSector());
		UpdateFloor(target.sector(), &pSequence->frames[frameIndex]);
		break;
	case SS_SPRITE:
	{
		assert(target.isActor());
		auto actor = target.actor();
		if (!actor) break;
		UpdateSprite(actor, &pSequence->frames[frameIndex]);
		if (pSequence->frames[frameIndex].playsound) {

			int sound = pSequence->soundId;

			// by NoOne: add random sound range feature
			if (!VanillaMode() && pSequence->frames[frameIndex].soundRange > 0)
				sound += Random(((pSequence->frames[frameIndex].soundRange == 1) ? 2 : pSequence->frames[frameIndex].soundRange));

			sfxPlay3DSound(actor, sound, -1, 0);
		}


		// by NoOne: add surfaceSound trigger feature
		if (!VanillaMode() && pSequence->frames[frameIndex].surfaceSound && actor->vel.Z == 0 && actor->vel.X != 0) {

			if (actor->sector()->upperLink) break; // don't play surface sound for stacked sectors
			int surf = tprops[actor->sector()->floortexture()].surfType;
			if (!surf) break;
			static int surfSfxMove[15][4] = {
				/* {snd1, snd2, gameVolume, myVolume} */
				{800,801,80,25},
				{802,803,80,25},
				{804,805,80,25},
				{806,807,80,25},
				{808,809,80,25},
				{810,811,80,25},
				{812,813,80,25},
				{814,815,80,25},
				{816,817,80,25},
				{818,819,80,25},
				{820,821,80,25},
				{822,823,80,25},
				{824,825,80,25},
				{826,827,80,25},
				{828,829,80,25},
			};

			int sndId = surfSfxMove[surf][Random(2)];
			auto snd = soundEngine->FindSoundByResID(sndId);
			if (snd.isvalid())
			{
				auto udata = soundEngine->GetUserData(snd);
				int relVol = udata ? udata[2] : 255;
				sfxPlay3DSoundCP(actor, sndId, -1, 0, 0, (surfSfxMove[surf][2] != relVol) ? relVol : surfSfxMove[surf][3]);
			}
		}
		break;
	}
	case SS_MASKED:
		assert(target.isWall());
		UpdateMasked(target.wall(), &pSequence->frames[frameIndex]);
		break;
	}

	// all seq callbacks are for sprites, but there's no sanity checks here that what gets passed is meant to be for a sprite...
	if (pSequence->frames[frameIndex].trigger && callback != -1)
	{
		assert(type == SS_SPRITE);
		if (target.isActor()) seqClientCallback[callback](type, target.actor());
	}

}

//---------------------------------------------------------------------------
//
//
//
//---------------------------------------------------------------------------

struct ActiveList
{
	TArray<SEQINST> list;

	void clear() { list.Clear(); }
	int getSize() { return list.Size(); }
	SEQINST* getInst(int num) { return &list[num]; }
	EventObject getElement(int num) { return list[num].target; }
	//DBloodActor* getActor(int num) { return list[num].actor; }
	int getType(int num) { return list[num].type; }

	void remove(int num)
	{
		list[num] = list.Last();
		list.Pop();
	}

	SEQINST* getNew()
	{
		list.Reserve(1);
		return &list.Last();
	}

	SEQINST* get(int type, const EventObject& eob)
	{
		for (auto& n : list)
		{
			if (n.type == type && n.target == eob) return &n;
		}
		return nullptr;
	}

	SEQINST* get(DBloodActor* actor)
	{
		return get(SS_SPRITE, EventObject(actor));
	}

	void remove(int type, const EventObject& eob)
	{
		for (unsigned i = 0; i < list.Size(); i++)
		{
			auto& n = list[i];
			if (n.type == type && n.target == eob)
			{
				remove(i);
				return;
			}
		}
	}

	void remove(DBloodActor* actor)
	{
		remove(SS_SPRITE, EventObject(actor));
	}

};

static ActiveList activeList;

//---------------------------------------------------------------------------
//
//
//
//---------------------------------------------------------------------------

SEQINST* GetInstance(int type, const EventObject& eob)
{
	return activeList.get(type, eob);
}

SEQINST* GetInstance(DBloodActor* actor)
{
	return activeList.get(actor);
}

void seqKill(int type, const EventObject& nIndex)
{
	activeList.remove(type, nIndex);
}

void seqKillAll()
{
	activeList.clear();
}

void seqKill(int type, sectortype* actor)
{
	activeList.remove(type, EventObject(actor));
}

void seqKill(int type, walltype* actor)
{
	activeList.remove(type, EventObject(actor));
}

void seqKill(DBloodActor* actor)
{
	activeList.remove(actor);
}


//---------------------------------------------------------------------------
//
//
//
//---------------------------------------------------------------------------

static void ByteSwapSEQ(Seq* pSeq)
{
	pSeq->version = LittleShort(pSeq->version);
	pSeq->nFrames = LittleShort(pSeq->nFrames);
	pSeq->ticksPerFrame = LittleShort(pSeq->ticksPerFrame);
	pSeq->soundId = LittleShort(pSeq->soundId);
	pSeq->flags = LittleLong(pSeq->flags);
	for (int i = 0; i < pSeq->nFrames; i++)
	{
		SEQFRAME* pFrame = &pSeq->frames[i];
		BitReader bitReader((uint8_t*)pFrame, sizeof(SEQFRAME));
		SEQFRAME swapFrame;
		swapFrame.tile = bitReader.readUnsigned(12);
		swapFrame.transparent = bitReader.readBit();
		swapFrame.transparent2 = bitReader.readBit();
		swapFrame.blockable = bitReader.readBit();
		swapFrame.hittable = bitReader.readBit();
		swapFrame.scalex = bitReader.readUnsigned(8);
		swapFrame.scaley = bitReader.readUnsigned(8);
		swapFrame.shade = bitReader.readSigned(8);
		swapFrame.palette = bitReader.readUnsigned(5);
		swapFrame.trigger = bitReader.readBit();
		swapFrame.smoke = bitReader.readBit();
		swapFrame.aiming = bitReader.readBit();
		swapFrame.pushable = bitReader.readBit();
		swapFrame.playsound = bitReader.readBit();
		swapFrame.invisible = bitReader.readBit();
		swapFrame.flipx = bitReader.readBit();
		swapFrame.flipy = bitReader.readBit();
		swapFrame.tile2 = bitReader.readUnsigned(4);
		swapFrame.soundRange = bitReader.readUnsigned(4);
		swapFrame.surfaceSound = bitReader.readBit();
		swapFrame.reserved = bitReader.readUnsigned(2);
		*pFrame = swapFrame;
	}
}

FMemArena seqcache;
static TMap<int, Seq*> sequences;
Seq* getSequence(int res_id)
{
	auto p = sequences.CheckKey(res_id);
	if (p != nullptr) return *p;

	int index = fileSystem.FindResource(res_id, "SEQ");
	if (index < 0)
	{
		return nullptr;
	}
	auto fr = fileSystem.OpenFileReader(index);
	auto seqdata = (Seq*)seqcache.Alloc(fr.GetLength());
	fr.Read(seqdata, fr.GetLength());
	sequences.Insert(res_id, seqdata);
	ByteSwapSEQ(seqdata);
	return seqdata;
}

//---------------------------------------------------------------------------
//
//
//
//---------------------------------------------------------------------------

void seqSpawn_(int nSeqID, int type, const EventObject& eob, int callback)
{
	Seq* pSequence = getSequence(nSeqID);

	if (pSequence == nullptr) return;

	SEQINST* pInst = activeList.get(type, eob);
	if (!pInst)
	{
		pInst = activeList.getNew();
	}
	else
	{
		// already playing this sequence?
		if (pInst->nSeqID == nSeqID)
			return;
	}

	pInst->pSequence = pSequence;
	pInst->nSeqID = nSeqID;
	pInst->callback = callback;
	pInst->timeCounter = (short)pSequence->ticksPerFrame;
	pInst->frameIndex = 0;
	pInst->type = type;
	pInst->target = eob;
	pInst->Update();
}

void seqSpawn(int nSeqID, DBloodActor* actor, int callback)
{
	seqSpawn_(nSeqID, SS_SPRITE, EventObject(actor), callback);
}

void seqSpawn(int nSeqID, int type, sectortype* sect, int callback)
{
	assert(type == SS_FLOOR || type == SS_CEILING);
	seqSpawn_(nSeqID, type, EventObject(sect), callback);
}

void seqSpawn(int nSeqID, int type, walltype* wal, int callback)
{
	assert(type == SS_WALL || type == SS_MASKED);
	seqSpawn_(nSeqID, type, EventObject(wal), callback);
}

//---------------------------------------------------------------------------
//
//
//
//---------------------------------------------------------------------------

int seqGetStatus(int type, sectortype* nIndex)
{
	SEQINST* pInst = activeList.get(type, EventObject(nIndex));
	if (pInst) return pInst->frameIndex;
	return -1;
}

int seqGetID(int type, sectortype* nIndex)
{
	SEQINST* pInst = activeList.get(type, EventObject(nIndex));
	if (pInst) return pInst->nSeqID;
	return -1;
}

int seqGetStatus(int type, walltype* nIndex)
{
	SEQINST* pInst = activeList.get(type, EventObject(nIndex));
	if (pInst) return pInst->frameIndex;
	return -1;
}

int seqGetID(int type, walltype* nIndex)
{
	SEQINST* pInst = activeList.get(type, EventObject(nIndex));
	if (pInst) return pInst->nSeqID;
	return -1;
}

int seqGetStatus(DBloodActor* actor)
{
	SEQINST* pInst = activeList.get(actor);
	if (pInst) return pInst->frameIndex;
	return -1;
}

int seqGetID(DBloodActor* actor)
{
	SEQINST* pInst = activeList.get(actor);
	if (pInst) return pInst->nSeqID;
	return -1;
}


//---------------------------------------------------------------------------
//
//
//
//---------------------------------------------------------------------------

void seqProcess(int nTicks)
{
	for (int i = 0; i < activeList.getSize(); i++)
	{
		SEQINST* pInst = activeList.getInst(i);
		Seq* pSeq = pInst->pSequence;
		auto target = pInst->target;

		assert(pInst->frameIndex < pSeq->nFrames);

		pInst->timeCounter -= nTicks;
		while (pInst->timeCounter < 0)
		{
			pInst->timeCounter += pSeq->ticksPerFrame;
			++pInst->frameIndex;

			if (pInst->frameIndex == pSeq->nFrames)
			{
				if (!pSeq->isLooping())
				{
					if (pSeq->isRemovable())
					{
						if (pInst->type == SS_SPRITE)
						{
							assert(target.isActor());
							auto actor = target.actor();
							if (actor)
							{
								evKillActor(actor);
								if ((actor->spr.hitag & kAttrRespawn) != 0 && (actor->spr.inittype >= kDudeBase && actor->spr.inittype < kDudeMax))
									evPostActor(actor, gGameOptions.nMonsterRespawnTime, kCallbackRespawn);
								else DeleteSprite(actor);	// safe to not use actPostSprite here
							}
						}

						else if (pInst->type == SS_MASKED)
						{
							assert(target.isWall());
							auto pWall = target.wall();
							pWall->cstat &= ~(CSTAT_WALL_XFLIP | CSTAT_WALL_MASKED | CSTAT_WALL_1WAY);
							if (pWall->twoSided())
								pWall->nextWall()->cstat &= ~(CSTAT_WALL_XFLIP | CSTAT_WALL_MASKED | CSTAT_WALL_1WAY);
						}
					}

					// remove it from the active list
					activeList.remove(i--);
					break;
				}
				else pInst->frameIndex = 0;
			}

			pInst->Update();
		}
	}
}

//---------------------------------------------------------------------------
//
//
//
//---------------------------------------------------------------------------

FSerializer& Serialize(FSerializer& arc, const char* keyname, SEQINST& w, SEQINST* def)
{
	if (arc.BeginObject(keyname))
	{
		arc("type", w.type)
			("callback", w.callback)
			("seqid", w.nSeqID)
			("timecounter", w.timeCounter)
			("frameindex", w.frameIndex)
			("target", w.target);

		arc.EndObject();
	}
	return arc;
}

void SerializeSequences(FSerializer& arc)
{
	arc("sequences", activeList.list);
	if (arc.isReading()) for (unsigned i = 0; i < activeList.list.Size(); i++)
	{
		activeList.list[i].pSequence = getSequence(activeList.list[i].nSeqID);
	}
}

END_BLD_NS
