# A3XX FMGC Autopilot
# Based off IT-AUTOFLIGHT System Controller V4.0.X
# Copyright (c) 2023 Josh Davidson (Octal450)

# Initialize all used variables and property nodes
# Sim
var Controls = {
	aileron: props.globals.getNode("/controls/flight/aileron", 1),
	aileron2: props.globals.getNode("/controls/flight/aileron[1]", 1),
	elevator: props.globals.getNode("/controls/flight/elevator", 1),
	elevator2: props.globals.getNode("/controls/flight/elevator[1]", 1),
	rudder: props.globals.getNode("/controls/flight/rudder", 1),
	rudder2: props.globals.getNode("/controls/flight/rudder[1]", 1),
};

var FPLN = {
	active: props.globals.getNode("/autopilot/route-manager/active", 1),
	activeTemp: 0,
	currentCourse: 0,
	currentWP: props.globals.getNode("/autopilot/route-manager/current-wp", 1),
	currentWPTemp: 0,
	deltaAngle: 0,
	deltaAngleRad: 0,
	distCoeff: 0,
	maxBank: 0,
	maxBankLimit: 0,
	nextCourse: 0,
	radius: 0,
	R: 0,
	turnDist: 0,
	wp0Dist: props.globals.getNode("/autopilot/route-manager/wp[0]/dist", 1),
	wpFlyFrom: 0,
	wpFlyTo: 0,
};

var Gear = {
	wow0: props.globals.getNode("/gear/gear[0]/wow", 1),
	wow1: props.globals.getNode("/gear/gear[1]/wow", 1),
	wow1Temp: 1,
	wow2: props.globals.getNode("/gear/gear[2]/wow", 1),
	wow2Temp: 1,
};

var Misc = {
	elapsedSec: props.globals.getNode("/sim/time/elapsed-sec", 1),
	fbwLaw: props.globals.getNode("/it-fbw/law", 1),
	flapNorm: props.globals.getNode("/surface-positions/flap-pos-norm", 1),
	pfdHeadingScale: props.globals.getNode("/instrumentation/pfd/heading-scale", 1),
};

var Position = {
	airborne5Secs: props.globals.getNode("/systems/fmgc/airborne-5-secs"),
	gearAglFtTemp: 0,
	gearAglFt: props.globals.getNode("/position/gear-agl-ft", 1),
	indicatedAltitudeFt: props.globals.getNode("/instrumentation/altimeter/indicated-altitude-ft", 1),
	indicatedAltitudeFtTemp: 0,
};

var Radio = {
	gsDefl: props.globals.getNode("/instrumentation/nav[0]/gs-needle-deflection-norm", 1),
	gsDeflTemp: 0,
	inRange: props.globals.getNode("/instrumentation/nav[0]/in-range", 1),
	locDefl: props.globals.getNode("/instrumentation/nav[0]/heading-needle-deflection-norm", 1),
	locDeflTemp: 0,
	signalQuality: props.globals.getNode("/instrumentation/nav[0]/signal-quality-norm", 1),
	signalQualityTemp: 0,
};

var Velocities = {
	airspeedKt: props.globals.getNode("/velocities/airspeed-kt", 1), # Only used for gain scheduling
	groundspeedKt: props.globals.getNode("/velocities/groundspeed-kt", 1),
	groundspeedMps: 0,
	indicatedAirspeedKt: props.globals.getNode("/instrumentation/airspeed-indicator/indicated-speed-kt", 1),
	indicatedMach: props.globals.getNode("/instrumentation/airspeed-indicator/indicated-mach", 1),
	trueAirspeedKt: props.globals.getNode("/instrumentation/airspeed-indicator/true-speed-kt", 1),
	trueAirspeedKtTemp: 0,
};

# IT-AUTOFLIGHT
var Fd = {
	pitchBar: props.globals.initNode("/it-autoflight/fd/pitch-bar", 0, "DOUBLE"),
	rollBar: props.globals.initNode("/it-autoflight/fd/roll-bar", 0, "DOUBLE"),
};

var Input = {
	alt: props.globals.initNode("/it-autoflight/input/alt", 10000, "INT"),
	ap1: props.globals.initNode("/it-autoflight/input/ap1", 0, "BOOL"),
	ap2: props.globals.initNode("/it-autoflight/input/ap2", 0, "BOOL"),
	athr: props.globals.initNode("/it-autoflight/input/athr", 0, "BOOL"),
	altDiff: 0,
	bankLimitSw: props.globals.initNode("/it-autoflight/input/bank-limit-sw", 0, "INT"),
	bankLimitSwTemp: 0,
	fd1: props.globals.initNode("/it-autoflight/input/fd1", 1, "BOOL"),
	fd2: props.globals.initNode("/it-autoflight/input/fd2", 1, "BOOL"),
	fpa: props.globals.initNode("/it-autoflight/input/fpa", 0, "DOUBLE"),
	fpaAbs: props.globals.initNode("/it-autoflight/input/fpa-abs", 0, "DOUBLE"), # Set by property rule
	hdg: props.globals.initNode("/it-autoflight/input/hdg", 0, "INT"),
	hdgCalc: 0,
	kts: props.globals.initNode("/it-autoflight/input/kts", 100, "INT"),
	ktsMach: props.globals.initNode("/it-autoflight/input/kts-mach", 0, "BOOL"),
	lat: props.globals.initNode("/it-autoflight/input/lat", 5, "INT"),
	latTemp: 5,
	mach: props.globals.initNode("/it-autoflight/input/mach", 0.5, "DOUBLE"),
	toga: props.globals.initNode("/it-autoflight/input/toga", 0, "BOOL"),
	trk: props.globals.initNode("/it-autoflight/input/trk", 0, "BOOL"),
	trueCourse: props.globals.initNode("/it-autoflight/input/true-course", 0, "BOOL"),
	vert: props.globals.initNode("/it-autoflight/input/vert", 7, "INT"),
	vertTemp: 7,
	vs: props.globals.initNode("/it-autoflight/input/vs", 0, "INT"),
	vsAbs: props.globals.initNode("/it-autoflight/input/vs-abs", 0, "INT"), # Set by property rule
};

