/* ScummVM - Graphic Adventure Engine
 *
 * ScummVM is the legal property of its developers, whose names
 * are too numerous to list here. Please refer to the COPYRIGHT
 * file distributed with this source distribution.
 *
 * 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.
 *
 * 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 "common/coroutines.h"
#include "common/algorithm.h"
#include "common/debug.h"
#include "common/hashmap.h"
#include "common/hash-str.h"
#include "common/system.h"
#include "common/textconsole.h"

namespace Common {

/** Helper null context instance */
CoroContext nullContext = nullptr;

DECLARE_SINGLETON(CoroutineScheduler);

#ifdef COROUTINE_DEBUG
namespace {
/** Count of active coroutines */
static int s_coroCount = 0;

typedef Common::HashMap<Common::String, int> CoroHashMap;
static CoroHashMap *s_coroFuncs = 0;

/**
 * Change the current coroutine status
 */
static void changeCoroStats(const char *func, int change) {
	if (!s_coroFuncs)
		s_coroFuncs = new CoroHashMap();

	(*s_coroFuncs)[func] += change;
}

/**
 * Display the details of active coroutines
 */
static void displayCoroStats() {
	debug("%d active coros", s_coroCount);

	// Loop over s_coroFuncs and print info about active coros
	if (!s_coroFuncs)
		return;
	for (CoroHashMap::const_iterator it = s_coroFuncs->begin();
	        it != s_coroFuncs->end(); ++it) {
		if (it->_value != 0)
			debug("  %3d x %s", it->_value, it->_key.c_str());
	}
}

} // End of anonymous namespace
#endif

CoroBaseContext::CoroBaseContext(const char *func)
	: _line(0), _sleep(0), _subctx(nullptr) {
#ifdef COROUTINE_DEBUG
	_funcName = func;
	changeCoroStats(_funcName, +1);
	s_coroCount++;
#endif
}

CoroBaseContext::~CoroBaseContext() {
#ifdef COROUTINE_DEBUG
	s_coroCount--;
	changeCoroStats(_funcName, -1);
	debug("Deleting coro in %s at %p (subctx %p)",
	      _funcName, (void *)this, (void *)_subctx);
	displayCoroStats();
#endif
	delete _subctx;
}

//--------------------- Scheduler Class ------------------------

CoroutineScheduler::CoroutineScheduler() {
	processList = nullptr;
	pFreeProcesses = nullptr;
	pCurrent = nullptr;

#ifdef DEBUG
	// diagnostic process counters
	numProcs = 0;
	maxProcs = 0;
#endif

	pRCfunction = nullptr;
	pidCounter = 0;

	active = new PROCESS;
	active->pPrevious = nullptr;
	active->pNext = nullptr;

	reset();
}

CoroutineScheduler::~CoroutineScheduler() {
	// Kill all running processes (i.e. free memory allocated for their state).
	PROCESS *pProc = active->pNext;
	while (pProc != nullptr) {
		delete pProc->state;
		pProc->state = nullptr;
		pProc = pProc->pNext;
	}

	free(processList);
	processList = nullptr;

	delete active;
	active = nullptr;

	// Clear the event list
	Common::List<EVENT *>::iterator i;
	for (i = _events.begin(); i != _events.end(); ++i)
		delete *i;
}

void CoroutineScheduler::reset() {
#ifdef DEBUG
	// clear number of process in use
	numProcs = 0;
#endif

	if (processList == nullptr) {
		// first time - allocate memory for process list
		processList = (PROCESS *)calloc(CORO_MAX_PROCESSES, sizeof(PROCESS));

		// make sure memory allocated
		if (processList == nullptr) {
			error("Cannot allocate memory for process data");
		}

		// fill with garbage
		memset(processList, 'S', CORO_MAX_PROCESSES * sizeof(PROCESS));
	}

	// Kill all running processes (i.e. free memory allocated for their state).
	PROCESS *pProc = active->pNext;
	while (pProc != nullptr) {
		delete pProc->state;
		pProc->state = nullptr;
		Common::fill(&pProc->pidWaiting[0], &pProc->pidWaiting[CORO_MAX_PID_WAITING], 0);
		pProc = pProc->pNext;
	}

	// no active processes
	pCurrent = active->pNext = nullptr;

	// place first process on free list
	pFreeProcesses = processList;

	// link all other processes after first
	for (int i = 1; i <= CORO_NUM_PROCESS; i++) {
		processList[i - 1].pNext = (i == CORO_NUM_PROCESS) ? nullptr : processList + i;
		processList[i - 1].pPrevious = (i == 1) ? active : processList + (i - 2);
	}
}


