# These classes provide basic functions for use in aircraft specific
# Nasal context. Note that even if a class is called "door" or "light"
# this doesn't mean that it can't be used for other purposes.
#
# Class instances don't have to be assigned to variables. They do also
# work if they remain anonymous. It's even a good idea to keep them
# anonymous if you don't need further access to their members. On the
# other hand, you can assign the class and apply setters at the same time:
#
#   aircraft.light.new("sim/model/foo/beacon", [1, 1]);    # anonymous
#   var strobe = aircraft.light.new("sim/model/foo/strobe", [1, 1]).cont().switch(1);
#
#
# Classes do create properties, but they don't usually overwrite the contents
# of an existing property. This makes it possible to preset them in
# a *-set.xml file or on the command line. For example:
#
#   $ fgfs --aircraft=bo105 --prop:/controls/doors/door[0]/position-norm=1
#
#
# Wherever a property argument can be given, this can either be a path,
# or a node (i.e. property node hash). In return, the property node can
# always be accessed directly as member "node", and turned into a path
# string with node.getPath():
#
#   var beacon = aircraft.light.new("sim/model/foo/beacon", [1, 1]);
#   print(beacon.node.getPath());
#
#   var strobe_node = props.globals.getNode("sim/model/foo/strobe", 1);
#   var strobe = aircraft.light.new(strobe_node, [0.05, 1.0]);
#
#
# The classes implement only commonly used features, but are easy to
# extend, as all class members are accessible from outside. For example:
#
#   # add custom property to door node:
#   frontdoor.node.getNode("name", 1).setValue("front door");
#
#   # add method to class instance (or base class -> aircraft.door.print)
#   frontdoor.print = func { print(me.position.getValue()) };
#
#

# constants
# ==============================================================================
var D2R = math.pi / 180;
var R2D = 180 / math.pi;




# helper functions
# ==============================================================================

# creates (if necessary) and returns a property node from arg[0],
# which can be a property node already, or a property path
#
var makeNode = func(n) {
	if (isa(n, props.Node))
		return n;
	else
		return props.globals.getNode(n, 1);
}


# returns arg[1]-th optional argument of vector arg[0] or default value arg[2]
#
var optarg = func {
	if (size(arg[0]) > arg[1] and arg[0][arg[1]] != nil)
		arg[0][arg[1]];
	else
		arg[2];
}




# door
# ==============================================================================
# class for objects moving at constant speed, with the ability to
# reverse moving direction at any point. Appropriate for doors, canopies, etc.
#
# SYNOPSIS:
#	door.new(<property>, <swingtime> [, <startpos>]);
#
#	property   ... door node: property path or node
#	swingtime  ... time in seconds for full movement (0 -> 1)
#	startpos   ... initial position      (default: 0)
#
# PROPERTIES:
#	./position-norm   (double)     (default: <startpos>)
#	./enabled         (bool)       (default: 1)
#
# EXAMPLE:
#	var canopy = aircraft.door.new("sim/model/foo/canopy", 5);
#	canopy.open();
#
var door = {
	new : func {
		m = { parents : [door] };
		m.node = makeNode(arg[0]);
		m.swingtime = arg[1];
		m.positionN = m.node.getNode("position-norm", 1);
		m.enabledN = m.node.getNode("enabled", 1);
		if (m.enabledN.getValue() == nil)
			m.enabledN.setBoolValue(1);

		var pos = optarg(arg, 2, 0);
		if (m.positionN.getValue() == nil)
			m.positionN.setDoubleValue(pos);

		m.target = pos < 0.5;
		return m;
	},
	# door.enable(bool)    ->  set ./enabled
	enable : func(v) {
		me.enabledN.setBoolValue(v);
		me;
	},
	# door.setpos(double)  ->  set ./position-norm without movement
	setpos : func(pos) {
		me.positionN.setValue(pos);
		me.target = pos < 0.5;
		me;
	},
	# double door.getpos() ->  return current position as double
	getpos : func { me.positionN.getValue() },

	# door.close()         ->  move to closed state
	close : func { me.move(me.target = 0) },

	# door.open()          ->  move to open state
	open : func { me.move(me.target = 1) },

	# door.toggle()        ->  move to opposite end position
	toggle : func { me.move(me.target) },

	# door.stop()          ->  stop movement
	stop : func { interpolate(me.positionN) },

	# door.move(double)    ->  move to arbitrary position
	move : func(target) {
		var pos = me.getpos();
		if (pos != target) {
			var time = abs(pos - target) * me.swingtime;
			interpolate(me.positionN, target, time);
		}
		me.target = !me.target;
	},
};



