# updateloop.nas - Generic Nasal update loop for implementing sytems,
# instruments or physics simulation.
#
# Copyright (C) 2014 Anton Gomez Alvedro
#
# 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.


##
# The UpdateLoop accepts a vector of Updatable components that it will call
# at regular intervals. In order to define custom objects to be controlled by
# the loop, the Updatable interface must be implemented:
#
# var MyComponent = {
#	parents: [Updatable],
#
#	reset: func {
#		...
#	},
#	update: func(dt) {
#		...
#	},
#   ...
# };

var Updatable = {

	##
	# When reset() is called, the component shall clean all internal state and
	# reinitialize itself as if starting from scratch.
	#
	# Reset will be called automatically after a teleport, and then on demand
	# when UpdateLoop.reset() is explicitly called.

	reset: func { },

	##
	# Called at regular intervals from the UdateLoop.
	# dt:  Elapsed time in seconds since the last call.

	update: func(dt) { },
};

##
# Wraps a set of user provided Updatable objects and calls them periodically.
# Takes care of critical sim signals (reinit, fdm-initialized, speed-up).

var UpdateLoop = {

	# UpdateLoop.new(components, [update_period], [enable])
	#
	# components:    Vector of components to update on every iteration.
	# update_period: Time in seconds between updates.
	# enable:        Enable the loop immediately after creation.
	# ignore_fdm:    Do not wait for the fdm to be ready for starting the loop.

	new: func(components, update_period = 0, enable = 1, ignore_fdm = 0) {

		var m = { parents: [UpdateLoop] };
		m.initialized = 0;
		m.enabled = enable;
		m.ignore_fdm = ignore_fdm;
		m.update_period = update_period;
		m.time_last = 0;
		m.sim_speed = 1;
		m.components = (components != nil)? components : [];

		m.timer = maketimer(update_period,
			func { call(me._update, [], m) });

		m.lst = [];

		append(m.lst, setlistener("/sim/speed-up",
			func(n) { m.sim_speed = n.getValue() }, 1, 0));

		append(m.lst, setlistener("sim/signals/reinit",
		                          func(n) { m._on_teleport(n) }));

		if (ignore_fdm or getprop("sim/signals/fdm-initialized")) {
			m.reset();
			enable and m.timer.start();
		}

		if (!ignore_fdm) {
			append(m.lst, setlistener("sim/signals/fdm-initialized",
		                              func(n) { m._on_teleport(n) }));
		}

		return m;
	},

	del: func {
		me.disable();
		foreach (var l; me.lst) removelistener(l);
	},

	##
	# Resets internal state for all components controlled by the loop.
	# It is of course responsibility of every component to clean their internal
	# state appropriately.

	reset: func {
		me.time_last = getprop("/sim/time/elapsed-sec");

		foreach (var component; me.components)
			component.reset();

		me.initialized = 1;
	},

	##
	# The loop will start updating the components under its control.

	enable: func {
		if (me.initialized) me.timer.start();
		me.enabled = 1;
	},

	##
	# The loop will freeze and its components will not get updates until
	# enabled again.

	disable: func {
		me.timer.stop();
		me.enabled = 0;
	},

	_update: func {
		var time_now = getprop("/sim/time/elapsed-sec");
		var dt = (time_now - me.time_last) * me.sim_speed;
		if (dt == 0) return;

		me.time_last = time_now;

		foreach (var component; me.components)
			component.update(dt);
	},

	_on_teleport: func(n) {
		var signal = n.getName();

		if (signal == "reinit" and n.getValue()) {
			me.timer.stop();
			me.initialized = 0;
		}
		elsif (me.ignore_fdm or signal == "fdm-initialized") {
			me.timer.isRunning and me.timer.stop();
			me.reset();
			me.enabled and me.timer.start();
		}
	}
};