#ifdef DEBUG
void CoroutineScheduler::printStats() {
	debug("%i process of %i used", maxProcs, CORO_NUM_PROCESS);
}
#endif

#ifdef DEBUG
void CoroutineScheduler::checkStack() {
	Common::List<PROCESS *> pList;

	// Check both the active and free process lists
	for (int i = 0; i < 2; ++i) {
		PROCESS *p = (i == 0) ? active : pFreeProcesses;

		if (p != NULL) {
			// Make sure the linkages are correct
			while (p->pNext != NULL) {
				assert(p->pNext->pPrevious == p);
				pList.push_back(p);
				p = p->pNext;
			}
			pList.push_back(p);
		}
	}

	// Make sure all processes are accounted for
	for (int idx = 0; idx < CORO_NUM_PROCESS; idx++) {
		bool found = false;
		for (Common::List<PROCESS *>::iterator i = pList.begin(); i != pList.end(); ++i) {
			PROCESS *pTemp = *i;
			if (*i == &processList[idx]) {
				found = true;
				break;
			}
		}

		assert(found);
	}
}
#endif

void CoroutineScheduler::schedule() {
	// start dispatching active process list
	PROCESS *pNext;
	PROCESS *pProc = active->pNext;
	while (pProc != nullptr) {
		pNext = pProc->pNext;

		if (--pProc->sleepTime <= 0) {
			// process is ready for dispatch, activate it
			pCurrent = pProc;
			pProc->coroAddr(pProc->state, pProc->param);

			if (!pProc->state || pProc->state->_sleep <= 0) {
				// Coroutine finished
				pCurrent = pCurrent->pPrevious;
				killProcess(pProc);
			} else {
				pProc->sleepTime = pProc->state->_sleep;
			}

			// pCurrent may have been changed
			pNext = pCurrent->pNext;
			pCurrent = nullptr;
		}

		pProc = pNext;
	}

	// Disable any events that were pulsed
	Common::List<EVENT *>::iterator i;
	for (i = _events.begin(); i != _events.end(); ++i) {
		EVENT *evt = *i;
		if (evt->pulsing) {
			evt->pulsing = evt->signalled = false;
		}
	}
}

void CoroutineScheduler::rescheduleAll() {
	assert(pCurrent);

	// Unlink current process
	pCurrent->pPrevious->pNext = pCurrent->pNext;
	if (pCurrent->pNext)
		pCurrent->pNext->pPrevious = pCurrent->pPrevious;

	// Add process to the start of the active list
	pCurrent->pNext = active->pNext;
	active->pNext->pPrevious = pCurrent;
	active->pNext = pCurrent;
	pCurrent->pPrevious = active;
}

void CoroutineScheduler::reschedule(PPROCESS pReSchedProc) {
	// If not currently processing the schedule list, then no action is needed
	if (!pCurrent)
		return;

	if (!pReSchedProc)
		pReSchedProc = pCurrent;

	PPROCESS pEnd;

	// Find the last process in the list.
	// But if the target process is down the list from here, do nothing
	for (pEnd = pCurrent; pEnd->pNext != nullptr; pEnd = pEnd->pNext) {
		if (pEnd->pNext == pReSchedProc)
			return;
	}

	assert(pEnd->pNext == nullptr);

	// Could be in the middle of a KillProc()!
	// Dying process was last and this process was penultimate
	if (pReSchedProc->pNext == nullptr)
		return;

	// If we're moving the current process, move it back by one, so that the next
	// schedule() iteration moves to the now next one
	if (pCurrent == pReSchedProc)
		pCurrent = pCurrent->pPrevious;

	// Unlink the process, and add it at the end
	pReSchedProc->pPrevious->pNext = pReSchedProc->pNext;
	pReSchedProc->pNext->pPrevious = pReSchedProc->pPrevious;
	pEnd->pNext = pReSchedProc;
	pReSchedProc->pPrevious = pEnd;
	pReSchedProc->pNext = nullptr;
}