# light
# ==============================================================================
# class for generation of pulsing values. Appropriate for controlling
# beacons, strobes, etc.
#
# SYNOPSIS:
#	light.new(<property>, <pattern> [, <switch>]);
#	light.new(<property>, <stretch>, <pattern> [, <switch>]);
#
#	property   ... light node: property path or node
#	stretch    ... multiplicator for all pattern values
#	pattern    ... array of on/off time intervals (in seconds)
#	switch     ... property path or node to use as switch   (default: ./enabled)
#                      instead of ./enabled
#
# PROPERTIES:
#	./state           (bool)   (default: 0)
#	./enabled         (bool)   (default: 0) except if <switch> given)
#
# EXAMPLES:
#	aircraft.light.new("sim/model/foo/beacon", [0.4, 0.4]);    # anonymous light
#-------
#	var strobe = aircraft.light.new("sim/model/foo/strobe", [0.05, 0.05, 0.05, 1],
#	                "controls/lighting/strobe");
#	strobe.switch(1);
#-------
#	var switch = props.globals.getNode("controls/lighting/strobe", 1);
#	var pattern = [0.02, 0.03, 0.02, 1];
#	aircraft.light.new("sim/model/foo/strobe-top", 1.001, pattern, switch);
#	aircraft.light.new("sim/model/foo/strobe-bot", 1.005, pattern, switch);
#
var light = {
	new : func {
		m = { parents : [light] };
		m.node = makeNode(arg[0]);
		var stretch = 1.0;
		var c = 1;
		if (typeof(arg[c]) == "scalar") {
			stretch = arg[c];
			c += 1;
		}
		m.pattern = arg[c];
		c += 1;
		if (size(arg) > c and arg[c] != nil)
			m.switchN = makeNode(arg[c]);
		else
			m.switchN = m.node.getNode("enabled", 1);

		if (m.switchN.getValue() == nil)
			m.switchN.setBoolValue(0);

		m.stateN = m.node.getNode("state", 1);
		if (m.stateN.getValue() == nil)
			m.stateN.setBoolValue(0);

		forindex (var i; m.pattern)
			m.pattern[i] *= stretch;

		m.index = 0;
		m.loopid = 0;
		m.continuous = 0;
		m.lastswitch = 0;
		m.seqcount = -1;
		m.endstate = 0;
		m.count = nil;
		m.switchL = setlistener(m.switchN, func m._switch_(), 1);
		return m;
	},
	# class destructor
	del : func {
		removelistener(me.switchL);
	},
	# light.switch(bool)   ->  set light switch (also affects other lights
	#                          that use the same switch)
	switch : func(v) { me.switchN.setBoolValue(v); me },

	# light.toggle()       ->  toggle light switch
	toggle : func { me.switchN.setBoolValue(!me.switchN.getValue()); me },

	# light.cont()         ->  continuous light
	cont : func {
		if (!me.continuous) {
			me.continuous = 1;
			me.loopid += 1;
			me.stateN.setBoolValue(me.lastswitch);
		}
		me;
	},

	# light.blink()        ->  blinking light  (default)
	# light.blink(3)       ->  when switched on, only run three blink sequences;
	#                          second optional arg defines state after the sequences
	blink : func(count = -1, endstate = 0) {
		me.seqcount = count;
		me.endstate = endstate;
		if (me.continuous) {
			me.continuous = 0;
			me.index = 0;
			me.stateN.setBoolValue(0);
			me.lastswitch and me._loop_(me.loopid += 1);
		}
		me;
	},

	_switch_ : func {
		var switch = me.switchN.getBoolValue();
		switch != me.lastswitch or return;
		me.lastswitch = switch;
		me.loopid += 1;
		if (me.continuous or !switch) {
			me.stateN.setBoolValue(switch);
		} elsif (switch) {
			me.stateN.setBoolValue(0);
			me.index = 0;
			me.count = me.seqcount;
			me._loop_(me.loopid);
		}
	},

	_loop_ : func(id) {
		id == me.loopid or return;
		if (!me.count) {
			me.loopid += 1;
			me.stateN.setBoolValue(me.endstate);
			return;
		}
		me.stateN.setBoolValue(me.index == 2 * int(me.index / 2));
		settimer(func me._loop_(id), me.pattern[me.index]);
		if ((me.index += 1) >= size(me.pattern)) {
			me.index = 0;
			if (me.count > 0)
				me.count -= 1;
		}
	},
};


