/*
	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
*/
/**
 * @file transporter.c
 *
 * Code to deal with loading/unloading, interface and flight of transporters.
 */
#include <string.h>
#include <sys/stat.h>

#include "lib/framework/frame.h"
#include "lib/framework/strres.h"
#include "lib/framework/math_ext.h"
#include "lib/widget/label.h"
#include "lib/widget/widget.h"

#include "stats.h"
#include "hci.h"
#include "intdisplay.h"
#include "objmem.h"
#include "transporter.h"
#include "group.h"
#include "move.h"
#include "display3d.h"
#include "mission.h"
#include "objects.h"
#include "display.h"
#include "qtscript.h"
#include "order.h"
#include "action.h"
#include "lib/gamelib/gtime.h"
#include "console.h"
#include "lib/ivis_opengl/bitimage.h"
#include "warcam.h"
#include "selection.h"
#include "lib/sound/audio.h"
#include "lib/sound/audio_id.h"
#include "mapgrid.h"
#include "visibility.h"
#include "multiplay.h"
#include "hci/groups.h"
#include "wzapi.h"

//#define IDTRANS_FORM			9000	//The Transporter base form
#define IDTRANS_CLOSE			9002	//The close button icon
#define IDTRANS_CONTCLOSE		9005	//The close icon on the Contents form
#define IDTRANS_DROIDTAB		9007	//The Droid tab form
#define IDTRANS_DROIDCLOSE		9008	//The close icon for the Droid form

#define IDTRANS_START			9100	//The first button on the Transporter tab form
#define	IDTRANS_END			9199	//The last button on the Transporter tab form
#define IDTRANS_STATSTART		9200	//The status button for the first Transporter
#define IDTRANS_STATEND			9299	//The status button for the last Transporter
#define IDTRANS_CONTSTART		9300	//The first button on the Transporter contents tab form
#define	IDTRANS_CONTEND			9399	//The last button on the Transporter contents tab form
#define	IDTRANS_DROIDSTART		9400	//The first button on the Droid tab form
#define	IDTRANS_DROIDEND		9499	//The last button on the Droid tab form
#define IDTRANS_REPAIRBARSTART		9600    //The first repair status bar on Droid button
#define IDTRANS_REPAIRBAREND		9699    //The last repair status bar on Droid button

/* Transporter screen positions */
#define TRANS_X					OBJ_BACKX
#define TRANS_Y					OBJ_BACKY
#define TRANS_WIDTH				OBJ_BACKWIDTH
#define TRANS_HEIGHT			OBJ_BACKHEIGHT

/*tabbed form screen positions */
#define TRANS_TABY				OBJ_TABY

/*Transported contents screen positions */
#define TRANSCONT_X				STAT_X
#define TRANSCONT_Y				STAT_Y
#define TRANSCONT_WIDTH			STAT_WIDTH
#define TRANSCONT_HEIGHT		STAT_HEIGHT

/*contents tabbed form screen positions */
#define TRANSCONT_TABY			STAT_TABFORMY

/*droid form screen positions */
#define TRANSDROID_X			RADTLX
#define TRANSDROID_Y			STAT_Y
#define TRANSDROID_WIDTH		STAT_WIDTH
#define TRANSDROID_HEIGHT		STAT_HEIGHT

//start y position of the available droids buttons
#define AVAIL_STARTY			0

#define MAX_TRANSPORT_FULL_MESSAGE_PAUSE 20000

/* Maximum distance to the nearest transport (16 tiles) */
#define MAX_NEAREST_TRANSPORT_SQ_DIST (TILE_WIDTH * TILE_WIDTH * 256)

/* the widget screen */
extern std::shared_ptr<W_SCREEN> psWScreen;

/* Static variables */
static DROID *psCurrTransporter = nullptr;
static	bool			onMission;
static	UDWORD			g_iLaunchTime = 0;
//used for audio message for reinforcements
static  bool            bFirstTransporter;
//the tab positions of the DroidsAvail window
static  size_t          objMajor = 0;
// Last time the transporter is full message was displayed
static UDWORD lastTransportIsFullMsgTime = 0;

/*functions */
static bool intAddTransporterContents();
static void setCurrentTransporter(UDWORD id);
static void intRemoveTransContentNoAnim();
static bool intAddTransButtonForm();
static bool intAddTransContentsForm();
static bool intAddDroidsAvailForm();
static void intRemoveTransContent();
static DroidList* transInterfaceDroidList();
static void intTransporterAddDroid(UDWORD id);
static void intRemoveTransDroidsAvail();
static void intRemoveTransDroidsAvailNoAnim();

//initialises Transporter variables
void initTransporters()
{
	onMission = false;
	psCurrTransporter = nullptr;
}

// Call to refresh the transporter screen, ie when a droids boards it.
//
bool intRefreshTransporter()
{
	// Is the transporter screen up?
	if (intMode == INT_TRANSPORTER && widgGetFromID(psWScreen, IDTRANS_FORM) != nullptr)
	{
		bool Ret;
		// Refresh it by re-adding it.
		Ret = intAddTransporter(psCurrTransporter, onMission);
		intMode = INT_TRANSPORTER;
		return Ret;
	}

	return true;
}