void CoroutineScheduler::giveWay(PPROCESS pReSchedProc) {
	// If not currently processing the schedule list, then no action is needed
	if (!pCurrent)
		return;

	if (!pReSchedProc)
		pReSchedProc = pCurrent;

	// If the process is already at the end of the queue, nothing has to be done
	if (!pReSchedProc->pNext)
		return;

	PPROCESS pEnd;

	// Find the last process in the list.
	for (pEnd = pCurrent; pEnd->pNext != nullptr; pEnd = pEnd->pNext)
		;
	assert(pEnd->pNext == nullptr);


	// If we're moving the current process, move it back by one, so that the next
	// schedule() iteration moves to the now next one
	if (pCurrent == pReSchedProc)
		pCurrent = pCurrent->pPrevious;

	// Unlink the process, and add it at the end
	pReSchedProc->pPrevious->pNext = pReSchedProc->pNext;
	pReSchedProc->pNext->pPrevious = pReSchedProc->pPrevious;
	pEnd->pNext = pReSchedProc;
	pReSchedProc->pPrevious = pEnd;
	pReSchedProc->pNext = nullptr;
}

void CoroutineScheduler::waitForSingleObject(CORO_PARAM, int pid, uint32 duration, bool *expired) {
	if (!pCurrent)
		error("Called CoroutineScheduler::waitForSingleObject from the main process");

	CORO_BEGIN_CONTEXT;
		uint32 endTime;
		PROCESS *pProcess;
		EVENT *pEvent;
	CORO_END_CONTEXT(_ctx);

	CORO_BEGIN_CODE(_ctx);

	// Signal the process Id this process is now waiting for
	pCurrent->pidWaiting[0] = pid;

	_ctx->endTime = (duration == CORO_INFINITE) ? CORO_INFINITE : g_system->getMillis() + duration;
	if (expired)
		// Presume it will expire
		*expired = true;

	// Outer loop for doing checks until expiry
	while (g_system->getMillis() <= _ctx->endTime) {
		// Check to see if a process or event with the given Id exists
		_ctx->pProcess = getProcess(pid);
		_ctx->pEvent = !_ctx->pProcess ? getEvent(pid) : nullptr;

		// If there's no active process or event, presume it's a process that's finished,
		// so the waiting can immediately exit
		if ((_ctx->pProcess == nullptr) && (_ctx->pEvent == nullptr)) {
			if (expired)
				*expired = false;
			break;
		}

		// If a process was found, don't go into the if statement, and keep waiting.
		// Likewise if it's an event that's not yet signalled
		if ((_ctx->pEvent != nullptr) && _ctx->pEvent->signalled) {
			// Unless the event is flagged for manual reset, reset it now
			if (!_ctx->pEvent->manualReset)
				_ctx->pEvent->signalled = false;

			if (expired)
				*expired = false;
			break;
		}

		// Sleep until the next cycle
		CORO_SLEEP(1);
	}

	// Signal waiting is done
	Common::fill(&pCurrent->pidWaiting[0], &pCurrent->pidWaiting[CORO_MAX_PID_WAITING], 0);

	CORO_END_CODE;
}

void CoroutineScheduler::waitForMultipleObjects(CORO_PARAM, int nCount, uint32 *pidList, bool bWaitAll,
                                                uint32 duration, bool *expired) {
	if (!pCurrent)
		error("Called CoroutineScheduler::waitForMultipleObjects from the main process");

	CORO_BEGIN_CONTEXT;
		uint32 endTime;
		bool signalled;
		bool pidSignalled;
		int i;
		PROCESS *pProcess;
		EVENT *pEvent;
	CORO_END_CONTEXT(_ctx);

	CORO_BEGIN_CODE(_ctx);

	// Signal the waiting events
	assert(nCount < CORO_MAX_PID_WAITING);
	Common::copy(pidList, pidList + nCount, pCurrent->pidWaiting);

	_ctx->endTime = (duration == CORO_INFINITE) ? CORO_INFINITE : g_system->getMillis() + duration;
	if (expired)
		// Presume that delay will expire
		*expired = true;

	// Outer loop for doing checks until expiry
	while (g_system->getMillis() <= _ctx->endTime) {
		_ctx->signalled = bWaitAll;

		for (_ctx->i = 0; _ctx->i < nCount; ++_ctx->i) {
			_ctx->pProcess = getProcess(pidList[_ctx->i]);
			_ctx->pEvent = !_ctx->pProcess ? getEvent(pidList[_ctx->i]) : nullptr;

			// Determine the signalled state
			_ctx->pidSignalled = (_ctx->pProcess) || !_ctx->pEvent ? false : _ctx->pEvent->signalled;

			if (bWaitAll && !_ctx->pidSignalled)
				_ctx->signalled = false;
			else if (!bWaitAll && _ctx->pidSignalled)
				_ctx->signalled = true;
		}

		// At this point, if the signalled variable is set, waiting is finished
		if (_ctx->signalled) {
			// Automatically reset any events not flagged for manual reset
			for (_ctx->i = 0; _ctx->i < nCount; ++_ctx->i) {
				_ctx->pEvent = getEvent(pidList[_ctx->i]);

				if (!_ctx->pEvent->manualReset)
					_ctx->pEvent->signalled = false;
			}

			if (expired)
				*expired = false;
			break;
		}

		// Sleep until the next cycle
		CORO_SLEEP(1);
	}

	// Signal waiting is done
	Common::fill(&pCurrent->pidWaiting[0], &pCurrent->pidWaiting[CORO_MAX_PID_WAITING], 0);

	CORO_END_CODE;
}

