/* $Id$
 *
 * Signal/Driver handling related functions.
 *
 * Copyright (C) 2008-2009 FAUmachine Team <info@faumachine.org>.
 * This program is free software. You can redistribute it and/or modify it
 * under the terms of the GNU General Public License, either version 2 of
 * the License, or (at your option) any later version. See COPYING.
 */

#include "signals.h"
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#include "glue-main.h" /* for time_virt */
#include "glue-log.h"
#include "kernel.h"

/** transaction element of a driver */
struct drv_trans {
	/** simulation time at which the transaction is to happen */
	universal_integer sim_time;
	/** value of the driver at given time. */
	union fauhdli_value val;
};

/** ordering function for transactions of a driver in ascending simulation 
 *  time order.
 */
static int
__attribute__((__pure__))
drv_trans_compare(const void *_t1, const void *_t2)
{
	const struct drv_trans *t1 = (const struct drv_trans *)_t1;
	const struct drv_trans *t2 = (const struct drv_trans *)_t2;

	if (t1->sim_time < t2->sim_time) {
		return -1;
	}

	if (t1->sim_time == t2->sim_time) {
		return 0;
	}

	return 1;
}

static void
driver_connect_non_foreign(
	struct driver *driver,
	struct signal *_signal,
	const struct glue_vhdl_cb *callbacks
)
{
	assert(driver != NULL);
	assert(_signal != NULL);

	slset_add(_signal->connected_drivers, driver, callbacks->malloc);
	driver->connected_signal = _signal;
}

bool
driver_connect(
	struct driver *driver, 
	struct signal *_signal,
	struct fauhdli *instance
)
{
	if (_signal->is_foreign) {
		/* foreign out drivers are NOT connected
		 * to the VHDL signal! these directly forward
		 * the value to the foreign signal via glue_vhdl
		 */
		assert(instance->callbacks.connect_out != NULL);
		instance->callbacks.connect_out(
					instance->glue_vhdl, 
					_signal->foreign_id,
					_signal->value,
					driver);
		driver->foreign_out = true;
		driver->foreign_id = _signal->foreign_id;
		return true;
	}

	driver_connect_non_foreign(driver, _signal, &instance->callbacks);
	return false;
}

static void
driver_disconnect(struct driver *driver, const struct glue_vhdl_cb *callbacks)
{
	if (driver->connected_signal == NULL) {
		/* not connected */
		return;
	}

	assert(driver->connected_signal->connected_drivers != NULL);
	slset_remove(
		driver->connected_signal->connected_drivers, 
		driver,
		callbacks->free);
	driver->connected_signal = NULL;
}

struct driver *
driver_create(union fauhdli_value init, const struct glue_vhdl_cb *callbacks)
{
	struct driver *ret;
	ret = callbacks->malloc(sizeof(struct driver));
	assert(ret != NULL);
	ret->driving_value = init;
	ret->connected_signal = NULL;
	ret->active = false;
	ret->transactions = 
		slset_create(drv_trans_compare, callbacks->malloc);
	ret->foreign_out = false;
	ret->foreign_id = 0;
	return ret;
}

void
driver_destroy(struct driver *driver, const struct glue_vhdl_cb *callbacks)
{
	driver_disconnect(driver, callbacks);
	slset_destroy_data(driver->transactions, callbacks->free);
	callbacks->free(driver);
}

void
driver_update(
	struct driver *driver,
	union fauhdli_value val,
	universal_integer sim_time,
	const struct glue_vhdl_cb *callbacks
)
{
	struct drv_trans *t;

	t = callbacks->malloc(sizeof(struct drv_trans));
	assert(t != NULL);

	t->sim_time = sim_time;
	t->val = val;

	slset_truncate_at(driver->transactions, t, true, callbacks->free);
	slset_add(driver->transactions, t, callbacks->malloc);
}


/** Update driving_value from the transactions at a given simulation
 *  time.
 *  @param driver driver to look up current transaction
 *  @param sim_time current simulation time.
 *  @return true, if the driver is now active, false if not
 *          (FIXME: actually active should be set correctly instead!)
 */
static bool 
update_driving_value(
	struct driver *driver, 
	universal_integer sim_time,
	const struct glue_vhdl_cb *callbacks
)
{
	struct drv_trans *t;

	if (driver->transactions->first == NULL) {
		return false;
	}

	t = (struct drv_trans *)driver->transactions->first->data;

	if (sim_time < t->sim_time) {
		/* entry in the future */
		return false;
	}
	/* entry now or in the past (FAUmachine's CPU isn't keeping
	 * exact track of the time, so registered event callbacks may
	 * come at a later point in simulation time than these
	 * have been scheduled -- however only if the schedule updated 
	 * *while* the CPU is running.
	 */

	driver->active = true;
	driver->driving_value = t->val;
	slset_remove(driver->transactions, t, callbacks->free);
	callbacks->free(t);
	return true;
}

static void
driver_propagate(
	struct driver *driver, 
	universal_integer sim_time,
	const struct glue_vhdl_cb *callbacks
)
{
	bool active;
	/* FIXME reset active somewhere */

	active = update_driving_value(driver, sim_time, callbacks);
	if (! active) {
		return;
	}

	if (memcmp(&driver->connected_signal->value, &driver->driving_value,
		sizeof(union fauhdli_value)) != 0) {

		driver->connected_signal->event = true;
		driver->connected_signal->value = driver->driving_value;
	}

	assert(! driver->foreign_out);
}