# lowpass
# ==============================================================================
# class that implements a variable-interval EWMA (Exponentially Weighted
# Moving Average) lowpass filter with characteristics independent of the
# frame rate.
#
# SYNOPSIS:
#	lowpass.new(<coefficient>);
#
# EXAMPLE:
#	var lp = aircraft.lowpass.new(0.5);
#	print(lp.filter(10));  # prints 10
#	print(lp.filter(0));
#
var lowpass = {
	new : func(coeff) {
		var m = { parents : [lowpass] };
		m.coeff = coeff >= 0 ? coeff : die("aircraft.lowpass(): coefficient must be >= 0");
		m.value = nil;
		return m;
	},
	# filter(raw_value)    -> push new value, returns filtered value
	filter : func(v) {
		me.filter = me._filter_;
		me.value = v;
	},
	# get()                -> returns filtered value
	get : func {
		me.value;
	},
	# set()                -> sets new average and returns it
	set : func(v) {
		me.value = v;
	},
	_filter_ : func(v) {
		var dt = getprop("/sim/time/delta-sec");
		var c = dt / (me.coeff + dt);
		me.value = v * c + me.value * (1 - c);
	},
};


# angular lowpass
# ==============================================================================
# same as above, but for angles. Filters sin/cos separately and calculates the
# angle again from them. This avoids unexpected jumps from 179.99 to -180 degree.
#
var angular_lowpass = {
	new : func(coeff) {
		var m = { parents : [angular_lowpass] };
		m.sin = lowpass.new(coeff);
		m.cos = lowpass.new(coeff);
		m.value = nil;
		return m;
	},
	filter : func(v) {
		v *= D2R;
		me.value = math.atan2(me.sin.filter(math.sin(v)), me.cos.filter(math.cos(v))) * R2D;
	},
	set : func(v) {
		v *= D2R;
		me.sin.set(math.sin(v));
		me.cos.set(math.cos(v));
	},
	get : func {
		me.value;
	},
};


# data
# ==============================================================================
# class that loads and saves properties to aircraft-specific data files in
# ~/.fgfs/aircraft-data/ (Unix) or %APPDATA%\flightgear.org\aircraft-data\.
# There's no public constructor, as the only needed instance gets created
# by the system.
#
# SYNOPSIS:
#	data.add(<properties>);
#	data.save([<interval>])
#
#	properties  ... about any combination of property nodes (props.Node)
#	                or path name strings, or lists or hashes of them,
#	                lists of lists of them, etc.
#	interval    ... save in <interval> minutes intervals, or only once
#	                if 'nil' or empty (and again at reinit/exit)
#
# SIGNALS:
#	/sim/signals/save   ... set to 'true' right before saving. Can be used
#	                        to update values that are to be saved
#
# EXAMPLE:
#	var p = props.globals.getNode("/sim/model", 1);
#	var vec = [p, p];
#	var hash = {"foo": p, "bar": p};
#
#	# add properties
#	aircraft.data.add("/sim/fg-root", p, "/sim/fg-home");
#	aircraft.data.add(p, vec, hash, "/sim/fg-root");
#
#	# now save only once (and at exit/reinit, which is automatically done)
#	aircraft.data.save();
#
#	# or save now and every 30 sec (and at exit/reinit)
#	aircraft.data.save(0.5);
#
var data = {
	init : func {
		me.path = getprop("/sim/fg-home") ~ "/aircraft-data/" ~ getprop("/sim/aircraft") ~ ".xml";
		me.signalN = props.globals.getNode("/sim/signals/save", 1);
		me.catalog = [];
		me.loopid = 0;
		me.interval = 0;

		setlistener("/sim/signals/reinit", func(n) { n.getBoolValue() and me._save_() });
		setlistener("/sim/signals/exit", func me._save_());
	},
	load : func {
		if (io.stat(me.path) != nil) {
			printlog("info", "loading aircraft data from ", me.path);
			io.read_properties(me.path, props.globals);
		}
	},
	save : func(v = nil) {
		me.loopid += 1;
		if (v == nil) {
			me._save_();
		} else {
			me.interval = 60 * v;
			me._loop_(me.loopid);
		}
	},
	_loop_ : func(id) {
		id == me.loopid or return;
		me._save_();
		settimer(func me._loop_(id), me.interval);
	},
	_save_ : func {
		size(me.catalog) or return;
		printlog("debug", "saving aircraft data to ", me.path);
		me.signalN.setBoolValue(1);
		var data = props.Node.new();
		foreach (var c; me.catalog) {
			if (c[0] == `/`)
				c = substr(c, 1);

			props.copy(props.globals.getNode(c, 1), data.getNode(c, 1));
		}
		io.write_properties(me.path, data);
	},
	add : func(p...) {
		foreach (var n; props.nodeList(p))
			append(me.catalog, n.getPath());
	},
};