bool intAddTransporter(DROID *psSelected, bool offWorld)
{
	bool Animate = true;

	onMission = offWorld;
	psCurrTransporter = psSelected;

	/*if transporter has died - close the interface - this can only happen in
	multiPlayer where the transporter can be killed*/
	if (bMultiPlayer && psCurrTransporter && isDead((BASE_OBJECT *)psCurrTransporter))
	{
		intRemoveTransNoAnim();
		return true;
	}

	// Add the main Transporter form
	// Is the form already up?
	if (widgGetFromID(psWScreen, IDTRANS_FORM) != nullptr)
	{
		intRemoveTransNoAnim();
		Animate = false;
	}

	if (intIsRefreshing())
	{
		Animate = false;
	}

	auto const &parent = psWScreen->psForm;

	auto transForm = std::make_shared<IntFormAnimated>(Animate);  // Do not animate the opening, if the window was already open.
	parent->attach(transForm);
	transForm->id = IDTRANS_FORM;
	transForm->setCalcLayout(LAMBDA_CALCLAYOUT_SIMPLE({
		psWidget->setGeometry(TRANS_X, TRANS_Y - (getGroupButtonEnabled() ? 80 : 0), TRANS_WIDTH, TRANS_HEIGHT);
	}));

	/* Add the close button */
	W_BUTINIT sButInit;
	sButInit.formID = IDTRANS_FORM;
	sButInit.id = IDTRANS_CLOSE;
	sButInit.x = TRANS_WIDTH - CLOSE_WIDTH;
	sButInit.y = 0;
	sButInit.width = CLOSE_WIDTH;
	sButInit.height = CLOSE_HEIGHT;
	sButInit.pTip = _("Close");
	sButInit.pDisplay = intDisplayImageHilight;
	sButInit.UserData = PACKDWORD_TRI(0, IMAGE_CLOSEHILIGHT , IMAGE_CLOSE);
	if (!widgAddButton(psWScreen, &sButInit))
	{
		return false;
	}

	if (!intAddTransButtonForm())
	{
		return false;
	}

	// Add the Transporter Contents form (and buttons)
	if (!intAddTransporterContents())
	{
		return false;
	}

	//if on a mission - add the Droids back at home base form
	if (onMission && !intAddDroidsAvailForm())
	{
		return false;
	}

	return true;
}

// Add the main Transporter Contents Interface
bool intAddTransporterContents()
{
	bool			Animate = true;

	// Is the form already up?
	if (widgGetFromID(psWScreen, IDTRANS_CONTENTFORM) != nullptr)
	{
		intRemoveTransContentNoAnim();
		Animate = false;
	}

	if (intIsRefreshing())
	{
		Animate = false;
	}

	auto const &parent = psWScreen->psForm;

	auto transContentForm = std::make_shared<IntFormAnimated>(Animate);  // Do not animate the opening, if the window was already open.
	parent->attach(transContentForm);
	transContentForm->id = IDTRANS_CONTENTFORM;
	transContentForm->setCalcLayout(LAMBDA_CALCLAYOUT_SIMPLE({
		psWidget->setGeometry(TRANSCONT_X, TRANSCONT_Y, TRANSCONT_WIDTH, TRANSCONT_HEIGHT);
	}));

	/* Add the close button */
	W_BUTINIT sButInit;
	sButInit.formID = IDTRANS_CONTENTFORM;
	sButInit.id = IDTRANS_CONTCLOSE;
	sButInit.calcLayout = (LAMBDA_CALCLAYOUT_SIMPLE({
		psWidget->setGeometry(STAT_WIDTH - CLOSE_WIDTH, 0, CLOSE_WIDTH, CLOSE_HEIGHT);
	}));
	sButInit.pTip = _("Close");
	sButInit.pDisplay = intDisplayImageHilight;
	sButInit.UserData = PACKDWORD_TRI(0, IMAGE_CLOSEHILIGHT , IMAGE_CLOSE);
	if (!widgAddButton(psWScreen, &sButInit))
	{
		return false;
	}
	if (bMultiPlayer)
	{
		//add the capacity label
		W_LABINIT sLabInit;
		sLabInit.formID = IDTRANS_CONTENTFORM;
		sLabInit.id = IDTRANS_CAPACITY;
		sLabInit.x = (SWORD)sButInit.x - 40;
		sLabInit.y = 0;
		sLabInit.width = 16;
		sLabInit.height = 16;
		sLabInit.pText = WzString::fromUtf8("00/10");
		sLabInit.pCallback = intUpdateTransCapacity;
		if (!widgAddLabel(psWScreen, &sLabInit))
		{
			return false;
		}
	}
	//add the Launch button if on a mission
	if (onMission)
	{
		W_FORMINIT sButFInit;
		sButFInit.formID = IDTRANS_CONTENTFORM;
		sButFInit.id = IDTRANS_LAUNCH;
		sButFInit.style = WFORM_CLICKABLE | WFORM_NOCLICKMOVE;

		sButFInit.x = OBJ_STARTX;
		sButFInit.y = (UWORD)(STAT_SLDY - 1);

		sButFInit.width = iV_GetImageWidth(IntImages, IMAGE_LAUNCHUP);
		sButFInit.height = iV_GetImageHeight(IntImages, IMAGE_LAUNCHUP);
		sButFInit.pTip = _("Launch Transport");
		sButFInit.pDisplay = intDisplayImageHilight;

		sButFInit.UserData = PACKDWORD_TRI(0, IMAGE_LAUNCHDOWN, IMAGE_LAUNCHUP);

		auto psForm = widgAddForm(psWScreen, &sButFInit);
		if (!psForm)
		{
			return false;
		}
		psForm->setHelp(WidgetHelp()
						.setTitle(_("Launch Transport"))
						.addInteraction({WidgetHelp::InteractionTriggers::PrimaryClick}, _("Launch the Transporter")));
	}

	if (!intAddTransContentsForm())
	{
		return false;
	}

	return true;
}