var Internal = {
	alt: props.globals.initNode("/it-autoflight/internal/alt", 10000, "INT"),
	altCaptureActive: 0,
	altDiff: 0,
	altTemp: 0,
	altPredicted: props.globals.initNode("/it-autoflight/internal/altitude-predicted", 0, "DOUBLE"),
	bankLimit: props.globals.initNode("/it-autoflight/internal/bank-limit", 0, "DOUBLE"),
	captVs: 0,
	driftAngle: props.globals.initNode("/it-autoflight/internal/drift-angle-deg", 0, "DOUBLE"),
	driftAngleTemp: 0,
	flchActive: 0,
	fpa: props.globals.initNode("/it-autoflight/internal/fpa", 0, "DOUBLE"),
	hdgErrorDeg: props.globals.initNode("/it-autoflight/internal/heading-error-deg", 0, "DOUBLE"),
	hdgPredicted: props.globals.initNode("/it-autoflight/internal/heading-predicted", 0, "DOUBLE"),
	hdgTrk: props.globals.initNode("/it-autoflight/internal/heading", 0, "DOUBLE"),
	lnavAdvanceNm: props.globals.initNode("/it-autoflight/internal/lnav-advance-nm", 0, "DOUBLE"),
	minVs: props.globals.initNode("/it-autoflight/internal/min-vs", -500, "INT"),
	maxVs: props.globals.initNode("/it-autoflight/internal/max-vs", 500, "INT"),
	navHeadingErrorDeg: props.globals.initNode("/it-autoflight/internal/nav-heading-error-deg", 0, "DOUBLE"),
	navHeadingErrorDegTemp: 0,
	vs: props.globals.initNode("/it-autoflight/internal/vert-speed-fpm", 0, "DOUBLE"),
	vsTemp: 0,
};

var Output = {
	ap1: props.globals.initNode("/it-autoflight/output/ap1", 0, "BOOL"),
	ap1Temp: 0,
	ap2: props.globals.initNode("/it-autoflight/output/ap2", 0, "BOOL"),
	ap2Temp: 0,
	apprArm: props.globals.initNode("/it-autoflight/output/appr-armed", 0, "BOOL"),
	athr: props.globals.initNode("/it-autoflight/output/athr", 0, "BOOL"),
	athrTemp: 0,
	fd1: props.globals.initNode("/it-autoflight/output/fd1", 1, "BOOL"),
	fd1Temp: 0,
	fd2: props.globals.initNode("/it-autoflight/output/fd2", 1, "BOOL"),
	fd2Temp: 0,
	lat: props.globals.initNode("/it-autoflight/output/lat", 5, "INT"),
	latTemp: 5,
	lnavArm: props.globals.initNode("/it-autoflight/output/lnav-armed", 0, "BOOL"),
	locArm: props.globals.initNode("/it-autoflight/output/loc-armed", 0, "BOOL"),
	thrMode: props.globals.initNode("/it-autoflight/output/thr-mode", 2, "INT"),
	vert: props.globals.initNode("/it-autoflight/output/vert", 7, "INT"),
	vertTemp: 7,
};

var Text = {
	lat: props.globals.initNode("/it-autoflight/mode/lat", "T/O", "STRING"),
	thr: props.globals.initNode("/it-autoflight/mode/thr", "PITCH", "STRING"),
	vert: props.globals.initNode("/it-autoflight/mode/vert", "T/O CLB", "STRING"),
	vertTemp: "T/O CLB",
};

var Settings = {
	accelFt: props.globals.initNode("/it-autoflight/settings/accel-ft", 1500, "INT"), # Changable from MCDU, eventually set to 1500 above runway
};

var Sound = {
	apOff: props.globals.initNode("/it-autoflight/sound/apoffsound", 0, "BOOL"), # Is this still needed??? -JD
	enableApOff: 0,
};

# A3XX Custom
var Custom = {
	apFdOn: 0,
	hdgTime: -45,
	ndTrkSel: [props.globals.getNode("/instrumentation/efis[0]/trk-selected", 1), props.globals.getNode("/instrumentation/efis[1]/trk-selected", 1)],
	showHdg: props.globals.initNode("/it-autoflight/custom/show-hdg", 1, "BOOL"),
	trkFpa: props.globals.initNode("/it-autoflight/custom/trk-fpa", 0, "BOOL"),
	Input: {
		spdManaged: props.globals.getNode("/it-autoflight/input/spd-managed", 1),
	},
	Output: {
		fmaPower: 0,
		vsFCU: props.globals.initNode("/it-autoflight/output/vs-fcu-display", "", "STRING"),
	},
	Sound: {
		athrOff: props.globals.initNode("/it-autoflight/sound/athrsound", 0, "BOOL"),
		enableAthrOff: 0,
	},
	ThrLock: props.globals.getNode("/fdm/jsbsim/fadec/thr-locked", 1)
};

