/*
 * \brief  Multiplexing one time source amongst different timeout subjects
 * \author Martin Stein
 * \date   2016-11-04
 */

/*
 * Copyright (C) 2016-2017 Genode Labs GmbH
 *
 * This file is part of the Genode OS framework, which is distributed
 * under the terms of the GNU Affero General Public License version 3.
 */

/* Genode includes */
#include <timer/timeout.h>

using namespace Genode;


/*************
 ** Timeout **
 *************/

void Timeout::schedule_periodic(Microseconds duration, Handler &handler)
{
	_alarm.handler = &handler;
	_alarm.periodic = true;
	_alarm.timeout_scheduler._schedule_periodic(*this, duration);
}


void Timeout::schedule_one_shot(Microseconds duration, Handler &handler)
{
	_alarm.handler = &handler;
	_alarm.periodic = false;
	_alarm.timeout_scheduler._schedule_one_shot(*this, duration);
}


void Timeout::discard()
{
	_alarm.timeout_scheduler._discard(*this);
	_alarm.handler = nullptr;
}


/********************
 ** Timeout::Alarm **
 ********************/

bool Timeout::Alarm::_on_alarm(uint64_t)
{
	if (handler) {
		Handler *current = handler;
		if (!periodic) {
			handler = nullptr;
		}
		current->handle_timeout(timeout_scheduler.curr_time());
	}
	return periodic;
}


Timeout::Alarm::~Alarm()
{
	if (_scheduler)
		_scheduler->_alarm_discard(this);
}


bool Timeout::Alarm::Raw::is_pending_at(uint64_t time, bool time_period) const
{
	return (time_period == deadline_period &&
	        time        >= deadline) ||
	       (time_period != deadline_period &&
	        time        <  deadline);
}


/*****************************
 ** Alarm_timeout_scheduler **
 *****************************/

void Alarm_timeout_scheduler::handle_timeout(Duration duration)
{
	uint64_t const curr_time_us = duration.trunc_to_plain_us().value;

	_alarm_handle(curr_time_us);

	/* sleep time is either until the next deadline or the maximum timout */
	uint64_t sleep_time_us;
	Alarm::Time deadline_us;
	if (_alarm_next_deadline(&deadline_us)) {
		sleep_time_us = deadline_us - curr_time_us;
	} else {
		sleep_time_us = _time_source.max_timeout().value; }

	/* limit max timeout to a more reasonable value, e.g. 60s */
	if (sleep_time_us > 60000000) {
		sleep_time_us = 60000000;
	} else if (sleep_time_us == 0) {
		sleep_time_us = 1; }

	_time_source.schedule_timeout(Microseconds(sleep_time_us), *this);
}


Alarm_timeout_scheduler::Alarm_timeout_scheduler(Time_source  &time_source,
                                                 Microseconds  min_handle_period)
:
	_time_source(time_source)
{
	Alarm::Time const deadline         = _now + min_handle_period.value;
	_min_handle_period.period          = min_handle_period.value;
	_min_handle_period.deadline        = deadline;
	_min_handle_period.deadline_period = _now > deadline ?
	                                     !_now_period : _now_period;
}


Alarm_timeout_scheduler::~Alarm_timeout_scheduler()
{
	Lock::Guard lock_guard(_lock);
	while (_active_head) {
		Alarm *next = _active_head->_next;
		_active_head->_alarm_reset();
		_active_head = next;
	}
}


void Alarm_timeout_scheduler::_enable()
{
	_time_source.schedule_timeout(Microseconds(0), *this);
}


void Alarm_timeout_scheduler::_schedule_one_shot(Timeout      &timeout,
                                                 Microseconds  duration)
{
	/* raise timeout duration by the age of the local time value */
	uint64_t us = _time_source.curr_time().trunc_to_plain_us().value;
	if (us >= _now) {
		us = duration.value + (us - _now); }
	else {
		us = duration.value + (~0UL - _now) + us; }
	if (us >= duration.value) {
		duration.value = us; }

	/* insert timeout into scheduling queue */
	_alarm_schedule_absolute(&timeout._alarm, duration.value);

	/* if new timeout is the closest to now, update the time-source timeout */
	if (_alarm_head_timeout(&timeout._alarm)) {
		_time_source.schedule_timeout(Microseconds(0), *this); }
}