/*This is used to display the transporter button and capacity when at the home base ONLY*/
bool intAddTransporterLaunch(DROID *psDroid)
{
	UDWORD          capacity;

	if (bMultiPlayer)
	{
		return true;
	}

	//do this first so that if the interface is already up it syncs with this transporter
	//set up the static transporter
	psCurrTransporter = psDroid;

	// Check that neither the launch button nor the transport timer are currently up
	if (widgGetFromID(psWScreen, IDTRANS_LAUNCH) != nullptr
		|| widgGetFromID(psWScreen, IDTRANTIMER_BUTTON) != nullptr)
	{
		return true;
	}

	W_FORMINIT sButInit;              //needs to be a clickable form now
	sButInit.formID = 0;
	sButInit.id = IDTRANS_LAUNCH;
	sButInit.style = WFORM_CLICKABLE | WFORM_NOCLICKMOVE;
	sButInit.x = RET_X;
	sButInit.y = (SWORD)TIMER_Y;
	sButInit.width = (UWORD)(10 + iV_GetImageWidth(IntImages, IMAGE_LAUNCHUP));
	sButInit.height = iV_GetImageHeight(IntImages, IMAGE_LAUNCHUP);
	sButInit.pTip = _("Launch Transport");
	sButInit.pDisplay = intDisplayImageHilight;
	sButInit.UserData = PACKDWORD_TRI(0, IMAGE_LAUNCHDOWN, IMAGE_LAUNCHUP);
	auto psForm = widgAddForm(psWScreen, &sButInit);
	if (!psForm)
	{
		return false;
	}
	psForm->setHelp(WidgetHelp()
					.setTitle(_("Launch Transport"))
					.addInteraction({WidgetHelp::InteractionTriggers::PrimaryClick}, _("Launch the Transporter")));

	//add the capacity label
	W_LABINIT sLabInit;
	sLabInit.formID = IDTRANS_LAUNCH;
	sLabInit.id = IDTRANS_CAPACITY;
	sLabInit.x = (SWORD)(sButInit.x + 40);
	sLabInit.y = 0;
	sLabInit.width = 16;
	sLabInit.height = 16;
	sLabInit.pText = WzString::fromUtf8("00/10");
	sLabInit.pCallback = intUpdateTransCapacity;
	if (!widgAddLabel(psWScreen, &sLabInit))
	{
		return false;
	}

	//when full flash the transporter button
	if (psCurrTransporter && psCurrTransporter->psGroup)
	{
		capacity = TRANSPORTER_CAPACITY;
		for (const DROID* psCurr : psCurrTransporter->psGroup->psList)
		{
			if (psCurr != psCurrTransporter)
			{
				capacity -= transporterSpaceRequired(psCurr);
			}
		}
		if (capacity <= 0)
		{
			flashMissionButton(IDTRANS_LAUNCH);
		}
	}

	return true;
}

/* Remove the Transporter Launch widget from the screen*/
void intRemoveTransporterLaunch()
{
	if (widgGetFromID(psWScreen, IDTRANS_LAUNCH) != nullptr)
	{
		widgDelete(psWScreen, IDTRANS_LAUNCH);
	}
}

/* Add the Transporter Button form */
bool intAddTransButtonForm()
{
	WIDGET *transForm = widgGetFromID(psWScreen, IDTRANS_FORM);

	/* Add the button form */
	auto transList = IntListTabWidget::make();
	transForm->attach(transList);
	transList->setChildSize(OBJ_BUTWIDTH, OBJ_BUTHEIGHT * 2);
	transList->setChildSpacing(OBJ_GAP, OBJ_GAP);
	int objListWidth = OBJ_BUTWIDTH * 5 + OBJ_GAP * 4;
	transList->setGeometry((OBJ_BACKWIDTH - objListWidth) / 2, TRANS_TABY, objListWidth, transForm->height() - TRANS_TABY);

	/* Add the transporter and status buttons */
	int nextObjButtonId = IDTRANS_START;
	int nextStatButtonId = IDTRANS_STATSTART;

	//add each button
	auto* transIntDroidList = transInterfaceDroidList();
	if (transIntDroidList)
	{
		for (DROID* psDroid : *transIntDroidList)
		{
			//only interested in Transporter droids
			if ((psDroid->isTransporter() && (psDroid->action == DACTION_TRANSPORTOUT ||
				psDroid->action == DACTION_TRANSPORTIN)) || !psDroid->isTransporter())
			{
				continue;
			}

			auto buttonHolder = std::make_shared<WIDGET>();
			transList->attach(buttonHolder);
			transList->addWidgetToLayout(buttonHolder);

			auto statButton = std::make_shared<IntStatusButton>();
			buttonHolder->attach(statButton);
			statButton->id = nextStatButtonId;
			statButton->setGeometry(0, 0, OBJ_BUTWIDTH, OBJ_BUTHEIGHT);

			auto objButton = std::make_shared<IntObjectButton>();
			buttonHolder->attach(objButton);
			objButton->id = nextObjButtonId;
			objButton->setGeometry(0, OBJ_STARTY, OBJ_BUTWIDTH, OBJ_BUTHEIGHT);

			/* Set the tip and add the button */
			objButton->setTip(droidGetName(psDroid));
			objButton->setObject(psDroid);

			//set the first Transporter to be the current one if not already set
			if (psCurrTransporter == nullptr)
			{
				psCurrTransporter = psDroid;
			}

			/* if the current droid matches psCurrTransporter lock the button */
			if (psDroid == psCurrTransporter)
			{
				objButton->setState(WBUT_LOCK);
				transList->setCurrentPage(transList->pages() - 1);
			}

			//now do status button
			statButton->setObject(nullptr);

			/* Update the init struct for the next buttons */
			++nextObjButtonId;
			ASSERT(nextObjButtonId < IDTRANS_END, "Too many Transporter buttons");

			++nextStatButtonId;
			ASSERT(nextStatButtonId < IDTRANS_STATEND, "Too many Transporter status buttons");
		}
	}
	return true;
}

/* Add the Transporter Contents form */
bool intAddTransContentsForm()
{
	WIDGET *contForm = widgGetFromID(psWScreen, IDTRANS_CONTENTFORM);

	/* Add the contents form */
	auto contList = IntListTabWidget::make();
	contForm->attach(contList);
	contList->setChildSize(OBJ_BUTWIDTH, OBJ_BUTHEIGHT);
	contList->setChildSpacing(OBJ_GAP, OBJ_GAP);
	int contListWidth = OBJ_BUTWIDTH * 2 + OBJ_GAP;
	contList->setGeometry((contForm->width() - contListWidth) / 2, TRANSCONT_TABY, contListWidth, contForm->height() - TRANSCONT_TABY);

	/* Add the transporter contents buttons */
	int nextButtonId = IDTRANS_CONTSTART;

	//add each button
	if (psCurrTransporter == nullptr)
	{
		return true;
	}

	ASSERT_OR_RETURN(false, psCurrTransporter->psGroup != nullptr, "Null transporter group");

	for (DROID* psDroid : psCurrTransporter->psGroup->psList)
	{
		if (psDroid == psCurrTransporter)
		{
			break;
		}
		if (psDroid->selected)
		{
			continue;  // Droid is queued to be ejected from the transport, so don't display it.
		}

		/* Set the tip and add the button */
		auto button = std::make_shared<IntTransportButton>();
		contList->attach(button);
		button->id = nextButtonId;
		button->setTip(droidGetName(psDroid));
		button->setObject(psDroid);
		contList->addWidgetToLayout(button);

		/* Update the init struct for the next button */
		++nextButtonId;
		ASSERT(nextButtonId < IDTRANS_CONTEND, "Too many Transporter Droid buttons");
	}
	return true;
}