var ITAF = {
	init: func() {
		Custom.ndTrkSel[0].setBoolValue(0);
		Custom.ndTrkSel[1].setBoolValue(0);
		Custom.trkFpa.setBoolValue(0);
		Input.ktsMach.setBoolValue(0);
		Input.ap1.setBoolValue(0);
		Input.ap2.setBoolValue(0);
		Input.athr.setBoolValue(0);
		Input.fd1.setBoolValue(1);
		Input.fd2.setBoolValue(1);
		Input.hdg.setValue(360);
		Input.alt.setValue(10000);
		Input.vs.setValue(0);
		Custom.Output.vsFCU.setValue(left(sprintf("%+05.0f",0),3));
		Input.fpa.setValue(0);
		Input.lat.setValue(9);
		Input.vert.setValue(9);
		Input.trk.setBoolValue(0);
		Input.trueCourse.setBoolValue(0);
		Input.toga.setBoolValue(0);
		Custom.Input.spdManaged.setBoolValue(0);
		Output.ap1.setBoolValue(0);
		Output.ap2.setBoolValue(0);
		Output.athr.setBoolValue(0);
		Output.fd1.setBoolValue(1);
		Output.fd2.setBoolValue(1);
		me.updateLnavArm(0);
		me.updateLocArm(0);
		me.updateApprArm(0);
		Output.thrMode.setValue(0);
		Output.lat.setValue(9);
		Output.vert.setValue(9);
		Internal.minVs.setValue(-500);
		Internal.maxVs.setValue(500);
		Internal.bankLimit.setValue(30);
		Internal.alt.setValue(10000);
		Internal.altCaptureActive = 0;
		Input.kts.setValue(100);
		Input.mach.setValue(0.5);
		Text.thr.setValue("THRUST");
		updateFma.arm();
		me.updateLatText("");
		me.updateVertText("");
		Custom.showHdg.setBoolValue(1);
		Custom.Output.fmaPower = 1;
		
		# Sync FMA
		fmaAp();
		fmaAthr();
		fmaFd();
		
		ManagedSPD.stop();
		loopTimer.start();
		slowLoopTimer.start();
	},
	loop: func() {
		Output.latTemp = Output.lat.getValue();
		Output.vertTemp = Output.vert.getValue();
		
		# VOR/ILS Revision
		if (Output.latTemp == 2 or Output.vertTemp == 2 or Output.vertTemp == 6) {
			me.checkRadioRevision(Output.latTemp, Output.vertTemp);
		}
		
		Gear.wow1Temp = Gear.wow1.getBoolValue();
		Gear.wow2Temp = Gear.wow2.getBoolValue();
		Output.ap1Temp = Output.ap1.getBoolValue();
		Output.ap2Temp = Output.ap2.getBoolValue();
		Output.latTemp = Output.lat.getValue();
		Output.vertTemp = Output.vert.getValue();
		Text.vertTemp = Text.vert.getValue();
		Position.gearAglFtTemp = Position.gearAglFt.getValue();
		Internal.vsTemp = Internal.vs.getValue();
		Position.indicatedAltitudeFtTemp = Position.indicatedAltitudeFt.getValue();
		
		# LNAV Engagement
		if (Output.lnavArm.getBoolValue()) {
			me.checkLnav(1);
		}
		
		# VOR/LOC or ILS/LOC Capture
		if (Output.locArm.getBoolValue()) {
			me.checkLoc(1);
		}
		
		# G/S Capture
		if (Output.apprArm.getBoolValue()) {
			me.checkAppr(1);
		}
		
		# Autoland Logic
		if (Output.latTemp == 2) {
			if (Position.gearAglFtTemp <= 50) { # ALIGN
				me.setLatMode(4);
			}
		}
		if (Output.vertTemp == 2) {
			if (Position.gearAglFtTemp <= 400 and Position.gearAglFtTemp >= 5) {
				me.updateVertText("LAND");

				if (Position.gearAglFtTemp <= 50) {
					me.setVertMode(6);
				}
			}
		} else if (Output.vertTemp == 6) {
			if (Gear.wow1Temp and Gear.wow2Temp and Text.vert.getValue() != "ROLLOUT") {
				me.updateLatText("RLOU");
				me.updateVertText("ROLLOUT");
			}
		}
		
		# FLCH Engagement
		if (Text.vertTemp == "T/O CLB") {
			me.checkFlch(Settings.accelFt.getValue());
		}
		
		# Altitude Capture/Sync Logic
		if (Output.vertTemp != 0) {
			Internal.alt.setValue(Input.alt.getValue());
		}
		Internal.altTemp = Internal.alt.getValue();
		Internal.altDiff = Internal.altTemp - Position.indicatedAltitudeFtTemp;
		
		if (Output.vertTemp != 0 and Output.vertTemp != 2 and Output.vertTemp != 6 and Output.vertTemp != 9) {
			Internal.captVs = math.clamp(math.round(abs(Internal.vs.getValue()) / 5, 100), 50, 2500); # Capture limits
			Custom.apFdOn = Output.ap1Temp or Output.ap2Temp or Output.fd1.getBoolValue() or Output.fd2.getBoolValue();
			if (abs(Internal.altDiff) <= Internal.captVs and !Gear.wow1Temp and !Gear.wow2Temp and Custom.apFdOn) {
				if (Internal.altTemp >= Position.indicatedAltitudeFtTemp and Internal.vsTemp >= -25) { # Don't capture if we are going the wrong way
					me.setVertMode(3);
				} else if (Internal.altTemp < Position.indicatedAltitudeFtTemp and Internal.vsTemp <= 25) { # Don't capture if we are going the wrong way
					me.setVertMode(3);
				}
			}
		}
		
		# Altitude Hold Min/Max Reset
		if (Internal.altCaptureActive) {
			if (abs(Internal.altDiff) <= 20 and Text.vert.getValue() != "ALT HLD") {
				me.resetClimbRateLim();
				me.updateVertText("ALT HLD");
			}
		}
		
		# Thrust Mode Selector
		me.updateThrustMode();
		
		# Custom Stuff Below
		# Heading Sync
		if (!Custom.showHdg.getBoolValue()) {
			Input.hdg.setValue(Misc.pfdHeadingScale.getValue());
		}
		
		# Preselect Heading
		if (Output.latTemp != 0 and Output.latTemp != 9) { # Modes that always show HDG
			if (Custom.hdgTime + 45 >= Misc.elapsedSec.getValue()) {
				Custom.showHdg.setBoolValue(1);
			} else {
				Custom.showHdg.setBoolValue(0);
			}
		}
		
		# Misc
		if (Output.ap1Temp == 1 or Output.ap2Temp == 1) { # Trip AP off
			if (abs(Controls.aileron.getValue()) >= 0.2 or abs(Controls.elevator.getValue()) >= 0.2 or abs(Controls.rudder.getValue()) >= 0.2 or abs(Controls.aileron2.getValue()) >= 0.2 or abs(Controls.elevator2.getValue()) >= 0.2 or abs(Controls.rudder2.getValue()) >= 0.2) {
				fcu.apOff("hard", 0);
			}
		}
		
		if ((systems.FADEC.n1Mode[0].getValue() > 0 or systems.FADEC.n1Mode[1].getValue() > 0) and Output.athr.getBoolValue()) {
			fcu.athrOff("hard");
		}
	},
	slowLoop: func() {
		Velocities.trueAirspeedKtTemp = Velocities.trueAirspeedKt.getValue();
		FPLN.activeTemp = FPLN.active.getValue();
		FPLN.currentWPTemp = FPLN.currentWP.getValue();
		
		# If in LNAV mode and route is not longer active, switch to HDG HLD
		if (Output.lat.getValue() == 1) { # Only evaulate the rest of the condition if we are in LNAV mode
			if (flightPlanController.num[2].getValue() == 0 or !FPLN.activeTemp) {
				me.setLatMode(3);
			}
		}
		
		# Waypoint Advance Logic
		if (flightPlanController.num[2].getValue() > 0 and FPLN.activeTemp == 1 and FPLN.currentWPTemp != -1) {
			if ((FPLN.currentWPTemp + 1) < flightPlanController.num[2].getValue()) {
				Velocities.groundspeedMps = pts.Velocities.groundspeedKt.getValue() * 0.5144444444444;
				FPLN.currentCourse = getprop("/autopilot/route-manager/route/wp[" ~ FPLN.currentWPTemp ~ "]/leg-bearing-true-deg");
				FPLN.nextCourse = getprop("/autopilot/route-manager/route/wp[" ~ (FPLN.currentWPTemp + 1) ~ "]/leg-bearing-true-deg");
				FPLN.maxBankLimit = Internal.bankLimit.getValue();

				FPLN.deltaAngle = math.abs(geo.normdeg180(FPLN.currentCourse - FPLN.nextCourse));
				FPLN.maxBank = FPLN.deltaAngle * 1.5;
				if (FPLN.maxBank > FPLN.maxBankLimit) {
					FPLN.maxBank = FPLN.maxBankLimit;
				}
				FPLN.radius = (Velocities.groundspeedMps * Velocities.groundspeedMps) / (9.81 * math.tan(FPLN.maxBank / 57.2957795131));
				FPLN.deltaAngleRad = (180 - FPLN.deltaAngle) / 114.5915590262;
				FPLN.R = FPLN.radius / math.sin(FPLN.deltaAngleRad);
				FPLN.distCoeff = FPLN.deltaAngle * -0.011111 + 2;
				if (FPLN.distCoeff < 1) {
					FPLN.distCoeff = 1;
				}
				FPLN.turnDist = math.cos(FPLN.deltaAngleRad) * FPLN.R * FPLN.distCoeff / 1852;
				if (Gear.wow0.getBoolValue() and FPLN.turnDist < 1) {
					FPLN.turnDist = 1;
				}
				
				# This is removed because sequencing is done by the flightplan controller
				# Internal.lnavAdvanceNm.setValue(FPLN.turnDist);
				
				# TODO - if the waypoint is the DEST waypoint, crosstrack error must be less than 0.5nm and course error less than 30 deg
				# TODO - if in HDG mode, if no distance, then crosstrack error must be less than 5nm
				# TODO - if in nav, no distance condition applies, but DEST course error must be less than 30 (CONFIRM)
				
				if (abs(FPLN.deltaAngle) < 120 and FPLN.wp0Dist.getValue() <= FPLN.turnDist and !Gear.wow1.getBoolValue() and fmgc.flightPlanController.flightplans[2].getWP(FPLN.currentWPTemp).fly_type == "flyBy") {
					flightPlanController.autoSequencing();
				} elsif (FPLN.wp0Dist.getValue() <= 0.15 and !Gear.wow1.getBoolValue()) {
					flightPlanController.autoSequencing();
				}
			}
		}
	},
	ap1Master: func(s) {
		if (s == 1) {
			if (Output.vert.getValue() != 6 and !Gear.wow1.getBoolValue() and !Gear.wow2.getBoolValue() and FMGCNodes.Power.FMGC1Powered.getBoolValue() and fbw.FBW.apOff == 0 and Position.gearAglFt.getValue() >= 100 and Position.airborne5Secs.getBoolValue()) {
				Output.ap1.setBoolValue(1);
				me.updateFma();
				Output.latTemp = Output.lat.getValue();
				if (Output.ap2.getBoolValue() and !Output.apprArm.getBoolValue() and Output.latTemp != 2 and Output.latTemp != 4) {
					me.ap2Master(0);
				}
				Sound.enableApOff = 1;
				Sound.apOff.setBoolValue(0);
			}
		} else {
			Output.ap1.setBoolValue(0);
			me.apOffFunction();
		}
		Output.ap1Temp = Output.ap1.getBoolValue();
		if (Input.ap1.getBoolValue() != Output.ap1Temp) {
			Input.ap1.setBoolValue(Output.ap1Temp);
		}
		fmaAp();
	},
	ap2Master: func(s) {
		if (s == 1) {
			if (Output.vert.getValue() != 6 and !Gear.wow1.getBoolValue() and !Gear.wow2.getBoolValue() and FMGCNodes.Power.FMGC2Powered.getBoolValue() and fbw.FBW.apOff == 0 and Position.gearAglFt.getValue() >= 100 and Position.airborne5Secs.getBoolValue()) {
				Output.ap2.setBoolValue(1);
				me.updateFma();
				Output.latTemp = Output.lat.getValue();
				if (Output.ap1.getBoolValue() and !Output.apprArm.getBoolValue() and Output.latTemp != 2 and Output.latTemp != 4) {
					me.ap1Master(0);
				}
				Sound.enableApOff = 1;
				Sound.apOff.setBoolValue(0);
			}
		} else {
			Output.ap2.setBoolValue(0);
			me.apOffFunction();
		}
		Output.ap2Temp = Output.ap2.getBoolValue();
		if (Input.ap2.getBoolValue() != Output.ap2Temp) {
			Input.ap2.setBoolValue(Output.ap2Temp);
		}
		fmaAp();
	},
	apOffFunction: func() {
		if (!Output.ap1.getBoolValue() and !Output.ap2.getBoolValue()) { # Only do if both APs are off
			me.updateFma();
			if (Sound.enableApOff) {
				Sound.apOff.setBoolValue(1);
				Sound.enableApOff = 0;
			}
		}
	},
	athrMaster: func(s) {
		if (s == 1) {
			if ((FMGCNodes.Power.FMGC1Powered.getBoolValue() or FMGCNodes.Power.FMGC2Powered.getBoolValue()) and !pts.FMGC.CasCompare.casRejectAll.getBoolValue() and fbw.FBW.apOff == 0 and systems.FADEC.n1Mode[0].getValue() == 0 and systems.FADEC.n1Mode[1].getValue() == 0) {

				Output.athr.setBoolValue(1);
				Custom.ThrLock.setValue(0);
				Custom.Sound.enableAthrOff = 1;
				Custom.Sound.athrOff.setBoolValue(0);
			}
		} else {
			Output.athr.setBoolValue(0);
			if (Custom.Sound.enableAthrOff) {
				Custom.Sound.athrOff.setBoolValue(1);
				Custom.Sound.enableAthrOff = 0;
			}
		}
		Output.athrTemp = Output.athr.getBoolValue();
		if (Input.athr.getBoolValue() != Output.athrTemp) {
			Input.athr.setBoolValue(Output.athrTemp);
		}
		fmaAthr();
	},
	fd1Master: func(s) {
		if (s == 1) {
			Output.fd1.setBoolValue(1);
			me.updateFma();
		} else {
			Output.fd1.setBoolValue(0);
			if (!Output.fd2.getBoolValue()) {
				me.updateFma();
			}
		}
		Output.fd1Temp = Output.fd1.getBoolValue();
		if (Input.fd1.getBoolValue() != Output.fd1Temp) {
			Input.fd1.setBoolValue(Output.fd1Temp);
		}
		fmaFd();
	},
	fd2Master: func(s) {
		if (s == 1) {
			Output.fd2.setBoolValue(1);
			me.updateFma();
		} else {
			Output.fd2.setBoolValue(0);
			if (!Output.fd1.getBoolValue()) {
				me.updateFma();
			}
		}
		Output.fd2Temp = Output.fd2.getBoolValue();
		if (Input.fd2.getBoolValue() != Output.fd2Temp) {
			Input.fd2.setBoolValue(Output.fd2Temp);
		}
		fmaFd();
	},
	setLatMode: func(n) {
		Output.vertTemp = Output.vert.getValue();
		if (n == 0) { # HDG SEL
			me.updateLnavArm(0);
			me.updateLocArm(0);
			me.updateApprArm(0);
			Output.lat.setValue(0);
			Custom.showHdg.setBoolValue(1);
			me.updateLatText("HDG");
			if (Output.vertTemp == 2 or Output.vertTemp == 6) { # Also cancel G/S or FLARE if active
				me.setVertMode(1);
			}
		} else if (n == 1) { # LNAV
			me.updateLocArm(0);
			me.updateApprArm(0);
			me.checkLnav(0);
		} else if (n == 2) { # VOR/LOC
			me.updateLnavArm(0);
			me.checkLoc(0);
		} else if (n == 3) { # HDG HLD
			me.updateLnavArm(0);
			me.updateLocArm(0);
			me.updateApprArm(0);
			me.syncHdg();
			Output.lat.setValue(0);
			Custom.showHdg.setBoolValue(1);
			me.updateLatText("HDG");
			if (Output.vertTemp == 2 or Output.vertTemp == 6) { # Also cancel G/S or FLARE if active
				me.setVertMode(1);
			}
		} else if (n == 4) { # ALIGN
			me.updateLnavArm(0);
			me.updateLocArm(0);
			me.updateApprArm(0);
			Output.lat.setValue(4);
			Custom.showHdg.setBoolValue(0);
			me.updateLatText("ALGN");
		} else if (n == 5) { # RWY
			me.updateLnavArm(0);
			me.updateLocArm(0);
			me.updateApprArm(0);
			Output.lat.setValue(5);
			Custom.showHdg.setBoolValue(0);
			me.updateLatText("T/O");
		} else if (n == 9) { # NONE
			me.updateLnavArm(0);
			me.updateLocArm(0);
			me.updateApprArm(0);
			Output.lat.setValue(9);
			Custom.showHdg.setBoolValue(1);
			me.updateLatText("");
		}
	},
	setLatArm: func(n) {
		if (n == 0) {
			me.updateLnavArm(0);
			Custom.showHdg.setBoolValue(1);
		} else if (n == 1) {
			if (flightPlanController.num[2].getValue() > 0 and FPLN.active.getBoolValue()) {
				me.updateLnavArm(1);
				Custom.showHdg.setBoolValue(0);
			}
		} else if (n == 3) {
			me.syncHdg();
			me.updateLnavArm(0);
			Custom.showHdg.setBoolValue(1);
		} 
	},
	setVertMode: func(n) {
		Input.altDiff = Input.alt.getValue() - Position.indicatedAltitudeFt.getValue();
		if (n == 0) { # ALT HLD
			Internal.flchActive = 0;
			Internal.altCaptureActive = 0;
			me.updateApprArm(0);
			Output.vert.setValue(0);
			me.resetClimbRateLim();
			me.updateVertText("ALT HLD");
			me.syncAlt();
			me.updateThrustMode();
		} else if (n == 1) { # V/S
			if (abs(Input.altDiff) >= 25) {
				Internal.flchActive = 0;
				Internal.altCaptureActive = 0;
				me.updateApprArm(0);
				Output.vert.setValue(1);
				me.updateVertText("V/S");
				me.syncVs();
				me.updateThrustMode();
			} else {
				me.updateApprArm(0);
			}
		} else if (n == 2) { # G/S
			me.updateLnavArm(0);
			me.checkLoc(0);
			me.checkAppr(0);
		} else if (n == 3) { # ALT CAP
			Internal.flchActive = 0;
			Output.vert.setValue(0);
			me.setClimbRateLim();
			Internal.altCaptureActive = 1;
			me.updateVertText("ALT CAP");
			me.updateThrustMode();
		} else if (n == 4) { # FLCH
			me.updateApprArm(0);
			Output.vert.setValue(1);
			Internal.alt.setValue(Input.alt.getValue());
			Internal.altDiff = Internal.alt.getValue() - Position.indicatedAltitudeFt.getValue();
			if (abs(Internal.altDiff) >= 250) { # SPD CLB or SPD DES
				Internal.altCaptureActive = 0;
				Output.vert.setValue(4);
				Internal.flchActive = 1;
				Internal.alt.setValue(Input.alt.getValue());
				me.updateThrustMode();
			} else { # ALT CAP
				Internal.flchActive = 0;
				Internal.alt.setValue(Input.alt.getValue());
				Internal.altCaptureActive = 1;
				Output.vert.setValue(0);
				me.updateVertText("ALT CAP");
				me.updateThrustMode();
			}
		} else if (n == 5) { # FPA
			if (abs(Input.altDiff) >= 25) {
				Internal.flchActive = 0;
				Internal.altCaptureActive = 0;
				me.updateApprArm(0);
				Output.vert.setValue(5);
				me.updateVertText("FPA");
				me.syncFpa();
				me.updateThrustMode();
			} else {
				me.updateApprArm(0);
			}
		} else if (n == 6) { # FLARE/ROLLOUT
			Internal.flchActive = 0;
			Internal.altCaptureActive = 0;
			me.updateApprArm(0);
			Output.vert.setValue(6);
			me.updateVertText("FLARE");
			me.updateThrustMode();
		} else if (n == 7) { # T/O CLB or G/A CLB, text is set by TOGA selector
			Internal.flchActive = 0;
			Internal.altCaptureActive = 0;
			me.updateApprArm(0);
			Output.vert.setValue(7);
			me.updateThrustMode();
		} else if (n == 9) { # NONE
			Internal.flchActive = 0;
			Internal.altCaptureActive = 0;
			me.updateApprArm(0);
			Output.vert.setValue(9);
			me.updateVertText("");
			me.updateThrustMode();
		}
	},
	updateThrustMode: func() {
		Output.vertTemp = Output.vert.getValue();
		if (Output.athr.getBoolValue() and Output.vertTemp != 7 and (Output.ap1.getBoolValue() or Output.ap2.getBoolValue()) and Position.gearAglFt.getValue() <= 30 and (Output.vertTemp == 2 or Output.vertTemp == 6)) {
			# Manual says 40 feet - but video reference shows 30!
			Output.thrMode.setValue(1);
			Text.thr.setValue("RETARD");
		} else if (Output.vertTemp == 4) {
			if (Internal.alt.getValue() >= Position.indicatedAltitudeFt.getValue()) {
				Output.thrMode.setValue(2);
				Text.thr.setValue("PITCH");
				if (Internal.flchActive and Text.vert.getValue() != "SPD CLB") {
					me.updateVertText("SPD CLB");
				}
			} else {
				Output.thrMode.setValue(1);
				Text.thr.setValue("PITCH");
				if (Internal.flchActive and Text.vert.getValue() != "SPD DES") {
					me.updateVertText("SPD DES");
				}
			}
		} else if (Output.vertTemp == 7) {
			Output.thrMode.setValue(2);
			Text.thr.setValue("PITCH");
		} else {
			Output.thrMode.setValue(0);
			Text.thr.setValue("THRUST");
		}
	},
	activateLnav: func() {
		if (Output.lat.getValue() != 1) {
			me.updateLnavArm(0);
			me.updateLocArm(0);
			me.updateApprArm(0);
			Output.lat.setValue(1);
			Custom.showHdg.setBoolValue(0);
			me.updateLatText("LNAV");
			if (Output.vertTemp == 2 or Output.vertTemp == 6) { # Also cancel G/S or FLARE if active
				me.setVertMode(1);
			}
		}
	},
	activateLoc: func() {
		if (Output.lat.getValue() != 2) {
			me.updateLnavArm(0);
			me.updateLocArm(0);
			Output.lat.setValue(2);
			Custom.showHdg.setBoolValue(0);
			me.updateLatText("LOC");
		}
	},
	activateGs: func() {
		if (Output.vert.getValue() != 2) {
			Internal.flchActive = 0;
			Internal.altCaptureActive = 0;
			me.updateApprArm(0);
			Output.vert.setValue(2);
			me.updateVertText("G/S");
			me.updateThrustMode();
		}
	},
	checkLnav: func(t) {
		FPLN.activeTemp = FPLN.active.getBoolValue();
		if (flightPlanController.num[2].getValue() > 0 and FPLN.activeTemp and Position.gearAglFt.getValue() >= 30) {
			me.activateLnav();
		} else if (FPLN.activeTemp and Output.lat.getValue() != 1 and t != 1) {
			me.updateLnavArm(1);
		}
	},
	checkFlch: func(a) {
		if (Position.indicatedAltitudeFt.getValue() >= a and a != 0 and !Gear.wow1.getBoolValue() and !Gear.wow2.getBoolValue()) {
			me.setVertMode(4);
		}
	},
	checkLoc: func(t) {
		if (Radio.inRange.getBoolValue()) { #  # Only evaulate the rest of the condition unless we are in range
			Internal.navHeadingErrorDegTemp = Internal.navHeadingErrorDeg.getValue();
			Radio.locDeflTemp = Radio.locDefl.getValue();
			Radio.signalQualityTemp = Radio.signalQuality.getValue();
			if (abs(Radio.locDeflTemp) <= 0.95 and Radio.locDeflTemp != 0 and Radio.signalQualityTemp >= 0.99) {
				if (abs(Radio.locDeflTemp) <= 0.25) {
					me.activateLoc();
				} else if (Radio.locDeflTemp >= 0 and Internal.navHeadingErrorDegTemp <= 0) {
					me.activateLoc();
				} else if (Radio.locDeflTemp < 0 and Internal.navHeadingErrorDegTemp >= 0) {
					me.activateLoc();
				} else if (t != 1) { # Do not do this if loop calls it
					if (Output.lat.getValue() != 2) {
						me.updateLnavArm(0);
						me.updateLocArm(1);
					}
				}
			} else if (t != 1) { # Do not do this if loop calls it
				if (Output.lat.getValue() != 2) {
					me.updateLnavArm(0);
					me.updateLocArm(1);
				}
			}
		} else {
			Radio.signalQuality.setValue(0); # Prevent bad behavior due to FG not updating it when not in range
			me.updateLocArm(0);
		}
	},
	checkAppr: func(t) {
		if (Radio.inRange.getBoolValue()) { #  # Only evaulate the rest of the condition unless we are in range
			Radio.gsDeflTemp = Radio.gsDefl.getValue();
			if (abs(Radio.gsDeflTemp) <= 0.2 and Radio.gsDeflTemp != 0 and Output.lat.getValue() == 2) { # Only capture if LOC is active
				me.activateGs();
			} else if (t != 1) { # Do not do this if loop calls it
				if (Output.vert.getValue() != 2) {
					me.updateApprArm(1);
				}
			}
		} else {
			Radio.signalQuality.setValue(0); # Prevent bad behavior due to FG not updating it when not in range
			me.updateApprArm(0);
		}
	},
	checkRadioRevision: func(l, v) { # Revert mode if signal lost
		if (!Radio.inRange.getBoolValue()) {
			if (l == 4 or v == 6) {
				me.ap1Master(0);
				me.ap2Master(0);
				me.setLatMode(3);
				me.setVertMode(1);
			} else {
				me.setLatMode(3); # Also cancels G/S if active
			}
		}
	},
	setClimbRateLim: func() {
		Internal.vsTemp = Internal.vs.getValue();
		if (Internal.alt.getValue() >= Position.indicatedAltitudeFt.getValue()) {
			Internal.maxVs.setValue(math.round(Internal.vsTemp));
			Internal.minVs.setValue(-500);
		} else {
			Internal.maxVs.setValue(500);
			Internal.minVs.setValue(math.round(Internal.vsTemp));
		}
	},
	resetClimbRateLim: func() {
		Internal.minVs.setValue(-500);
		Internal.maxVs.setValue(500);
	},
	takeoffGoAround: func() {
		Output.vertTemp = Output.vert.getValue();
		if ((Output.vertTemp == 2 or Output.vertTemp == 6) and Velocities.indicatedAirspeedKt.getValue() >= 80) {
			me.setLatMode(3);
			me.setVertMode(7); # Must be before kicking AP off
			me.updateVertText("G/A CLB");
			me.syncKtsGa();
			if (Gear.wow1.getBoolValue() or Gear.wow2.getBoolValue()) {
				me.ap1Master(0);
				me.ap2Master(0);
			}
		} else if (Gear.wow1Temp or Gear.wow2Temp) {
			me.athrMaster(1);
			if (Output.lat.getValue() != 5) { # Don't accidently disarm LNAV
				me.setLatMode(5);
			}
			me.setVertMode(7);
			me.updateVertText("T/O CLB");
		}
	},
	syncKts: func() {
		Input.kts.setValue(math.clamp(math.round(Velocities.indicatedAirspeedKt.getValue()), 100, 399));
	},
	syncKtsGa: func() { # Same as syncKts, except doesn't go below V2
		Input.kts.setValue(math.clamp(math.round(Velocities.indicatedAirspeedKt.getValue()), FMGCInternal.v2, 399));
	},
	syncMach: func() {
		Input.mach.setValue(math.clamp(math.round(Velocities.indicatedMach.getValue(), 0.001), 0.1, 0.99));
	},
	syncHdg: func() {
		Input.hdg.setValue(math.round(Internal.hdgPredicted.getValue())); # Switches to track automatically
	},
	syncAlt: func() {
		Input.alt.setValue(math.clamp(math.round(Internal.altPredicted.getValue(), 100), 0, 50000));
		Internal.alt.setValue(math.clamp(math.round(Internal.altPredicted.getValue(), 100), 0, 50000));
	},
	tempVS: 0,
	syncVs: func() {
		me.tempVS = math.clamp(math.round(Internal.vs.getValue(), 100), -6000, 6000);
		Input.vs.setValue(me.tempVS);
		fmgc.Custom.Output.vsFCU.setValue(left(sprintf("%+05.0f",me.tempVS),3));
	},
	syncFpa: func() {
		Input.fpa.setValue(math.clamp(math.round(Internal.fpa.getValue(), 0.1), -9.9, 9.9));
	},
	# Custom Stuff Below
	updateFma: func() {
		if (!Output.ap1.getBoolValue() and !Output.ap2.getBoolValue() and !Output.fd1.getBoolValue() and !Output.fd2.getBoolValue()) {
			me.setLatMode(9);
			me.setVertMode(9);
			me.setLatArm(0);
			Custom.Output.fmaPower = 0;
		} else {
			if (!Custom.Output.fmaPower) showAllBoxes();
			Custom.Output.fmaPower = 1;
			me.revertBasicMode();
		}
	},
	revertBasicMode: func() {
		if (!Gear.wow1.getBoolValue() and !Gear.wow2.getBoolValue()) { # Don't do this on ground
			if (Output.lat.getValue() == 9) {
				me.setLatMode(3);
			}
			if (Output.vert.getValue() == 9) {
				if (Custom.trkFpa.getBoolValue()) {
					me.setVertMode(5);
				} else {
					me.setVertMode(1);
				}
			}
		}
	},
	disarmLoc: func() {
		me.updateLocArm(0);
	},
	disarmAppr: func() {
		me.updateApprArm(0);
	},
	toggleTrkFpa: func() {
		if (Custom.trkFpa.getBoolValue()) {
			me.trkFpaOff();
		} else {
			me.trkFpaOn();
		}
	},
	trkFpaOn: func() {
		Custom.trkFpa.setBoolValue(1);
		if (Output.vert.getValue() == 1) {
			Input.vert.setValue(5); # This way we only do this if all conditions are true
		}
		Input.trk.setBoolValue(1);
		Custom.ndTrkSel[0].setBoolValue(1);
		Custom.ndTrkSel[1].setBoolValue(1);
		Input.hdgCalc = Input.hdg.getValue() + math.round(Internal.driftAngle.getValue());
		if (Input.hdgCalc > 360) { # It's rounded, so this is ok. Otherwise do >= 360.5
			Input.hdgCalc = Input.hdgCalc - 360;
		} else if (Input.hdgCalc < 1) { # It's rounded, so this is ok. Otherwise do < 0.5
			Input.hdgCalc = Input.hdgCalc + 360;
		}
		updateFma.lat();
		Input.hdg.setValue(Input.hdgCalc);
	},
	trkFpaOff: func() {
		Custom.trkFpa.setBoolValue(0);
		if (Output.vert.getValue() == 5) {
			Input.vert.setValue(1); # This way we only do this if all conditions are true
		}
		Input.trk.setBoolValue(0);
		Custom.ndTrkSel[0].setBoolValue(0);
		Custom.ndTrkSel[1].setBoolValue(0);
		Input.hdgCalc = Input.hdg.getValue() - math.round(Internal.driftAngle.getValue());
		if (Input.hdgCalc > 360) { # It's rounded, so this is ok. Otherwise do >= 360.5
			Input.hdgCalc = Input.hdgCalc - 360;
		} else if (Input.hdgCalc < 1) { # It's rounded, so this is ok. Otherwise do < 0.5
			Input.hdgCalc = Input.hdgCalc + 360;
		}
		updateFma.lat();
		Input.hdg.setValue(Input.hdgCalc);
	},
	updateLatText: func(t) {
		Text.lat.setValue(t);
		updateFma.lat();
	},
	updateVertText: func(t) {
		Text.vert.setValue(t);
		updateFma.vert();
	},
	updateLnavArm: func(n) {
		Output.lnavArm.setBoolValue(n);
		updateFma.arm();
	},
	updateLocArm: func(n) {
		Output.locArm.setBoolValue(n);
		updateFma.arm();
	},
	updateApprArm: func(n) {
		Output.apprArm.setBoolValue(n);
		updateFma.arm();
	},
};