void Alarm_timeout_scheduler::_schedule_periodic(Timeout      &timeout,
                                                 Microseconds  duration)
{
	_alarm_schedule(&timeout._alarm, duration.value);

	if (_alarm_head_timeout(&timeout._alarm)) {
		_time_source.schedule_timeout(Microseconds(0), *this); }
}


void Alarm_timeout_scheduler::_alarm_unsynchronized_enqueue(Alarm *alarm)
{
	if (alarm->_active) {
		error("trying to insert the same alarm twice!");
		return;
	}

	alarm->_active++;

	/* if active alarm list is empty add first element */
	if (!_active_head) {
		alarm->_next = 0;
		_active_head = alarm;
		return;
	}

	/* if deadline is smaller than any other deadline, put it on the head */
	if (alarm->_raw.is_pending_at(_active_head->_raw.deadline, _active_head->_raw.deadline_period)) {
		alarm->_next = _active_head;
		_active_head = alarm;
		return;
	}

	/* find list element with a higher deadline */
	Alarm *curr = _active_head;
	while (curr->_next &&
	       curr->_next->_raw.is_pending_at(alarm->_raw.deadline, alarm->_raw.deadline_period))
	{
		curr = curr->_next;
	}

	/* if end of list is reached, append new element */
	if (curr->_next == 0) {
		curr->_next = alarm;
		return;
	}

	/* insert element in middle of list */
	alarm->_next = curr->_next;
	curr->_next  = alarm;
}


void Alarm_timeout_scheduler::_alarm_unsynchronized_dequeue(Alarm *alarm)
{
	if (!_active_head) return;

	if (_active_head == alarm) {
		_active_head = alarm->_next;
		alarm->_alarm_reset();
		return;
	}

	/* find predecessor in alarm queue */
	Alarm *curr;
	for (curr = _active_head; curr && (curr->_next != alarm); curr = curr->_next);

	/* alarm is not enqueued */
	if (!curr) return;

	/* remove alarm from alarm queue */
	curr->_next = alarm->_next;
	alarm->_alarm_reset();
}


Timeout::Alarm *Alarm_timeout_scheduler::_alarm_get_pending_alarm()
{
	Lock::Guard lock_guard(_lock);

	if (!_active_head || !_active_head->_raw.is_pending_at(_now, _now_period)) {
		return nullptr; }

	/* remove alarm from head of the list */
	Alarm *pending_alarm = _active_head;
	_active_head = _active_head->_next;

	/*
	 * Acquire dispatch lock to defer destruction until the call of '_on_alarm'
	 * is finished
	 */
	pending_alarm->_dispatch_lock.lock();

	/* reset alarm object */
	pending_alarm->_next = nullptr;
	pending_alarm->_active--;

	return pending_alarm;
}