/* Add the Droids back at home form */
bool intAddDroidsAvailForm()
{
	// Is the form already up?
	bool Animate = true;
	if (widgGetFromID(psWScreen, IDTRANS_DROIDS) != nullptr)
	{
		intRemoveTransDroidsAvailNoAnim();
		Animate = false;
	}

	if (intIsRefreshing())
	{
		Animate = false;
	}

	ASSERT_OR_RETURN(false, selectedPlayer < MAX_PLAYERS, "Cannot be called for selectedPlayer: %" PRIu32 "", selectedPlayer);

	auto const &parent = psWScreen->psForm;

	/* Add the droids available form */
	auto transDroids = std::make_shared<IntFormAnimated>(Animate);  // Do not animate the opening, if the window was already open.
	parent->attach(transDroids);
	transDroids->id = IDTRANS_DROIDS;
	transDroids->setCalcLayout(LAMBDA_CALCLAYOUT_SIMPLE({
		psWidget->setGeometry(TRANSDROID_X, TRANSDROID_Y, TRANSDROID_WIDTH, TRANSDROID_HEIGHT);
	}));

	/* Add the close button */
	W_BUTINIT sButInit;
	sButInit.formID = IDTRANS_DROIDS;
	sButInit.id = IDTRANS_DROIDCLOSE;
	sButInit.calcLayout = (LAMBDA_CALCLAYOUT_SIMPLE({
		psWidget->setGeometry(TRANSDROID_WIDTH - CLOSE_WIDTH, 0, CLOSE_WIDTH, CLOSE_HEIGHT);
	}));
	sButInit.pTip = _("Close");
	sButInit.pDisplay = intDisplayImageHilight;
	sButInit.UserData = PACKDWORD_TRI(0, IMAGE_CLOSEHILIGHT , IMAGE_CLOSE);
	if (!widgAddButton(psWScreen, &sButInit))
	{
		return false;
	}

	//now add the tabbed droids available form
	auto droidList = IntListTabWidget::make();
	transDroids->attach(droidList);
	droidList->id = IDTRANS_DROIDTAB;
	droidList->setCalcLayout(LAMBDA_CALCLAYOUT_SIMPLE({
		IntListTabWidget *droidList = static_cast<IntListTabWidget *>(psWidget);
		assert(droidList != nullptr);
		droidList->setChildSize(OBJ_BUTWIDTH, OBJ_BUTHEIGHT);
		droidList->setChildSpacing(OBJ_GAP, OBJ_GAP);
		int droidListWidth = OBJ_BUTWIDTH * 2 + OBJ_GAP;
		droidList->setGeometry((TRANSDROID_WIDTH - droidListWidth) / 2, AVAIL_STARTY + 15, droidListWidth, TRANSDROID_HEIGHT - (AVAIL_STARTY + 15));
	}));

	/* Add the droids available buttons */
	int nextButtonId = IDTRANS_DROIDSTART;

	/* Add the state of repair bar for each droid*/
	W_BARINIT sBarInit;
	sBarInit.id = IDTRANS_REPAIRBARSTART;
	sBarInit.x = STAT_TIMEBARX;
	sBarInit.y = STAT_TIMEBARY;
	sBarInit.width = STAT_PROGBARWIDTH;
	sBarInit.height = STAT_PROGBARHEIGHT;
	sBarInit.size = 50;
	sBarInit.sCol = WZCOL_ACTION_PROGRESS_BAR_MAJOR;
	sBarInit.sMinorCol = WZCOL_ACTION_PROGRESS_BAR_MINOR;

	//add droids built before the mission
	for (DROID *psDroid : mission.apsDroidLists[selectedPlayer])
	{
		//stop adding the buttons once IDTRANS_DROIDEND has been reached
		if (nextButtonId == IDTRANS_DROIDEND)
		{
			break;
		}
		//don't add Transporter Droids!
		if (!psDroid->isTransporter())
		{
			/* Set the tip and add the button */
			auto button = std::make_shared<IntTransportButton>();
			droidList->attach(button);
			button->id = nextButtonId;
			button->setTip(droidGetName(psDroid));
			button->setObject(psDroid);
			droidList->addWidgetToLayout(button);

			//add bar to indicate stare of repair
			sBarInit.size = (UWORD) PERCENT(psDroid->body, psDroid->originalBody);
			if (sBarInit.size > 100)
			{
				sBarInit.size = 100;
			}

			sBarInit.formID = nextButtonId;
			//sBarInit.iRange = TBAR_MAX_REPAIR;
			if (!widgAddBarGraph(psWScreen, &sBarInit))
			{
				return false;
			}

			/* Update the init struct for the next button */
			++nextButtonId;
			ASSERT(nextButtonId < IDTRANS_DROIDEND, "Too many Droids Built buttons");

			//and bar
			sBarInit.id += 1;
		}
	}

	//reset which tab we were on
	droidList->setCurrentPage(objMajor);

	return true;
}

/*calculates how much space is remaining on the transporter - allows droids to take
up different amount depending on their body size - currently all are set to one!*/
int calcRemainingCapacity(const DROID *psTransporter)
{
	int capacity = TRANSPORTER_CAPACITY;

	// If it's dead then just return 0.
	if (isDead((const BASE_OBJECT *)psTransporter))
	{
		return 0;
	}

	// If for some reason it doesn't have an associated psGroup (is it being recycled?), just return 0.
	if (!psTransporter->psGroup)
	{
		return 0;
	}

	for (DROID* psDroid : psTransporter->psGroup->psList)
	{
		if (psDroid == psTransporter)
		{
			break;
		}
		const int space = transporterSpaceRequired(psDroid);
		ASSERT(space > 0, "Invalid space required for %s", objInfo(psDroid));
		capacity -= space;
	}

	if (capacity < 0)
	{
		capacity = 0;
	}

	return capacity;
}

