# Britten-Norman BN-2A-26 Electrical System
# based on turboprop engine electrical system by Syd Adams and C172S electrical system   ####

# Basic props.nas objects
var electrical = props.globals.getNode("systems/electrical");
var electrical_sw = electrical.initNode("internal-switches");
var output = electrical.getNode("outputs");
var breakers = props.globals.initNode("/controls/circuit-breakers");
var controls = props.globals.getNode("/controls/electric");
var light_ctrl = props.globals.getNode("/controls/lighting");
var antiice_ctrl = props.globals.getNode("/controls/anti-ice");

var fdm_starter = [
	props.globals.getNode("/controls/engines/engine[0]/starter", 1 ),
	props.globals.getNode("/controls/engines/engine[1]/starter", 1 ),
];

# Helper functions
var check_or_create = func ( prop, value, type ) {
	var obj = props.globals.getNode(prop, 1);
	if( obj.getValue() == nil ){
		return props.globals.initNode(prop, value, type);
	} else {
		return obj;
	}
}

#	Switches
var switches = {
	battery:		controls.initNode("battery-switch",		0,	"BOOL"),
	generator:	[
				controls.initNode("engine[0]/generator",	0,	"BOOL"),
				controls.initNode("engine[1]/generator",	0,	"BOOL"),
		],
	avionics:		controls.initNode("avionics-switch",		0,	"BOOL"),
	ext_supply:		controls.initNode("external-supply",		0,	"BOOL"),
	
	pitot_stall_heat:	antiice_ctrl.initNode("pitot-stall-heat",	0,	"BOOL"),
	airframe_deice:	antiice_ctrl.initNode("airframe-deice",	0,	"BOOL"),
	propeller_deice:	antiice_ctrl.initNode("propeller-deice",	0,	"BOOL"),
	wshld_deice:	antiice_ctrl.initNode("windshield-deice",	0,	"BOOL"),
	#	Lights
	nav_light:		light_ctrl.initNode("nav-lights",		0,	"BOOL"),
	landing_light: [
				light_ctrl.initNode("landing-lights[0]",	0,	"BOOL"),
				light_ctrl.initNode("landing-lights[1]",	0,	"BOOL"),
		],
	cabin_light:		light_ctrl.initNode("cabin-lights",		0,	"BOOL"),
	pax_notices:		light_ctrl.initNode("pax-notices",		0,	"BOOL"),
	acl_bcn_light:		light_ctrl.initNode("acl-beacon",		0,	"BOOL"),
	
	cockpit_light:		light_ctrl.initNode("cockpit-lights",		0.0,	"DOUBLE"),
	#	Engine
	fuel_pump:	[
				props.globals.initNode("/controls/fuel/tank[0]/aux-fuel-pump",	0,	"BOOL"),
				props.globals.initNode("/controls/fuel/tank[1]/aux-fuel-pump",	0,	"BOOL"),
		],
	starter:		props.globals.getNode("/controls/engines/starter-switch", 1 ),

};

#	Additional (not directly consumer-related) Circuit Breakers
var circuit_breakers = {
	main_bus_isol:		breakers.initNode("main-bus-isol",		1,	"BOOL"),
	generator: [
			breakers.initNode("generator[0]",	1,	"BOOL"),
			breakers.initNode("generator[1]",	1,	"BOOL"),
	],
	starter_relays:		breakers.initNode("starter-relays",		1,	"BOOL"),
};

#	Internal (virtual) switches
var int_switches = {
	acl_light:		props.globals.initNode("/sim/model/lights/anti-collision-light/state",	0,	"BOOL"),
};

var airframe_deice_bool = output.initNode("airframe-deice-bool", 0, "BOOL");
var propeller_deice_bool = output.initNode("propeller-deice-bool", 0, "BOOL");
var pitot_heat_bool = output.initNode("pitot-heat-bool", 0, "BOOL");
var stall_heat_bool = output.initNode("stall-heat-bool", 0, "BOOL");
var wshld_deice_bool = output.initNode("windshield-deice-bool", 0, "BOOL");

