/*
	This file is part of Warzone 2100.
	Copyright (C) 1999-2004  Eidos Interactive
	Copyright (C) 2005-2020  Warzone 2100 Project

	Warzone 2100 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.

	Warzone 2100 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 Warzone 2100; if not, write to the Free Software
	Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/

#include "lib/framework/frame.h"
#include "lib/netplay/sync_debug.h"
#include "lib/sound/audio.h"

#include "baseobject.h"
#include "droid.h"
#include "projectile.h"
#include "structure.h"
#include "feature.h"
#include "intdisplay.h"
#include "map.h"


static inline uint16_t interpolateAngle(uint16_t v1, uint16_t v2, uint32_t t1, uint32_t t2, uint32_t t)
{
	const int numer = t - t1, denom = t2 - t1;
	return v1 + angleDelta(v2 - v1) * numer / denom;
}

static Position interpolatePos(Position p1, Position p2, uint32_t t1, uint32_t t2, uint32_t t)
{
	const int numer = t - t1, denom = t2 - t1;
	return p1 + (p2 - p1) * numer / denom;
}

Rotation interpolateRot(Rotation v1, Rotation v2, uint32_t t1, uint32_t t2, uint32_t t)
{
	if (t1 == t2)
	{
		// spacetime overlap
#ifdef DEBUG
		ASSERT(t1 != t2, "Spacetime overlap!");
#endif
		// just return v2 (the current rotation)
		return v2;
	}
	//return v1 + (v2 - v1) * (t - t1) / (t2 - t1);
	return Rotation(interpolateAngle(v1.direction, v2.direction, t1, t2, t),
	                interpolateAngle(v1.pitch,     v2.pitch,     t1, t2, t),
	                interpolateAngle(v1.roll,      v2.roll,      t1, t2, t)
	               );
}

static Spacetime interpolateSpacetime(Spacetime st1, Spacetime st2, uint32_t t)
{
	// Cyp says this should never happen, #3037 and #3238 say it does though.
	ASSERT_OR_RETURN(st2, st1.time != st2.time, "Spacetime overlap!");
	return Spacetime(interpolatePos(st1.pos, st2.pos, st1.time, st2.time, t), interpolateRot(st1.rot, st2.rot, st1.time, st2.time, t), t);
}

Spacetime interpolateObjectSpacetime(const SIMPLE_OBJECT *obj, uint32_t t)
{
	switch (obj->type)
	{
	default:
		return getSpacetime(obj);
	case OBJ_DROID:
		return interpolateSpacetime(castDroid(obj)->prevSpacetime, getSpacetime(obj), t);
	case OBJ_PROJECTILE:
		return interpolateSpacetime(castProjectile(obj)->prevSpacetime, getSpacetime(obj), t);
	}
}

SIMPLE_OBJECT::SIMPLE_OBJECT(OBJECT_TYPE type, uint32_t id, unsigned player)
	: type(type)
	, id(id)
	, pos(0, 0, 0)
	, rot(0, 0, 0)
	, player(player)
	, born(gameTime)
	, died(0)
	, time(0)
{}

SIMPLE_OBJECT::~SIMPLE_OBJECT()
{
	const_cast<OBJECT_TYPE volatile &>(type) = (OBJECT_TYPE)(type + 1000000000);  // Hopefully this will trigger an assert              if someone uses the freed object.
	const_cast<UBYTE volatile &>(player) += 100;                                  // Hopefully this will trigger an assert and/or crash if someone uses the freed object.
}

BASE_OBJECT::BASE_OBJECT(OBJECT_TYPE type, uint32_t id, unsigned player)
	: SIMPLE_OBJECT(type, id, player)
	, selected(false)
	, lastEmission(0)
	, lastHitWeapon(WSC_NUM_WEAPON_SUBCLASSES)  // No such weapon.
	, timeLastHit(UDWORD_MAX)
	, body(0)
	, periodicalDamageStart(0)
	, periodicalDamage(0)
	, timeAnimationStarted(0)
	, animationEvent(ANIM_EVENT_NONE)
{
	memset(visible, 0, sizeof(visible));
	sDisplay.imd = nullptr;
	sDisplay.frameNumber = 0;
	sDisplay.screenX = 0;
	sDisplay.screenY = 0;
	sDisplay.screenR = 0;
}

BASE_OBJECT::~BASE_OBJECT()
{
	visRemoveVisibility(this);
}


WEAPON_STATS* BASE_OBJECT::getWeaponStats(int weaponSlot) const
{
	return &asWeaponStats[asWeaps[weaponSlot].nStat];
}

// Query visibility for display purposes (i.e. for `selectedPlayer`)
// *DO NOT USE TO QUERY VISIBILITY FOR CALCULATIONS INVOLVING GAME / SIMULATION STATE*
UBYTE BASE_OBJECT::visibleForLocalDisplay() const
{
	if (godMode)
	{
		// is visible to selectedPlayer
		return UBYTE_MAX;
	}
	if (selectedPlayer >= MAX_PLAYERS)
	{
		return 0;
	}
	return visible[selectedPlayer];
}

void checkObject(const SIMPLE_OBJECT *psObject, const char *const location_description, const char *function, const int recurse)
{
	if (recurse < 0)
	{
		return;
	}

	ASSERT(psObject != nullptr, "NULL pointer");

	switch (psObject->type)
	{
	case OBJ_DROID:
		checkDroid((const DROID *)psObject, location_description, function, recurse - 1);
		break;

	case OBJ_STRUCTURE:
		checkStructure((const STRUCTURE *)psObject, location_description, function, recurse - 1);
		break;

	case OBJ_PROJECTILE:
		checkProjectile((const PROJECTILE *)psObject, location_description, function, recurse - 1);
		break;

	case OBJ_FEATURE:
		break;

	default:
		ASSERT_HELPER(!"invalid object type", location_description, function, "CHECK_OBJECT: Invalid object type (type num %u)", (unsigned int)psObject->type);
		break;
	}
}

void _syncDebugObject(const char *function, SIMPLE_OBJECT const *psObject, char ch)
{
	switch (psObject->type)
	{
	case OBJ_DROID:      _syncDebugDroid(function, (const DROID *)     psObject, ch); break;
	case OBJ_STRUCTURE:  _syncDebugStructure(function, (const STRUCTURE *) psObject, ch); break;
	case OBJ_FEATURE:    _syncDebugFeature(function, (const FEATURE *)   psObject, ch); break;
	case OBJ_PROJECTILE: _syncDebugProjectile(function, (const PROJECTILE *)psObject, ch); break;
	default:             _syncDebug(function, "%c unidentified_object%d = p%d;objectType%d", ch, psObject->id, psObject->player, psObject->type);
		ASSERT_HELPER(!"invalid object type", "_syncDebugObject", function, "syncDebug: Invalid object type (type num %u)", (unsigned int)psObject->type);
		break;
	}
}

Vector2i getStatsSize(BASE_STATS const *pType, uint16_t direction)
{
	if (StatIsStructure(pType))
	{
		return static_cast<STRUCTURE_STATS const *>(pType)->size(direction);
	}
	else if (StatIsFeature(pType))
	{
		return static_cast<FEATURE_STATS const *>(pType)->size();
	}
	return Vector2i(1, 1);
}

StructureBounds getStructureBounds(BASE_OBJECT const *object)
{
	STRUCTURE const *psStructure = castStructure(object);
	FEATURE const *psFeature = castFeature(object);

	if (psStructure != nullptr)
	{
		return getStructureBounds(psStructure);
	}
	else if (psFeature != nullptr)
	{
		return getStructureBounds(psFeature);
	}

	return StructureBounds(Vector2i(32767, 32767), Vector2i(-65535, -65535));  // Default to an invalid area.
}

StructureBounds getStructureBounds(BASE_STATS const *stats, Vector2i pos, uint16_t direction)
{
	if (StatIsStructure(stats))
	{
		return getStructureBounds(static_cast<STRUCTURE_STATS const *>(stats), pos, direction);
	}
	else if (StatIsFeature(stats))
	{
		return getStructureBounds(static_cast<FEATURE_STATS const *>(stats), pos);
	}

	return StructureBounds(map_coord(pos), Vector2i(1, 1));  // Default to a 1×1 tile.
}

// Reset animation state so some units don't walk in place in the menu sometimes.
void resetObjectAnimationState(BASE_OBJECT *psObj)
{
	if (psObj == nullptr)
	{
		return;
	}
	if (psObj->type == OBJ_DROID || psObj->type == OBJ_STRUCTURE || psObj->type == OBJ_FEATURE)
	{
		psObj->timeAnimationStarted = 0;
		psObj->animationEvent = ANIM_EVENT_NONE;
	}
	else
	{
		debug(LOG_WARNING, "Tried resetting animation state on unknown object type");
	}
}