bool transporterIsEmpty(const DROID *psTransporter)
{
	ASSERT(psTransporter->isTransporter(), "Non-transporter droid given");

	// Assume dead droids and non-transporter droids to be empty
	return (isDead((const BASE_OBJECT *)psTransporter)
	        || !psTransporter->isTransporter()
	        || psTransporter->psGroup->psList.empty()
	        || psTransporter->psGroup->psList.front() == psTransporter);
}

static void intSetTransCapacityLabel(W_LABEL &Label)
{
	if (psCurrTransporter)
	{
		int capacity = calcRemainingCapacity(psCurrTransporter);

		//change round the way the remaining capacity is displayed - show 0/10 when empty now
		capacity = TRANSPORTER_CAPACITY - capacity;

		char tmp[40];
		ssprintf(tmp, "%02d/10", capacity);
		Label.setString(WzString::fromUtf8(tmp));
	}
}

/*updates the capacity of the current Transporter*/
void intUpdateTransCapacity(WIDGET *psWidget, const W_CONTEXT *psContext)
{
	W_LABEL		*Label = (W_LABEL *)psWidget;

	intSetTransCapacityLabel(*Label);
}

/* Process return codes from the Transporter Screen*/
void intProcessTransporter(UDWORD id)
{
	if (id >= IDTRANS_START && id <= IDTRANS_END)
	{
		/* A Transporter button has been pressed */
		setCurrentTransporter(id);
		/*refresh the Contents list */
		intAddTransporterContents();
	}
	else if (id >= IDTRANS_CONTSTART && id <= IDTRANS_CONTEND)
	{
		//got to have a current transporter for this to work - and can't be flying
		if (psCurrTransporter != nullptr && !transporterFlying(psCurrTransporter))
		{
			unsigned currID = IDTRANS_CONTSTART;
			const auto& groupList = psCurrTransporter->psGroup->psList;
			const auto psDroidIt = std::find_if(groupList.begin(), groupList.end(), [&currID, id](DROID* psDroid)
			{
				if (psDroid == psCurrTransporter)
				{
					return false;
				}
				if (psDroid->selected)
				{
					return false;  // Already scheduled this droid for removal.
				}
				return currID++ == id;
			});
			if (psDroidIt != groupList.end())
			{
				transporterRemoveDroid(psCurrTransporter, *psDroidIt, ModeQueue);
			}
			/*refresh the Contents list */
			intAddTransporterContents();
			if (onMission)
			{
				/*refresh the Avail list */
				intAddDroidsAvailForm();
			}
		}
	}
	else if (id == IDTRANS_CLOSE)
	{
		intRemoveTransContent();
		intRemoveTrans();
		psCurrTransporter = nullptr;
	}
	else if (id == IDTRANS_CONTCLOSE)
	{
		intRemoveTransContent();
	}
	else if (id == IDTRANS_DROIDCLOSE)
	{
		intRemoveTransDroidsAvail();
	}
	else if (id >= IDTRANS_DROIDSTART && id <= IDTRANS_DROIDEND)
	{
		//got to have a current transporter for this to work - and can't be flying
		if (psCurrTransporter != nullptr && !transporterFlying(psCurrTransporter))
		{
			intTransporterAddDroid(id);
			/*don't need to explicitly refresh here since intRefreshScreen()
			is called by intTransporterAddDroid()*/
		}
	}
}

/* Remove the Transporter widgets from the screen */
void intRemoveTrans(bool skipIntModeReset /*= false*/)
{
	// Start the window close animation.
	IntFormAnimated *form = (IntFormAnimated *)widgGetFromID(psWScreen, IDTRANS_FORM);
	if (form)
	{
		form->closeAnimateDelete();
	}

	intRemoveTransContent();
	intRemoveTransDroidsAvail();
	if (!skipIntModeReset)
	{
		intMode = INT_NORMAL;
	}
}

/* Remove the Transporter Content widgets from the screen w/o animation!*/
void intRemoveTransNoAnim(bool skipIntModeReset /*= false*/)
{
	//remove main screen
	widgDelete(psWScreen, IDTRANS_FORM);
	intRemoveTransContentNoAnim();
	intRemoveTransDroidsAvailNoAnim();
	if (!skipIntModeReset)
	{
		intMode = INT_NORMAL;
	}
}

/* Remove the Transporter Content widgets from the screen */
void intRemoveTransContent()
{
	// Start the window close animation.
	IntFormAnimated *form = (IntFormAnimated *)widgGetFromID(psWScreen, IDTRANS_CONTENTFORM);
	if (form)
	{
		form->closeAnimateDelete();
	}
}

/* Remove the Transporter Content widgets from the screen w/o animation!*/
void intRemoveTransContentNoAnim()
{
	//remove main screen
	widgDelete(psWScreen, IDTRANS_CONTENTFORM);
}

/* Remove the Transporter Droids Avail widgets from the screen */
void intRemoveTransDroidsAvail()
{
	// Start the window close animation.
	IntFormAnimated *form = (IntFormAnimated *)widgGetFromID(psWScreen, IDTRANS_DROIDS);
	if (form)
	{
		//remember which tab we were on
		ListTabWidget *droidList = (ListTabWidget *)widgGetFromID(psWScreen, IDTRANS_DROIDTAB);
		objMajor = droidList->currentPage();
		form->closeAnimateDelete();
	}
}

/* Remove the Transporter Droids Avail widgets from the screen w/o animation!*/
void intRemoveTransDroidsAvailNoAnim()
{
	IntFormAnimated *form = (IntFormAnimated *)widgGetFromID(psWScreen, IDTRANS_DROIDS);
	if (form != nullptr)
	{
		//remember which tab we were on
		ListTabWidget *droidList = (ListTabWidget *)widgGetFromID(psWScreen, IDTRANS_DROIDTAB);
		objMajor = droidList->currentPage();

		//remove main screen
		widgDelete(form);
	}
}