var delta_sec	=	props.globals.getNode("sim/time/delta-sec");

## Lights
#				EXTERIOR
#	Landing Lights
#		systems/electrical/outputs/landing-lights[0]
#		systems/electrical/outputs/landing-lights[1]
#	Navigation Lights
#		systems/electrical/outputs/navigation-lights
#	ACL/Beacon Lights
#		systems/electrical/outputs/anti-collision-lights


var acl = aircraft.light.new("/sim/model/lights/anti-collision-light", [0.1, 1.233], switches.acl_bcn_light);	# 45 flashes per minute https://cdn.shopify.com/s/files/1/0080/5598/0128/files/14B89.pdf?v=1590091422


#	TODO calculate battery temperature correctly
var BatteryClass = {
	new : func( switch, volt, amps, amp_hours, charge_percent, charge_amps, n){
		m = { 
			parents : [BatteryClass],
			switch:		switch,
			serviceable:	electrical.initNode("/systems/electrical/battery["~n~"]/serviceable", 1, "BOOL"),
			temp:		electrical.initNode("battery["~n~"]/temperature", 15.0, "DOUBLE"),
			ideal_volts:	volt,
			ideal_amps:	amps,
			volt_p:		electrical.initNode("battery["~n~"]/volts", 0.0, "DOUBLE"),
			amp_hours:	amp_hours,
			charge_percent:	charge_percent, 
			charge_amps:	charge_amps,
		};
		return m;
	},
	apply_load : func( load ) {
		var dt = delta_sec.getDoubleValue();
		if( me.switch.getBoolValue() ){
			var amphrs_used = load * dt / 3600.0;
			var percent_used = amphrs_used / me.amp_hours;
			me.charge_percent -= percent_used;
			if ( me.charge_percent < 0.0 ) {
				me.charge_percent = 0.0;
			} elsif ( me.charge_percent > 1.0 ) {
				me.charge_percent = 1.0;
			}
			var output =me.amp_hours * me.charge_percent;
			return output;
		}else return 0;
	},
	
	get_output_volts : func {
		if( me.switch.getBoolValue() ){
			var x = 1.0 - me.charge_percent;
			var tmp = -(3.0 * x - 1.0);
			var factor = (tmp*tmp*tmp*tmp*tmp + 32) / 32;
			var output =me.ideal_volts * factor;
			me.volt_p.setDoubleValue( output );
			return output;
		}else return 0;
	},
	
	get_output_amps : func {
		if( me.switch.getBoolValue() ){
			var x = 1.0 - me.charge_percent;
			var tmp = -(3.0 * x - 1.0);
			var factor = (tmp*tmp*tmp*tmp*tmp + 32) / 32;
			var output =me.ideal_amps * factor;
			return output;
		}else return 0;
	}
};