# timer
# ==============================================================================
# class that implements timer that can be started, stopped, reset, and can
# have its value saved to the aircraft specific data file. Saving the value
# is done automatically by the aircraft.Data class.
#
# SYNOPSIS:
#	timer.new(<property> [, <resolution:double> [, <save:bool>]])
#
#	<property>   ... property path or props.Node hash that holds the timer value
#	<resolution> ... timer update resolution -- interval in seconds in which the
#	                 timer property is updated while running (default: 1 s)
#	<save>       ... bool that defines whether the timer value should be saved
#	                 and restored next time, as needed for Hobbs meters
#	                 (default: 1)
#
# EXAMPLES:
#	var hobbs_turbine = aircraft.timer.new("/sim/time/hobbs/turbine[0]", 60);
#	hobbs_turbine.start();
#	
#	aircraft.timer.new("/sim/time/hobbs/battery", 60).start();  # anonymous timer
#
var timer = {
	new : func(prop, res = 1, save = 1) {
		var m = { parents : [timer] };
		m.node = makeNode(prop);
		if (m.node.getType() == "NONE")
			m.node.setDoubleValue(0);

		m.systimeN = props.globals.getNode("/sim/time/elapsed-sec", 1);
		m.last_systime = nil;
		m.interval = res;
		m.loopid = 0;
		m.running = 0;
		if (save) {
			data.add(m.node);
			m.saveL = setlistener("/sim/signals/save", func m._save_());
		} else {
			m.saveL = nil;
		}
		return m;
	},
	del : func {
		me.stop();
		if (me.saveL != nil)
			removelistener(me.saveL);
	},
	start : func {
		me.running and return;
		me.last_systime = me.systimeN.getValue();
		me.interval != nil and me._loop_(me.loopid);
		me.running = 1;
		me;
	},
	stop : func {
		me.running or return;
		me.running = 0;
		me.loopid += 1;
		me._apply_();
		me;
	},
	reset : func {
		me.node.setDoubleValue(0);
		me.last_systime = me.systimeN.getValue();
	},
	_apply_ : func {
		var sys = me.systimeN.getValue();
		me.node.setDoubleValue(me.node.getValue() + sys - me.last_systime);
		me.last_systime = sys;
	},
	_save_ : func {
		if (me.running)
			me._apply_();
	},
	_loop_ : func(id) {
		id != me.loopid and return;
		me._apply_();
		settimer(func me._loop_(id), me.interval);
	},
};