/*sets psCurrTransporter */
void setCurrentTransporter(UDWORD id)
{
	DROID	*psDroid = nullptr;
	UDWORD	currID;

	psCurrTransporter = nullptr;
	currID = IDTRANS_START;

	//loop thru all the droids to find the selected one
	auto* transIntDroidList = transInterfaceDroidList();
	if (transIntDroidList)
	{
		for (DROID* psCurr : *transIntDroidList)
		{
			if (psCurr->isTransporter() &&
				(psCurr->action != DACTION_TRANSPORTOUT &&
					psCurr->action != DACTION_TRANSPORTIN))
			{
				if (currID == id)
				{
					psDroid = psCurr;
					break;
				}
				currID++;
			}
		}
	}

	if (psDroid)
	{
		psCurrTransporter = psDroid;
		//set the data for the transporter timer
		widgSetUserData(psWScreen, IDTRANTIMER_DISPLAY, (void *)psCurrTransporter);
	}
}

/*removes a droid from the group associated with the transporter*/
void transporterRemoveDroid(DROID *psTransport, DROID *psDroid, QUEUE_MODE mode)
{
	ASSERT_OR_RETURN(, psTransport != nullptr && psDroid != nullptr && psTransport != psDroid, "Something NULL or unloading transporter from itself");

	if (bMultiMessages && mode == ModeQueue)
	{
		sendDroidDisembark(psTransport, psDroid);
		psDroid->selected = true;  // Remove from interface.
		return;
	}

	/*if we're offWorld we can't pick a tile without swapping the map
	pointers - can't be bothered so just do this...*/
	if (onMission)
	{
		psDroid->pos.x = INVALID_XY;
		psDroid->pos.y = INVALID_XY;
	}
	else
	{
		Vector2i droidPos;
		if (bMultiPlayer)
		{
			//set the units next to the transporter's current location
			droidPos = map_coord(psTransport->pos.xy());
		}
		else
		{
			//pick a tile because save games won't remember where the droid was when it was loaded
			droidPos = map_coord(Vector2i(getLandingX(0), getLandingY(0)));
		}
		if (!pickATileGen(&droidPos, LOOK_FOR_EMPTY_TILE, zonedPAT))
		{
			ASSERT(false, "Unable to find a valid location");
		}
		droidSetPosition(psDroid, world_coord(droidPos.x), world_coord(droidPos.y));
		updateDroidOrientation(psDroid);
	}

	// remove it from the transporter group
	psDroid->psGroup->remove(psDroid);

	//add it back into apsDroidLists
	if (onMission)
	{
		addDroid(psDroid, mission.apsDroidLists);
	}
	else
	{
		// add the droid back onto the droid list
		addDroid(psDroid, apsDroidLists);
	}

	if (psDroid->pos.x != INVALID_XY)
	{
		// We can update the orders now, since everyone has been
		// notified of the droid exiting the transporter
		updateDroidOrientation(psDroid);
	}
	//initialise the movement data
	initDroidMovement(psDroid);
	//reset droid orders
	orderDroid(psDroid, DORDER_STOP, ModeImmediate);
	// check if it is a commander
	if (psDroid->droidType == DROID_COMMAND)
	{
		DROID_GROUP *psGroup = grpCreate();
		psGroup->add(psDroid);
	}
	psDroid->selected = true;

	if (calcRemainingCapacity(psTransport))
	{
		//make sure the button isn't flashing
		stopMissionButtonFlash(IDTRANS_LAUNCH);
	}

	// Fire off disembark event
	triggerEvent(TRIGGER_TRANSPORTER_DISEMBARKED, psTransport);
}

/*adds a droid to the current transporter via the interface*/
static void intTransporterAddDroid(UDWORD id)
{
	UDWORD		currID;

	ASSERT(psCurrTransporter != nullptr, "intTransporterAddUnit:can't remove units");

	currID = IDTRANS_DROIDSTART;
	auto* transIntDroidList = transInterfaceDroidList();
	if (!transIntDroidList)
	{
		return;
	}
	DroidList::iterator droidIt = transIntDroidList->begin();
	while (droidIt != transIntDroidList->end())
	{
		if (!(*droidIt)->isTransporter())
		{
			if (currID == id)
			{
				break;
			}
			currID++;
		}
		++droidIt;
	}
	if (droidIt != transIntDroidList->end())
	{
		transporterAddDroid(psCurrTransporter, *droidIt);
	}
}

/*Adds a droid to the transporter, removing it from the world */
void transporterAddDroid(DROID *psTransporter, DROID *psDroidToAdd)
{
	bool    bDroidRemoved;

	ASSERT(psTransporter != nullptr, "Was passed a NULL transporter");
	ASSERT(psDroidToAdd != nullptr, "Was passed a NULL droid, can't add to transporter");

	if (!psTransporter || !psDroidToAdd)
	{
		debug(LOG_ERROR, "We can't add the unit to the transporter!");
		return;
	}
	/* check for space */
	if (!checkTransporterSpace(psTransporter, psDroidToAdd))
	{
		if (bMultiPlayer)
		{
			// search for the nearest transporter if the current one is already full
			for (auto psOtherDroid : apsDroidLists[psTransporter->player])
			{
				if (psOtherDroid->isTransporter() &&
					checkTransporterSpace(psOtherDroid, psDroidToAdd) &&
					droidSqDist(psOtherDroid, psTransporter) < MAX_NEAREST_TRANSPORT_SQ_DIST)
				{
					if (psOtherDroid->droidType == DROID_TRANSPORTER && !psDroidToAdd->isCyborg())
					{
						continue; // Only send cyborgs to a cyborg transporter.
					}
					orderDroidObj(psDroidToAdd, DORDER_EMBARK, psOtherDroid, ModeQueue);
					return;
				}
			}
		}
		if (!onMission && psDroidToAdd->isVtol())
		{
			moveStopDroid(psDroidToAdd); // So VTOLs are put into a MOVEHOVER status from the prior moveReallyStopDroid() in order.cpp's embark section.
		}
		if (psTransporter->player == selectedPlayer)
		{
			audio_PlayBuildFailedOnce();
			if (lastTransportIsFullMsgTime + MAX_TRANSPORT_FULL_MESSAGE_PAUSE < gameTime)
			{
				addConsoleMessage(_("There is not enough room in the Transport!"), DEFAULT_JUSTIFY, selectedPlayer);
				lastTransportIsFullMsgTime = gameTime;
			}
		}
		return;
	}

	resetObjectAnimationState(psDroidToAdd);

	if (onMission)
	{
		// removing from droid mission list
		bDroidRemoved = droidRemove(psDroidToAdd, mission.apsDroidLists);
	}
	else
	{
		// removing from droid list
		bDroidRemoved = droidRemove(psDroidToAdd, apsDroidLists);
	}

	if (bDroidRemoved)
	{
		// adding to transporter unit's group list
		psTransporter->psGroup->add(psDroidToAdd);
		psDroidToAdd->selected = false;  // Display in transporter interface.
	}
	else
	{
		debug(LOG_ERROR, "droid %d not found, so nothing added to transporter!", psDroidToAdd->id);
	}
	if (onMission)
	{
		visRemoveVisibilityOffWorld((BASE_OBJECT *)psDroidToAdd);
	}
	else
	{
		visRemoveVisibility((BASE_OBJECT *)psDroidToAdd);
	}
	fpathRemoveDroidData(psDroidToAdd->id);

	// This is called by droidRemove. But we still need to refresh after adding to the transporter group.
	intRefreshScreen();
}