setlistener(Gear.wow1, func(val) {
	if (!val.getBoolValue() and FPLN.currentWP.getValue() == 0) {
		flightPlanController.autoSequencing();
	}
});
	
setlistener("/it-autoflight/input/ap1", func() {
	Input.ap1Temp = Input.ap1.getBoolValue();
	if (Input.ap1Temp != Output.ap1.getBoolValue()) {
		ITAF.ap1Master(Input.ap1Temp);
	}
});

setlistener("/it-autoflight/input/ap2", func() {
	Input.ap2Temp = Input.ap2.getBoolValue();
	if (Input.ap2Temp != Output.ap2.getBoolValue()) {
		ITAF.ap2Master(Input.ap2Temp);
	}
});

setlistener("/it-autoflight/input/athr", func() {
	Input.athrTemp = Input.athr.getBoolValue();
	if (Input.athrTemp != Output.athr.getBoolValue()) {
		ITAF.athrMaster(Input.athrTemp);
	}
});

setlistener("/it-autoflight/input/fd1", func() {
	Input.fd1Temp = Input.fd1.getBoolValue();
	if (Input.fd1Temp != Output.fd1.getBoolValue()) {
		ITAF.fd1Master(Input.fd1Temp);
	}
});

setlistener("/it-autoflight/input/fd2", func() {
	Input.fd2Temp = Input.fd2.getBoolValue();
	if (Input.fd2Temp != Output.fd2.getBoolValue()) {
		ITAF.fd2Master(Input.fd2Temp);
	}
});

	
setlistener("/it-autoflight/input/kts-mach", func() {
	if (Output.vert.getValue() == 7) { # Mach is not allowed in Mode 7, and don't sync
		if (Input.ktsMach.getBoolValue()) {
			Input.ktsMach.setBoolValue(0);
		}
	} else {
		if (Input.ktsMach.getBoolValue()) {
			ITAF.syncMach();
		} else {
			ITAF.syncKts();
		}
	}
}, 0, 0);