# livery
# =============================================================================
# Class that maintains livery XML files (see English Electric Lightning for an
# example). The last used livery is saved on exit and restored next time. Livery
# files are regular PropertyList XML files whose properties are copied to the
# main tree (whereby the node types are ignored).
#
# SYNOPSIS:
#	livery.init(<livery-dir> [, <name-path> [, <sort-path>]]);
#
#	<livery-dir> ... directory with livery XML files, relative to $FG_ROOT
#	<name-path>  ... property path to the livery name in the livery files
#	                 and the property tree (default: /sim/model/livery/name)
#	<sort-path>  ... property path to the sort criterion (default: same as
#	                 <name-path> -- that is: alphabetic sorting)
#
# EXAMPLE:
#	aircraft.livery.init("Aircraft/Lightning/Models/Liveries",
#	                     "sim/model/livery/variant",
#	                     "sim/model/livery/index");  # optional
#
#	aircraft.livery.dialog.toggle();
#	aircraft.livery.select("OEBH");
#	aircraft.livery.next();
#
var livery = {
	init : func(livery_dir, name_path = "sim/model/livery/name", sort_path = nil) {
		me.dir = livery_dir;
		if (me.dir[-1] != `/`)
			me.dir ~= "/";
		me.name_path = name_path;
		me.sort_path = sort_path != nil ? sort_path : name_path;
		me.rescan();
		aircraft.data.add(name_path);
		me.dialog = gui.Dialog.new("livery-select");
	},
	rescan : func {
		me.data = [];
		var path = getprop("/sim/fg-root") ~ "/" ~ me.dir;
		foreach (var file; directory(path)) {
			if (substr(file, -4) != ".xml")
				continue;
			var n = io.read_properties(path ~ file);
			var name = n.getNode(me.name_path);
			var index = n.getNode(me.sort_path);
			if (name == nil or index == nil)
				continue;
			append(me.data, [name.getValue(), index.getValue(), n.getValues(),
					substr(file, 0, size(file) - 4)]);
		}
		me.data = sort(me.data, func(a, b) {
			num(a[1]) == nil or num(b[1]) == nil ? cmp(a[1], b[1]) : a[1] - b[1];
		});
		me.select(getprop(me.name_path));
	},
	# select by index (out-of-bounds indices are wrapped)
	set : func(i) {
		if (i < 0)
			i = size(me.data) - 1;
		if (i >= size(me.data))
			i = 0;
		me.current = i;
		props.globals.setValues(me.data[i][2]);
		setprop("sim/model/livery/file", me.data[i][3]);
	},
	# select by name
	select : func(name) {
		forindex (var i; me.data)
			if (me.data[i][0] == name)
				me.set(i);
	},
	next : func {
		me.set(me.current + 1);
	},
	previous : func {
		me.set(me.current - 1);
	},
};



# livery_update
# =============================================================================
# Class for maintaining liveries in MP aircraft. It is used in Nasal code that's
# embedded in aircraft animation XML files, and checks in intervals whether the
# parent aircraft has changed livery, in which case it changes the livery
# in the remote aircraft accordingly.
#
# SYNOPSIS:
#	livery_update.new(<livery-dir> [, <interval:10> [, <func>]]);
#
#	<livery-dir> ... directory with livery files, relative to $FG_ROOT
#	<interval>   ... checking interval in seconds (default: 10)
#	<func>       ... callback function that's called with the ./sim/model/livery/file
#	                 contents as argument whenever the livery has changed. This can
#	                 be used for post-processing.
#
# EXAMPLE:
#	<nasal>
#		<load>
#			var livery_update = aircraft.livery_update.new(
#					"Aircraft/R22/Models/Liveries", 30,
#					func { print("R22 livery update") });
#		</load>
#
#		<unload>
#			livery_update.stop();
#		</unload>
#	</nasal>
#
var livery_update = {
	new : func(liveriesdir, interval = 10, callback = nil) {
		var m = { parents : [livery_update] };
		var root = cmdarg();
		m.root = root.getPath();
		m.fileN = root.getNode("sim/model/livery/file", 1);
		m.dir = getprop("/sim/fg-root") ~ "/" ~ liveriesdir ~ "/";
		m.interval = interval;
		m.last = "";
		m.running = 1;
		m.callback = callback;
		if (root.getName() == "multiplayer")
			m._loop_();
		return m;
	},
	stop : func {
		me.running = 0;
	},
	_loop_ : func {
		me.running or return;
		var file = me.fileN.getValue();
		if (file != nil and file != me.last) {
			io.read_properties(me.dir ~ file ~ ".xml", me.root);
			me.last = file;
			if (me.callback != nil)
				me.callback(file);
		}
		settimer(func me._loop_(), me.interval);
	},
};