/*check to see if the droid can fit on the Transporter - return true if fits*/
bool checkTransporterSpace(DROID const *psTransporter, DROID const *psAssigned, bool mayFlash)
{
	UDWORD		capacity;

	ASSERT_OR_RETURN(false, psTransporter != nullptr, "Invalid droid pointer");
	ASSERT_OR_RETURN(false, psAssigned != nullptr, "Invalid droid pointer");
	ASSERT_OR_RETURN(false, psTransporter->isTransporter(), "Droid is not a Transporter");
	ASSERT_OR_RETURN(false, psTransporter->psGroup != nullptr, "transporter doesn't have a group");

	//work out how much space is currently left
	capacity = TRANSPORTER_CAPACITY;
	for (DROID* psDroid : psTransporter->psGroup->psList)
	{
		if (psDroid == psTransporter)
		{
			break;
		}
		capacity -= transporterSpaceRequired(psDroid);
	}
	if (capacity >= transporterSpaceRequired(psAssigned))
	{
		//when full flash the transporter button
		if (mayFlash && capacity - transporterSpaceRequired(psAssigned) == 0)
		{
			flashMissionButton(IDTRANS_LAUNCH);
		}
		return true;
	}
	else
	{
		return false;
	}
}

/*returns the space the droid occupies on a transporter based on the body size*/
int transporterSpaceRequired(const DROID *psDroid)
{
	// all droids are the same weight for campaign games.
	// TODO - move this into a droid flag
	return bMultiPlayer ? psDroid->getBodyStats()->size + 1 : 1;
}

/*sets which list of droids to use for the transporter interface*/
DroidList* transInterfaceDroidList()
{
	ASSERT_OR_RETURN(nullptr, selectedPlayer < MAX_PLAYERS, "Cannot be called for selectedPlayer: %" PRIu32 "", selectedPlayer);
	if (onMission)
	{
		return &mission.apsDroidLists[selectedPlayer];
	}
	else
	{
		return &apsDroidLists[selectedPlayer];
	}
}

UDWORD transporterGetLaunchTime()
{
	return g_iLaunchTime;
}

void transporterSetLaunchTime(UDWORD time)
{
	g_iLaunchTime = time;
}

/*launches the defined transporter to the offworld map*/
bool launchTransporter(DROID *psTransporter)
{
	UDWORD	iX, iY;

	//close the interface
	intResetScreen(true);

	//this launches the mission if on homebase when the button is pressed
	if (!onMission)
	{
		//tell the transporter to move to the new offworld location
		missionGetTransporterExit(psTransporter->player, &iX, &iY);
		orderDroidLoc(psTransporter, DORDER_TRANSPORTOUT, iX, iY, ModeQueue);
		//g_iLaunchTime = gameTime;
		transporterSetLaunchTime(gameTime);
	}
	//otherwise just launches the Transporter
	else
	{
		if (!psTransporter->isTransporter())
		{
			ASSERT(false, "Invalid Transporter Droid");
			return false;
		}

		orderDroid(psTransporter, DORDER_TRANSPORTIN, ModeImmediate);
		/* set action transporter waits for timer */
		actionDroid(psTransporter, DACTION_TRANSPORTWAITTOFLYIN);

		missionSetReinforcementTime(gameTime);
	}

	return true;
}

#define	TRANSPORTOUT_TIME	4*GAME_TICKS_PER_SEC

/*checks how long the transporter has been travelling to see if it should
have arrived - returns true when there*/
bool updateTransporter(DROID *psTransporter)
{
	ASSERT_OR_RETURN(true, psTransporter != nullptr, "Invalid droid pointer");
	ASSERT_OR_RETURN(true, psTransporter->isTransporter(), "Invalid droid type");

	//if not moving to mission site, exit
	if (psTransporter->action != DACTION_TRANSPORTOUT && psTransporter->action != DACTION_TRANSPORTIN)
	{
		return true;
	}

	// moving to a location
	// if we're coming back for more droids then we want the transporter to
	// fly to edge of map before turning round again
	if (psTransporter->sMove.Status == MOVEINACTIVE ||
	    psTransporter->sMove.Status == MOVEHOVER ||
	    (psTransporter->action == DACTION_TRANSPORTOUT && !missionIsOffworld() &&
	     (gameTime > transporterGetLaunchTime() + TRANSPORTOUT_TIME) &&
	     !getDroidsToSafetyFlag()))
	{
		audio_StopObjTrack(psTransporter, ID_SOUND_BLIMP_FLIGHT);
		if (psTransporter->action == DACTION_TRANSPORTIN)
		{
			/* !!!! GJ Hack - should be landing audio !!!! */
			audio_PlayObjDynamicTrack(psTransporter, ID_SOUND_BLIMP_TAKE_OFF, nullptr);
		}

		if (!bFirstTransporter && missionForReInforcements() &&
		    psTransporter->action == DACTION_TRANSPORTIN &&
		    psTransporter->player == selectedPlayer)
		{
			//play reinforcements have arrived message
			audio_QueueTrackPos(ID_SOUND_TRANSPORT_LANDING,
			                    psTransporter->pos.x, psTransporter->pos.y, psTransporter->pos.z);
			addConsoleMessage(_("Reinforcements landing"), LEFT_JUSTIFY, SYSTEM_MESSAGE);
			//reset the data for the transporter timer
			widgSetUserData(psWScreen, IDTRANTIMER_DISPLAY, (void *)nullptr);
			return true;
		}

		//Remove visibility so tiles are not bright around where the transporter left the map
		if (psTransporter->action != DACTION_TRANSPORTIN)
		{
			visRemoveVisibility((BASE_OBJECT *) psTransporter);
		}

		// Got to destination
		psTransporter->action = DACTION_NONE;

		//reset the flag to trigger the audio message
		bFirstTransporter = false;

		return true;
	}

	//not arrived yet...
	return false;
}