# var alternator = AlternatorClass.new( "/engines/engine[0]/rpm", 800.0, 14.0, 12.0 );
##
# Alternator model class.
#
var AlternatorClass = {
	new: func ( rpm_source, rpm_threshold, ideal_volts, ideal_amps, n, switch = nil ) {
		var obj = { 
			parents : [AlternatorClass],
			rpm_source : props.globals.getNode( rpm_source, 1 ),
			rpm_threshold : rpm_threshold,
			ideal_volts : ideal_volts,
			ideal_amps : ideal_amps,
			amps_prop: props.globals.initNode("/systems/electrical/generators["~ n ~"]/amps", 0.0, "DOUBLE"),
			switch: switch,
		};
		obj.rpm_source.setDoubleValue( 0.0 );
		return obj;
	},
	apply_load: func( amps ){
		if( me.switch != nil and !me.switch.getBoolValue() ) return 0.0;
		
		var dt = delta_sec.getDoubleValue();
		
		me.amps_prop.setDoubleValue( amps );
		
		# Computes available amps and returns remaining amps after load is applied
		# Scale alternator output for rpms < 800.  For rpms >= 800
		# give full output.  This is just a WAG, and probably not how
		# it really works but I'm keeping things "simple" to start.
		var factor = me.rpm_source.getDoubleValue() / me.rpm_threshold;
		if ( factor > 1.0 ) {
			factor = 1.0;
		}
		
		# print( "alternator amps = ", me.ideal_amps * factor );
		var available_amps = me.ideal_amps * factor;
		return available_amps - amps;
	},
	get_output_volts: func {
		if( me.switch != nil and !me.switch.getBoolValue() ) return 0.0;
		
		# Return output volts based on rpm
		
		# scale alternator output for rpms < 800.  For rpms >= 800
		# give full output.  This is just a WAG, and probably not how
		# it really works but I'm keeping things "simple" to start.
		var rpm = me.rpm_source.getDoubleValue();
		if( rpm < 100 ){
			return 0.0;
		}
		var factor = rpm / me.rpm_threshold;
		if ( factor > 1.0 ) {
			factor = 1.0;
		}
		# print( "alternator volts = ", me.ideal_volts * factor );
		return me.ideal_volts * factor;
	},
	get_output_amps: func {
		if( me.switch != nil and !me.switch.getBoolValue() ) return 0.0;
		
		# Return output amps available based on rpm.
		
		# scale alternator output for rpms < 800.  For rpms >= 800
		# give full output.  This is just a WAG, and probably not how
		# it really works but I'm keeping things "simple" to start.
		var factor = me.rpm_source.getDoubleValue() / me.rpm_threshold;
		if ( factor > 1.0 ) {
			factor = 1.0;
		}
		
		# print( "alternator amps = ", ideal_amps * factor );
		return me.ideal_amps * factor;
	},

};

#	Ref. FM, p. 127:	battery is 24V, either 17 Ah or 25 Ah
var battery = BatteryClass.new(switches.battery, 24.0, 30, 17.0, 1.0, 7.0, 0);

#	Ref. FM, p. 127:	two Prestolite ALT 8404 generators
var generators = [
	AlternatorClass.new( "/engines/engine[0]/rpm", 800.0, 28.0, 70.0, 0, switches.generator[0] ),
	AlternatorClass.new( "/engines/engine[1]/rpm", 800.0, 28.0, 70.0, 1, switches.generator[1] ),
];

#	Consumer Class
#		Functions:
#			* power: takes bus_volts, applies to relevant outputs/ property and returns electrical load
#			* automatically trips its circuit breaker when maximum load is exceeded
var consumer = {
	new: func( name, switch, watts, cb_max ){
		m = { parents : [consumer] };
		m.cb = breakers.initNode(name, 1, "BOOL");
		m.switch_type = "none";
		if( switch != nil ){
			m.switch = switch;
			if ( switch.getType() == "DOUBLE" or switch.getType() == "FLOAT" ) {
				m.switch_type = "double";
			} else if ( switch.getType() == "BOOL" ) {
				m.switch_type = "bool";
			} else {
				die("Consumer (non-int) switch of unsupported type: "~ switch.getType() ~ "!");
			}
		} else {
			m.switch = nil;
		}
		m.output = output.initNode(name, 0.0, "DOUBLE");
		m.watts = watts;
		m.cb_max = cb_max;
		return m;
	},
	power: func( bus_volts ){
		if( me.cb.getBoolValue() and bus_volts != 0.0 ){
			if ( me.switch_type == "none" or ( me.switch_type == "bool" and me.switch.getBoolValue() ) ) {
				me.output.setDoubleValue( bus_volts );
				# if( me.watts/bus_volts > me.cb_max ){
				# 	me.cb.setBoolValue( 0 );
				# 	return 0.0;
				# }
				return me.watts / bus_volts;
			} else if ( me.switch_type == "double" ) {
				me.output.setDoubleValue( bus_volts * me.switch.getDoubleValue() );
				# if( me.watts / bus_volts * me.switch.getDoubleValue() > me.cb_max ){
				# 	me.cb.setBoolValue( 0 );
				# 	return 0.0;
				# }
				return me.watts / bus_volts * me.switch.getDoubleValue();
			} else {
				me.output.setDoubleValue( 0.0 );
				return 0.0;
			}
		} else {
			me.output.setDoubleValue( 0.0 );
			return 0.0;
		}
	},
};
# Consumer with support for integer switches
var consumer_int = {
	new: func( name, switch, watts, int, mode ){
		m = { parents : [consumer_int] };
		m.cb = breakers.initNode(name, 1, "BOOL");
		if ( switch.getType() == "INT" ) {
			m.switch = switch;
			m.int = int;
			# Mode: 0 means "=="; 1 means "!="
			if( mode != nil ){
				m.mode = mode;
			} else {
				m.mode = 0;
			}
		} else {
			die("Consumer (int) switch of unsupported type: "~ switch.getType() ~ "!");
		}
		m.output = output.initNode(name, 0.0, "DOUBLE");
		m.watts = watts;
		return m;
	},
	power: func( bus_volts ){
		if( me.cb.getBoolValue() and bus_volts != 0.0 ){
			if ( ( ( me.mode == 0 and me.switch.getIntValue() == me.int ) or ( me.mode == 1 and me.switch.getIntValue() != me.int ) ) ) {
				me.output.setDoubleValue( bus_volts );
				return me.watts / bus_volts;
			} else {
				me.output.setDoubleValue( 0.0 );
				return 0.0;
			}
		} else {
			me.output.setDoubleValue( 0.0 );
			return 0.0;
		}
	},
};