# formation
# =============================================================================
# A modification of the livery class. This class maintains formation
# XML files (see Blackburn Buccaneer for an example). Files are regular
# PropertyList XML files whose properties are copied to the
# main tree (whereby the node types are ignored).
#
# SYNOPSIS:
#	formation.init(<formation-dir> [, <name-path> [, <sort-path>]]);
#
#	<formation-dir> ... directory with livery XML files, relative to $FG_ROOT
#	<name-path>     ... property path to the livery name in the livery files
#	                    and the property tree (default: /sim/model/formation/name)
#	<sort-path>     ... property path to the sort criterion (default: same as
#	                    <name-path> -- that is: alphabetic sorting)
#
# EXAMPLE:
#	aircraft.formation.init("Aircraft/Buccaneer/Formations",
#	                        "sim/model/formation/variant",
#	                        "sim/model/formation/index");  # optional
#
#	aircraft.formation.dialog.toggle();
#	aircraft.formation.select("take off");
#	aircraft.formation.next();
#
var formation = {
	init : func(formation_dir, name_path = "sim/model/formation/name", sort_path = nil) {
		me.dir = formation_dir;
		if (me.dir[-1] != `/`)
			me.dir ~= "/";
		me.name_path = name_path;
		me.sort_path = sort_path != nil ? sort_path : name_path;
		me.rescan();
		aircraft.data.add(name_path);
		me.dialog = gui.Dialog.new("formation-select");
	},
	rescan : func {
		me.data = [];
		var path = getprop("/sim/fg-root") ~ "/" ~ me.dir;
		foreach (var file; directory(path)) {
			if (substr(file, -4) != ".xml")
				continue;
			var n = io.read_properties(path ~ file);
			var name = n.getNode(me.name_path);
			var index = n.getNode(me.sort_path);
			if (name == nil or index == nil)
				continue;
			append(me.data, [name.getValue(), index.getValue(), n.getValues()]);
		}
		me.data = sort(me.data, func(a, b) {
			num(a[1]) == nil or num(b[1]) == nil ? cmp(a[1], b[1]) : a[1] - b[1];
		});
		me.select(getprop(me.name_path));
	},
	# select by index (out-of-bounds indices are wrapped)
	set : func(i) {
		if (i < 0)
			i = size(me.data) - 1;
		if (i >= size(me.data))
			i = 0;
		props.globals.setValues(me.data[i][2]);
		me.current = i;
	},
	# select by name
	select : func(name) {
		forindex (var i; me.data)
			if (me.data[i][0] == name)
				me.set(i);
	},
	next : func {
		me.set(me.current + 1);
	},
	previous : func {
		me.set(me.current - 1);
	},
};



# steering
# =============================================================================
# Class that implements differential braking depending on rudder position.
# Note that this overrides the controls.applyBrakes() wrapper. If you need
# your own version, then override it again after the steering.init() call.
#
# SYNOPSIS:
#	steering.init([<property> [, <threshold>]]);
#
#	<property>  ... property path or props.Node hash that enables/disables
#	                brake steering (usually bound to the js trigger button)
#	<threshold> ... defines range (+- threshold) around neutral rudder
#	                position in which both brakes are applied
#
# EXAMPLES:
#	aircraft.steering.init("/controls/gear/steering", 0.2);
#	aircraft.steering.init();
#
var steering = {
	init : func(switch = "/controls/gear/brake-steering", threshold = 0.3) {
		me.threshold = threshold;
		me.switchN = makeNode(switch);
		me.switchN.setBoolValue(me.switchN.getBoolValue());
		me.leftN = props.globals.getNode("/controls/gear/brake-left", 1);
		me.rightN = props.globals.getNode("/controls/gear/brake-right", 1);
		me.rudderN = props.globals.getNode("/controls/flight/rudder", 1);
		me.loopid = 0;

		controls.applyBrakes = func(v, w = 0) {
			if (w < 0)
				steering.leftN.setValue(v);
			elsif (w > 0)
				steering.rightN.setValue(v);
			else
				steering.switchN.setValue(v);
		}
		setlistener(me.switchN, func(n) {
			me.loopid += 1;
			if (n.getValue())
				me._loop_(me.loopid);
			else
				me.setbrakes(0, 0);
		}, 1);
	},
	_loop_ : func(id) {
		id == me.loopid or return;
		var rudder = me.rudderN.getValue();
		if (rudder > me.threshold)
			me.setbrakes(0, rudder);
		elsif (rudder < -me.threshold)
			me.setbrakes(-rudder, 0);
		else
			me.setbrakes(1, 1);

		settimer(func me._loop_(id), 0);
	},
	setbrakes : func(left, right) {
		me.leftN.setDoubleValue(left);
		me.rightN.setDoubleValue(right);
	},
};