void Alarm_timeout_scheduler::_alarm_handle(Alarm::Time curr_time)
{
	/*
	 * Raise the time counter and if it wraps, update also in which
	 * period of the time counter we are.
	 */
	if (_now > curr_time) {
		_now_period = !_now_period;
	}
	_now = curr_time;

	if (!_min_handle_period.is_pending_at(_now, _now_period)) {
		return;
	}
	Alarm::Time const deadline         = _now + _min_handle_period.period;
	_min_handle_period.deadline        = deadline;
	_min_handle_period.deadline_period = _now > deadline ?
	                                     !_now_period : _now_period;

	/*
	 * Dequeue all pending alarms before starting to re-schedule. Otherwise,
	 * a long-lasting alarm that has a deadline in the next now_period might
	 * get scheduled as head of this now_period falsely because the code
	 * thinks that it belongs to the last now_period.
	 */
	while (Alarm *curr = _alarm_get_pending_alarm()) {

		/* enqueue alarm into list of pending alarms */
		curr->_next = _pending_head;
		_pending_head = curr;
	}
	while (Alarm *curr = _pending_head) {

		/* dequeue alarm from list of pending alarms */
		_pending_head = _pending_head->_next;
		curr->_next = nullptr;

		uint64_t triggered = 1;

		if (curr->_raw.period) {
			Alarm::Time deadline = curr->_raw.deadline;

			/* schedule next event */
			if (deadline == 0)
				 deadline = curr_time;

			triggered += (curr_time - deadline) / curr->_raw.period;
		}

		/* do not reschedule if alarm function returns 0 */
		bool reschedule = curr->_on_alarm(triggered);

		if (reschedule) {

			/*
			 * At this point, the alarm deadline normally is somewhere near
			 * the current time but If the alarm had no deadline by now,
			 * initialize it with the current time.
			 */
			if (curr->_raw.deadline == 0) {
				curr->_raw.deadline        = _now;
				curr->_raw.deadline_period = _now_period;
			}
			/*
			 * Raise the deadline value by one period of the alarm and
			 * if the deadline value wraps thereby, update also in which
			 * period it is located.
			 */
			Alarm::Time const deadline = curr->_raw.deadline +
			                             triggered * curr->_raw.period;
			if (curr->_raw.deadline > deadline) {
				curr->_raw.deadline_period = !curr->_raw.deadline_period;
			}
			curr->_raw.deadline = deadline;

			/* synchronize enqueue operation */
			Lock::Guard lock_guard(_lock);
			_alarm_unsynchronized_enqueue(curr);
		}

		/* release alarm, resume concurrent destructor operation */
		curr->_dispatch_lock.unlock();
	}
}


void Alarm_timeout_scheduler::_alarm_setup_alarm(Alarm &alarm, Alarm::Time period, Alarm::Time first_duration)
{
	/*
	 * If the alarm is already present in the queue, re-consider its queue
	 * position because its deadline might have changed. I.e., if an alarm is
	 * rescheduled with a new timeout before the original timeout triggered.
	 */
	if (alarm._active)
		_alarm_unsynchronized_dequeue(&alarm);

	Alarm::Time deadline = _now + first_duration;
	alarm._alarm_assign(period, deadline, _now > deadline ? !_now_period : _now_period, this);

	_alarm_unsynchronized_enqueue(&alarm);
}


void Alarm_timeout_scheduler::_alarm_schedule_absolute(Alarm *alarm, Alarm::Time duration)
{
	Lock::Guard alarm_list_lock_guard(_lock);

	_alarm_setup_alarm(*alarm, 0, duration);
}


void Alarm_timeout_scheduler::_alarm_schedule(Alarm *alarm, Alarm::Time period)
{
	Lock::Guard alarm_list_lock_guard(_lock);

	/*
	 * Refuse to schedule a periodic timeout of 0 because it would trigger
	 * infinitely in the 'handle' function. To account for the case where the
	 * alarm object was already scheduled, we make sure to remove it from the
	 * queue.
	 */
	if (period == 0) {
		_alarm_unsynchronized_dequeue(alarm);
		return;
	}

	/* first deadline is overdue */
	_alarm_setup_alarm(*alarm, period, 0);
}


void Alarm_timeout_scheduler::_alarm_discard(Alarm *alarm)
{
	/*
	 * Make sure that nobody is inside the '_alarm_get_pending_alarm' when
	 * grabbing the '_dispatch_lock'. This is important when this function
	 * is called from the 'Alarm' destructor. Without the '_dispatch_lock',
	 * we could take the lock and proceed with destruction just before
	 * '_alarm_get_pending_alarm' tries to grab the lock. When the destructor is
	 * finished, '_alarm_get_pending_alarm' would proceed with operating on a
	 * dangling pointer.
	 */
	Lock::Guard alarm_list_lock_guard(_lock);

	if (alarm) {
		Lock::Guard alarm_lock_guard(alarm->_dispatch_lock);
		_alarm_unsynchronized_dequeue(alarm);
	}
}


bool Alarm_timeout_scheduler::_alarm_next_deadline(Alarm::Time *deadline)
{
	Lock::Guard alarm_list_lock_guard(_lock);

	if (!_active_head) return false;

	if (deadline)
		*deadline = _active_head->_raw.deadline;

	if (*deadline < _min_handle_period.deadline) {
		*deadline = _min_handle_period.deadline;
	}
	return true;
}