var amps = {};

#	Electrical Bus Class
var bus = {
	new: func( name, on_update, consumers ) {
		m = {	
			parents: [bus],
			name: name,
			volts: check_or_create("systems/electrical/bus/" ~ name ~ "-volts", 0.0, "DOUBLE"),
			serviceable: check_or_create("systems/electrical/bus/" ~ name ~ "-serviceable", 1, "BOOL"),
			on_update: on_update,
			bus_volts: 0.0,
			consumers: consumers,
			extra_load: 0.0,
			src: "",
		};
		amps.name = 0.0;	# register amps
		return m;
	},
	update_consumers: func () {
		#print("Update consumers of bus "~ me.name);
		load = 0.0;
		foreach( var c; me.consumers ) {
			load += c.power( me.bus_volts );
		}
		return load;
	},
};
#	Emergency Bus
#		connected to main battery
#		connected to external power port
#		powers:
#			Main Bus
#			Essential Consumers
var emergency_bus = bus.new(
	"emergency-bus",
	func() {
		me.src = "";
		if( me.serviceable.getBoolValue() ){
			me.bus_volts = battery.get_output_volts();
			if( main_bus.bus_volts > me.bus_volts and main_bus.src != me.name ){
				me.bus_volts = main_bus.bus_volts;
				me.src = "main-bus";
			} else {
				me.src = "battery";
			}
		} else {
			me.bus_volts = 0.0;
		}
		
		var load = me.update_consumers();
		load += me.extra_load;
		
		if( me.consumers[8].output.getDoubleValue() > 9.0 ){
			stall_heat_bool.setBoolValue( 1 );
		} else {
			stall_heat_bool.setBoolValue( 0 );
		}
		if( me.consumers[9].output.getDoubleValue() > 9.0 ){
			pitot_heat_bool.setBoolValue( 1 );
		} else {
			pitot_heat_bool.setBoolValue( 0 );
		}
		if( me.consumers[14].output.getDoubleValue() > 9.0 ){
			wshld_deice_bool.setBoolValue( 1 );
		} else {
			wshld_deice_bool.setBoolValue( 0 );
		}
		
		if( me.src == "main-bus" ){
			load += battery.charge_amps;
			battery.apply_load( -battery.charge_amps );
			main_bus.extra_load += load;
		} else {
			battery.apply_load( load );
		}
		
		me.extra_load = 0.0;
		me.volts.setDoubleValue( me.bus_volts );
	},
	[
		consumer.new( "aux-fuel-pump[2]", switches.fuel_pump[0], 0.1, 5 ), # 2 Port
		consumer.new( "aux-fuel-pump[3]", switches.fuel_pump[1], 0.1, 5 ), # 2 Stbd
		consumer.new( "fuel-contents[0]", nil, 0.1, 5 ),
		consumer.new( "fuel-contents[1]", nil, 0.1, 5 ),
		consumer.new( "navigation-lights", switches.nav_light, 0.1, 5 ),
		consumer.new( "map-light", nil, 0.1, 5 ),
		consumer.new( "turn-coordinator", nil, 0.1, 5 ),
		consumer.new( "stall-warning", nil, 0.1, 5 ),
		consumer.new( "pitot-heat", switches.pitot_stall_heat, 0.1, 5 ),
		consumer.new( "stall-heat", switches.pitot_stall_heat, 0.1, 5 ),
		consumer.new( "tachometer", nil, 0.1, 5 ),
		consumer.new( "audio-panel", nil, 0.1, 5 ),
		consumer.new( "comm[0]", nil, 0.1, 5 ),
		consumer.new( "nav[0]", nil, 0.1, 5 ),
		consumer.new( "windshield-deice", switches.wshld_deice, 0.1, 5 ),
	],
);