setlistener("/it-autoflight/input/toga", func() {
	if (Input.toga.getBoolValue()) {
		ITAF.takeoffGoAround();
		Input.toga.setBoolValue(0);
	}
});

setlistener("/it-autoflight/input/lat", func() {
	Input.latTemp = Input.lat.getValue();
	Output.ap1Temp = Output.ap1.getBoolValue();
	Output.ap2Temp = Output.ap2.getBoolValue();
	Output.fd1Temp = Output.fd1.getBoolValue();
	Output.fd2Temp = Output.fd2.getBoolValue();
	
	if (!Gear.wow1.getBoolValue() and !Gear.wow2.getBoolValue()) {
		if (Output.ap1Temp or Output.ap2Temp or Output.fd1Temp or Output.fd2Temp) {
			ITAF.setLatMode(Input.latTemp);
		} else {
			ITAF.setLatMode(9);
		}
	} else {
		if (Output.ap1Temp or Output.ap2Temp or Output.fd1Temp or Output.fd2Temp) {
			ITAF.setLatArm(Input.latTemp);
		} else {
			ITAF.setLatArm(0);
		}
	}
});

setlistener("/it-autoflight/input/vert", func() {
	if (!Gear.wow1.getBoolValue() and !Gear.wow2.getBoolValue() and (Output.ap1.getBoolValue() or Output.ap2.getBoolValue() or Output.fd1.getBoolValue() or Output.fd2.getBoolValue())) {
		ITAF.setVertMode(Input.vert.getValue());
	} else {
		ITAF.setVertMode(9);
	}
});