# autotrim
# =============================================================================
# Singleton class that supports quick trimming and compensates for the lack
# of resistance/force feedback in most joysticks. Normally the pilot trims such
# that no real or artificially generated (by means of servo motors and spring
# preloading) forces act on the stick/yoke and it is in a comfortable position.
# This doesn't work well on computer joysticks.
#
# SYNOPSIS:
#	autotrim.start();  # on key/button press
#	autotrim.stop();   # on key/button release (mod-up)
#
# USAGE:
#	(1) move the stick such that the aircraft is in an orientation that
#	    you want to trim for (forward flight, hover, ...)
#	(2) press autotrim button and keep it pressed
#	(3) move stick/yoke to neutral position (center)
#	(4) release autotrim button
#
var autotrim = {
	init : func {
		me.elevator = me.Trim.new("elevator");
		me.aileron = me.Trim.new("aileron");
		me.rudder = me.Trim.new("rudder");
		me.loopid = 0;
		me.active = 0;
	},
	start : func {
		me.active and return;
		me.active = 1;
		me.elevator.start();
		me.aileron.start();
		me.rudder.start();
		me._loop_(me.loopid += 1);
	},
	stop : func {
		me.active or return;
		me.active = 0;
		me.loopid += 1;
		me.update();
	},
	_loop_ : func(id) {
		id == me.loopid or return;
		me.update();
		settimer(func me._loop_(id), 0);
	},
	update : func {
		me.elevator.update();
		me.aileron.update();
		me.rudder.update();
	},
	Trim : {
		new : func(name) {
			var m = { parents : [ autotrim.Trim ] };
			m.trimN = props.globals.getNode("/controls/flight/" ~ name ~ "-trim", 1);
			m.ctrlN = props.globals.getNode("/controls/flight/" ~ name, 1);
			return m;
		},
		start : func {
			me.last = me.ctrlN.getValue();
		},
		update : func {
			var v = me.ctrlN.getValue();
			me.trimN.setDoubleValue(me.trimN.getValue() + me.last - v);
			me.last = v;
		},
	},
};



# tyre smoke
# =============================================================================
# Provides a property which can be used to contol particles used to simulate tyre
# smoke on landing. Weight on wheels, vertical speed, ground speed, ground friction
# factor are taken into account. Tyre slip is simulated by low pass filters.
#
# Modifications to the model file are required.
#
# Generic XML particle files are available, but are not mandatory
# (see Hawker Seahawk for an example).
#
# SYNOPSIS:
#	aircraft.tyresmoke.new(<gear index>)
#	<gear index> - the index of the gear to which the tyre smoke is attached
#	aircraft.tyresmoke.update()
#
# EXAMPLE:
#	var tyresmoke_0 = aircraft.tyresmoke.new(0);
#	tyresmoke_0.update();
#
var tyresmoke = {
	new : func(number) {
		var m = { parents: [tyresmoke] };
		m.vertical_speed = props.globals.getNode("velocities/vertical-speed-fps", 1);
		m.speed = props.globals.getNode("velocities/groundspeed-kt", 1);

		var gear = props.globals.getNode("gear", 1).getChild("gear", number, 1);
		m.wow = props.initNode(gear.getNode("wow", 1));
		m.tyresmoke = props.initNode(gear.getNode("tyre-smoke", 1), 0, "BOOL");
		m.friction_factor = props.initNode(gear.getNode("ground-friction-factor", 1), 1);
		m.rollspeed = props.initNode(gear.getNode("rollspeed-ms", 1));
		m.lp = lowpass.new(2);
		return m;
	},
	update : func {
		var rollspeed = me.rollspeed.getValue();
		var vert_speed = me.vertical_speed.getValue();
		var groundspeed = me.speed.getValue();
		var friction_factor = me.friction_factor.getValue();
		var wow = me.wow.getValue();

		var filtered_rollspeed = me.lp.filter(rollspeed);
		var diff = math.abs(rollspeed - filtered_rollspeed);
		var diff_norm = diff > 0 ? diff / rollspeed : 0;

		if (wow and vert_speed < -0.05 and diff_norm > 0.05
				and friction_factor > 0.7 and groundspeed > 50)
			me.tyresmoke.setValue(1);
		else
			me.tyresmoke.setValue(0);
	},
};