#	Main Bus
#		powers:
#			own consumers
#			avionics bus
var main_bus = bus.new(
	"main-bus",
	func(){
		me.src = "";
		me.bus_volts = 0.0;
		
		if( me.serviceable.getBoolValue() ){
			if( circuit_breakers.main_bus_isol.getBoolValue() and emergency_bus.src != me.name ){
				me.bus_volts = emergency_bus.bus_volts;
				me.src = "emergency-bus";
			}
			if( circuit_breakers.generator[0].getBoolValue() and generators[0].get_output_volts() > me.bus_volts ){
				if( circuit_breakers.generator[1].getBoolValue() and generators[1].get_output_volts() > me.bus_volts and math.abs( generators[0].get_output_volts() - generators[1].get_output_volts() ) < 4 ){
					me.bus_volts = generators[0].get_output_volts();
					me.src = "both_generators";
				} else {
					me.bus_volts = generators[0].get_output_volts();
					me.src = "generator0";
				}
			} elsif( circuit_breakers.generator[1].getBoolValue() and generators[1].get_output_volts() > me.bus_volts ){
				me.bus_volts = generators[1].get_output_volts();
				me.src = "generator1";
			}
		}
		
		var load = me.update_consumers();
		load += avionic_bus.on_update( me.bus_volts );
		load += me.extra_load;

		if( me.consumers[11].output.getDoubleValue() >= 10.0 ){
			if( switches.starter.getIntValue() == -1 ){
				fdm_starter[0].setBoolValue( 1 );
				fdm_starter[1].setBoolValue( 0 );
			} elsif ( switches.starter.getIntValue() == 1 ){
				fdm_starter[0].setBoolValue( 0 );
				fdm_starter[1].setBoolValue( 1 );
			} else {
				fdm_starter[0].setBoolValue( 0 );
				fdm_starter[1].setBoolValue( 0 );
			}
		} else {
			fdm_starter[0].setBoolValue( 0 );
			fdm_starter[1].setBoolValue( 0 );
		}
		
		if( me.consumers[14].output.getDoubleValue() > 9.0 ){
			airframe_deice_bool.setBoolValue( 1 );
		} else {
			airframe_deice_bool.setBoolValue( 0 );
		}
		if( me.consumers[15].output.getDoubleValue() > 9.0 ){
			propeller_deice_bool.setBoolValue( 1 );
		} else {
			propeller_deice_bool.setBoolValue( 0 );
		}
		
		if( me.src == "emergency-bus" ){
			emergency_bus.extra_load = load;
		} elsif( me.src == "both_generators" ){
			generators[0].apply_load( load / 2 );
			generators[1].apply_load( load / 2 );
		} elsif( me.src == "generator0" ){
			generators[0].apply_load( load / 2 );
		} elsif( me.src == "generator1" ){
			generators[1].apply_load( load / 2 );
		}
		
		me.volts.setDoubleValue( me.bus_volts );
		
		me.extra_load = 0.0;
	},
	[
		# TODO: Electrical load
		consumer.new( "aux-fuel-pump[0]", switches.fuel_pump[0], 0.1, 5 ),
		consumer.new( "aux-fuel-pump[1]", switches.fuel_pump[1], 0.1, 5 ),
		consumer.new( "landing-light[0]", switches.landing_light[0], 0.1, 5 ),
		consumer.new( "landing-light[1]", switches.landing_light[1], 0.1, 5 ),
		consumer.new( "cabin-lights", switches.cabin_light, 0.1, 5 ),
		consumer.new( "panel-lights[0]", nil, 0.1, 5 ),
		consumer.new( "panel-lights[1]", nil, 0.1, 5 ),
		consumer.new( "acl-beacon", int_switches.acl_light, 39.2, 5 ),
		consumer.new( "flaps", nil, 0.1, 5 ),
		consumer.new( "tank-selector[0]", nil, 0.1, 5 ),
		consumer.new( "tank-selector[1]", nil, 0.1, 5 ),
		consumer.new( "starter-relays", nil, 0.1, 5 ),
		consumer.new( "engine-cluster[0]", nil, 0.1, 5 ),
		consumer.new( "engine-cluster[1]", nil, 0.1, 5 ),
		consumer.new( "airframe-deice", switches.airframe_deice, 0.1, 5 ),
		consumer.new( "propeller-deice", switches.propeller_deice, 0.1, 5 ),
		consumer.new( "cabin-heat", nil, 0.1, 5 ),
		# TODO Add:
		#	Rudder Servo
		#	Aileron Servo
		#	Elevator servo
		#	Electric trim
	],
);