void CoroutineScheduler::sleep(CORO_PARAM, uint32 duration) {
	if (!pCurrent)
		error("Called CoroutineScheduler::sleep from the main process");

	CORO_BEGIN_CONTEXT;
		uint32 endTime;
		PROCESS *pProcess;
		EVENT *pEvent;
	CORO_END_CONTEXT(_ctx);

	CORO_BEGIN_CODE(_ctx);

	_ctx->endTime = g_system->getMillis() + duration;

	// Outer loop for doing checks until expiry
	while (g_system->getMillis() < _ctx->endTime) {
		// Sleep until the next cycle
		CORO_SLEEP(1);
	}

	CORO_END_CODE;
}

PROCESS *CoroutineScheduler::createProcess(uint32 pid, CORO_ADDR coroAddr, const void *pParam, int sizeParam) {
	PROCESS *pProc;

	// get a free process
	pProc = pFreeProcesses;

	// trap no free process
	assert(pProc != nullptr); // Out of processes

#ifdef DEBUG
	// one more process in use
	if (++numProcs > maxProcs)
		maxProcs = numProcs;
#endif

	// get link to next free process
	pFreeProcesses = pProc->pNext;
	if (pFreeProcesses)
		pFreeProcesses->pPrevious = nullptr;

	if (pCurrent != nullptr) {
		// place new process before the next active process
		pProc->pNext = pCurrent->pNext;
		if (pProc->pNext)
			pProc->pNext->pPrevious = pProc;

		// make this new process the next active process
		pCurrent->pNext = pProc;
		pProc->pPrevious = pCurrent;

	} else { // no active processes, place process at head of list
		pProc->pNext = active->pNext;
		pProc->pPrevious = active;

		if (pProc->pNext)
			pProc->pNext->pPrevious = pProc;
		active->pNext = pProc;

	}

	// set coroutine entry point
	pProc->coroAddr = coroAddr;

	// clear coroutine state
	pProc->state = nullptr;

	// wake process up as soon as possible
	pProc->sleepTime = 1;

	// set new process id
	pProc->pid = pid;

	// set new process specific info
	if (sizeParam) {
		assert(sizeParam > 0 && sizeParam <= CORO_PARAM_SIZE);

		// set new process specific info
		memcpy(pProc->param, pParam, sizeParam);
	}

	// return created process
	return pProc;
}

uint32 CoroutineScheduler::createProcess(CORO_ADDR coroAddr, const void *pParam, int sizeParam) {
	PROCESS *pProc = createProcess(++pidCounter, coroAddr, pParam, sizeParam);
	return pProc->pid;
}

uint32 CoroutineScheduler::createProcess(CORO_ADDR coroAddr, const void *pParam) {
	return createProcess(coroAddr, &pParam, sizeof(void *));
}