# teleport
# =============================================================================
# Usage:  aircraft.teleport(lat:48.3, lon:32.4, alt:5000);
#
var teleport = func(airport = "", runway = "", lat = -9999, lon = -9999, alt = 0,
		speed = 0, distance = 0, azimuth = 0, glideslope = 0, heading = 9999) {
	setprop("/sim/presets/airport-id", airport);
	setprop("/sim/presets/runway", runway);
	setprop("/sim/presets/latitude-deg", lat);
	setprop("/sim/presets/longitude-deg", lon);
	setprop("/sim/presets/altitude-ft", alt);
	setprop("/sim/presets/airspeed-kt", speed);
	setprop("/sim/presets/offset-distance-nm", distance);
	setprop("/sim/presets/offset-azimuth-nm", azimuth);
	setprop("/sim/presets/glideslope-deg", glideslope);
	setprop("/sim/presets/heading-deg", heading);
	fgcommand("presets-commit");
}



# HUD control class to handle both HUD implementations
# ==============================================================================
#
var HUD = {
	init : func {
		me.vis0N = props.globals.getNode("/sim/hud/visibility[0]", 1);
		me.vis1N = props.globals.getNode("/sim/hud/visibility[1]", 1);
		me.currcolN = props.globals.getNode("/sim/hud/current-color", 1);
		me.paletteN = props.globals.getNode("/sim/hud/palette", 1);
		me.brightnessN = props.globals.getNode("/sim/hud/color/brightness", 1);
		me.currentN = me.vis0N;
	},
	cycle_color : func {		# h-key
		if (!me.currentN.getBoolValue())		# if off, turn on
			return me.currentN.setBoolValue(1);

		var i = me.currcolN.getValue() + 1;		# if through, turn off
		if (i < 0 or i >= size(me.paletteN.getChildren("color"))) {
			me.currentN.setBoolValue(0);
			me.currcolN.setIntValue(0);
		} else {					# otherwise change color
			me.currentN.setBoolValue(1);
			me.currcolN.setIntValue(i);
		}
	},
	cycle_brightness : func {	# H-key
		me.is_active() or return;
		var br = me.brightnessN.getValue() - 0.2;
		me.brightnessN.setValue(br > 0.01 ? br : 1);
	},
	normal_type : func {		# i-key
		me.is_active() or return;
		me.oldinit1();
		me.vis0N.setBoolValue(1);
		me.vis1N.setBoolValue(0);
		me.currentN = me.vis0N;
	},
	cycle_type : func {		# I-key
		me.is_active() or return;
		if (me.currentN == me.vis0N) {
			me.vis0N.setBoolValue(0);
			me.vis1N.setBoolValue(1);
			me.currentN = me.vis1N;
		} elsif (me.currentN == me.vis1N) {
			me.vis0N.setBoolValue(1);
			me.vis1N.setBoolValue(0);
			me.oldinit2();
			me.currentN = me.vis0N;
		}
	},
	oldinit1 : func { fgcommand("hud-init") },
	oldinit2 : func { fgcommand("hud-init2") },
	is_active : func { me.vis0N.getValue() or me.vis1N.getValue() },
};



# module initialization
# ==============================================================================
#

_setlistener("/sim/signals/nasal-dir-initialized", func {
	props.initNode("/sim/time/delta-sec", 0);
	props.initNode("/sim/time/delta-realtime-sec", 0.00000001);

	HUD.init();
	data.init();
	autotrim.init();

	if (getprop("/sim/startup/save-on-exit")) {
		data.load();
		var n = props.globals.getNode("/sim/aircraft-data");
		if (n != nil)
			foreach (var c; n.getChildren("path"))
				if (c.getType() != "NONE")
					data.add(c.getValue());
	} else {
		data._save_ = func nil;
		data._loop_ = func nil;
	}
});