#	Avionic Bus
#		powers:
#			own consumers
var avionic_bus = bus.new(
	"avionic-bus",
	func( bv ) {
		if( me.serviceable.getBoolValue() and switches.avionics.getBoolValue() ){
			me.bus_volts = bv;
		} else {
			me.bus_volts = 0.0;
		}
		
		var load = me.update_consumers();
		
		me.volts.setDoubleValue( me.bus_volts );
		
		return load;
	},
	[
		# TODO: Electrical load
		consumer.new( "adf", nil, 0.1, 1 ),
		consumer.new( "comm[1]", nil, 0.1, 5 ),
		consumer.new( "nav[1]", nil, 0.1, 5 ),
		consumer.new( "marker-beacon", nil, 0.1, 5 ),
		consumer.new( "gps", nil, 0.1, 5 ),
		consumer.new( "transponder", nil, 0.1, 5 ),
		consumer.new( "dme", nil, 0.1, 5 ),
		consumer.new( "autopilot", nil, 0.1, 5 ),
	],
);

var update_electrical = func {
	
	emergency_bus.on_update();
	main_bus.on_update();
	
}

var electrical_updater = maketimer( 0.0, update_electrical );
electrical_updater.simulatedTime = 1;
electrical_updater.start();

var turn_indicator_spin = props.globals.getNode("/instrumentation/turn-indicator/spin");

var check_watts = func {
	var tc_spin = turn_indicator_spin.getDoubleValue();
	
	if( tc_spin == 1.0 ){
		main_bus.consumers[6].watts = 4.2;
		# print( "Turn Coordinator running" );
	} else {
		main_bus.consumers[6].watts = 18;
		# print( "Turn Coordinator starting" );
	}
}

var watts_updater = maketimer( 0.1, check_watts );
watts_updater.simulatedTime = 1;
watts_updater.start();

setlistener( "/controls/circuit-breakers/vhf[0]", func( i ){
	setprop("/controls/circuit-breakers/comm[0]", i.getValue() );
	setprop("/controls/circuit-breakers/nav[0]", i.getValue() );
});
setlistener( "/controls/circuit-breakers/vhf[1]", func( i ){
	setprop("/controls/circuit-breakers/comm[1]", i.getValue() );
	setprop("/controls/circuit-breakers/nav[1]", i.getValue() );
});
setprop("/controls/circuit-breakers/vhf[0]", 1 );
setprop("/controls/circuit-breakers/vhf[1]", 1 );