void
driver_forward_foreign(
	struct fauhdli *instance,
	struct driver *driver,
	universal_integer sim_time
)
{
	bool active;

	assert(driver->foreign_out);
	active = update_driving_value(
					driver, 
					sim_time, 
					&instance->callbacks);
	if (! active) {
		return;
	}

	assert(driver->connected_signal == NULL);
	assert(instance->callbacks.drv_set != NULL);
	instance->callbacks.drv_set(instance->glue_vhdl,
					driver->foreign_id,
					driver->driving_value,
					driver);
}

static void
signal_drivers_propagate_unresolved(
	const struct glue_vhdl_cb *callbacks,
	struct signal *sig, 
	universal_integer sim_time
)
{
	static bool error_reported = false;
	unsigned int n_drivers = 0;
	struct slset_entry *i;

	for (i = sig->connected_drivers->first; i != NULL; i = i->next) {
		struct driver *d = (struct driver *)i->data;
		
		assert(! d->foreign_out);
		driver_propagate(d, sim_time, callbacks);
		n_drivers++;
	}

	if ((1 < n_drivers) && (! error_reported)) {
		error_reported = true;

		callbacks->log(FAUHDLI_LOG_CRITICAL, "fauhdli", __func__,
			"There is more than one driver for an unresoved "
			"signal.\n");
	}

}

void
signal_drivers_propagate(
	struct fauhdli *instance, 
	struct signal *sig, 
	universal_integer sim_time
)
{
	struct slset_entry *i;
	union fauhdli_value drv_args[10];
	union fauhdli_value ret;
	size_t num_drvs = 0;
	bool active = false;

	if (sig->resolver == NULL) {
		signal_drivers_propagate_unresolved(
				&instance->callbacks, sig, sim_time);
	}

	/* resolved signal */

	/* collect driving values */
	for (i = sig->connected_drivers->first; i != NULL; i = i->next) {
		struct driver *d = (struct driver *)i->data;
		
		assert(! d->foreign_out);
		active |= update_driving_value(d, 
						sim_time, 
						&instance->callbacks);

		assert(num_drvs < (sizeof(drv_args) / sizeof(drv_args[0])));
		drv_args[num_drvs] = d->driving_value;
		num_drvs++;
	}

	/* FIXME does that adhere to lrm? */
	if (! active) {
		return;
	}

	/* let the kernel call the resolution function */
	fauhdli_kernel_resolve(instance, 
				sig->resolver, 
				&ret, 
				drv_args, 
				num_drvs);

	if (memcmp(&sig->value, &ret, sizeof(union fauhdli_value)) != 0) {
		sig->event = true;
		sig->value = ret;
	}
}

universal_integer
driver_get_next_event(const struct driver *drv)
{
	const struct slset_entry *i;
	const struct drv_trans *t;

	if (slset_empty(drv->transactions)) {
		return INT64_MAX;
	}

	i = drv->transactions->first;
	t = (const struct drv_trans *)i->data;

	return t->sim_time;
}

struct signal *
signal_create(
	union fauhdli_value init,
	const char *foreign,
	struct fauhdli *instance,
	const char *name,
	const struct code_container *resolver
)
{
	struct signal *ret;

	ret = instance->callbacks.malloc(sizeof(struct signal));
	assert(ret != NULL);
	ret->value = init;
	ret->connected_drivers = 
		slset_create(NULL, instance->callbacks.malloc);
	ret->event = false;
	ret->resolver = resolver;

	if (foreign == NULL) {
		ret->is_foreign = false;
		ret->foreign_id = 0;
		return ret;
	}

	/* foreign signal */
	ret->is_foreign = true;
	assert(instance->callbacks.signal_create != NULL);
	ret->foreign_id = 
		instance->callbacks.signal_create(
						instance->glue_vhdl,
						foreign,
						name);
	return ret;
}

void
signal_destroy(struct signal *_signal, const struct glue_vhdl_cb *callbacks)
{
	if (_signal->connected_drivers != NULL) {
		slset_destroy(_signal->connected_drivers, callbacks->free);
	}
	callbacks->free(_signal);
}

void
signal_connect_foreign_in_driver(
	struct signal *sig,
	struct fauhdli *instance,
	struct slset *driver_set
)
{
	struct driver *drv;
	union fauhdli_value init;

	/* FIXME obtain foreign value */
	init.univ_int = 0;

	drv = driver_create(init, &instance->callbacks);
	driver_connect_non_foreign(drv, sig, &instance->callbacks);
	sig->value = init;
	
	assert(instance->callbacks.connect_in != NULL);
	instance->callbacks.connect_in(
				instance->glue_vhdl,
				sig->foreign_id, 
				drv);
	slset_add(driver_set, drv, instance->callbacks.malloc);
}

unsigned int
fauhdli_get_sig_id(const void *_sig)
{
	const struct signal *sig = (const struct signal*)_sig;

	assert(sig != NULL);
	assert(sig->is_foreign);

	return sig->foreign_id;
}

unsigned int
fauhdli_get_sig_id_driver(const void *_drv)
{
	const struct driver *drv = (const struct driver *)_drv;

	assert(drv != NULL);
	assert(drv->foreign_out == true);
	return drv->foreign_id;
}