void CoroutineScheduler::killProcess(PROCESS *pKillProc) {
	// make sure a valid process pointer
	assert(pKillProc >= processList && pKillProc <= processList + CORO_NUM_PROCESS - 1);

	// can not kill the current process using killProcess !
	assert(pCurrent != pKillProc);

#ifdef DEBUG
	// one less process in use
	--numProcs;
	assert(numProcs >= 0);
#endif

	// Free process' resources
	if (pRCfunction != nullptr)
		(pRCfunction)(pKillProc);

	delete pKillProc->state;
	pKillProc->state = nullptr;

	// Take the process out of the active chain list
	pKillProc->pPrevious->pNext = pKillProc->pNext;
	if (pKillProc->pNext)
		pKillProc->pNext->pPrevious = pKillProc->pPrevious;

	// link first free process after pProc
	pKillProc->pNext = pFreeProcesses;
	if (pFreeProcesses)
		pKillProc->pNext->pPrevious = pKillProc;
	pKillProc->pPrevious = nullptr;

	// make pKillProc the first free process
	pFreeProcesses = pKillProc;
}

PROCESS *CoroutineScheduler::getCurrentProcess() {
	return pCurrent;
}

int CoroutineScheduler::getCurrentPID() const {
	PROCESS *pProc = pCurrent;

	// make sure a valid process pointer
	assert(pProc >= processList && pProc <= processList + CORO_NUM_PROCESS - 1);

	// return processes PID
	return pProc->pid;
}

int CoroutineScheduler::killMatchingProcess(uint32 pidKill, int pidMask) {
	int numKilled = 0;
	PROCESS *pProc, *pPrev; // process list pointers

	for (pProc = active->pNext, pPrev = active; pProc != nullptr; pPrev = pProc, pProc = pProc->pNext) {
		if ((pProc->pid & (uint32)pidMask) == pidKill) {
			// found a matching process

			// dont kill the current process
			if (pProc != pCurrent) {
				// kill this process
				numKilled++;

				// Free the process' resources
				if (pRCfunction != nullptr)
					(pRCfunction)(pProc);

				delete pProc->state;
				pProc->state = nullptr;

				// make prev point to next to unlink pProc
				pPrev->pNext = pProc->pNext;
				if (pProc->pNext)
					pPrev->pNext->pPrevious = pPrev;

				// link first free process after pProc
				pProc->pNext = pFreeProcesses;
				pProc->pPrevious = nullptr;
				pFreeProcesses->pPrevious = pProc;

				// make pProc the first free process
				pFreeProcesses = pProc;

				// set to a process on the active list
				pProc = pPrev;
			}
		}
	}

#ifdef DEBUG
	// adjust process in use
	numProcs -= numKilled;
	assert(numProcs >= 0);
#endif

	// return number of processes killed
	return numKilled;
}

void CoroutineScheduler::setResourceCallback(VFPTRPP pFunc) {
	pRCfunction = pFunc;
}

PROCESS *CoroutineScheduler::getProcess(uint32 pid) {
	PROCESS *pProc = active->pNext;
	while ((pProc != nullptr) && (pProc->pid != pid))
		pProc = pProc->pNext;

	return pProc;
}

EVENT *CoroutineScheduler::getEvent(uint32 pid) {
	Common::List<EVENT *>::iterator i;
	for (i = _events.begin(); i != _events.end(); ++i) {
		EVENT *evt = *i;
		if (evt->pid == pid)
			return evt;
	}

	return nullptr;
}


uint32 CoroutineScheduler::createEvent(bool bManualReset, bool bInitialState) {
	EVENT *evt = new EVENT();
	evt->pid = ++pidCounter;
	evt->manualReset = bManualReset;
	evt->signalled = bInitialState;
	evt->pulsing = false;

	_events.push_back(evt);
	return evt->pid;
}

void CoroutineScheduler::closeEvent(uint32 pidEvent) {
	EVENT *evt = getEvent(pidEvent);
	if (evt) {
		_events.remove(evt);
		delete evt;
	}
}

void CoroutineScheduler::setEvent(uint32 pidEvent) {
	EVENT *evt = getEvent(pidEvent);
	if (evt)
		evt->signalled = true;
}

void CoroutineScheduler::resetEvent(uint32 pidEvent) {
	EVENT *evt = getEvent(pidEvent);
	if (evt)
		evt->signalled = false;
}

void CoroutineScheduler::pulseEvent(uint32 pidEvent) {
	EVENT *evt = getEvent(pidEvent);
	if (!evt)
		return;

	// Set the event as signalled and pulsing
	evt->signalled = true;
	evt->pulsing = true;

	// If there's an active process, and it's not the first in the queue, then reschedule all
	// the other prcoesses in the queue to run again this frame
	if (pCurrent && pCurrent != active->pNext)
		rescheduleAll();
}

} // end of namespace Common