//process the launch transporter button click
void processLaunchTransporter()
{
	UDWORD		capacity = TRANSPORTER_CAPACITY;
	W_CLICKFORM *psForm;

	//launch the Transporter
	if (psCurrTransporter)
	{
		//check there is something on the transporter
		capacity = calcRemainingCapacity(psCurrTransporter);
		if (capacity != TRANSPORTER_CAPACITY)
		{
			//make sure the button doesn't flash once launched
			stopMissionButtonFlash(IDTRANS_LAUNCH);
			//disable the form so can't add any more droids into the transporter
			psForm = (W_CLICKFORM *)widgGetFromID(psWScreen, IDTRANS_LAUNCH);
			if (psForm)
			{
				psForm->setState(WBUT_LOCK);
			}

			//disable the form so can't add any more droids into the transporter
			psForm = (W_CLICKFORM *)widgGetFromID(psWScreen, IDTRANTIMER_BUTTON);
			if (psForm)
			{
				psForm->setState(WBUT_LOCK);
			}

			launchTransporter(psCurrTransporter);
			//set the data for the transporter timer
			widgSetUserData(psWScreen, IDTRANTIMER_DISPLAY, (void *)psCurrTransporter);

			executeFnAndProcessScriptQueuedRemovals([]() { triggerEvent(TRIGGER_TRANSPORTER_LAUNCH, psCurrTransporter); });
		}
	}
}

SDWORD	bobTransporterHeight()
{
	// Because 4320/12 = 360 degrees
	// this gives us a bob frequency of 4.32 seconds.
	// we scale amplitude to 10 (world coordinate metric).
	// we need to use 360 degrees and not 180, as otherwise
	// it will not 'bounce' off the top _and_ bottom of
	// it's movemment arc.

	return iSinSR(gameTime, 4320, 10);
}

/*causes one of the mission buttons (Launch Button or Mission Timer) to start flashing*/
void flashMissionButton(UDWORD buttonID)
{
	//get the button from the id
	WIDGET *psForm = widgGetFromID(psWScreen, buttonID);
	if (psForm)
	{
		switch (buttonID)
		{
		case IDTRANS_LAUNCH:
			psForm->UserData = PACKDWORD_TRI(1, IMAGE_LAUNCHDOWN, IMAGE_LAUNCHUP);
			break;
		case IDTIMER_FORM:
			psForm->UserData = PACKDWORD_TRI(1, IMAGE_MISSION_CLOCK, IMAGE_MISSION_CLOCK_UP);
			break;
		default:
			//do nothing other than in debug
			ASSERT(false, "flashMissionButton: Unknown button ID");
			break;
		}
	}
}

/*stops one of the mission buttons (Launch Button or Mission Timer) flashing*/
void stopMissionButtonFlash(UDWORD buttonID)
{
	//get the button from the id
	WIDGET *psForm = widgGetFromID(psWScreen, buttonID);
	if (psForm)
	{
		switch (buttonID)
		{
		case IDTRANS_LAUNCH:
			psForm->UserData = PACKDWORD_TRI(0, IMAGE_LAUNCHDOWN, IMAGE_LAUNCHUP);
			break;
		case IDTIMER_FORM:
			psForm->UserData = PACKDWORD_TRI(0, IMAGE_MISSION_CLOCK, IMAGE_MISSION_CLOCK_UP);
			break;
		default:
			//do nothing other than in debug
			ASSERT(false, "stopMissionButtonFlash: Unknown button ID");
			break;
		}
	}
}

/*called when a Transporter has arrived back at the LZ when sending droids to safety*/
void resetTransporter()
{
	W_CLICKFORM *psForm;

	//enable the form so can add more droids into the transporter
	psForm = (W_CLICKFORM *)widgGetFromID(psWScreen, IDTRANS_LAUNCH);
	if (psForm)
	{
		psForm->setState(0);
	}
}

/*checks the order of the droid to see if its currently flying*/
bool transporterFlying(const DROID *psTransporter)
{
	ASSERT_OR_RETURN(false, psTransporter != nullptr, "Invalid droid pointer");
	ASSERT_OR_RETURN(false, psTransporter->isTransporter(), "Droid is not a Transporter");

	return psTransporter->order.type == DORDER_TRANSPORTOUT ||
	       psTransporter->order.type == DORDER_TRANSPORTIN ||
	       psTransporter->order.type == DORDER_TRANSPORTRETURN ||
	       //in multiPlayer mode the Transporter can be moved around
	       (bMultiPlayer && psTransporter->order.type == DORDER_MOVE) ||
	       //in multiPlayer mode the Transporter can be moved and emptied!
	       (bMultiPlayer && psTransporter->order.type == DORDER_DISEMBARK) ||
	       //in multiPlayer, descending still counts as flying
	       (bMultiPlayer && psTransporter->order.type == DORDER_NONE &&
	        psTransporter->sMove.iVertSpeed != 0);
}

//initialise the flag to indicate the first transporter has arrived - set in startMission()
void initFirstTransporterFlag()
{
	bFirstTransporter = true;
}