# Mode Reversions
setlistener(pts.Systems.Navigation.ADR.Output.overspeed, func(v) {
	if (v.getBoolValue() and !Output.ap1.getBoolValue() and !Output.ap2.getBoolValue() and Output.athr.getBoolValue() and Modes.PFD.FMA.pitchMode == "OP CLB" and Modes.PFD.FMA.throttleMode == "THR CLB") {
		Input.fd1.setValue(0);
		Input.fd2.setValue(0);
		ecam.aural[5].setBoolValue(0);
		settimer(func() {
			ecam.aural[5].setBoolValue(1);
		}, 0.15);
	}
}, 0, 0);

setlistener(pts.Systems.Navigation.ADR.Output.underspeed, func(v) {
	if (v.getBoolValue() and !Output.ap1.getBoolValue() and !Output.ap2.getBoolValue() and Output.athr.getBoolValue() and Modes.PFD.FMA.pitchMode == "OP DES" and Modes.PFD.FMA.throttleMode == "THR IDLE") {
		Input.fd1.setValue(0);
		Input.fd2.setValue(0);
		ecam.aural[5].setBoolValue(0);
		settimer(func() {
			ecam.aural[5].setBoolValue(1);
		}, 0.15);
	}
}, 0, 0);

setlistener("/sim/signals/fdm-initialized", func {
	ITAF.init();
});

# For Canvas Nav Display.
setlistener("/it-autoflight/input/hdg", func() {
	pts.Autopilot.Settings.headingBugDeg.setValue(Input.hdg.getValue());
}, 0, 0);

setlistener("/it-autoflight/internal/alt", func() {
	pts.Autopilot.Settings.targetAltitudeFt.setValue(Internal.alt.getValue());
}, 0, 0);

var loopTimer = maketimer(0.1, ITAF, ITAF.loop);
var slowLoopTimer = maketimer(1, ITAF, ITAF.slowLoop);