/* 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.
 *
 * Additional copyright for this file:
 * Copyright (C) 1995-1997 Presto Studios, Inc.
 *
 * 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 "pegasus/pegasus.h"
#include "pegasus/notification.h"
#include "pegasus/timers.h"

namespace Pegasus {

Idler::Idler() {
	_isIdling = false;
	_nextIdler = 0;
	_prevIdler = 0;
}

Idler::~Idler() {
	stopIdling();
}

void Idler::startIdling() {
	if (!isIdling()) {
		((PegasusEngine *)g_engine)->addIdler(this);
		_isIdling = true;
	}
}

void Idler::stopIdling() {
	if (isIdling()) {
		((PegasusEngine *)g_engine)->removeIdler(this);
		_isIdling = false;
	}
}

TimeBase::TimeBase(const TimeScale preferredScale) {
	_preferredScale = preferredScale;
	_callBackList = 0;
	_paused = false;
	_flags = 0;
	_lastMillis = 0;
	_time = 0;
	_rate = 0;
	_startTime = 0;
	_startScale = 1;
	_stopTime = 0xffffffff;
	_stopScale = 1;
	_master = 0;
	_pausedRate = 0;
	_pauseStart = 0;

	((PegasusEngine *)g_engine)->addTimeBase(this);
}

TimeBase::~TimeBase() {
	((PegasusEngine *)g_engine)->removeTimeBase(this);
	disposeAllCallBacks();
}

void TimeBase::setTime(const TimeValue time, const TimeScale scale) {
	_time = Common::Rational(time, (scale == 0) ? _preferredScale : scale);
	_lastMillis = 0;
}

TimeValue TimeBase::getTime(const TimeScale scale) {
	// HACK: Emulate the master TimeBase code here for the one case that needs it in the
	// game. Note that none of the master TimeBase code in this file should actually be
	// used as a reference for anything ever.
	if (_master)
		return _master->getTime(scale);

	return _time.getNumerator() * ((scale == 0) ? _preferredScale : scale) / _time.getDenominator();
}

void TimeBase::setRate(const Common::Rational rate) {
	_rate = rate;
	_lastMillis = 0;

	if (_rate == 0)
		_paused = false;
}

void TimeBase::start() {
	if (_paused)
		_pausedRate = 1;
	else
		setRate(1);
}

void TimeBase::stop() {
	setRate(0);
	_paused = false;
}

void TimeBase::pause() {
	if (isRunning() && !_paused) {
		_pausedRate = getRate();
		_rate = 0;
		_paused = true;
		_pauseStart = g_system->getMillis();
	}
}

void TimeBase::resume() {
	if (_paused) {
		_rate = _pausedRate;
		_paused = false;

		if (isRunning())
			_lastMillis += g_system->getMillis() - _pauseStart;
	}
}

bool TimeBase::isRunning() {
	if (_paused && _pausedRate != 0)
		return true;

	Common::Rational rate = getRate();

	if (rate == 0)
		return false;

	if (getFlags() & kLoopTimeBase)
		return true;

	if (rate > 0)
		return getTime() != getStop();

	return getTime() != getStart();
}

void TimeBase::setStart(const TimeValue startTime, const TimeScale scale) {
	_startTime = startTime;
	_startScale = (scale == 0) ? _preferredScale : scale;
}

TimeValue TimeBase::getStart(const TimeScale scale) const {
	if (scale)
		return _startTime * scale / _startScale;

	return _startTime * _preferredScale / _startScale;
}

void TimeBase::setStop(const TimeValue stopTime, const TimeScale scale) {
	_stopTime = stopTime;
	_stopScale = (scale == 0) ? _preferredScale : scale;
}

TimeValue TimeBase::getStop(const TimeScale scale) const {
	if (scale)
		return _stopTime * scale / _stopScale;

	return _stopTime * _preferredScale / _stopScale;
}

void TimeBase::setSegment(const TimeValue startTime, const TimeValue stopTime, const TimeScale scale) {
	setStart(startTime, scale);
	setStop(stopTime, scale);
}

void TimeBase::getSegment(TimeValue &startTime, TimeValue &stopTime, const TimeScale scale) const {
	startTime = getStart(scale);
	stopTime = getStop(scale);
}

TimeValue TimeBase::getDuration(const TimeScale scale) const {
	TimeValue startTime, stopTime;
	getSegment(startTime, stopTime, scale);
	return stopTime - startTime;
}

void TimeBase::setMasterTimeBase(TimeBase *tb) {
	_master = tb;
}

void TimeBase::updateTime() {
	if (_master) {
		_master->updateTime();
		return;
	}

	if (_lastMillis == 0) {
		_lastMillis = g_system->getMillis();
	} else {
		uint32 curTime = g_system->getMillis();
		if (_lastMillis == curTime) // No change
			return;

		_time += Common::Rational(curTime - _lastMillis, 1000) * getRate();
		_lastMillis = curTime;
	}
}

void TimeBase::checkCallBacks() {
	// Nothing to do if we're paused or not running
	if (_paused || !isRunning())
		return;

	Common::Rational startTime = Common::Rational(_startTime, _startScale);
	Common::Rational stopTime = Common::Rational(_stopTime, _stopScale);

	// First step: update the times
	updateTime();

	// Clip time to the boundaries
	if (_time >= stopTime)
		_time = stopTime;
	else if (_time <= startTime)
		_time = startTime;

	Common::Rational time = Common::Rational(getTime(), getScale());

	// Check if we've triggered any callbacks
	for (TimeBaseCallBack *runner = _callBackList; runner != 0; runner = runner->_nextCallBack) {
		if (runner->_hasBeenTriggered)
			continue;

		if (runner->_type == kCallBackAtTime && runner->_trigger == kTriggerTimeFwd) {
			if (getTime() >= (runner->_param2 * _preferredScale / runner->_param3) && getRate() > 0) {
				uint param2 = runner->_param2, param3 = runner->_param3;
				runner->callBack();
				// HACK: Only stop future time forward callbacks if the parameters have not been changed
				// This fixes striding callbacks. Since only striding callbacks do this kind of
				// craziness, I'm not too worried about this.
				runner->_hasBeenTriggered = (runner->_param2 == param2 && runner->_param3 == param3);
			}
		} else if (runner->_type == kCallBackAtExtremes) {
			if (runner->_trigger == kTriggerAtStop) {
				if (time == stopTime) {
					runner->callBack();
					runner->_hasBeenTriggered = true;
				}
			} else if (runner->_trigger == kTriggerAtStart) {
				if (time == startTime) {
					runner->callBack();
					runner->_hasBeenTriggered = true;
				}
			}
		}
	}

	if (getFlags() & kLoopTimeBase) {
		// Loop if necessary
		if (getRate() < 0 && time == startTime)
			setTime(_stopTime, _stopScale);
		else if (getRate() > 0 && time == stopTime)
			setTime(_startTime, _startScale);
	} else {
		// Stop at the end
		if ((getRate() > 0 && time == stopTime) || (getRate() < 0 && time == startTime))
			stop();
	}
}

// Protected functions only called by TimeBaseCallBack.

void TimeBase::addCallBack(TimeBaseCallBack *callBack) {
	callBack->_nextCallBack = _callBackList;
	_callBackList = callBack;
}

void TimeBase::removeCallBack(TimeBaseCallBack *callBack) {
	if (_callBackList == callBack) {
		_callBackList = callBack->_nextCallBack;
	} else {
		TimeBaseCallBack *runner, *prevRunner;

		for (runner = _callBackList->_nextCallBack, prevRunner = _callBackList; runner != callBack; prevRunner = runner, runner = runner->_nextCallBack)
			;

		prevRunner->_nextCallBack = runner->_nextCallBack;
	}

	callBack->_nextCallBack = 0;
}

void TimeBase::disposeAllCallBacks() {
	TimeBaseCallBack *nextRunner;

	for (TimeBaseCallBack *runner = _callBackList; runner != 0; runner = nextRunner) {
		nextRunner = runner->_nextCallBack;
		runner->disposeCallBack();
		runner->_nextCallBack = 0;
	}

	_callBackList = 0;
}

TimeBaseCallBack::TimeBaseCallBack() {
	_timeBase = 0;
	_nextCallBack = 0;
	_trigger = kTriggerNone;
	_type = kCallBackNone;
	_hasBeenTriggered = false;
}

TimeBaseCallBack::~TimeBaseCallBack() {
	releaseCallBack();
}

void TimeBaseCallBack::initCallBack(TimeBase *tb, CallBackType type) {
	releaseCallBack();
	_timeBase = tb;
	_timeBase->addCallBack(this);
	_type = type;
}

void TimeBaseCallBack::releaseCallBack() {
	if (_timeBase)
		_timeBase->removeCallBack(this);
	disposeCallBack();
}

void TimeBaseCallBack::disposeCallBack() {
	_timeBase = 0;
	_trigger = kTriggerNone;
	_hasBeenTriggered = false;
}

void TimeBaseCallBack::scheduleCallBack(CallBackTrigger trigger, uint32 param2, uint32 param3) {
	// TODO: Rename param2/param3?
	_trigger = trigger;
	_param2 = param2;
	_param3 = param3;
	_hasBeenTriggered = false;
}

void TimeBaseCallBack::cancelCallBack() {
	_trigger = kTriggerNone;
	_hasBeenTriggered = false;
}

IdlerTimeBase::IdlerTimeBase() {
	_lastTime = 0xffffffff;
	startIdling();
}

void IdlerTimeBase::useIdleTime() {
	uint32 currentTime = getTime();
	if (currentTime != _lastTime) {
		_lastTime = currentTime;
		timeChanged(_lastTime);
	}
}

NotificationCallBack::NotificationCallBack() {
	_callBackFlag = 0;
	_notifier = 0;
}

void NotificationCallBack::callBack() {
	if (_notifier)
		_notifier->setNotificationFlags(_callBackFlag, _callBackFlag);
}

static const NotificationFlags kFuseExpiredFlag = 1;

Fuse::Fuse() : _fuseNotification(0, (NotificationManager *)((PegasusEngine *)g_engine)) {
	_fuseNotification.notifyMe(this, kFuseExpiredFlag, kFuseExpiredFlag);
	_fuseCallBack.setNotification(&_fuseNotification);
	_fuseCallBack.initCallBack(&_fuseTimer, kCallBackAtExtremes);
	_fuseCallBack.setCallBackFlag(kFuseExpiredFlag);
}

void Fuse::primeFuse(const TimeValue frequency, const TimeScale scale) {
	stopFuse();
	_fuseTimer.setScale(scale);
	_fuseTimer.setSegment(0, frequency);
	_fuseTimer.setTime(0);
}

void Fuse::lightFuse() {
	if (!_fuseTimer.isRunning()) {
		_fuseCallBack.scheduleCallBack(kTriggerAtStop, 0, 0);
		_fuseTimer.start();
	}
}

void Fuse::stopFuse() {
	_fuseTimer.stop();
	_fuseCallBack.cancelCallBack();
	// Make sure the fuse has not triggered but not been caught yet...
	_fuseNotification.setNotificationFlags(0, 0xffffffff);
}

void Fuse::advanceFuse(const TimeValue time) {
	if (_fuseTimer.isRunning()) {
		_fuseTimer.stop();
		_fuseTimer.setTime(_fuseTimer.getTime() + time);
		_fuseTimer.start();
	}
}

TimeValue Fuse::getTimeRemaining() {
	return _fuseTimer.getStop() - _fuseTimer.getTime();
}

void Fuse::receiveNotification(Notification *, const NotificationFlags) {
	stopFuse();
	invokeAction();
}

} // End of namespace Pegasus
