import { i18n } from "../localization";
import { colorTables, getColorForPercentage } from "../utils/css.js";
import GUI, { TABS } from "../gui";
import { tracking } from "../Analytics";
import { have_sensor } from "../sensor_helpers";
import { mspHelper } from "../msp/MSPHelper";
import FC from "../fc";
import MSP from "../msp";
import TuningSliders from "../TuningSliders";
import Model from "../model";
import RateCurve from "../RateCurve";
import MSPCodes from "../msp/MSPCodes";
import { API_VERSION_1_45, API_VERSION_1_46, API_VERSION_1_47 } from "../data_storage";
import { gui_log } from "../gui_log";
import { degToRad, isInt } from "../utils/common";
import semver from "semver";
import * as THREE from "three";
import $ from "jquery";

const pid_tuning = {
    RATE_PROFILE_MASK: 128,
    showAllPids: false,
    updating: true,
    dirty: false,
    previousFilterDynQ: null,
    previousFilterDynCount: null,
    currentProfile: null,
    currentRateProfile: null,
    currentRatesType: null,
    previousRatesType: null,
    SETPOINT_WEIGHT_RANGE_LOW: 2.55,
    SETPOINT_WEIGHT_RANGE_HIGH: 20,
    SETPOINT_WEIGHT_RANGE_LEGACY: 2.54,
    activeSubtab: "pid",
    analyticsChanges: {},

    CONFIGURATOR_PIDS: [],
    CONFIGURATOR_ADVANCED_TUNING: {},
    CONFIGURATOR_FILTER_CONFIG: {},
    CONFIGURATOR_RC_TUNING: {},
    CONFIGURATOR_FEATURE_CONFIG: {},
    CONFIGURATOR_TUNING_SLIDERS: {},
};

pid_tuning.initialize = function (callback) {
    const self = this;

    if (GUI.active_tab !== "pid_tuning") {
        GUI.active_tab = "pid_tuning";
        self.activeSubtab = "pid";
    }

    // Update filtering and pid defaults based on API version
    const FILTER_DEFAULT = FC.getFilterDefaults();

    MSP.promise(MSPCodes.MSP_PID_CONTROLLER)
        .then(() => MSP.promise(MSPCodes.MSP_PIDNAMES))
        .then(() => MSP.promise(MSPCodes.MSP_PID))
        .then(() => MSP.promise(MSPCodes.MSP_PID_ADVANCED))
        .then(() => MSP.promise(MSPCodes.MSP_RC_TUNING))
        .then(() => MSP.promise(MSPCodes.MSP_FILTER_CONFIG))
        .then(() => MSP.promise(MSPCodes.MSP_RC_DEADBAND))
        .then(() => MSP.promise(MSPCodes.MSP_MOTOR_CONFIG))
        .then(() =>
            semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_45)
                ? MSP.promise(
                    MSPCodes.MSP2_GET_TEXT,
                    mspHelper.crunch(MSPCodes.MSP2_GET_TEXT, MSPCodes.PID_PROFILE_NAME),
                )
                : true,
        )
        .then(() =>
            semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_45)
                ? MSP.promise(
                    MSPCodes.MSP2_GET_TEXT,
                    mspHelper.crunch(MSPCodes.MSP2_GET_TEXT, MSPCodes.RATE_PROFILE_NAME),
                )
                : true,
        )
        .then(() => (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_47) ? MSP.promise(MSPCodes.MSP_STATUS_EX) : true))
        .then(() => MSP.promise(MSPCodes.MSP_SIMPLIFIED_TUNING))
        .then(() => MSP.promise(MSPCodes.MSP_ADVANCED_CONFIG))
        .then(() => MSP.send_message(MSPCodes.MSP_MIXER_CONFIG, false, false, load_html));

    function load_html() {
        $("#content").load("./tabs/pid_tuning.html", process_html);
    }

    const vbatpidcompensationIsUsed = false; // removed in API 1_44

    function pid_and_rc_to_form() {
        self.setProfile();
        self.setRateProfile();

        // Profile names
        if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_45)) {
            $('input[name="pidProfileName"]').val(FC.CONFIG.pidProfileNames[FC.CONFIG.profile]);
            $('input[name="rateProfileName"]').val(FC.CONFIG.rateProfileNames[FC.CONFIG.rateProfile]);
        } else {
            $(".profile_name").hide();
        }

        // Fill in the data from PIDs array for each pid name
        FC.PID_NAMES.forEach(function (elementPid, indexPid) {
            // Look into the PID table to a row with the name of the pid
            const searchRow = $(`.pid_tuning .${elementPid} input`);

            // Assign each value
            searchRow.each((indexInput, element) => {
                if (FC.PIDS[indexPid][indexInput] !== undefined) {
                    $(element).val(FC.PIDS_ACTIVE[indexPid][indexInput]);
                }
            });
        });

        // Fill in data from RC_tuning object
        $('.pid_tuning input[name="rc_rate"]').val(FC.RC_TUNING.RC_RATE.toFixed(2));
        $('.pid_tuning input[name="roll_pitch_rate"]').val(FC.RC_TUNING.roll_pitch_rate.toFixed(2));
        $('.pid_tuning input[name="roll_rate"]').val(FC.RC_TUNING.roll_rate.toFixed(2));
        $('.pid_tuning input[name="pitch_rate"]').val(FC.RC_TUNING.pitch_rate.toFixed(2));
        $('.pid_tuning input[name="yaw_rate"]').val(FC.RC_TUNING.yaw_rate.toFixed(2));
        $('.pid_tuning input[name="rc_expo"]').val(FC.RC_TUNING.RC_EXPO.toFixed(2));
        $('.pid_tuning input[name="rc_yaw_expo"]').val(FC.RC_TUNING.RC_YAW_EXPO.toFixed(2));

        $('.throttle input[name="mid"]').val(FC.RC_TUNING.throttle_MID.toFixed(2));
        $('.throttle input[name="expo"]').val(FC.RC_TUNING.throttle_EXPO.toFixed(2));

        if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_47)) {
            $('.throttle input[name="hover"]').val(FC.RC_TUNING.throttle_HOVER.toFixed(2));
        } else {
            $('.throttle input[name="hover"]').parent().hide();
            $(".throttle thead th:nth-child(2)").hide();
        }

        if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_45)) {
            // Moved tpa to profile
            $('select[id="tpaMode"]').val(FC.ADVANCED_TUNING.tpaMode);
            $('input[id="tpaRate"]').val(FC.ADVANCED_TUNING.tpaRate * 100);
            $('input[id="tpaBreakpoint"]').val(FC.ADVANCED_TUNING.tpaBreakpoint);
        } else {
            $('.tpa-old input[name="tpa"]').val(FC.RC_TUNING.dynamic_THR_PID.toFixed(2));
            $('.tpa-old input[name="tpa-breakpoint"]').val(FC.RC_TUNING.dynamic_THR_breakpoint);
        }

        $(".vbatpidcompensation").toggle(vbatpidcompensationIsUsed);
        $('input[id="vbatpidcompensation"]').prop("checked", FC.ADVANCED_TUNING.vbatPidCompensation !== 0);

        $("#pid-tuning .delta select").val(FC.ADVANCED_TUNING.deltaMethod);

        $('.pid_tuning input[name="rc_rate_yaw"]').val(FC.RC_TUNING.rcYawRate.toFixed(2));
        $('.pid_filter input[name="gyroLowpassFrequency"]').val(FC.FILTER_CONFIG.gyro_lowpass_hz);
        $('.pid_filter input[name="dtermLowpassFrequency"]').val(FC.FILTER_CONFIG.dterm_lowpass_hz);
        $('.pid_filter input[name="yawLowpassFrequency"]').val(FC.FILTER_CONFIG.yaw_lowpass_hz);

        $("#pid-tuning .rate").text(i18n.getMessage("pidTuningSuperRate"));

        $('.pid_filter input[name="gyroNotch1Frequency"]').val(FC.FILTER_CONFIG.gyro_notch_hz);
        $('.pid_filter input[name="gyroNotch1Cutoff"]').val(FC.FILTER_CONFIG.gyro_notch_cutoff);
        $('.pid_filter input[name="dTermNotchFrequency"]').val(FC.FILTER_CONFIG.dterm_notch_hz);
        $('.pid_filter input[name="dTermNotchCutoff"]').val(FC.FILTER_CONFIG.dterm_notch_cutoff);

        $('input[name="dtermSetpointTransition-number"]').val(FC.ADVANCED_TUNING.dtermSetpointTransition / 100);

        $('input[name="dtermSetpoint-number"]').val(FC.ADVANCED_TUNING.dtermSetpointWeight / 100);

        $('.pid_filter input[name="gyroNotch2Frequency"]').val(FC.FILTER_CONFIG.gyro_notch2_hz);
        $('.pid_filter input[name="gyroNotch2Cutoff"]').val(FC.FILTER_CONFIG.gyro_notch2_cutoff);

        $('.pid_tuning input[name="angleLimit"]').val(FC.ADVANCED_TUNING.levelAngleLimit);

        $('.pid_tuning input[name="sensitivity"]').hide();
        $(".pid_tuning .levelSensitivityHeader").empty();

        const antiGravitySwitch = $("#antiGravitySwitch");
        const antiGravityGain = $('.antigravity input[name="itermAcceleratorGain"]');
        const levelAngleLimit = $('.antigravity input[name="angleLimit"]');

        $('.pid_filter select[name="dtermLowpassType"]').val(FC.FILTER_CONFIG.dterm_lowpass_type);

        const ITERM_ACCELERATOR_GAIN_OFF = 0;

        if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_45)) {
            // we keep the same name in html - just switching variable.
            antiGravityGain.val(FC.ADVANCED_TUNING.antiGravityGain / 10);
            antiGravitySwitch.prop("checked", FC.ADVANCED_TUNING.antiGravityGain !== ITERM_ACCELERATOR_GAIN_OFF);
        } else {
            $('.antigravity input[name="itermThrottleThreshold"]').val(FC.ADVANCED_TUNING.itermThrottleThreshold);
            antiGravityGain.val(FC.ADVANCED_TUNING.itermAcceleratorGain / 1000);
            antiGravitySwitch.prop("checked", FC.ADVANCED_TUNING.itermAcceleratorGain !== ITERM_ACCELERATOR_GAIN_OFF);
        }

        if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_45)) {
            antiGravityGain.attr({ min: "0.1", max: "25.0", step: "0.1" });
            levelAngleLimit.attr({ min: "0", max: "85", step: "1" });
        }

        if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_47)) {
            levelAngleLimit.attr({ min: "0", max: "80", step: "1" });
        }

        antiGravitySwitch.on("change", function () {
            if (antiGravitySwitch.is(":checked")) {
                if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_45)) {
                    antiGravityGain.val(Number.parseFloat(FC.ADVANCED_TUNING.antiGravityGain / 10 || 8).toFixed(1));
                } else {
                    const DEFAULT_ACCELERATOR_GAIN = 3.5;

                    if (FC.ADVANCED_TUNING.itermAcceleratorGain === ITERM_ACCELERATOR_GAIN_OFF) {
                        antiGravityGain.val(DEFAULT_ACCELERATOR_GAIN);
                    } else {
                        const itermAcceleratorGain = FC.ADVANCED_TUNING.itermAcceleratorGain / 1000;
                        antiGravityGain.val(itermAcceleratorGain);
                    }
                }

                $(".antigravity .suboption").show();
                $(".antigravity .antiGravityThres").toggle(
                    semver.lt(FC.CONFIG.apiVersion, API_VERSION_1_45) && FC.ADVANCED_TUNING.itermAcceleratorGain === 0,
                );
                $(".antigravity .antiGravityMode").toggle(semver.lt(FC.CONFIG.apiVersion, API_VERSION_1_45));
            } else {
                if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_45)) {
                    antiGravityGain.val(ITERM_ACCELERATOR_GAIN_OFF / 1000);
                } else {
                    $('.antigravity select[id="antiGravityMode"]').val(0);
                    antiGravityGain.val(ITERM_ACCELERATOR_GAIN_OFF);
                }

                $(".antigravity .suboption").hide();
            }
        });
        antiGravitySwitch.trigger("change");

        $('.pid_tuning input[name="rc_rate_pitch"]').val(FC.RC_TUNING.rcPitchRate.toFixed(2));
        $('.pid_tuning input[name="rc_pitch_expo"]').val(FC.RC_TUNING.RC_PITCH_EXPO.toFixed(2));

        $('.pid_filter input[name="gyroLowpass2Frequency"]').val(FC.FILTER_CONFIG.gyro_lowpass2_hz);
        $('.pid_filter select[name="gyroLowpassType"]').val(FC.FILTER_CONFIG.gyro_lowpass_type);
        $('.pid_filter select[name="gyroLowpass2Type"]').val(FC.FILTER_CONFIG.gyro_lowpass2_type);
        $('.pid_filter input[name="dtermLowpass2Frequency"]').val(FC.FILTER_CONFIG.dterm_lowpass2_hz);
        $('.pid_filter select[name="dtermLowpass2Type"]').val(FC.FILTER_CONFIG.dterm_lowpass2_type);

        // I Term Rotation
        $('input[id="itermrotation"]').prop("checked", FC.ADVANCED_TUNING.itermRotation !== 0);

        // Smart Feed Forward
        $('input[id="smartfeedforward"]').prop("checked", FC.ADVANCED_TUNING.smartFeedforward !== 0);

        // I Term Relax
        const itermRelaxCheck = $('input[id="itermrelax"]');

        itermRelaxCheck.prop("checked", FC.ADVANCED_TUNING.itermRelax !== 0);
        $('select[id="itermrelaxAxes"]').val(FC.ADVANCED_TUNING.itermRelax > 0 ? FC.ADVANCED_TUNING.itermRelax : 1);
        $('select[id="itermrelaxType"]').val(FC.ADVANCED_TUNING.itermRelaxType);
        $('input[name="itermRelaxCutoff"]').val(FC.ADVANCED_TUNING.itermRelaxCutoff);

        itermRelaxCheck.change(function () {
            const checked = $(this).is(":checked");

            if (checked) {
                $(".itermrelax .suboption").show();
                $(".itermRelaxCutoff").show();
            } else {
                $(".itermrelax .suboption").hide();
            }
        });
        itermRelaxCheck.change();

        // Absolute Control
        const absoluteControlGainNumberElement = $('input[name="absoluteControlGain-number"]');
        absoluteControlGainNumberElement.val(FC.ADVANCED_TUNING.absoluteControlGain).trigger("input");

        // Throttle Boost
        const throttleBoostNumberElement = $('input[name="throttleBoost-number"]');
        throttleBoostNumberElement.val(FC.ADVANCED_TUNING.throttleBoost).trigger("input");

        // Acro Trainer
        const acroTrainerAngleLimitNumberElement = $('input[name="acroTrainerAngleLimit-number"]');
        acroTrainerAngleLimitNumberElement.val(FC.ADVANCED_TUNING.acroTrainerAngleLimit).trigger("input");

        // Yaw D
        $('.pid_tuning .YAW input[name="d"]').val(FC.PIDS[2][2]); // PID Yaw D

        // Feedforward
        $('.pid_tuning .ROLL input[name="f"]').val(FC.ADVANCED_TUNING.feedforwardRoll);
        $('.pid_tuning .PITCH input[name="f"]').val(FC.ADVANCED_TUNING.feedforwardPitch);
        $('.pid_tuning .YAW input[name="f"]').val(FC.ADVANCED_TUNING.feedforwardYaw);

        const feedforwardTransitionNumberElement = $('input[name="feedforwardTransition-number"]');
        feedforwardTransitionNumberElement.val(
            Number.parseFloat(FC.ADVANCED_TUNING.feedforwardTransition / 100).toFixed(2),
        );

        if (semver.lt(FC.CONFIG.apiVersion, API_VERSION_1_45)) {
            // AntiGravity Mode
            const antiGravityModeSelect = $('.antigravity select[id="antiGravityMode"]');

            antiGravityModeSelect.on("change", function () {
                const antiGravityModeValue = antiGravityModeSelect.val();

                // Smooth removes threshold
                $(".antiGravityThres").toggle(antiGravityModeValue !== 0);
            });

            antiGravityModeSelect.val(FC.ADVANCED_TUNING.antiGravityMode).trigger("change");
        }

        $('select[id="throttleLimitType"]').val(FC.RC_TUNING.throttleLimitType);
        $('.throttle_limit input[name="throttleLimitPercent"]').val(FC.RC_TUNING.throttleLimitPercent);

        $('.pid_filter input[name="gyroLowpassDynMinFrequency"]').val(FC.FILTER_CONFIG.gyro_lowpass_dyn_min_hz);
        $('.pid_filter input[name="gyroLowpassDynMaxFrequency"]').val(FC.FILTER_CONFIG.gyro_lowpass_dyn_max_hz);
        $('.pid_filter select[name="gyroLowpassDynType"]').val(FC.FILTER_CONFIG.gyro_lowpass_type);

        $('.pid_filter input[name="dtermLowpassDynMinFrequency"]').val(FC.FILTER_CONFIG.dterm_lowpass_dyn_min_hz);
        $('.pid_filter input[name="dtermLowpassDynMaxFrequency"]').val(FC.FILTER_CONFIG.dterm_lowpass_dyn_max_hz);
        $('.pid_filter select[name="dtermLowpassDynType"]').val(FC.FILTER_CONFIG.dterm_lowpass_type);

        $('.pid_tuning input[name="dMaxRoll"]').val(FC.ADVANCED_TUNING.dMaxRoll);
        $('.pid_tuning input[name="dMaxPitch"]').val(FC.ADVANCED_TUNING.dMaxPitch);
        $('.pid_tuning input[name="dMaxYaw"]').val(FC.ADVANCED_TUNING.dMaxYaw);
        $('.dMaxGroup input[name="dMaxGain"]').val(FC.ADVANCED_TUNING.dMaxGain);
        $('.dMaxGroup input[name="dMaxAdvance"]').val(FC.ADVANCED_TUNING.dMaxAdvance);

        $('input[id="useIntegratedYaw"]').prop("checked", FC.ADVANCED_TUNING.useIntegratedYaw !== 0);

        $(".smartfeedforward").hide();

        // Dynamic Notch Filter
        const sampleRateHz = FC.CONFIG.sampleRateHz / FC.PID_ADVANCED_CONFIG.pid_process_denom;

        let isDynamicNotchActive = FC.FEATURE_CONFIG.features.isEnabled("DYNAMIC_FILTER");
        isDynamicNotchActive = isDynamicNotchActive || FC.FILTER_CONFIG.dyn_notch_count !== 0;
        isDynamicNotchActive = isDynamicNotchActive && sampleRateHz >= 2000;

        const dynamicNotchRange_e = $('.pid_filter select[name="dynamicNotchRange"]');
        const dynamicNotchWidthPercent_e = $('.pid_filter input[name="dynamicNotchWidthPercent"]');
        const dynamicNotchCount_e = $('.pid_filter input[name="dynamicNotchCount"]');
        const dynamicNotchQ_e = $('.pid_filter input[name="dynamicNotchQ"]');
        const dynamicNotchMinHz_e = $('.pid_filter input[name="dynamicNotchMinHz"]');
        const dynamicNotchMaxHz_e = $('.pid_filter input[name="dynamicNotchMaxHz"]');

        dynamicNotchRange_e.val(FC.FILTER_CONFIG.dyn_notch_range);
        dynamicNotchWidthPercent_e.val(FC.FILTER_CONFIG.dyn_notch_width_percent);
        dynamicNotchCount_e.val(FC.FILTER_CONFIG.dyn_notch_count);
        dynamicNotchQ_e.val(FC.FILTER_CONFIG.dyn_notch_q);
        dynamicNotchMinHz_e.val(FC.FILTER_CONFIG.dyn_notch_min_hz);
        dynamicNotchMaxHz_e.val(FC.FILTER_CONFIG.dyn_notch_max_hz);

        $('.pid_filter input[id="dynamicNotchEnabled"]')
            .on("change", function () {
                const count = parseInt(dynamicNotchCount_e.val());
                const checked = $(this).is(":checked");

                if (checked && !count) {
                    dynamicNotchCount_e.val(FILTER_DEFAULT.dyn_notch_count);
                }

                $(".dynamicNotch span.suboption").toggle(checked);
                $(".dynamicNotchMaxHz").toggle(checked);
                $(".dynamicNotchCount").toggle(checked);
            })
            .prop("checked", isDynamicNotchActive)
            .trigger("change");

        // RPM Filter
        $(".rpmFilter").toggle(FC.MOTOR_CONFIG.use_dshot_telemetry);

        const rpmFilterHarmonics_e = $('.pid_filter input[name="rpmFilterHarmonics"]');
        const rpmFilterMinHz_e = $('.pid_filter input[name="rpmFilterMinHz"]');

        rpmFilterHarmonics_e.val(FC.FILTER_CONFIG.gyro_rpm_notch_harmonics);
        rpmFilterMinHz_e.val(FC.FILTER_CONFIG.gyro_rpm_notch_min_hz);

        $(".pid_filter #rpmFilterEnabled")
            .on("change", function () {
                const harmonics = rpmFilterHarmonics_e.val();
                const checked = $(this).is(":checked") && harmonics !== 0;

                rpmFilterHarmonics_e.attr("disabled", !checked);
                rpmFilterMinHz_e.attr("disabled", !checked);
                self.previousFilterDynQ = FC.FILTER_CONFIG.dyn_notch_q;
                self.previousFilterDynCount = FC.FILTER_CONFIG.dyn_notch_count;

                if (harmonics == 0) {
                    rpmFilterHarmonics_e.val(FILTER_DEFAULT.gyro_rpm_notch_harmonics);
                }

                const dialogDynFilterSettings = {
                    title: i18n.getMessage("dialogDynFiltersChangeTitle"),
                    text: i18n.getMessage("dialogDynFiltersChangeNote"),
                    buttonYesText: i18n.getMessage("presetsWarningDialogYesButton"),
                    buttonNoText: i18n.getMessage("presetsWarningDialogNoButton"),
                    buttonYesCallback: () => _dynFilterChange(),
                    buttonNoCallback: null,
                };

                const _dynFilterChange = function () {
                    if (checked) {
                        dynamicNotchCount_e.val(FILTER_DEFAULT.dyn_notch_count_rpm);
                        dynamicNotchQ_e.val(FILTER_DEFAULT.dyn_notch_q_rpm);
                    } else {
                        dynamicNotchCount_e.val(FILTER_DEFAULT.dyn_notch_count);
                        dynamicNotchQ_e.val(FILTER_DEFAULT.dyn_notch_q);
                    }
                };

                if (checked !== (FC.FILTER_CONFIG.gyro_rpm_notch_harmonics !== 0)) {
                    GUI.showYesNoDialog(dialogDynFilterSettings);
                } else {
                    dynamicNotchCount_e.val(self.previousFilterDynCount);
                    dynamicNotchQ_e.val(self.previousFilterDynQ);
                }

                $(".rpmFilter span.suboption").toggle(checked);
            })
            .prop("checked", FC.FILTER_CONFIG.gyro_rpm_notch_harmonics !== 0)
            .trigger("change");

        if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_45)) {
            $('input[name="idleMinRpm-number"]').attr("max", 200);
        }

        $('.tab-pid_tuning input[name="motorLimit"]').val(FC.ADVANCED_TUNING.motorOutputLimit);
        $('.tab-pid_tuning select[name="cellCount"]').val(FC.ADVANCED_TUNING.autoProfileCellCount);
        $('input[name="idleMinRpm-number"]')
            .val(FC.ADVANCED_TUNING.idleMinRpm)
            .prop("disabled", !FC.MOTOR_CONFIG.use_dshot_telemetry);

        if (FC.MOTOR_CONFIG.use_dshot_telemetry) {
            $("span.pidTuningIdleMinRpmDisabled").text(i18n.getMessage("pidTuningIdleMinRpm"));
        } else {
            $("span.pidTuningIdleMinRpmDisabled").text(i18n.getMessage("pidTuningIdleMinRpmDisabled"));
        }

        const ratesTypeListElement = $('select[id="ratesType"]'); // generates list
        const ratesList = [
            { name: "Betaflight" },
            { name: "Raceflight" },
            { name: "KISS" },
            { name: "Actual" },
            { name: "QuickRates" },
        ];
        // add future rates types here with FC.CONFIG.apiVersion check
        for (let i = 0; i < ratesList.length; i++) {
            ratesTypeListElement.append(`<option value="${i}">${ratesList[i].name}</option>`);
        }

        ratesTypeListElement.sortSelect();

        self.currentRatesType = FC.RC_TUNING.rates_type;
        self.previousRatesType = null;
        ratesTypeListElement.val(self.currentRatesType);

        self.changeRatesType(self.currentRatesType); // update rate type code when updating the tab

        $('.pid_filter input[name="dtermLowpassExpo"]').val(FC.FILTER_CONFIG.dyn_lpf_curve_expo);

        // Feedforward
        $('select[id="feedforwardAveraging"]').val(FC.ADVANCED_TUNING.feedforward_averaging);
        $('input[name="feedforwardSmoothFactor"]').val(FC.ADVANCED_TUNING.feedforward_smooth_factor);
        $('input[name="feedforwardBoost"]').val(FC.ADVANCED_TUNING.feedforward_boost);
        $('input[name="feedforwardMaxRateLimit"]').val(FC.ADVANCED_TUNING.feedforward_max_rate_limit);
        $('input[name="feedforwardJitterFactor"]').val(FC.ADVANCED_TUNING.feedforward_jitter_factor);

        // Vbat Sag Compensation
        const enableVbatSagCompensation = FC.BATTERY_CONFIG.voltageMeterSource !== 1;
        const vbatSagCompensationCheck = $('input[id="vbatSagCompensation"]')
            .attr("disabled", enableVbatSagCompensation)
            .change();

        vbatSagCompensationCheck.prop("checked", FC.ADVANCED_TUNING.vbat_sag_compensation !== 0);
        $('input[name="vbatSagValue"]').val(
            FC.ADVANCED_TUNING.vbat_sag_compensation > 0 ? FC.ADVANCED_TUNING.vbat_sag_compensation : 100,
        );

        vbatSagCompensationCheck
            .change(function () {
                const checked = $(this).is(":checked");
                $(".vbatSagCompensation .suboption").toggle(checked);
            })
            .change();

        // Thrust Linearization
        const thrustLinearizationCheck = $('input[id="thrustLinearization"]');

        thrustLinearizationCheck.prop("checked", FC.ADVANCED_TUNING.thrustLinearization !== 0);
        $('input[name="thrustLinearValue"]').val(
            FC.ADVANCED_TUNING.thrustLinearization > 0 ? FC.ADVANCED_TUNING.thrustLinearization : 20,
        );

        thrustLinearizationCheck
            .change(function () {
                const checked = $(this).is(":checked");
                $(".thrustLinearization .suboption").toggle(checked);
            })
            .change();

        $('input[id="useIntegratedYaw"]')
            .change(function () {
                const checked = $(this).is(":checked");
                // 4.3 firmware has RP mode.
                $("#pidTuningIntegratedYawCaution").toggle(checked);
            })
            .change();

        // if user decreases Dmax, don't allow D above Dmax
        // if user increases D, don't allow Dmax below D
        function adjustDValues(dElement, dMaxElement) {
            const dValue = parseInt(dElement.val());
            const dMaxValue = parseInt(dMaxElement.val());
            const dMaxLimit = Math.min(Math.max(dValue, 0), 250);
            const dLimit = Math.min(Math.max(dMaxValue, 0), 250);
            if (dMaxValue > dMaxLimit) {
                dMaxElement.val(dMaxLimit);
            }
            if (dValue < dLimit) {
                dElement.val(dLimit);
            }
        }

        function setupDAdjustmentHandlers(axis) {
            const dMaxElement = $(`.pid_tuning input[name="dMax${axis}"]`);
            const dElement = $(`.pid_tuning .${axis} input[name="d"]`);

            dMaxElement
                .change(function () {
                    adjustDValues(dElement, dMaxElement);
                })
                .change();

            dElement
                .change(function () {
                    adjustDValues(dElement, dMaxElement);
                })
                .change();
        }

        ["Roll", "Pitch", "Yaw"].forEach((axis) => setupDAdjustmentHandlers(axis));

        $('input[id="gyroNotch1Enabled"]').change(function () {
            const checked = $(this).is(":checked");
            const hz =
                FC.FILTER_CONFIG.gyro_notch_hz > 0 ? FC.FILTER_CONFIG.gyro_notch_hz : FILTER_DEFAULT.gyro_notch_hz;

            $('.pid_filter input[name="gyroNotch1Frequency"]')
                .val(checked ? hz : 0)
                .attr("disabled", !checked)
                .attr("min", checked ? 1 : 0)
                .change();
            $('.pid_filter input[name="gyroNotch1Cutoff"]').attr("disabled", !checked).change();

            $(".gyroNotch1 span.suboption").toggle(checked);
        });

        $('input[id="gyroNotch2Enabled"]').change(function () {
            const checked = $(this).is(":checked");
            const hz =
                FC.FILTER_CONFIG.gyro_notch2_hz > 0 ? FC.FILTER_CONFIG.gyro_notch2_hz : FILTER_DEFAULT.gyro_notch2_hz;

            $('.pid_filter input[name="gyroNotch2Frequency"]')
                .val(checked ? hz : 0)
                .attr("disabled", !checked)
                .attr("min", checked ? 1 : 0)
                .change();
            $('.pid_filter input[name="gyroNotch2Cutoff"]').attr("disabled", !checked).change();

            $(".gyroNotch2 span.suboption").toggle(checked);
        });

        $('input[id="dtermNotchEnabled"]').change(function () {
            const checked = $(this).is(":checked");
            const hz =
                FC.FILTER_CONFIG.dterm_notch_hz > 0 ? FC.FILTER_CONFIG.dterm_notch_hz : FILTER_DEFAULT.dterm_notch_hz;

            $('.pid_filter input[name="dTermNotchFrequency"]')
                .val(checked ? hz : 0)
                .attr("disabled", !checked)
                .attr("min", checked ? 1 : 0)
                .change();
            $('.pid_filter input[name="dTermNotchCutoff"]').attr("disabled", !checked).change();

            $(".dtermNotch span.suboption").toggle(checked);
        });

        // gyro filter selectors
        const gyroLowpassDynMinFrequency = $('.pid_filter input[name="gyroLowpassDynMinFrequency"]');
        const gyroLowpassDynMaxFrequency = $('.pid_filter input[name="gyroLowpassDynMaxFrequency"]');
        const gyroLowpassFrequency = $('.pid_filter input[name="gyroLowpassFrequency"]');
        const gyroLowpass2Frequency = $('.pid_filter input[name="gyroLowpass2Frequency"]');
        const gyroLowpassEnabled = $('.pid_filter input[id="gyroLowpassEnabled"]');
        const gyroLowpass2Enabled = $('.pid_filter input[id="gyroLowpass2Enabled"]');

        const gyroLowpassOption = $(".gyroLowpass span.suboption");
        const gyroLowpassOptionStatic = $(".gyroLowpass span.suboption.static");
        const gyroLowpassOptionDynamic = $(".gyroLowpass span.suboption.dynamic");
        const gyroLowpass2Option = $(".gyroLowpass2 span.suboption");

        const gyroLowpassFilterMode = $('.pid_filter select[name="gyroLowpassFilterMode"]');

        // dterm filter selectors
        const dtermLowpassDynMinFrequency = $('.pid_filter input[name="dtermLowpassDynMinFrequency"]');
        const dtermLowpassDynMaxFrequency = $('.pid_filter input[name="dtermLowpassDynMaxFrequency"]');
        const dtermLowpassFrequency = $('.pid_filter input[name="dtermLowpassFrequency"]');
        const dtermLowpass2Frequency = $('.pid_filter input[name="dtermLowpass2Frequency"]');

        const dtermLowpassEnabled = $('input[id="dtermLowpassEnabled"]');
        const dtermLowpass2Enabled = $('input[id="dtermLowpass2Enabled"]');

        const dtermLowpassOption = $(".dtermLowpass span.suboption");
        const dtermLowpassOptionStatic = $(".dtermLowpass span.suboption.static");
        const dtermLowpassOptionDynamic = $(".dtermLowpass span.suboption.dynamic");
        const dtermLowpass2Option = $(".dtermLowpass2 span.suboption");

        const dtermLowpassFilterMode = $('.pid_filter select[name="dtermLowpassFilterMode"]');

        // firmware 4.3 filter selectors for lowpass 1 and 2; sliders are not yet initialized here
        gyroLowpassEnabled.change(function () {
            const checked = $(this).is(":checked");

            if (checked) {
                if (FC.FILTER_CONFIG.gyro_lowpass_dyn_min_hz > 0 || FC.FILTER_CONFIG.gyro_lowpass_hz > 0) {
                    // lowpass1 is enabled, set the master switch on, show the label, mode selector and type fields
                    gyroLowpassFilterMode.val(FC.FILTER_CONFIG.gyro_lowpass_dyn_min_hz > 0 ? 1 : 0).change();
                } else {
                    // lowpass 1 is disabled, set the master switch off, only show label
                    // user is trying to enable the lowpass filter, but it was off (both cutoffs are zero)
                    // initialise in dynamic mode with values at sliders, or use defaults
                    gyroLowpassFilterMode.val(1).change();
                }
            } else {
                // the user is disabling Lowpass 1 so set everything to zero
                gyroLowpassDynMinFrequency.val(0);
                gyroLowpassDynMaxFrequency.val(0);
                gyroLowpassFrequency.val(0);

                self.calculateNewGyroFilters();
            }

            gyroLowpassOption.toggle(checked);
            gyroLowpassOptionStatic.toggle(checked && FC.FILTER_CONFIG.gyro_lowpass_dyn_min_hz === 0);
            gyroLowpassOptionDynamic.toggle(checked && FC.FILTER_CONFIG.gyro_lowpass_dyn_min_hz !== 0);
        });

        gyroLowpassFilterMode.change(function () {
            const dynMode = parseInt($(this).val());

            const cutoff =
                FC.FILTER_CONFIG.gyro_lowpass_hz > 0
                    ? FC.FILTER_CONFIG.gyro_lowpass_hz
                    : FILTER_DEFAULT.gyro_lowpass_hz;
            const cutoffMin =
                FC.FILTER_CONFIG.gyro_lowpass_dyn_min_hz > 0
                    ? FC.FILTER_CONFIG.gyro_lowpass_dyn_min_hz
                    : FILTER_DEFAULT.gyro_lowpass_dyn_min_hz;
            const cutoffMax =
                FC.FILTER_CONFIG.gyro_lowpass_dyn_max_hz > 0
                    ? FC.FILTER_CONFIG.gyro_lowpass_dyn_max_hz
                    : FILTER_DEFAULT.gyro_lowpass_dyn_max_hz;

            gyroLowpassFrequency.val(dynMode ? 0 : cutoff);
            gyroLowpassDynMinFrequency.val(dynMode ? cutoffMin : 0);
            gyroLowpassDynMaxFrequency.val(dynMode ? cutoffMax : 0);

            self.calculateNewGyroFilters();

            gyroLowpassOptionStatic.toggle(!dynMode);
            gyroLowpassOptionDynamic.toggle(!!dynMode);
        });

        // switch gyro lpf2
        gyroLowpass2Enabled.change(function () {
            const checked = $(this).is(":checked");
            const cutoff =
                FC.FILTER_CONFIG.gyro_lowpass2_hz > 0
                    ? FC.FILTER_CONFIG.gyro_lowpass2_hz
                    : FILTER_DEFAULT.gyro_lowpass2_hz;

            gyroLowpass2Frequency.val(checked ? cutoff : 0).attr("disabled", !checked);

            self.calculateNewGyroFilters();

            gyroLowpass2Option.toggle(checked);
            self.updateFilterWarning();
        });

        dtermLowpassEnabled.change(function () {
            const checked = $(this).is(":checked");

            if (checked) {
                if (FC.FILTER_CONFIG.dterm_lowpass_dyn_min_hz > 0 || FC.FILTER_CONFIG.dterm_lowpass_hz > 0) {
                    // lowpass1 is enabled, set the master switch on, show the label, mode selector and type fields
                    dtermLowpassFilterMode.val(FC.FILTER_CONFIG.dterm_lowpass_dyn_min_hz > 0 ? 1 : 0).change();
                } else {
                    // lowpass 1 is disabled, set the master switch off, only show label
                    // user is trying to enable the lowpass filter, but it was off (both cutoffs are zero)
                    // initialise in dynamic mode with values at sliders, or use defaults
                    dtermLowpassFilterMode.val(1).change();
                }
            } else {
                // the user is disabling Lowpass 1 so set everything to zero
                dtermLowpassDynMinFrequency.val(0);
                dtermLowpassDynMaxFrequency.val(0);
                dtermLowpassFrequency.val(0);

                self.calculateNewDTermFilters();
            }

            dtermLowpassOption.toggle(checked);
            dtermLowpassOptionStatic.toggle(checked && FC.FILTER_CONFIG.dterm_lowpass_dyn_min_hz === 0);
            dtermLowpassOptionDynamic.toggle(checked && FC.FILTER_CONFIG.dterm_lowpass_dyn_min_hz !== 0);
        });

        dtermLowpassFilterMode.change(function () {
            const dynMode = parseInt($(this).val());

            const cutoff =
                FC.FILTER_CONFIG.dterm_lowpass_hz > 0
                    ? FC.FILTER_CONFIG.dterm_lowpass_hz
                    : FILTER_DEFAULT.dterm_lowpass_hz;
            const cutoffMin =
                FC.FILTER_CONFIG.dterm_lowpass_dyn_min_hz > 0
                    ? FC.FILTER_CONFIG.dterm_lowpass_dyn_min_hz
                    : FILTER_DEFAULT.dterm_lowpass_dyn_min_hz;
            const cutoffMax =
                FC.FILTER_CONFIG.dterm_lowpass_dyn_max_hz > 0
                    ? FC.FILTER_CONFIG.dterm_lowpass_dyn_max_hz
                    : FILTER_DEFAULT.dterm_lowpass_dyn_max_hz;

            dtermLowpassFrequency.val(dynMode ? 0 : cutoff);
            dtermLowpassDynMinFrequency.val(dynMode ? cutoffMin : 0);
            dtermLowpassDynMaxFrequency.val(dynMode ? cutoffMax : 0);

            self.calculateNewDTermFilters();

            dtermLowpassOptionStatic.toggle(!dynMode);
            dtermLowpassOptionDynamic.toggle(!!dynMode);
        });

        dtermLowpass2Enabled.change(function () {
            const checked = $(this).is(":checked");
            const cutoff =
                FC.FILTER_CONFIG.dterm_lowpass2_hz > 0
                    ? FC.FILTER_CONFIG.dterm_lowpass2_hz
                    : FILTER_DEFAULT.dterm_lowpass2_hz;

            dtermLowpass2Frequency.val(checked ? cutoff : 0).attr("disabled", !checked);

            self.calculateNewDTermFilters();

            dtermLowpass2Option.toggle(checked);
            self.updateFilterWarning();
        });

        $('input[id="yawLowpassEnabled"]').change(function () {
            const checked = $(this).is(":checked");
            const cutoff =
                FC.FILTER_CONFIG.yaw_lowpass_hz > 0 ? FC.FILTER_CONFIG.yaw_lowpass_hz : FILTER_DEFAULT.yaw_lowpass_hz;

            $('.pid_filter input[name="yawLowpassFrequency"]')
                .val(checked ? cutoff : 0)
                .attr("disabled", !checked);
            $(".yawLowpass span.suboption").toggle(checked);
        });

        // The notch cutoff must be smaller than the notch frecuency
        function adjustNotchCutoff(frequencyName, cutoffName) {
            const frecuency = parseInt($(`.pid_filter input[name='${frequencyName}']`).val());
            const cutoff = parseInt($(`.pid_filter input[name='${cutoffName}']`).val());

            // Change the max and refresh the value if needed
            const maxCutoff = frecuency == 0 ? 0 : frecuency - 1;
            $(`.pid_filter input[name='${cutoffName}']`).attr("max", maxCutoff);
            if (cutoff >= frecuency) {
                $(`.pid_filter input[name='${cutoffName}']`).val(maxCutoff);
            }
        }

        $('input[name="gyroNotch1Frequency"]')
            .change(function () {
                adjustNotchCutoff("gyroNotch1Frequency", "gyroNotch1Cutoff");
            })
            .change();

        $('input[name="gyroNotch2Frequency"]')
            .change(function () {
                adjustNotchCutoff("gyroNotch2Frequency", "gyroNotch2Cutoff");
            })
            .change();

        $('input[name="dTermNotchFrequency"]')
            .change(function () {
                adjustNotchCutoff("dTermNotchFrequency", "dTermNotchCutoff");
            })
            .change();

        // Initial state of the filters: enabled or disabled
        $('input[id="gyroNotch1Enabled"]')
            .prop("checked", FC.FILTER_CONFIG.gyro_notch_hz !== 0)
            .change();
        $('input[id="gyroNotch2Enabled"]')
            .prop("checked", FC.FILTER_CONFIG.gyro_notch2_hz !== 0)
            .change();
        $('input[id="dtermNotchEnabled"]')
            .prop("checked", FC.FILTER_CONFIG.dterm_notch_hz !== 0)
            .change();

        gyroLowpassEnabled
            .prop("checked", FC.FILTER_CONFIG.gyro_lowpass_hz !== 0 || FC.FILTER_CONFIG.gyro_lowpass_dyn_min_hz !== 0)
            .change();
        dtermLowpassEnabled
            .prop("checked", FC.FILTER_CONFIG.dterm_lowpass_hz !== 0 || FC.FILTER_CONFIG.dterm_lowpass_dyn_min_hz !== 0)
            .change();

        gyroLowpass2Enabled.prop("checked", FC.FILTER_CONFIG.gyro_lowpass2_hz !== 0).change();
        dtermLowpass2Enabled.prop("checked", FC.FILTER_CONFIG.dterm_lowpass2_hz !== 0).change();
        $('input[id="yawLowpassEnabled"]')
            .prop("checked", FC.FILTER_CONFIG.yaw_lowpass_hz !== 0)
            .change();

        self.updatePIDColors();
    }

    function form_to_pid_and_rc() {
        // Fill in the data from PIDs array
        // Catch all the changes and stuff the inside PIDs array

        // Profile names
        if (semver.gte(FC.CONFIG.apiVersion, "1.45.0")) {
            FC.CONFIG.pidProfileNames[FC.CONFIG.profile] = $('input[name="pidProfileName"]').val().trim();
            FC.CONFIG.rateProfileNames[FC.CONFIG.rateProfile] = $('input[name="rateProfileName"]').val().trim();
        }

        // For each pid name
        FC.PID_NAMES.forEach(function (elementPid, indexPid) {
            // Look into the PID table to a row with the name of the pid
            const searchRow = $(`.pid_tuning .${elementPid} input`);

            // Assign each value
            searchRow.each(function (indexInput) {
                if ($(this).val()) {
                    FC.PIDS[indexPid][indexInput] = parseInt($(this).val());
                }
            });
        });

        // catch RC_tuning changes
        const pitch_rate_e = $('.pid_tuning input[name="pitch_rate"]');
        const roll_rate_e = $('.pid_tuning input[name="roll_rate"]');
        const yaw_rate_e = $('.pid_tuning input[name="yaw_rate"]');
        const rc_rate_pitch_e = $('.pid_tuning input[name="rc_rate_pitch"]');
        const rc_rate_e = $('.pid_tuning input[name="rc_rate"]');
        const rc_rate_yaw_e = $('.pid_tuning input[name="rc_rate_yaw"]');
        const rc_pitch_expo_e = $('.pid_tuning input[name="rc_pitch_expo"]');
        const rc_expo_e = $('.pid_tuning input[name="rc_expo"]');
        const rc_yaw_expo_e = $('.pid_tuning input[name="rc_yaw_expo"]');

        FC.RC_TUNING.roll_pitch_rate = parseFloat($('.pid_tuning input[name="roll_pitch_rate"]').val());
        FC.RC_TUNING.RC_RATE = parseFloat(rc_rate_e.val());
        FC.RC_TUNING.roll_rate = parseFloat(roll_rate_e.val());
        FC.RC_TUNING.pitch_rate = parseFloat(pitch_rate_e.val());
        FC.RC_TUNING.yaw_rate = parseFloat(yaw_rate_e.val());
        FC.RC_TUNING.RC_EXPO = parseFloat(rc_expo_e.val());
        FC.RC_TUNING.RC_YAW_EXPO = parseFloat(rc_yaw_expo_e.val());
        FC.RC_TUNING.rcYawRate = parseFloat(rc_rate_yaw_e.val());
        FC.RC_TUNING.rcPitchRate = parseFloat(rc_rate_pitch_e.val());
        FC.RC_TUNING.RC_PITCH_EXPO = parseFloat(rc_pitch_expo_e.val());

        switch (self.currentRatesType) {
            case FC.RATES_TYPE.RACEFLIGHT:
                FC.RC_TUNING.pitch_rate = parseFloat(pitch_rate_e.val()) / 100;
                FC.RC_TUNING.roll_rate = parseFloat(roll_rate_e.val()) / 100;
                FC.RC_TUNING.yaw_rate = parseFloat(yaw_rate_e.val()) / 100;
                FC.RC_TUNING.rcPitchRate = parseFloat(rc_rate_pitch_e.val()) / 1000;
                FC.RC_TUNING.RC_RATE = parseFloat(rc_rate_e.val()) / 1000;
                FC.RC_TUNING.rcYawRate = parseFloat(rc_rate_yaw_e.val()) / 1000;
                FC.RC_TUNING.RC_PITCH_EXPO = parseFloat(rc_pitch_expo_e.val()) / 100;
                FC.RC_TUNING.RC_EXPO = parseFloat(rc_expo_e.val()) / 100;
                FC.RC_TUNING.RC_YAW_EXPO = parseFloat(rc_yaw_expo_e.val()) / 100;

                break;

            case FC.RATES_TYPE.ACTUAL:
                FC.RC_TUNING.pitch_rate = parseFloat(pitch_rate_e.val()) / 1000;
                FC.RC_TUNING.roll_rate = parseFloat(roll_rate_e.val()) / 1000;
                FC.RC_TUNING.yaw_rate = parseFloat(yaw_rate_e.val()) / 1000;
                FC.RC_TUNING.rcPitchRate = parseFloat(rc_rate_pitch_e.val()) / 1000;
                FC.RC_TUNING.RC_RATE = parseFloat(rc_rate_e.val()) / 1000;
                FC.RC_TUNING.rcYawRate = parseFloat(rc_rate_yaw_e.val()) / 1000;

                break;

            case FC.RATES_TYPE.QUICKRATES:
                FC.RC_TUNING.pitch_rate = parseFloat(pitch_rate_e.val()) / 1000;
                FC.RC_TUNING.roll_rate = parseFloat(roll_rate_e.val()) / 1000;
                FC.RC_TUNING.yaw_rate = parseFloat(yaw_rate_e.val()) / 1000;

                break;

            // add future rates types here
            default: // BetaFlight
                break;
        }

        FC.RC_TUNING.throttle_MID = parseFloat($('.throttle input[name="mid"]').val());
        FC.RC_TUNING.throttle_EXPO = parseFloat($('.throttle input[name="expo"]').val());
        FC.RC_TUNING.throttle_HOVER = parseFloat($('.throttle input[name="hover"]').val());

        if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_45)) {
            FC.ADVANCED_TUNING.tpaMode = $('select[id="tpaMode"]').val();
            FC.ADVANCED_TUNING.tpaRate = parseInt($('input[id="tpaRate"]').val()) / 100;
            FC.ADVANCED_TUNING.tpaBreakpoint = parseInt($('input[id="tpaBreakpoint"]').val());
        } else {
            FC.RC_TUNING.dynamic_THR_PID = parseFloat($('.tpa-old input[name="tpa"]').val());
            FC.RC_TUNING.dynamic_THR_breakpoint = parseInt($('.tpa-old input[name="tpa-breakpoint"]').val());
        }

        if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_46)) {
            $('input[id="tpaBreakpoint"]').attr({ max: 2000, min: 1000 });
        }

        FC.FILTER_CONFIG.gyro_lowpass_hz = parseInt($('.pid_filter input[name="gyroLowpassFrequency"]').val());
        FC.FILTER_CONFIG.dterm_lowpass_hz = parseInt($('.pid_filter input[name="dtermLowpassFrequency"]').val());
        FC.FILTER_CONFIG.yaw_lowpass_hz = parseInt($('.pid_filter input[name="yawLowpassFrequency"]').val());

        if (vbatpidcompensationIsUsed) {
            const element = $('input[id="vbatpidcompensation"]');
            const value = element.is(":checked") ? 1 : 0;
            let analyticsValue = undefined;
            if (value !== FC.ADVANCED_TUNING.vbatPidCompensation) {
                analyticsValue = element.is(":checked");
            }
            self.analyticsChanges["VbatPidCompensation"] = analyticsValue;

            FC.ADVANCED_TUNING.vbatPidCompensation = value;
        }

        FC.ADVANCED_TUNING.deltaMethod = $("#pid-tuning .delta select").val();

        FC.ADVANCED_TUNING.dtermSetpointTransition = parseInt(
            $('input[name="dtermSetpointTransition-number"]').val() * 100,
        );
        FC.ADVANCED_TUNING.dtermSetpointWeight = parseInt($('input[name="dtermSetpoint-number"]').val() * 100);

        FC.FILTER_CONFIG.gyro_notch_hz = parseInt($('.pid_filter input[name="gyroNotch1Frequency"]').val());
        FC.FILTER_CONFIG.gyro_notch_cutoff = parseInt($('.pid_filter input[name="gyroNotch1Cutoff"]').val());
        FC.FILTER_CONFIG.dterm_notch_hz = parseInt($('.pid_filter input[name="dTermNotchFrequency"]').val());
        FC.FILTER_CONFIG.dterm_notch_cutoff = parseInt($('.pid_filter input[name="dTermNotchCutoff"]').val());
        FC.FILTER_CONFIG.gyro_notch2_hz = parseInt($('.pid_filter input[name="gyroNotch2Frequency"]').val());
        FC.FILTER_CONFIG.gyro_notch2_cutoff = parseInt($('.pid_filter input[name="gyroNotch2Cutoff"]').val());

        FC.ADVANCED_TUNING.levelAngleLimit = parseInt($('.pid_tuning input[name="angleLimit"]').val());

        const antiGravityGain = $('.antigravity input[name="itermAcceleratorGain"]');

        FC.FILTER_CONFIG.dterm_lowpass_type = parseInt($('.pid_filter select[name="dtermLowpassType"]').val());
        if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_45)) {
            FC.ADVANCED_TUNING.antiGravityGain = parseInt(antiGravityGain.val() * 10);
        } else {
            FC.ADVANCED_TUNING.itermThrottleThreshold = parseInt(
                $('.antigravity input[name="itermThrottleThreshold"]').val(),
            );
            FC.ADVANCED_TUNING.itermAcceleratorGain = parseInt(antiGravityGain.val() * 1000);
        }

        FC.FILTER_CONFIG.gyro_lowpass2_hz = parseInt($('.pid_filter input[name="gyroLowpass2Frequency"]').val());
        FC.FILTER_CONFIG.gyro_lowpass_type = parseInt($('.pid_filter select[name="gyroLowpassType"]').val());
        FC.FILTER_CONFIG.gyro_lowpass2_type = parseInt($('.pid_filter select[name="gyroLowpass2Type"]').val());
        FC.FILTER_CONFIG.dterm_lowpass2_hz = parseInt($('.pid_filter input[name="dtermLowpass2Frequency"]').val());
        FC.FILTER_CONFIG.dterm_lowpass2_type = parseInt($('.pid_filter select[name="dtermLowpass2Type"]').val());

        FC.ADVANCED_TUNING.itermRotation = $('input[id="itermrotation"]').is(":checked") ? 1 : 0;
        FC.ADVANCED_TUNING.smartFeedforward = $('input[id="smartfeedforward"]').is(":checked") ? 1 : 0;

        FC.ADVANCED_TUNING.itermRelax = $('input[id="itermrelax"]').is(":checked")
            ? $('select[id="itermrelaxAxes"]').val()
            : 0;
        FC.ADVANCED_TUNING.itermRelaxType = $('select[id="itermrelaxType"]').val();
        FC.ADVANCED_TUNING.itermRelaxCutoff = parseInt($('input[name="itermRelaxCutoff"]').val());

        FC.ADVANCED_TUNING.absoluteControlGain = $('input[name="absoluteControlGain-number"]').val();

        FC.ADVANCED_TUNING.throttleBoost = $('input[name="throttleBoost-number"]').val();

        FC.ADVANCED_TUNING.acroTrainerAngleLimit = $('input[name="acroTrainerAngleLimit-number"]').val();

        FC.ADVANCED_TUNING.feedforwardRoll = parseInt($('.pid_tuning .ROLL input[name="f"]').val());
        FC.ADVANCED_TUNING.feedforwardPitch = parseInt($('.pid_tuning .PITCH input[name="f"]').val());
        FC.ADVANCED_TUNING.feedforwardYaw = parseInt($('.pid_tuning .YAW input[name="f"]').val());

        FC.ADVANCED_TUNING.feedforwardTransition = parseInt(
            $('input[name="feedforwardTransition-number"]').val() * 100,
        );

        FC.ADVANCED_TUNING.antiGravityMode = semver.lt(FC.CONFIG.apiVersion, API_VERSION_1_45)
            ? $('select[id="antiGravityMode"]').val()
            : 0;

        FC.RC_TUNING.throttleLimitType = $('select[id="throttleLimitType"]').val();
        FC.RC_TUNING.throttleLimitPercent = parseInt($('.throttle_limit input[name="throttleLimitPercent"]').val());

        FC.FILTER_CONFIG.gyro_lowpass_dyn_min_hz = parseInt(
            $('.pid_filter input[name="gyroLowpassDynMinFrequency"]').val(),
        );
        FC.FILTER_CONFIG.gyro_lowpass_dyn_max_hz = parseInt(
            $('.pid_filter input[name="gyroLowpassDynMaxFrequency"]').val(),
        );
        FC.FILTER_CONFIG.dterm_lowpass_dyn_min_hz = parseInt(
            $('.pid_filter input[name="dtermLowpassDynMinFrequency"]').val(),
        );
        FC.FILTER_CONFIG.dterm_lowpass_dyn_max_hz = parseInt(
            $('.pid_filter input[name="dtermLowpassDynMaxFrequency"]').val(),
        );

        FC.ADVANCED_TUNING.dMaxRoll = parseInt($('.pid_tuning input[name="dMaxRoll"]').val());
        FC.ADVANCED_TUNING.dMaxPitch = parseInt($('.pid_tuning input[name="dMaxPitch"]').val());
        FC.ADVANCED_TUNING.dMaxYaw = parseInt($('.pid_tuning input[name="dMaxYaw"]').val());
        FC.ADVANCED_TUNING.dMaxGain = parseInt($('.dMaxGroup input[name="dMaxGain"]').val());
        FC.ADVANCED_TUNING.dMaxAdvance = parseInt($('.dMaxGroup input[name="dMaxAdvance"]').val());

        FC.ADVANCED_TUNING.useIntegratedYaw = $('input[id="useIntegratedYaw"]').is(":checked") ? 1 : 0;

        FC.FILTER_CONFIG.dyn_notch_range = parseInt($('.pid_filter select[name="dynamicNotchRange"]').val());
        FC.FILTER_CONFIG.dyn_notch_width_percent = parseInt(
            $('.pid_filter input[name="dynamicNotchWidthPercent"]').val(),
        );
        FC.FILTER_CONFIG.dyn_notch_q = parseInt($('.pid_filter input[name="dynamicNotchQ"]').val());
        FC.FILTER_CONFIG.dyn_notch_min_hz = parseInt($('.pid_filter input[name="dynamicNotchMinHz"]').val());

        const rpmFilterEnabled = $(".pid_filter #rpmFilterEnabled").is(":checked");
        FC.FILTER_CONFIG.gyro_rpm_notch_harmonics = rpmFilterEnabled
            ? parseInt($('.pid_filter input[name="rpmFilterHarmonics"]').val())
            : 0;
        FC.FILTER_CONFIG.gyro_rpm_notch_min_hz = parseInt($('.pid_filter input[name="rpmFilterMinHz"]').val());

        FC.FILTER_CONFIG.dyn_notch_max_hz = parseInt($('.pid_filter input[name="dynamicNotchMaxHz"]').val());
        FC.ADVANCED_TUNING.motorOutputLimit = parseInt($('.tab-pid_tuning input[name="motorLimit"]').val());
        FC.ADVANCED_TUNING.autoProfileCellCount = parseInt($('.tab-pid_tuning select[name="cellCount"]').val());
        FC.ADVANCED_TUNING.idleMinRpm = parseInt($('input[name="idleMinRpm-number"]').val());

        const selectedRatesType = $('select[id="ratesType"]').val(); // send analytics for rates type
        let selectedRatesTypeName = undefined;
        if (selectedRatesType !== FC.RC_TUNING.rates_type) {
            selectedRatesTypeName = $('select[id="ratesType"]').find("option:selected").text();
        }
        self.analyticsChanges["RatesType"] = selectedRatesTypeName;

        FC.RC_TUNING.rates_type = selectedRatesType;

        FC.ADVANCED_TUNING.feedforward_averaging = $('select[id="feedforwardAveraging"]').val();
        FC.ADVANCED_TUNING.feedforward_smooth_factor = parseInt($('input[name="feedforwardSmoothFactor"]').val());
        FC.ADVANCED_TUNING.feedforward_boost = parseInt($('input[name="feedforwardBoost"]').val());
        FC.ADVANCED_TUNING.feedforward_max_rate_limit = parseInt($('input[name="feedforwardMaxRateLimit"]').val());
        FC.ADVANCED_TUNING.feedforward_jitter_factor = parseInt($('input[name="feedforwardJitterFactor"]').val());
        FC.FILTER_CONFIG.dyn_lpf_curve_expo = parseInt($('.pid_filter input[name="dtermLowpassDynExpo"]').val());
        FC.ADVANCED_TUNING.vbat_sag_compensation = $('input[id="vbatSagCompensation"]').is(":checked")
            ? parseInt($('input[name="vbatSagValue"]').val())
            : 0;
        FC.ADVANCED_TUNING.thrustLinearization = $('input[id="thrustLinearization"]').is(":checked")
            ? parseInt($('input[name="thrustLinearValue"]').val())
            : 0;
        FC.FILTER_CONFIG.dyn_lpf_curve_expo = parseInt($('.pid_filter input[name="dtermLowpassExpo"]').val());

        const dynamicNotchEnabled = $('.pid_filter input[id="dynamicNotchEnabled"]').is(":checked");
        FC.FILTER_CONFIG.dyn_notch_count = dynamicNotchEnabled
            ? parseInt($('.pid_filter input[name="dynamicNotchCount"]').val())
            : 0;

        FC.TUNING_SLIDERS.slider_pids_mode = TuningSliders.sliderPidsMode;
        //round slider values to nearest multiple of 5 and passes to the FW. Avoid dividing calc by (* x 100)/5 = 20
        FC.TUNING_SLIDERS.slider_master_multiplier = Math.round(TuningSliders.sliderMasterMultiplier * 20) * 5;
        FC.TUNING_SLIDERS.slider_d_gain = Math.round(TuningSliders.sliderDGain * 20) * 5;
        FC.TUNING_SLIDERS.slider_pi_gain = Math.round(TuningSliders.sliderPIGain * 20) * 5;
        FC.TUNING_SLIDERS.slider_feedforward_gain = Math.round(TuningSliders.sliderFeedforwardGain * 20) * 5;
        FC.TUNING_SLIDERS.slider_i_gain = Math.round(TuningSliders.sliderIGain * 20) * 5;
        FC.TUNING_SLIDERS.slider_dmax_gain = Math.round(TuningSliders.sliderDMaxGain * 20) * 5;
        FC.TUNING_SLIDERS.slider_roll_pitch_ratio = Math.round(TuningSliders.sliderRollPitchRatio * 20) * 5;
        FC.TUNING_SLIDERS.slider_pitch_pi_gain = Math.round(TuningSliders.sliderPitchPIGain * 20) * 5;

        FC.TUNING_SLIDERS.slider_dterm_filter = TuningSliders.sliderDTermFilter;
        FC.TUNING_SLIDERS.slider_dterm_filter_multiplier =
            Math.round(TuningSliders.sliderDTermFilterMultiplier * 20) * 5;
        FC.TUNING_SLIDERS.slider_gyro_filter = TuningSliders.sliderGyroFilter;
        FC.TUNING_SLIDERS.slider_gyro_filter_multiplier = Math.round(TuningSliders.sliderGyroFilterMultiplier * 20) * 5;
    }

    function showAllPids() {
        // Hide all optional elements
        $(".pid_optional tr").hide(); // Hide all rows
        $(".pid_optional table").hide(); // Hide tables
        $(".pid_optional").hide(); // Hide general div

        // Only show rows supported by the firmware
        FC.PID_NAMES.forEach(function (elementPid) {
            // Show rows for the PID
            $(`.pid_tuning .${elementPid}`).show();

            // Show titles and other elements needed by the PID
            $(`.needed_by_${elementPid}`).show();
        });
    }

    function hideUnusedPids() {
        if (!have_sensor(FC.CONFIG.activeSensors, "acc")) {
            $("#pid_accel").hide();
            $(".acroTrainerAngleLimit").hide();
        }

        // Hide all optional elements
        $("#pid_baro_mag_gps").hide();
    }

    function drawAxes(curveContext, width, height) {
        curveContext.strokeStyle = "#888888";
        curveContext.lineWidth = 4;

        // Horizontal
        curveContext.beginPath();
        curveContext.moveTo(0, height / 2);
        curveContext.lineTo(width, height / 2);
        curveContext.stroke();

        // Vertical
        curveContext.beginPath();
        curveContext.moveTo(width / 2, 0);
        curveContext.lineTo(width / 2, height);
        curveContext.stroke();
    }

    function checkInput(element) {
        let value = parseFloat(element.val());
        if (value < parseFloat(element.prop("min")) || value > parseFloat(element.prop("max"))) {
            value = undefined;
        }

        return value;
    }

    const useLegacyCurve = false;

    self.rateCurve = new RateCurve(useLegacyCurve);

    $('.pid_tuning input[name="sensitivity"]').hide();
    $(".pid_tuning .levelSensitivityHeader").empty();

    function printMaxAngularVel(rate, rcRate, rcExpo, useSuperExpo, deadband, limit, maxAngularVelElement) {
        const maxAngularVel = self.rateCurve
            .getMaxAngularVel(rate, rcRate, rcExpo, useSuperExpo, deadband, limit)
            .toFixed(0);
        maxAngularVelElement.text(maxAngularVel);

        return maxAngularVel;
    }

    function drawCurve(rate, rcRate, rcExpo, useSuperExpo, deadband, limit, maxAngularVel, colour, yOffset, context) {
        context.save();
        context.strokeStyle = colour;
        context.translate(0, yOffset);
        self.rateCurve.draw(rate, rcRate, rcExpo, useSuperExpo, deadband, limit, maxAngularVel, context);
        context.restore();
    }

    function process_html() {
        TABS.pid_tuning.isHtmlProcessing = true;
        FC.FEATURE_CONFIG.features.generateElements($(".tab-pid_tuning .features"));

        $(".tab-pid_tuning .pidTuningSuperexpoRates").hide();

        if (semver.lt(FC.CONFIG.apiVersion, API_VERSION_1_47)) {
            const derivativeTip = document.querySelector(".derivative .cf_tip");
            const dMaxTip = document.querySelector(".dmax .cf_tip");

            ["i18n", "i18n_title"].forEach((attr) => {
                const tmp = derivativeTip.getAttribute(attr);
                derivativeTip.setAttribute(attr, dMaxTip.getAttribute(attr));
                dMaxTip.setAttribute(attr, tmp);
            });
        }

        // translate to user-selected language
        i18n.localizePage();

        self.currentRates = self.rateCurve.getCurrentRates();

        $(".tab-pid_tuning .tab-container .pid").on("click", () => activateSubtab("pid"));

        $(".tab-pid_tuning .tab-container .rates").on("click", () => activateSubtab("rates"));

        $(".tab-pid_tuning .tab-container .filter").on("click", () => activateSubtab("filter"));

        function loadProfilesList() {
            const numberOfProfiles = FC.CONFIG.numProfiles;

            const profileElements = [];
            for (let i = 0; i < numberOfProfiles; i++) {
                profileElements.push(i18n.getMessage("pidTuningProfileOption", [i + 1]));
            }
            return profileElements;
        }

        function loadRateProfilesList() {
            let numberOfRateProfiles = 6;

            if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_45)) {
                numberOfRateProfiles = 4;
            }

            if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_47)) {
                numberOfRateProfiles = FC.CONFIG.numberOfRateProfiles;
            }

            const rateProfileElements = [];
            for (let i = 0; i < numberOfRateProfiles; i++) {
                rateProfileElements.push(i18n.getMessage("pidTuningRateProfileOption", [i + 1]));
            }
            return rateProfileElements;
        }

        // This vars are used here for populate the profile (and rate profile) selector AND in the copy profile (and rate profile) window
        const selectRateProfileValues = loadRateProfilesList();
        const selectProfileValues = loadProfilesList();

        function populateProfilesSelector(_selectProfileValues) {
            const profileSelect = $('select[name="profile"]');
            _selectProfileValues.forEach(function (value, key) {
                profileSelect.append(`<option value="${key}">${value}</option>`);
            });
        }

        populateProfilesSelector(selectProfileValues);

        function populateRateProfilesSelector(_selectRateProfileValues) {
            const rateProfileSelect = $('select[name="rate_profile"]');
            _selectRateProfileValues.forEach(function (value, key) {
                rateProfileSelect.append(`<option value="${key}">${value}</option>`);
            });
        }

        populateRateProfilesSelector(selectRateProfileValues);

        const showAllButton = $("#showAllPids");

        function updatePidDisplay() {
            if (!self.showAllPids) {
                hideUnusedPids();

                showAllButton.text(i18n.getMessage("pidTuningShowAllPids"));
            } else {
                showAllPids();

                showAllButton.text(i18n.getMessage("pidTuningHideUnusedPids"));
            }
        }

        showAllPids();
        updatePidDisplay();

        showAllButton.on("click", function () {
            self.showAllPids = !self.showAllPids;

            updatePidDisplay();
        });

        $("#resetPidProfile").on("click", function () {
            self.updating = true;

            MSP.promise(MSPCodes.MSP_SET_RESET_CURR_PID).then(function () {
                self.refresh(function () {
                    self.updating = false;

                    gui_log(i18n.getMessage("pidTuningPidProfileReset"));
                });
            });
        });

        $('.tab-pid_tuning select[name="profile"]').change(function () {
            self.currentProfile = parseInt($(this).val());
            self.updating = true;
            $(this).prop("disabled", "true");
            MSP.promise(MSPCodes.MSP_SELECT_SETTING, [self.currentProfile]).then(function () {
                self.refresh(function () {
                    self.updating = false;

                    $('.tab-pid_tuning select[name="profile"]').prop("disabled", "false");
                    FC.CONFIG.profile = self.currentProfile;

                    gui_log(i18n.getMessage("pidTuningLoadedProfile", [self.currentProfile + 1]));
                });
            });
        });

        $('.tab-pid_tuning select[name="rate_profile"]').change(function () {
            self.currentRateProfile = parseInt($(this).val());
            self.updating = true;
            $(this).prop("disabled", "true");
            MSP.promise(MSPCodes.MSP_SELECT_SETTING, [self.currentRateProfile + self.RATE_PROFILE_MASK]).then(
                function () {
                    self.refresh(function () {
                        self.updating = false;

                        $('.tab-pid_tuning select[name="rate_profile"]').prop("disabled", "false");
                        FC.CONFIG.rateProfile = self.currentRateProfile;
                        self.currentRates = self.rateCurve.getCurrentRates();

                        gui_log(i18n.getMessage("pidTuningLoadedRateProfile", [self.currentRateProfile + 1]));
                    });
                },
            );
        });

        const dtermTransitionNumberElement = $('input[name="dtermSetpointTransition-number"]');
        const dtermTransitionWarningElement = $("#pid-tuning .dtermSetpointTransitionWarning");

        function checkUpdateDtermTransitionWarning(value) {
            if (value > 0 && value < 0.1) {
                dtermTransitionWarningElement.show();
            } else {
                dtermTransitionWarningElement.hide();
            }
        }
        checkUpdateDtermTransitionWarning(dtermTransitionNumberElement.val());

        //Use 'input' event for coupled controls to allow synchronized update
        dtermTransitionNumberElement.on("input", function () {
            checkUpdateDtermTransitionWarning($(this).val());
        });

        $("#pid-tuning .delta").hide();
        $(".tab-pid_tuning .note").hide();

        // Add a name to each row of PIDs if empty
        $(".pid_tuning tr").each(function () {
            for (const pidName of FC.PID_NAMES) {
                if ($(this).hasClass(pidName)) {
                    const firstColumn = $(this).find("td:first");
                    if (!firstColumn.text()) {
                        firstColumn.text(pidName);
                    }
                }
            }
        });

        // DTerm filter options
        function loadFilterTypeValues() {
            const filterTypeValues = [];
            filterTypeValues.push("PT1");
            filterTypeValues.push("BIQUAD");
            filterTypeValues.push("PT2");
            filterTypeValues.push("PT3");

            return filterTypeValues;
        }

        function populateFilterTypeSelector(name, selectDtermValues) {
            const dtermFilterSelect = $(`select[name="${name}"]`);
            selectDtermValues.forEach(function (value, key) {
                dtermFilterSelect.append(`<option value="${key}">${value}</option>`);
            });
        }
        // Added in API 1.42.0
        function loadDynamicNotchRangeValues() {
            return ["HIGH", "MEDIUM", "LOW", "AUTO"];
        }
        function populateDynamicNotchRangeSelect(selectDynamicNotchRangeValues) {
            const dynamicNotchRangeSelect = $('select[name="dynamicNotchRange"]');
            selectDynamicNotchRangeValues.forEach(function (value, key) {
                dynamicNotchRangeSelect.append(`<option value="${key}">${value}</option>`);
            });
        }

        populateDynamicNotchRangeSelect(loadDynamicNotchRangeValues());

        populateFilterTypeSelector("gyroLowpassType", loadFilterTypeValues());
        populateFilterTypeSelector("gyroLowpass2Type", loadFilterTypeValues());
        populateFilterTypeSelector("dtermLowpassType", loadFilterTypeValues());
        populateFilterTypeSelector("dtermLowpass2Type", loadFilterTypeValues());

        pid_and_rc_to_form();

        function activateSubtab(subtabName) {
            const names = ["pid", "rates", "filter"];
            if (!names.includes(subtabName)) {
                console.debug(`Invalid subtab name: "${subtabName}"`);
                return;
            }
            for (const tabname of names) {
                const el = $(`.tab-pid_tuning .subtab-${tabname}`);
                el[tabname === subtabName ? "show" : "hide"]();
            }
            $(".tab-pid_tuning .tab-container .tab").removeClass("active");
            $(`.tab-pid_tuning .tab-container .${subtabName}`).addClass("active");
            self.activeSubtab = subtabName;
            if (subtabName === "rates") {
                // force drawing of throttle curve once the throttle curve container element is available
                // deferring drawing like this is needed to acquire the exact pixel size of the canvas
                redrawThrottleCurve(true);
                self.throttleDrawInterval = setInterval(redrawThrottleCurve, 100);
            } else if (self.throttleDrawInterval) {
                clearInterval(self.throttleDrawInterval);
                self.throttleDrawInterval = null;
            }
        }

        activateSubtab(self.activeSubtab);

        $(".tab-pid_tuning div.controller").hide();

        self.updatePidControllerParameters();

        $(".pid_tuning .roll_pitch_rate").hide();

        if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_45)) {
            $(".tpa-old").hide();
        } else {
            $(".tpa").hide();
        }

        $(".pid_tuning .bracket").hide();
        $(".pid_tuning input[name=rc_rate]").parent().attr("class", "pid_data");
        $(".pid_tuning input[name=rc_rate]").parent().attr("rowspan", 1);
        $(".pid_tuning input[name=rc_expo]").parent().attr("class", "pid_data");
        $(".pid_tuning input[name=rc_expo]").parent().attr("rowspan", 1);

        if (useLegacyCurve) {
            $(".new_rates").hide();
        }

        // Getting the DOM elements for curve display
        const rcCurveElement = $(".rate_curve canvas#rate_curve_layer0").get(0);
        const curveContext = rcCurveElement.getContext("2d");
        let updateNeeded = true;
        let maxAngularVel;

        // make these variables global scope so that they can be accessed by the updateRates function.
        self.maxAngularVelRollElement = $(".pid_tuning .maxAngularVelRoll");
        self.maxAngularVelPitchElement = $(".pid_tuning .maxAngularVelPitch");
        self.maxAngularVelYawElement = $(".pid_tuning .maxAngularVelYaw");

        // Only used with Betaflight Rates
        self.acroCenterSensitivityRollElement = $(".pid_tuning .acroCenterSensitivityRoll");
        self.acroCenterSensitivityPitchElement = $(".pid_tuning .acroCenterSensitivityPitch");
        self.acroCenterSensitivityYawElement = $(".pid_tuning .acroCenterSensitivityYaw");

        rcCurveElement.width = 1000;
        rcCurveElement.height = 1000;

        function updateRates(event) {
            setTimeout(function () {
                // let global validation trigger and adjust the values first
                if (event) {
                    // if an event is passed, then use it
                    const targetElement = $(event.target);
                    let targetValue = checkInput(targetElement);

                    if (self.currentRates.hasOwnProperty(targetElement.attr("name")) && targetValue !== undefined) {
                        const stepValue = parseFloat(targetElement.prop("step")); // adjust value to match step (change only the result, not the the actual value)
                        if (stepValue != null) {
                            targetValue = Math.round(targetValue / stepValue) * stepValue;
                        }

                        self.currentRates[targetElement.attr("name")] = targetValue;

                        updateNeeded = true;
                    }

                    if (targetElement.attr("name") === "SUPEREXPO_RATES") {
                        self.currentRates.superexpo = targetElement.is(":checked");

                        updateNeeded = true;
                    }

                    if (targetElement.attr("id") === "ratesType") {
                        self.changeRatesType(targetValue);

                        updateNeeded = true;
                    }
                } else {
                    // no event was passed, just force a graph update
                    updateNeeded = true;
                }
                if (updateNeeded) {
                    const curveHeight = rcCurveElement.height;
                    const curveWidth = rcCurveElement.width;
                    const lineScale = curveContext.canvas.width / curveContext.canvas.clientWidth;

                    curveContext.clearRect(0, 0, curveWidth, curveHeight);

                    if (!useLegacyCurve) {
                        maxAngularVel = Math.max(
                            printMaxAngularVel(
                                self.currentRates.roll_rate,
                                self.currentRates.rc_rate,
                                self.currentRates.rc_expo,
                                self.currentRates.superexpo,
                                self.currentRates.deadband,
                                self.currentRates.roll_rate_limit,
                                self.maxAngularVelRollElement,
                            ),
                            printMaxAngularVel(
                                self.currentRates.pitch_rate,
                                self.currentRates.rc_rate_pitch,
                                self.currentRates.rc_pitch_expo,
                                self.currentRates.superexpo,
                                self.currentRates.deadband,
                                self.currentRates.pitch_rate_limit,
                                self.maxAngularVelPitchElement,
                            ),
                            printMaxAngularVel(
                                self.currentRates.yaw_rate,
                                self.currentRates.rc_rate_yaw,
                                self.currentRates.rc_yaw_expo,
                                self.currentRates.superexpo,
                                self.currentRates.yawDeadband,
                                self.currentRates.yaw_rate_limit,
                                self.maxAngularVelYawElement,
                            ),
                        );

                        // make maxAngularVel multiple of 200deg/s so that the auto-scale doesn't keep changing for small changes of the maximum curve
                        maxAngularVel = self.rateCurve.setMaxAngularVel(maxAngularVel);

                        drawAxes(curveContext, curveWidth, curveHeight);
                    } else {
                        maxAngularVel = 0;
                    }

                    curveContext.lineWidth = 2 * lineScale;
                    drawCurve(
                        self.currentRates.roll_rate,
                        self.currentRates.rc_rate,
                        self.currentRates.rc_expo,
                        self.currentRates.superexpo,
                        self.currentRates.deadband,
                        self.currentRates.roll_rate_limit,
                        maxAngularVel,
                        "#ff0000",
                        0,
                        curveContext,
                    );
                    drawCurve(
                        self.currentRates.pitch_rate,
                        self.currentRates.rc_rate_pitch,
                        self.currentRates.rc_pitch_expo,
                        self.currentRates.superexpo,
                        self.currentRates.deadband,
                        self.currentRates.pitch_rate_limit,
                        maxAngularVel,
                        "#00ff00",
                        -4,
                        curveContext,
                    );
                    drawCurve(
                        self.currentRates.yaw_rate,
                        self.currentRates.rc_rate_yaw,
                        self.currentRates.rc_yaw_expo,
                        self.currentRates.superexpo,
                        self.currentRates.yawDeadband,
                        self.currentRates.yaw_rate_limit,
                        maxAngularVel,
                        "#0000ff",
                        4,
                        curveContext,
                    );

                    self.updateRatesLabels();

                    updateNeeded = false;
                }
            }, 0);
        }

        // UI Hooks
        // curves
        $("input.feature").on("input change", function () {
            const element = $(this);

            FC.FEATURE_CONFIG.features.updateData(element);

            updateRates();
        });

        $(".pid_tuning").on("input change", updateRates).trigger("input");

        function redrawThrottleCurve(forced = false) {
            if (!forced && !self.checkThrottle()) {
                return;
            }

            /*
            Quadratic curve formula taken from:
                https://stackoverflow.com/a/9195706/176210
            */

            function getQBezierValue(t, p1, p2, p3) {
                const iT = 1 - t;
                return iT * iT * p1 + 2 * iT * t * p2 + t * t * p3;
            }

            function getQuadraticCurvePoint(startX, startY, cpX, cpY, endX, endY, position) {
                return {
                    x: getQBezierValue(position, startX, cpX, endX),
                    y: getQBezierValue(position, startY, cpY, endY),
                };
            }

            /*
            Maths from: https://stackoverflow.com/questions/40918569/quadratic-bezier-curve-calculate-x-for-any-given-y
            Finds the 't' parameter (position) for a given 'y' value on a quadratic Bezier curve defined by y-coordinates (startY, cpY, endY).
            */
            function getTfromYBezier(y, startY, cpY, endY) {
                // Equation: y = (1-t)^2 * startY + 2 * (1-t)*t*cpY + t^2 * endY
                // Rearranged into quadratic form At^2 + Bt + C = 0 where t is the variable:
                // A = startY - 2*cpY + endY
                // B = 2 * (cpY - startY)
                // C = startY - y
                const A = startY - 2 * cpY + endY;
                const B = 2 * (cpY - startY);
                const C = startY - y;

                // Handle near-linear case (A is very small)
                if (Math.abs(A) < 1e-6) {
                    if (Math.abs(B) < 1e-6) {
                        // Should not happen for a valid curve unless startY=cpY=endY
                        return 0; // Or handle as error/edge case
                    }
                    // Linear equation: Bt + C = 0 => t = -C / B
                    return -C / B;
                }

                // Solve quadratic equation: t = [-B ± sqrt(B^2 - 4AC)] / 2A
                const disc = B * B - 4 * A * C;
                if (disc < 0) {
                    // No real solution (y is outside the curve's range) - return nearest valid t (0 or 1)
                    return Math.abs(y - startY) < Math.abs(y - endY) ? 0 : 1;
                }

                const t1 = (-B + Math.sqrt(disc)) / (2 * A);
                const t2 = (-B - Math.sqrt(disc)) / (2 * A);

                // We need the solution for t that is within the valid range [0, 1]
                if (t1 >= 0 && t1 <= 1) {
                    if (t2 >= 0 && t2 <= 1) {
                        // Both roots are valid, typically t1 is preferred for monotonic curves.
                        return t1;
                    }
                    return t1; // t1 is valid, t2 is not
                } else if (t2 >= 0 && t2 <= 1) {
                    return t2; // t2 is valid, t1 is not
                } else {
                    // Neither solution is valid
                    return Math.abs(y - startY) < Math.abs(y - endY) ? 0 : 1; // Return closest boundary
                }
            }

            // helper: invert x(t) for a quadratic Bézier to find t from x
            // x(t) = (1–t)² x0 + 2(1–t)t cx + t² x1  ⇒  a t² + b t + c = 0
            function getTfromXBezier(x, x0, cx, x1) {
                const a = x0 + x1 - 2 * cx;
                const b = 2 * (cx - x0);
                const c = x0 - x;
                if (Math.abs(a) < 1e-6) {
                    // linear case
                    if (Math.abs(b) < 1e-6) return 0; // Avoid division by zero if x0=cx=x1
                    return -c / b;
                }
                const disc = b * b - 4 * a * c;
                if (disc < 0) return 0; // No real solution, return start
                const t1 = (-b + Math.sqrt(disc)) / (2 * a);
                const t2 = (-b - Math.sqrt(disc)) / (2 * a);

                // pick the root in [0,1]
                const t1_valid = !isNaN(t1) && 0 <= t1 && t1 <= 1;
                const t2_valid = !isNaN(t2) && 0 <= t2 && t2 <= 1;

                if (t1_valid && t2_valid) {
                    // If both are valid, prioritize t1.
                    return t1;
                } else if (t1_valid) {
                    return t1;
                } else if (t2_valid) {
                    return t2;
                } else {
                    // No valid root in [0, 1], return closest boundary t
                    return Math.abs(x - x0) < Math.abs(x - x1) ? 0 : 1;
                }
            }

            const THROTTLE_LIMIT_TYPES = {
                OFF: 0,
                SCALE: 1,
                CLIP: 2,
            };
            // let global validation trigger and adjust the values first
            const throttleMidE = $('.throttle input[name="mid"]');
            const throttleExpoE = $('.throttle input[name="expo"]');
            const throttleHoverE = $('.throttle input[name="hover"]');
            const throttleLimitPercentE = $('.throttle_limit input[name="throttleLimitPercent"]');
            const throttleLimitTypeE = $('.throttle_limit select[id="throttleLimitType"]');
            const mid = parseFloat(throttleMidE.val()); // Value 0-1
            const expo = parseFloat(throttleExpoE.val()); // Value 0-1

            // Hover parameter is only available from 1.47 so use mid value for older versions
            const hover = semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_47) ? parseFloat(throttleHoverE.val()) : mid;

            const throttleLimitPercent = parseInt(throttleLimitPercentE.val()) / 100; // Value 0-1
            const throttleLimitType = parseInt(throttleLimitTypeE.val());
            const throttleCurve = $(".throttle .throttle_curve canvas").get(0);
            const context = throttleCurve.getContext("2d");

            // local validation to deal with input event
            if (
                isNaN(mid) ||
                isNaN(expo) ||
                isNaN(hover) ||
                isNaN(throttleLimitPercent) ||
                isNaN(throttleLimitType) ||
                mid < parseFloat(throttleMidE.prop("min")) ||
                mid > parseFloat(throttleMidE.prop("max")) ||
                expo < parseFloat(throttleExpoE.prop("min")) ||
                expo > parseFloat(throttleExpoE.prop("max")) ||
                hover < parseFloat(throttleHoverE.prop("min")) ||
                hover > parseFloat(throttleHoverE.prop("max")) ||
                throttleLimitPercent < parseInt(throttleLimitPercentE.prop("min")) / 100 ||
                throttleLimitPercent > parseInt(throttleLimitPercentE.prop("max")) / 100
            ) {
                return; // Exit if values are invalid or not numbers
            }

            throttleCurve.width = throttleCurve.height * (throttleCurve.clientWidth / throttleCurve.clientHeight);

            const canvasHeight = throttleCurve.height;
            const canvasWidth = throttleCurve.width;

            // --- Calculate Base Curve Parameters (Unscaled, Unclipped) ---
            // These points define the curve shape based *only* on mid, expo, hover
            const originalTopY = 0; // Top of the canvas corresponds to 100% output
            const originalMidX = canvasWidth * mid; // Central anchor X based on 'mid' input
            const originalMidY = canvasHeight * (1 - hover); // Central anchor Y based on 'hover' input (Y=0 is top, Y=canvasHeight is bottom)

            // Calculate control points for the two quadratic Bezier segments forming the curve, relative to the anchor point (originalMidX, originalMidY)
            const originalMidXl = originalMidX * 0.5; // Control point X for the lower segment
            const originalMidYl = canvasHeight - (canvasHeight - originalMidY) * 0.5 * (expo + 1); // Control point Y for the lower segment, influenced by 'expo'
            const originalMidXr = (canvasWidth + originalMidX) * 0.5; // Control point X for the upper segment
            const originalMidYr = originalTopY + (originalMidY - originalTopY) * 0.5 * (expo + 1); // Control point Y for the upper segment, influenced by 'expo'

            context.clearRect(0, 0, canvasWidth, canvasHeight);
            context.lineWidth = 2;
            context.strokeStyle = "#ffbb00"; // Curve color

            let thrpos; // To store the current throttle indicator position {x, y}
            const thrPercent = Math.max(0, Math.min(1, (FC.RC.channels[3] - 1000) / 1000)); // Ensure 0-1 range
            const thrX = thrPercent * canvasWidth; // X position corresponding to input throttle stick

            // draw
            // --- Draw Curve based on Limit Type ---
            if (throttleLimitType === THROTTLE_LIMIT_TYPES.CLIP && throttleLimitPercent < 1.0) {
                const throttleClipY = canvasHeight * (1 - throttleLimitPercent); // Y coordinate of the limit line

                // Find the intersection point (intersectX, throttleClipY) on the base (unscaled) curve
                let intersectT;
                let intersectX;

                if (throttleClipY >= originalMidY) {
                    // Intersection is on the lower curve segment
                    intersectT = getTfromYBezier(throttleClipY, canvasHeight, originalMidYl, originalMidY);
                    intersectX = getQBezierValue(intersectT, 0, originalMidXl, originalMidX);
                } else {
                    // Intersection is on the upper curve segment
                    intersectT = getTfromYBezier(throttleClipY, originalMidY, originalMidYr, originalTopY);
                    intersectT = Math.max(0, Math.min(1, intersectT)); // Ensure t is in [0,1]
                    intersectX = getQBezierValue(intersectT, originalMidX, originalMidXr, canvasWidth);
                }
                intersectX = Math.max(0, Math.min(canvasWidth, intersectX)); // Ensure intersectX is within bounds

                // Draw the clipped curve
                context.save();
                context.beginPath();
                context.rect(0, throttleClipY, canvasWidth, canvasHeight - throttleClipY); // Define rectangle below the clip line
                context.clip(); // Apply clipping

                // Draw the *entire base* curve; only the part within the clip region will be visible
                context.beginPath();
                context.moveTo(0, canvasHeight);
                context.quadraticCurveTo(originalMidXl, originalMidYl, originalMidX, originalMidY);
                context.quadraticCurveTo(originalMidXr, originalMidYr, canvasWidth, originalTopY);
                context.stroke();

                context.restore(); // Remove clipping region

                // Draw the horizontal line segment from intersection to the right edge
                context.beginPath();
                context.moveTo(intersectX, throttleClipY);
                context.lineTo(canvasWidth, throttleClipY);
                context.stroke();

                // Calculate thrpos based on the base (unscaled) curve first
                let original_thrpos;
                if (thrPercent <= mid) {
                    const t = getTfromXBezier(thrX, 0, originalMidXl, originalMidX);
                    original_thrpos = getQuadraticCurvePoint(
                        0,
                        canvasHeight,
                        originalMidXl,
                        originalMidYl,
                        originalMidX,
                        originalMidY,
                        t,
                    );
                } else {
                    const t = getTfromXBezier(thrX, originalMidX, originalMidXr, canvasWidth);
                    original_thrpos = getQuadraticCurvePoint(
                        originalMidX,
                        originalMidY,
                        originalMidXr,
                        originalMidYr,
                        canvasWidth,
                        originalTopY,
                        t,
                    );
                }
                // Apply the clip to the indicator position
                thrpos = {
                    x: original_thrpos.x,
                    y: Math.max(throttleClipY, original_thrpos.y), // Clamp Y at the clip line
                };
            } else {
                // Handles SCALE and OFF (no limit)
                let scaleFactor = 1.0;
                if (throttleLimitType === THROTTLE_LIMIT_TYPES.SCALE) {
                    scaleFactor = throttleLimitPercent;
                }

                // Calculate potentially scaled curve points
                const currentTopY = canvasHeight * (1 - scaleFactor); // Y position of 100% output after scaling
                const currentMidX = originalMidX; // Mid X anchor remains the same as the base curve
                const currentMidY = canvasHeight * (1 - scaleFactor * hover); // Mid Y anchor is scaled by scaleFactor and hover

                // Calculate control points relative to the scaled anchor (currentMidX, currentMidY) and scaled top (currentTopY)
                const currentMidXl = currentMidX * 0.5;
                const currentMidYl = canvasHeight - (canvasHeight - currentMidY) * 0.5 * (expo + 1);
                const currentMidXr = (canvasWidth + currentMidX) * 0.5;
                const currentMidYr = currentTopY + (currentMidY - currentTopY) * 0.5 * (expo + 1);

                // Draw the (potentially scaled) curve
                context.beginPath();
                context.moveTo(0, canvasHeight); // Start bottom-left
                context.quadraticCurveTo(currentMidXl, currentMidYl, currentMidX, currentMidY);
                context.quadraticCurveTo(currentMidXr, currentMidYr, canvasWidth, currentTopY); // End top-right (potentially scaled)
                context.stroke();

                // Calculate thrpos directly on the (potentially scaled) curve
                if (thrPercent <= mid) {
                    const t = getTfromXBezier(thrX, 0, currentMidXl, currentMidX);
                    thrpos = getQuadraticCurvePoint(
                        0,
                        canvasHeight,
                        currentMidXl,
                        currentMidYl,
                        currentMidX,
                        currentMidY,
                        t,
                    );
                } else {
                    const t = getTfromXBezier(thrX, currentMidX, currentMidXr, canvasWidth);
                    thrpos = getQuadraticCurvePoint(
                        currentMidX,
                        currentMidY,
                        currentMidXr,
                        currentMidYr,
                        canvasWidth,
                        currentTopY,
                        t,
                    );
                }
            }

            // --- Draw Throttle Position Indicator ---
            if (thrpos) {
                // Clamp final thrpos to canvas bounds
                thrpos.x = Math.max(0, Math.min(canvasWidth, thrpos.x));
                thrpos.y = Math.max(0, Math.min(canvasHeight, thrpos.y));

                context.beginPath();
                context.arc(thrpos.x, thrpos.y, 4, 0, 2 * Math.PI); // Draw circle marker
                context.fillStyle = context.strokeStyle; // Use same color as curve
                context.fill();

                // --- Draw Text Label ---
                context.save();
                let fontSize = 10;
                context.font = `${fontSize}pt Verdana, Arial, sans-serif`;
                context.fillStyle = "#888888"; // Text color

                // Calculate real input throttle % and the resulting output throttle %
                let realInputThr = thrPercent * 100.0;
                // Output Y goes from canvasHeight (0%) to 0 (100%), so invert and scale
                let outputThr = Math.max(0, Math.min(100, (1 - thrpos.y / canvasHeight) * 100.0));

                let thrlabel = `${Math.round(realInputThr)}%` + ` = ${Math.round(outputThr)}%`;

                // Position text top-left
                let textX = 5;
                let textY = 5 + fontSize;

                context.fillText(thrlabel, textX, textY);
                context.restore();
            } else {
                // Should not happen if logic is correct
                console.error("thrpos calculation failed");
            }
        } // end of redrawThrottleCurve

        $(".throttle input, .throttle_limit input, .throttle_limit select").on("change", () =>
            setTimeout(() => redrawThrottleCurve(true), 0),
        );

        $("a.refresh").click(function () {
            self.refresh(function () {
                gui_log(i18n.getMessage("pidTuningDataRefreshed"));
            });
        });

        // exclude integratedYaw from setDirty for 4.3 as it uses RP mode.
        $("#pid-tuning")
            .find("input")
            .each(function (k, item) {
                if ($(item).attr("class") !== "feature toggle" && $(item).attr("class") !== "nonProfile") {
                    $(item).change(function () {
                        self.setDirty(true);
                    });
                }
            });

        const dialogCopyProfile = $(".dialogCopyProfile")[0];
        const DIALOG_MODE_PROFILE = 0;
        const DIALOG_MODE_RATEPROFILE = 1;
        let dialogCopyProfileMode;

        const selectProfile = $(".selectProfile");
        const selectRateProfile = $(".selectRateProfile");

        $.each(selectProfileValues, function (key, value) {
            if (key !== FC.CONFIG.profile) {
                selectProfile.append(new Option(value, key));
            }
        });
        $.each(selectRateProfileValues, function (key, value) {
            if (key !== FC.CONFIG.rateProfile) {
                selectRateProfile.append(new Option(value, key));
            }
        });

        $(".copyprofilebtn").click(function () {
            $(".dialogCopyProfile").find(".contentProfile").show();
            $(".dialogCopyProfile").find(".contentRateProfile").hide();
            dialogCopyProfileMode = DIALOG_MODE_PROFILE;
            dialogCopyProfile.showModal();
        });

        $(".copyrateprofilebtn").click(function () {
            $(".dialogCopyProfile").find(".contentProfile").hide();
            $(".dialogCopyProfile").find(".contentRateProfile").show();
            dialogCopyProfileMode = DIALOG_MODE_RATEPROFILE;
            dialogCopyProfile.showModal();
        });

        $(".dialogCopyProfile-cancelbtn").click(function () {
            dialogCopyProfile.close();
        });

        $(".dialogCopyProfile-confirmbtn").click(function () {
            switch (dialogCopyProfileMode) {
                case DIALOG_MODE_PROFILE:
                    FC.COPY_PROFILE.type = DIALOG_MODE_PROFILE; // 0 = pid profile
                    FC.COPY_PROFILE.dstProfile = parseInt(selectProfile.val());
                    FC.COPY_PROFILE.srcProfile = FC.CONFIG.profile;

                    MSP.send_message(
                        MSPCodes.MSP_COPY_PROFILE,
                        mspHelper.crunch(MSPCodes.MSP_COPY_PROFILE),
                        false,
                        close_dialog,
                    );

                    break;

                case DIALOG_MODE_RATEPROFILE:
                    FC.COPY_PROFILE.type = DIALOG_MODE_RATEPROFILE; // 1 = rate profile
                    FC.COPY_PROFILE.dstProfile = parseInt(selectRateProfile.val());
                    FC.COPY_PROFILE.srcProfile = FC.CONFIG.rateProfile;

                    MSP.send_message(
                        MSPCodes.MSP_COPY_PROFILE,
                        mspHelper.crunch(MSPCodes.MSP_COPY_PROFILE),
                        false,
                        close_dialog,
                    );

                    break;

                default:
                    close_dialog();
                    break;
            }

            function close_dialog() {
                dialogCopyProfile.close();
            }
        });

        /*
         *  TuningSliders
         */

        TuningSliders.initialize();

        // UNSCALED non expert slider constrain values
        const NON_EXPERT_SLIDER_MAX = 1.4;
        const NON_EXPERT_SLIDER_MIN = 0.7;

        const sliderPidsModeSelect = $("#sliderPidsModeSelect");
        const sliderGyroFilterModeSelect = $("#sliderGyroFilterModeSelect");
        const sliderDTermFilterModeSelect = $("#sliderDTermFilterModeSelect");

        const useIntegratedYaw = $('input[id="useIntegratedYaw"]');

        useIntegratedYaw.on("change", () => {
            // set slider to RP mode if Integrated Yaw is enabled and sliders are enabled
            if (useIntegratedYaw.is(":checked") && TuningSliders.sliderPidsMode) {
                sliderPidsModeSelect.val(1).trigger("change");
            }
        });

        sliderPidsModeSelect.on("change", function () {
            const setMode = parseInt($(this).val());

            TuningSliders.sliderPidsMode = setMode;

            TuningSliders.calculateNewPids();
            TuningSliders.updatePidSlidersDisplay();

            // disable Integrated Yaw when going into RPY mode
            if (setMode === 2) {
                useIntegratedYaw.prop("checked", false).trigger("change");
            }
        });

        sliderGyroFilterModeSelect.change(function () {
            const mode = parseInt($(this).find(":selected").val());

            if (mode === 1) {
                TuningSliders.gyroFilterSliderEnable();
            } else {
                TuningSliders.gyroFilterSliderDisable();
            }
        });

        sliderDTermFilterModeSelect.change(function () {
            const mode = parseInt($(this).find(":selected").val());

            if (mode !== 0) {
                TuningSliders.dtermFilterSliderEnable();
            } else {
                TuningSliders.dtermFilterSliderDisable();
            }
        });

        const allPidTuningSliders = $(
            "#sliderMasterMultiplier, #sliderDGain, #sliderPIGain, #sliderFeedforwardGain, #sliderIGain, #sliderDMaxGain, #sliderRollPitchRatio, #sliderPitchPIGain",
        );

        $(".tab-pid-tuning .legacySlider").hide();

        allPidTuningSliders.on("input mouseup", function () {
            const slider = $(this);

            if (!TuningSliders.expertMode) {
                if (slider.val() > NON_EXPERT_SLIDER_MAX) {
                    slider.val(NON_EXPERT_SLIDER_MAX);
                } else if (slider.val() < NON_EXPERT_SLIDER_MIN) {
                    slider.val(NON_EXPERT_SLIDER_MIN);
                }
            }

            const sliderValue = isInt(slider.val()) ? parseInt(slider.val()) : parseFloat(slider.val());
            if (slider.is("#sliderDGain")) {
                TuningSliders.sliderDGain = sliderValue;
            } else if (slider.is("#sliderPIGain")) {
                TuningSliders.sliderPIGain = sliderValue;
            } else if (slider.is("#sliderFeedforwardGain")) {
                TuningSliders.sliderFeedforwardGain = sliderValue;
            } else if (slider.is("#sliderDMaxGain")) {
                TuningSliders.sliderDMaxGain = sliderValue;
            } else if (slider.is("#sliderIGain")) {
                TuningSliders.sliderIGain = sliderValue;
            } else if (slider.is("#sliderRollPitchRatio")) {
                TuningSliders.sliderRollPitchRatio = sliderValue;
            } else if (slider.is("#sliderPitchPIGain")) {
                TuningSliders.sliderPitchPIGain = sliderValue;
            } else if (slider.is("#sliderMasterMultiplier")) {
                TuningSliders.sliderMasterMultiplier = sliderValue;
            }

            self.calculateNewPids();
            self.analyticsChanges["PidTuningSliders"] = "On";
        });

        allPidTuningSliders.each(function () {
            self.sliderOnScroll($(this));
        });

        // reset to middle with double click
        allPidTuningSliders.dblclick(function () {
            const slider = $(this);
            let value;

            if (slider.is("#sliderDGain")) {
                value = FC.DEFAULT_TUNING_SLIDERS.slider_d_gain / 100;
                TuningSliders.sliderDGain = value;
            } else if (slider.is("#sliderPIGain")) {
                value = FC.DEFAULT_TUNING_SLIDERS.slider_pi_gain / 100;
                TuningSliders.sliderPIGain = value;
            } else if (slider.is("#sliderFeedforwardGain")) {
                value = FC.DEFAULT_TUNING_SLIDERS.slider_feedforward_gain / 100;
                TuningSliders.sliderFeedforwardGain = value;
            } else if (slider.is("#sliderDMaxGain")) {
                value = FC.DEFAULT_TUNING_SLIDERS.slider_dmax_gain / 100;
                TuningSliders.sliderDMaxGain = value;
            } else if (slider.is("#sliderIGain")) {
                value = FC.DEFAULT_TUNING_SLIDERS.slider_i_gain / 100;
                TuningSliders.sliderIGain = value;
            } else if (slider.is("#sliderRollPitchRatio")) {
                value = FC.DEFAULT_TUNING_SLIDERS.slider_roll_pitch_ratio / 100;
                TuningSliders.sliderRollPitchRatio = value;
            } else if (slider.is("#sliderPitchPIGain")) {
                value = FC.DEFAULT_TUNING_SLIDERS.slider_pitch_pi_gain / 100;
                TuningSliders.sliderPitchPIGain = value;
            } else if (slider.is("#sliderMasterMultiplier")) {
                value = FC.DEFAULT_TUNING_SLIDERS.slider_master_multiplier / 100;
                TuningSliders.sliderMasterMultiplier = value;
            }

            slider.val(value);
            self.calculateNewPids();
        });

        // enable filter sliders inputs
        const allFilterTuningSliders = $("#sliderGyroFilterMultiplier, #sliderDTermFilterMultiplier");
        allFilterTuningSliders.on("input mouseup", function () {
            const slider = $(this);

            if (!TuningSliders.expertMode) {
                const NON_EXPERT_SLIDER_MIN_GYRO = 0.5;
                const NON_EXPERT_SLIDER_MAX_GYRO = 1.5;
                const NON_EXPERT_SLIDER_MIN_DTERM = 0.8;
                const NON_EXPERT_SLIDER_MAX_DTERM = 1.2;

                if (slider.is("#sliderGyroFilterMultiplier")) {
                    if (slider.val() > NON_EXPERT_SLIDER_MAX_GYRO) {
                        slider.val(NON_EXPERT_SLIDER_MAX_GYRO);
                    } else if (slider.val() < NON_EXPERT_SLIDER_MIN_GYRO) {
                        slider.val(NON_EXPERT_SLIDER_MIN_GYRO);
                    }
                } else if (slider.is("#sliderDTermFilterMultiplier")) {
                    if (slider.val() > NON_EXPERT_SLIDER_MAX_DTERM) {
                        slider.val(NON_EXPERT_SLIDER_MAX_DTERM);
                    } else if (slider.val() < NON_EXPERT_SLIDER_MIN_DTERM) {
                        slider.val(NON_EXPERT_SLIDER_MIN_DTERM);
                    }
                }
            }

            let sliderValue = isInt(slider.val()) ? parseInt(slider.val()) : parseFloat(slider.val());

            if (slider.is("#sliderGyroFilterMultiplier")) {
                TuningSliders.sliderGyroFilterMultiplier = sliderValue;
                self.calculateNewGyroFilters();
                self.analyticsChanges["GyroFilterTuningSlider"] = "On";
            } else if (slider.is("#sliderDTermFilterMultiplier")) {
                TuningSliders.sliderDTermFilterMultiplier = sliderValue;
                self.calculateNewDTermFilters();
                self.analyticsChanges["DTermFilterTuningSlider"] = "On";
            }
        });

        allFilterTuningSliders.each(function () {
            self.sliderOnScroll($(this));
        });

        // reset to middle with double click
        allFilterTuningSliders.dblclick(function () {
            const slider = $(this);
            slider.val(1);
            if (slider.is("#sliderGyroFilterMultiplier")) {
                TuningSliders.sliderGyroFilterMultiplier = 1;
                self.calculateNewGyroFilters();
            } else if (slider.is("#sliderDTermFilterMultiplier")) {
                TuningSliders.sliderDTermFilterMultiplier = 1;
                self.calculateNewDTermFilters();
            }
        });

        // update on filter value or type changes
        $(".pid_filter tr:not(.newFilter) input, .pid_filter tr:not(.newFilter) select").on("input", function (e) {
            // because legacy / firmware slider inputs for lowpass1 are duplicate the value isn't updated so set it here.
            if (e.target.type === "number") {
                $(`.pid_filter input[name="${e.target.name}"]`).val(e.target.value);
            } else if (e.target.type === "select-one") {
                $(`.pid_filter select[name="${e.target.name}"]`).val(e.target.value);
            }

            if (TuningSliders.GyroSliderUnavailable) {
                self.analyticsChanges["GyroFilterTuningSlider"] = "Off";
            }
            if (TuningSliders.DTermSliderUnavailable) {
                self.analyticsChanges["DTermFilterTuningSlider"] = "Off";
            }
        });

        // update on filter switch changes
        $(".pid_filter tr:not(.newFilter) .inputSwitch input").change(() => {
            $(".pid_filter input").triggerHandler("input");
            self.setDirty(true);
        });

        $(".tuningHelp").hide();

        $("#pid-tuning .delta select").change(function () {
            self.setDirty(true);
        });

        // update == save.
        $("a.update").click(function () {
            form_to_pid_and_rc();
            self.updating = true;

            MSP.promise(MSPCodes.MSP_SET_PID, mspHelper.crunch(MSPCodes.MSP_SET_PID))
                .then(() => MSP.promise(MSPCodes.MSP_SET_PID_ADVANCED, mspHelper.crunch(MSPCodes.MSP_SET_PID_ADVANCED)))
                .then(() =>
                    semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_45)
                        ? MSP.promise(
                            MSPCodes.MSP2_SET_TEXT,
                            mspHelper.crunch(MSPCodes.MSP2_SET_TEXT, MSPCodes.PID_PROFILE_NAME),
                        )
                        : true,
                )
                .then(() =>
                    semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_45)
                        ? MSP.promise(
                            MSPCodes.MSP2_SET_TEXT,
                            mspHelper.crunch(MSPCodes.MSP2_SET_TEXT, MSPCodes.RATE_PROFILE_NAME),
                        )
                        : true,
                )
                .then(() => {
                    self.updatePIDColors();
                    return MSP.promise(
                        MSPCodes.MSP_SET_FILTER_CONFIG,
                        mspHelper.crunch(MSPCodes.MSP_SET_FILTER_CONFIG),
                    );
                })
                .then(() => MSP.promise(MSPCodes.MSP_SET_RC_TUNING, mspHelper.crunch(MSPCodes.MSP_SET_RC_TUNING)))
                .then(() =>
                    MSP.promise(MSPCodes.MSP_SET_FEATURE_CONFIG, mspHelper.crunch(MSPCodes.MSP_SET_FEATURE_CONFIG)),
                )
                .then(() =>
                    MSP.promise(
                        MSPCodes.MSP_SET_SIMPLIFIED_TUNING,
                        mspHelper.crunch(MSPCodes.MSP_SET_SIMPLIFIED_TUNING),
                    ),
                )
                .then(() => MSP.promise(MSPCodes.MSP_EEPROM_WRITE))
                .then(() => {
                    self.updating = false;

                    self.setDirty(false);

                    gui_log(i18n.getMessage("pidTuningEepromSaved"));

                    self.refresh();
                });

            tracking.sendSaveAndChangeEvents(
                tracking.EVENT_CATEGORIES.FLIGHT_CONTROLLER,
                self.analyticsChanges,
                "pid_tuning",
            );
            self.analyticsChanges = {};
        });

        // Setup model for rates preview
        self.initRatesPreview();
        self.renderModel();

        self.updating = false;

        // enable RC data pulling for rates preview
        GUI.interval_add("receiver_pull", self.getReceiverData, 250, true);

        // status data pulled via separate timer with static speed
        GUI.interval_add(
            "update_profile",
            function update_profile() {
                self.checkUpdateProfile(true);
            },
            500,
            true,
        );

        self.analyticsChanges = {};

        GUI.content_ready(callback);
        TABS.pid_tuning.isHtmlProcessing = false;
    }
};

pid_tuning.getReceiverData = function () {
    MSP.send_message(MSPCodes.MSP_RC, false, false);
};

pid_tuning.initRatesPreview = function () {
    this.keepRendering = true;
    this.model = new Model($(".rates_preview"), $(".rates_preview canvas"));

    $(".tab-pid_tuning .tab-container .rates").on("click", $.proxy(this.model.resize, this.model));
    $(".tab-pid_tuning .tab-container .rates").on("click", $.proxy(this.updateRatesLabels, this));

    $(window).on("resize", $.proxy(this.model.resize, this.model));
    $(window).on("resize", $.proxy(this.updateRatesLabels, this));
};

pid_tuning.renderModel = function () {
    if (!this.keepRendering) {
        return;
    }
    requestAnimationFrame(this.renderModel.bind(this));

    if (!this.clock) {
        this.clock = new THREE.Clock();
    }

    if (FC.RC.channels[0] && FC.RC.channels[1] && FC.RC.channels[2]) {
        const delta = this.clock.getDelta();

        const roll =
            delta *
            this.rateCurve.rcCommandRawToDegreesPerSecond(
                FC.RC.channels[0],
                this.currentRates.roll_rate,
                this.currentRates.rc_rate,
                this.currentRates.rc_expo,
                this.currentRates.superexpo,
                this.currentRates.deadband,
                this.currentRates.roll_rate_limit,
            );
        const pitch =
            delta *
            this.rateCurve.rcCommandRawToDegreesPerSecond(
                FC.RC.channels[1],
                this.currentRates.pitch_rate,
                this.currentRates.rc_rate_pitch,
                this.currentRates.rc_pitch_expo,
                this.currentRates.superexpo,
                this.currentRates.deadband,
                this.currentRates.pitch_rate_limit,
            );
        const yaw =
            delta *
            this.rateCurve.rcCommandRawToDegreesPerSecond(
                FC.RC.channels[2],
                this.currentRates.yaw_rate,
                this.currentRates.rc_rate_yaw,
                this.currentRates.rc_yaw_expo,
                this.currentRates.superexpo,
                this.currentRates.yawDeadband,
                this.currentRates.yaw_rate_limit,
            );

        this.model.rotateBy(-degToRad(pitch), -degToRad(yaw), -degToRad(roll));

        if (this.checkRC()) this.updateRatesLabels(); // has the RC data changed ?
    }
};

pid_tuning.cleanup = function (callback) {
    const self = this;

    self.keepRendering = false;

    if (self.model) {
        $(window).off("resize", $.proxy(self.model.resize, self.model));
        self.model.dispose();
    }

    $(window).off("resize", $.proxy(this.updateRatesLabels, this));

    if (self.throttleDrawInterval) {
        clearInterval(self.throttleDrawInterval);
    }

    if (callback) callback();
};

pid_tuning.refresh = function (callback) {
    const self = this;

    GUI.tab_switch_cleanup(function () {
        self.initialize();

        self.setDirty(false);

        if (callback) {
            callback();
        }
    });
};

pid_tuning.setProfile = function () {
    const self = this;

    self.currentProfile = FC.CONFIG.profile;
    $('.tab-pid_tuning select[name="profile"]').val(self.currentProfile);
};

pid_tuning.setRateProfile = function () {
    const self = this;

    self.currentRateProfile = FC.CONFIG.rateProfile;
    $('.tab-pid_tuning select[name="rate_profile"]').val(self.currentRateProfile);
};

pid_tuning.setDirty = function (isDirty) {
    const self = this;

    self.dirty = isDirty;
    $('.tab-pid_tuning select[name="profile"]').prop("disabled", isDirty);
    $('.tab-pid_tuning select[name="rate_profile"]').prop("disabled", isDirty);
};

pid_tuning.checkUpdateProfile = function (updateRateProfile) {
    const self = this;

    if (GUI.active_tab === "pid_tuning") {
        if (!self.updating && !self.dirty) {
            let changedProfile = false;
            if (self.currentProfile !== FC.CONFIG.profile) {
                self.setProfile();

                changedProfile = true;
            }

            let changedRateProfile = false;
            if (updateRateProfile && self.currentRateProfile !== FC.CONFIG.rateProfile) {
                self.setRateProfile();

                changedRateProfile = true;
            }

            if (changedProfile || changedRateProfile) {
                self.updating = true;
                self.refresh(function () {
                    self.updating = false;
                    if (changedProfile) {
                        gui_log(i18n.getMessage("pidTuningReceivedProfile", [FC.CONFIG.profile + 1]));
                        FC.CONFIG.profile = self.currentProfile;
                    }

                    if (changedRateProfile) {
                        gui_log(i18n.getMessage("pidTuningReceivedRateProfile", [FC.CONFIG.rateProfile + 1]));
                        FC.CONFIG.rateProfile = self.currentRateProfile;
                    }
                });
            }
        }
    }
};

pid_tuning.checkRC = function () {
    // Function monitors for change in the primary axes rc received data and returns true if a change is detected.

    if (!this.oldRC) {
        this.oldRC = [FC.RC.channels[0], FC.RC.channels[1], FC.RC.channels[2]];
    }

    // Monitor FC.RC.channels and detect change of value;
    let rateCurveUpdateRequired = false;
    for (let i = 0; i < this.oldRC.length; i++) {
        // has the value changed ?
        if (this.oldRC[i] !== FC.RC.channels[i]) {
            this.oldRC[i] = FC.RC.channels[i];
            rateCurveUpdateRequired = true; // yes, then an update of the values displayed on the rate curve graph is required
        }
    }
    return rateCurveUpdateRequired;
};

pid_tuning.checkThrottle = function () {
    // Function monitors for change in the received rc throttle data and returns true if a change is detected.
    if (!this.oldThrottle) {
        this.oldThrottle = FC.RC.channels[3];
        return true;
    }
    const updateRequired = this.oldThrottle !== FC.RC.channels[3];
    this.oldThrottle = FC.RC.channels[3];
    return updateRequired;
};

pid_tuning.updatePidControllerParameters = function () {
    $(".pid_tuning .YAW_JUMP_PREVENTION").hide();
    $("#pid-tuning .dtermSetpointTransition").hide();
    $("#pid-tuning .dtermSetpoint").hide();
    $("#pid-tuning .delta").hide();
};

pid_tuning.updateRatesLabels = function () {
    const self = this;
    if (!self.rateCurve.useLegacyCurve && self.rateCurve.maxAngularVel) {
        const drawAxisLabel = function (context, axisLabel, x, y, align, color) {
            context.fillStyle = color || "#888888";
            context.textAlign = align || "center";
            context.fillText(axisLabel, x, y);
        };

        const drawBalloonLabel = function (context, axisLabel, x, y, align, colors, dirty) {
            /**
             * curveContext is the canvas to draw on
             * axisLabel is the string to display in the center of the balloon
             * x, y are the coordinates of the point of the balloon
             * align is whether the balloon appears to the left (align 'right') or right (align left) of the x,y coordinates
             * colors is an object defining color, border and text are the fill color, border color and text color of the balloon
             */

            const DEFAULT_OFFSET = 125; // in canvas scale; this is the horizontal length of the pointer
            const DEFAULT_RADIUS = 10; // in canvas scale, this is the radius around the balloon
            const DEFAULT_MARGIN = 5; // in canvas scale, this is the margin around the balloon when it overlaps

            const fontSize = parseInt(context.font);

            // calculate the width and height required for the balloon
            const width = context.measureText(axisLabel).width * 1.2;
            const height = fontSize * 1.5; // the balloon is bigger than the text height
            const pointerY = y; // always point to the required Y
            // coordinate, even if we move the balloon itself to keep it on the canvas

            // setup balloon background
            context.fillStyle = colors.color || "#ffffff";
            context.strokeStyle = colors.border || "#000000";

            // correct x position to account for window scaling
            x *= context.canvas.clientWidth / context.canvas.clientHeight;

            // adjust the coordinates for determine where the balloon background should be drawn
            x += (align == "right" ? -(width + DEFAULT_OFFSET) : 0) + (align == "left" ? DEFAULT_OFFSET : 0);
            y -= height / 2;
            if (y < 0) {
                y = 0;
            } else if (y > context.height) {
                y = context.height; // prevent balloon from going out of canvas
            }

            // check that the balloon does not already overlap
            for (let i = 0; i < dirty.length; i++) {
                if (
                    (x >= dirty[i].left && x <= dirty[i].right) ||
                    (x + width >= dirty[i].left && x + width <= dirty[i].right)
                ) {
                    // does it overlap horizontally
                    if (
                        (y >= dirty[i].top && y <= dirty[i].bottom) ||
                        (y + height >= dirty[i].top && y + height <= dirty[i].bottom)
                    ) {
                        // this overlaps another balloon
                        // snap above or snap below
                        if (y <= (dirty[i].bottom - dirty[i].top) / 2 && dirty[i].top - height > 0) {
                            y = dirty[i].top - height;
                        } else {
                            // snap down
                            y = dirty[i].bottom;
                        }
                    }
                }
            }

            // Add the draw area to the dirty array
            dirty.push({ left: x, right: x + width, top: y - DEFAULT_MARGIN, bottom: y + height + DEFAULT_MARGIN });

            const pointerLength = (height - 2 * DEFAULT_RADIUS) / 6;

            context.beginPath();
            context.moveTo(x + DEFAULT_RADIUS, y);
            context.lineTo(x + width - DEFAULT_RADIUS, y);
            context.quadraticCurveTo(x + width, y, x + width, y + DEFAULT_RADIUS);

            if (align === "right") {
                // point is to the right
                context.lineTo(x + width, y + DEFAULT_RADIUS + pointerLength);
                context.lineTo(x + width + DEFAULT_OFFSET, pointerY); // point
                context.lineTo(x + width, y + height - DEFAULT_RADIUS - pointerLength);
            }
            context.lineTo(x + width, y + height - DEFAULT_RADIUS);

            context.quadraticCurveTo(x + width, y + height, x + width - DEFAULT_RADIUS, y + height);
            context.lineTo(x + DEFAULT_RADIUS, y + height);
            context.quadraticCurveTo(x, y + height, x, y + height - DEFAULT_RADIUS);

            if (align === "left") {
                // point is to the left
                context.lineTo(x, y + height - DEFAULT_RADIUS - pointerLength);
                context.lineTo(x - DEFAULT_OFFSET, pointerY); // point
                context.lineTo(x, y + DEFAULT_RADIUS - pointerLength);
            }
            context.lineTo(x, y + DEFAULT_RADIUS);

            context.quadraticCurveTo(x, y, x + DEFAULT_RADIUS, y);
            context.closePath();

            // fill in the balloon background
            context.fill();
            context.stroke();

            // and add the label
            drawAxisLabel(context, axisLabel, x + width / 2, y + (height + fontSize) / 2 - 4, "center", colors.text);
        };

        const BALLOON_COLORS = {
            roll: { color: "rgba(255,128,128,0.4)", border: "rgba(255,128,128,0.6)", text: "#000000" },
            pitch: { color: "rgba(128,255,128,0.4)", border: "rgba(128,255,128,0.6)", text: "#000000" },
            yaw: { color: "rgba(128,128,255,0.4)", border: "rgba(128,128,255,0.6)", text: "#000000" },
        };

        const rcStickElement = $(".rate_curve canvas#rate_curve_layer1").get(0);
        if (rcStickElement) {
            rcStickElement.width = 1000;
            rcStickElement.height = 1000;

            const stickContext = rcStickElement.getContext("2d");

            stickContext.save();

            const maxAngularVelRoll = `${self.maxAngularVelRollElement.text()} deg/s`;
            const maxAngularVelPitch = `${self.maxAngularVelPitchElement.text()} deg/s`;
            const maxAngularVelYaw = `${self.maxAngularVelYawElement.text()} deg/s`;
            let currentValues = [];
            let balloonsDirty = [];
            const curveHeight = rcStickElement.height;
            const curveWidth = rcStickElement.width;
            const maxAngularVel = self.rateCurve.maxAngularVel;
            const windowScale = 400 / stickContext.canvas.clientHeight;
            const rateScale = curveHeight / 2 / maxAngularVel;
            const lineScale = stickContext.canvas.width / stickContext.canvas.clientWidth;
            const textScale = stickContext.canvas.clientHeight / stickContext.canvas.clientWidth;

            stickContext.clearRect(0, 0, curveWidth, curveHeight);

            // calculate the fontSize based upon window scaling
            if (windowScale <= 1) {
                stickContext.font = "24pt Verdana, Arial, sans-serif";
            } else {
                stickContext.font = `${24 * windowScale}pt Verdana, Arial, sans-serif`;
            }

            if (FC.RC.channels[0] && FC.RC.channels[1] && FC.RC.channels[2]) {
                currentValues.push(
                    `${self.rateCurve.drawStickPosition(
                        FC.RC.channels[0],
                        self.currentRates.roll_rate,
                        self.currentRates.rc_rate,
                        self.currentRates.rc_expo,
                        self.currentRates.superexpo,
                        self.currentRates.deadband,
                        self.currentRates.roll_rate_limit,
                        maxAngularVel,
                        stickContext,
                        "#FF8080",
                    )} deg/s`,
                );
                currentValues.push(
                    `${self.rateCurve.drawStickPosition(
                        FC.RC.channels[1],
                        self.currentRates.pitch_rate,
                        self.currentRates.rc_rate_pitch,
                        self.currentRates.rc_pitch_expo,
                        self.currentRates.superexpo,
                        self.currentRates.deadband,
                        self.currentRates.pitch_rate_limit,
                        maxAngularVel,
                        stickContext,
                        "#80FF80",
                    )} deg/s`,
                );
                currentValues.push(
                    `${self.rateCurve.drawStickPosition(
                        FC.RC.channels[2],
                        self.currentRates.yaw_rate,
                        self.currentRates.rc_rate_yaw,
                        self.currentRates.rc_yaw_expo,
                        self.currentRates.superexpo,
                        self.currentRates.yawDeadband,
                        self.currentRates.yaw_rate_limit,
                        maxAngularVel,
                        stickContext,
                        "#8080FF",
                    )} deg/s`,
                );
            } else {
                currentValues = [];
            }

            stickContext.lineWidth = lineScale;

            // use a custom scale so that the text does not appear stretched
            stickContext.scale(textScale, 1);

            // add the maximum range label
            drawAxisLabel(
                stickContext,
                `${maxAngularVel.toFixed(0)} deg/s`,
                (curveWidth / 2 - 10) / textScale,
                parseInt(stickContext.font) * 1.2,
                "right",
            );

            // and then the balloon labels.
            balloonsDirty = []; // reset the dirty balloon draw area (for overlap detection)
            // create an array of balloons to draw
            const balloons = [
                {
                    value: parseInt(maxAngularVelRoll),
                    balloon: function () {
                        drawBalloonLabel(
                            stickContext,
                            maxAngularVelRoll,
                            curveWidth,
                            rateScale * (maxAngularVel - parseInt(maxAngularVelRoll)),
                            "right",
                            BALLOON_COLORS.roll,
                            balloonsDirty,
                        );
                    },
                },
                {
                    value: parseInt(maxAngularVelPitch),
                    balloon: function () {
                        drawBalloonLabel(
                            stickContext,
                            maxAngularVelPitch,
                            curveWidth,
                            rateScale * (maxAngularVel - parseInt(maxAngularVelPitch)),
                            "right",
                            BALLOON_COLORS.pitch,
                            balloonsDirty,
                        );
                    },
                },
                {
                    value: parseInt(maxAngularVelYaw),
                    balloon: function () {
                        drawBalloonLabel(
                            stickContext,
                            maxAngularVelYaw,
                            curveWidth,
                            rateScale * (maxAngularVel - parseInt(maxAngularVelYaw)),
                            "right",
                            BALLOON_COLORS.yaw,
                            balloonsDirty,
                        );
                    },
                },
            ];
            // show warning message if any axis angular velocity exceeds 1800d/s
            const MAX_RATE_WARNING = 1800;
            const warningRates =
                parseInt(maxAngularVelRoll) > MAX_RATE_WARNING ||
                parseInt(maxAngularVelPitch) > MAX_RATE_WARNING ||
                parseInt(maxAngularVelYaw) > MAX_RATE_WARNING;
            $(".maxRateWarning").toggle(warningRates);

            // and sort them in descending order so the largest value is at the top always
            balloons.sort(function (a, b) {
                return b.value - a.value;
            });

            // add the current rc values
            if (currentValues[0] && currentValues[1] && currentValues[2]) {
                balloons.push(
                    {
                        value: parseInt(currentValues[0]),
                        balloon: function () {
                            drawBalloonLabel(
                                stickContext,
                                currentValues[0],
                                10,
                                150,
                                "none",
                                BALLOON_COLORS.roll,
                                balloonsDirty,
                            );
                        },
                    },
                    {
                        value: parseInt(currentValues[1]),
                        balloon: function () {
                            drawBalloonLabel(
                                stickContext,
                                currentValues[1],
                                10,
                                250,
                                "none",
                                BALLOON_COLORS.pitch,
                                balloonsDirty,
                            );
                        },
                    },
                    {
                        value: parseInt(currentValues[2]),
                        balloon: function () {
                            drawBalloonLabel(
                                stickContext,
                                currentValues[2],
                                10,
                                350,
                                "none",
                                BALLOON_COLORS.yaw,
                                balloonsDirty,
                            );
                        },
                    },
                );
            }

            // Only show Acro Center - Max Sensitivity for betaflight rates
            const centerSensitivyLabel = $("#pid-tuning .pid_titlebar .centerSensitivity");

            const isBetaflightRates = self.currentRatesType === FC.RATES_TYPE.BETAFLIGHT;

            centerSensitivyLabel.toggle(isBetaflightRates);

            self.acroCenterSensitivityRollElement.toggle(isBetaflightRates);
            self.acroCenterSensitivityPitchElement.toggle(isBetaflightRates);
            self.acroCenterSensitivityYawElement.toggle(isBetaflightRates);

            $("#pid-tuning .pid_titlebar .maxVel").toggle(!isBetaflightRates);
            self.maxAngularVelRollElement.toggle(!isBetaflightRates);
            self.maxAngularVelPitchElement.toggle(!isBetaflightRates);
            self.maxAngularVelYawElement.toggle(!isBetaflightRates);

            // Add labels for Angle Center Sensitivity
            const angleLimit = FC.ADVANCED_TUNING.levelAngleLimit;
            const maxAngleRollRate = parseInt(maxAngularVelRoll);
            const maxAnglePitchRate = parseInt(maxAngularVelPitch);
            const maxAngleYawRate = parseInt(maxAngularVelYaw);

            const rcRate = self.currentRates.rc_rate;
            const rcRatePitch = self.currentRates.rc_rate_pitch;
            const rcRateYaw = self.currentRates.rc_rate_yaw;

            function getOffsetForBalloon(value) {
                return (
                    curveWidth -
                    Math.ceil(stickContext.measureText(value).width) /
                        (stickContext.canvas.clientWidth / stickContext.canvas.clientHeight) -
                    40
                );
            }

            const angleModeText = `Angle Mode`;

            if (self.currentRatesType === FC.RATES_TYPE.ACTUAL) {
                drawAxisLabel(stickContext, angleModeText, (curveWidth - 10) / textScale, curveHeight - 250, "right");

                const angleCenterSensitivityRoll = ((rcRate / maxAngleRollRate) * angleLimit).toFixed(1);
                const angleCenterSensitivityPitch = ((rcRatePitch / maxAnglePitchRate) * angleLimit).toFixed(1);

                const angleCenterSensitivityRollText = `${angleCenterSensitivityRoll}...${angleLimit}`;
                const angleCenterSensitivityPitchText = `${angleCenterSensitivityPitch}...${angleLimit}`;

                const angleCenterSensitivityRollOffset = getOffsetForBalloon(angleCenterSensitivityRollText);
                const angleCenterSensitivityPitchOffset = getOffsetForBalloon(angleCenterSensitivityPitchText);

                balloons.push(
                    {
                        value: parseInt(angleCenterSensitivityRoll),
                        balloon: function () {
                            drawBalloonLabel(
                                stickContext,
                                angleCenterSensitivityRollText,
                                angleCenterSensitivityRollOffset,
                                curveHeight - 150,
                                "none",
                                BALLOON_COLORS.roll,
                                balloonsDirty,
                            );
                        },
                    },
                    {
                        value: parseInt(angleCenterSensitivityPitch),
                        balloon: function () {
                            drawBalloonLabel(
                                stickContext,
                                angleCenterSensitivityPitchText,
                                angleCenterSensitivityPitchOffset,
                                curveHeight - 50,
                                "none",
                                BALLOON_COLORS.pitch,
                                balloonsDirty,
                            );
                        },
                    },
                );
            }

            if (self.currentRatesType === FC.RATES_TYPE.BETAFLIGHT) {
                drawAxisLabel(stickContext, angleModeText, (curveWidth - 10) / textScale, curveHeight - 250, "right");

                const RC_RATE_INCREMENTAL = 14.54;

                const getRcRateModified = (rate) => (rate > 2.0 ? (rate - 2.0) * RC_RATE_INCREMENTAL + 2.0 : rate);
                const getAcroSensitivityFraction = (exponent, rate) =>
                    ((1 - exponent) * getRcRateModified(rate) * 200).toFixed(0);
                const getAngleSensitivityFraction = (exponent, rate) =>
                    (angleLimit * (((1 - exponent) * getRcRateModified(rate) * 200) / maxAngleRollRate)).toFixed(1);

                // ROLL
                const expo = self.currentRates.rc_expo;
                const angleCenterSensitivityFractionRoll = getAngleSensitivityFraction(expo, rcRate);
                const angleCenterSensitivityFractionRollText = `${angleCenterSensitivityFractionRoll} - ${angleLimit}`;
                const angleCenterSensitivityFractionRollOffset = getOffsetForBalloon(
                    angleCenterSensitivityFractionRollText,
                );
                const acroCenterSensitivityFractionRoll = getAcroSensitivityFraction(expo, rcRate);
                self.acroCenterSensitivityRollElement.text(
                    `${acroCenterSensitivityFractionRoll} - ${maxAngleRollRate}`,
                );
                // PITCH
                const expoPitch = self.currentRates.rc_pitch_expo;
                const angleCenterSensitivityFractionPitch = getAngleSensitivityFraction(expoPitch, rcRatePitch);
                const angleCenterSensitivityFractionPitchText = `${angleCenterSensitivityFractionPitch} - ${angleLimit}`;
                const angleCenterSensitivityFractionPitchOffset = getOffsetForBalloon(
                    angleCenterSensitivityFractionPitchText,
                );
                const acroCenterSensitivityFractionPitch = getAcroSensitivityFraction(expoPitch, rcRatePitch);
                self.acroCenterSensitivityPitchElement.text(
                    `${acroCenterSensitivityFractionPitch} - ${maxAnglePitchRate}`,
                );
                // YAW
                const expoYaw = self.currentRates.rc_yaw_expo;
                const acroCenterSensitivityFractionYaw = getAcroSensitivityFraction(expoYaw, rcRateYaw);
                self.acroCenterSensitivityYawElement.text(`${acroCenterSensitivityFractionYaw} - ${maxAngleYawRate}`);

                balloons.push(
                    {
                        value: parseInt(angleCenterSensitivityFractionRoll),
                        balloon: function () {
                            drawBalloonLabel(
                                stickContext,
                                angleCenterSensitivityFractionRollText,
                                angleCenterSensitivityFractionRollOffset,
                                curveHeight - 150,
                                "none",
                                BALLOON_COLORS.roll,
                                balloonsDirty,
                            );
                        },
                    },
                    {
                        value: parseInt(angleCenterSensitivityFractionPitch),
                        balloon: function () {
                            drawBalloonLabel(
                                stickContext,
                                angleCenterSensitivityFractionPitchText,
                                angleCenterSensitivityFractionPitchOffset,
                                curveHeight - 50,
                                "none",
                                BALLOON_COLORS.pitch,
                                balloonsDirty,
                            );
                        },
                    },
                );
            }

            // then display them on the chart
            for (const balloon of balloons) {
                balloon.balloon();
            }

            stickContext.restore();
        }
    }
};

pid_tuning.calculateNewPids = function () {
    if (!TABS.pid_tuning.isHtmlProcessing) {
        TuningSliders.calculateNewPids();
    }
};

pid_tuning.calculateNewGyroFilters = function () {
    if (!TABS.pid_tuning.isHtmlProcessing) {
        if (TuningSliders.sliderGyroFilter) {
            TuningSliders.calculateNewGyroFilters();
        }
    }
};

pid_tuning.calculateNewDTermFilters = function () {
    if (!TABS.pid_tuning.isHtmlProcessing) {
        if (TuningSliders.sliderDTermFilter) {
            TuningSliders.calculateNewDTermFilters();
        }
    }
};

pid_tuning.updateFilterWarning = function () {
    const gyroLowpassFilterMode = parseInt($('.pid_filter select[name="gyroLowpassFilterMode"]').val());
    const gyroDynamicLowpassEnabled = gyroLowpassFilterMode === 1;
    const gyroLowpass1Enabled = !gyroLowpassFilterMode;
    const dtermLowpassFilterMode = parseInt($('.pid_filter select[name="dtermLowpassFilterMode"]').val());
    const dtermDynamicLowpassEnabled = dtermLowpassFilterMode === 1;
    const dtermLowpass1Enabled = !dtermLowpassFilterMode;
    const warningE = $("#pid-tuning .filterWarning");
    const warningDynamicNotchNyquistE = $("#pid-tuning .dynamicNotchNyquistWarningNote");

    warningE.toggle(
        !(gyroDynamicLowpassEnabled || gyroLowpass1Enabled) || !(dtermDynamicLowpassEnabled || dtermLowpass1Enabled),
    );
    warningDynamicNotchNyquistE.toggle(FC.CONFIG.sampleRateHz / FC.PID_ADVANCED_CONFIG.pid_process_denom < 2000);
};

pid_tuning.updatePIDColors = function (clear = false) {
    // Would be nice to make colors work again in the future
    if (semver.gte(FC.CONFIG.apiVersion, "1.44.0")) {
        return;
    }

    const setTuningElementColor = function (element, mspValue, currentValue) {
        if (clear) {
            element.css({ "background-color": "transparent" });
            return;
        }

        if (currentValue === undefined || mspValue === undefined) {
            return;
        }

        const change = (currentValue - mspValue) / 50;
        element.css({ "background-color": getColorForPercentage(change, colorTables.pidSlider) });
    };

    FC.PID_NAMES.forEach(function (elementPid, indexPid) {
        $(`.pid_tuning .${elementPid} input`).each(function (indexInput) {
            setTuningElementColor($(this), FC.PIDS_ACTIVE[indexPid][indexInput], FC.PIDS[indexPid][indexInput]);
        });
    });

    setTuningElementColor(
        $('.pid_tuning input[name="dMaxRoll"]'),
        FC.ADVANCED_TUNING_ACTIVE.dMaxRoll,
        FC.ADVANCED_TUNING.dMaxRoll,
    );
    setTuningElementColor(
        $('.pid_tuning input[name="dMaxPitch"]'),
        FC.ADVANCED_TUNING_ACTIVE.dMaxPitch,
        FC.ADVANCED_TUNING.dMaxPitch,
    );
    setTuningElementColor(
        $('.pid_tuning input[name="dMaxYaw"]'),
        FC.ADVANCED_TUNING_ACTIVE.dMaxYaw,
        FC.ADVANCED_TUNING.dMaxYaw,
    );
    setTuningElementColor(
        $('.pid_tuning .ROLL input[name="f"]'),
        FC.ADVANCED_TUNING_ACTIVE.feedforwardRoll,
        FC.ADVANCED_TUNING.feedforwardRoll,
    );
    setTuningElementColor(
        $('.pid_tuning .PITCH input[name="f"]'),
        FC.ADVANCED_TUNING_ACTIVE.feedforwardPitch,
        FC.ADVANCED_TUNING.feedforwardPitch,
    );
    setTuningElementColor(
        $('.pid_tuning .YAW input[name="f"]'),
        FC.ADVANCED_TUNING_ACTIVE.feedforwardYaw,
        FC.ADVANCED_TUNING.feedforwardYaw,
    );
};

pid_tuning.changeRatesType = function (rateTypeID) {
    const self = this;
    const dialogRatesType = $(".dialogRatesType")[0];

    if (self.previousRatesType == null) {
        self.currentRatesType = rateTypeID;
        self.changeRatesTypeLogo();
        self.changeRatesSystem(true);
        self.previousRatesType = self.currentRatesType;
        return;
    }

    if (!dialogRatesType.hasAttribute("open")) {
        dialogRatesType.showModal();

        $(".dialogRatesType-cancelbtn").click(function () {
            $('.rates_type select[id="ratesType"]').val(self.currentRatesType);
            self.previousRatesType = self.currentRatesType;
            dialogRatesType.close();
        });

        $(".dialogRatesType-confirmbtn").click(function () {
            self.currentRatesType = rateTypeID;
            self.changeRatesTypeLogo();
            self.changeRatesSystem(false);
            self.previousRatesType = self.currentRatesType;
            dialogRatesType.close();

            FC.RC_TUNING.rates_type = self.currentRatesType;
            self.currentRates = self.rateCurve.getCurrentRates();
        });
    }
};

pid_tuning.changeRatesSystem = function (sameType) {
    const self = this;

    let rcRateMax = 2.55,
        rcRateMin = 0.01,
        rcRateStep = 0.01;
    let rateMax = 1.0,
        rateStep = 0.01;
    let expoMax = 1.0,
        expoStep = 0.01;
    let rateMin = 0;
    const expoMin = 0;

    const pitch_rate_e = $('.pid_tuning input[name="pitch_rate"]');
    const roll_rate_e = $('.pid_tuning input[name="roll_rate"]');
    const yaw_rate_e = $('.pid_tuning input[name="yaw_rate"]');
    const rc_rate_pitch_e = $('.pid_tuning input[name="rc_rate_pitch"]');
    const rc_rate_e = $('.pid_tuning input[name="rc_rate"]');
    const rc_rate_yaw_e = $('.pid_tuning input[name="rc_rate_yaw"]');
    const rc_pitch_expo_e = $('.pid_tuning input[name="rc_pitch_expo"]');
    const rc_expo_e = $('.pid_tuning input[name="rc_expo"]');
    const rc_yaw_expo_e = $('.pid_tuning input[name="rc_yaw_expo"]');

    const rcRateLabel = $("#pid-tuning .pid_titlebar .rc_rate");
    const rateLabel = $("#pid-tuning .pid_titlebar .rate");
    const rcExpoLabel = $("#pid-tuning .pid_titlebar .rc_expo");
    const centerSensitivyLabel = $("#pid-tuning .pid_titlebar .centerSensitivity");

    // default values for betaflight curve. all the default values produce the same betaflight default curve (or at least near enough)
    let rcRateDefault = (1).toFixed(2),
        rateDefault = (0.7).toFixed(2),
        expoDefault = (0).toFixed(2);

    if (sameType) {
        // if selected rates type is different from the saved one, set values to default instead of reading
        pitch_rate_e.val(FC.RC_TUNING.pitch_rate.toFixed(2));
        roll_rate_e.val(FC.RC_TUNING.roll_rate.toFixed(2));
        yaw_rate_e.val(FC.RC_TUNING.yaw_rate.toFixed(2));
        rc_rate_pitch_e.val(FC.RC_TUNING.rcPitchRate.toFixed(2));
        rc_rate_e.val(FC.RC_TUNING.RC_RATE.toFixed(2));
        rc_rate_yaw_e.val(FC.RC_TUNING.rcYawRate.toFixed(2));
        rc_pitch_expo_e.val(FC.RC_TUNING.RC_PITCH_EXPO.toFixed(2));
        rc_expo_e.val(FC.RC_TUNING.RC_EXPO.toFixed(2));
        rc_yaw_expo_e.val(FC.RC_TUNING.RC_YAW_EXPO.toFixed(2));
    }

    switch (self.currentRatesType) {
        case FC.RATES_TYPE.RACEFLIGHT:
            rcRateLabel.text(i18n.getMessage("pidTuningRcRateRaceflight"));
            rateLabel.text(i18n.getMessage("pidTuningRateRaceflight"));
            rcExpoLabel.text(i18n.getMessage("pidTuningRcExpoRaceflight"));

            rcRateMax = 2000;
            rcRateMin = 10;
            rcRateStep = 10;
            rateMax = 255;
            rateStep = 1;
            expoMax = 100;
            expoStep = 1;

            if (sameType) {
                pitch_rate_e.val((FC.RC_TUNING.pitch_rate * 100).toFixed(0));
                roll_rate_e.val((FC.RC_TUNING.roll_rate * 100).toFixed(0));
                yaw_rate_e.val((FC.RC_TUNING.yaw_rate * 100).toFixed(0));
                rc_rate_pitch_e.val((FC.RC_TUNING.rcPitchRate * 1000).toFixed(0));
                rc_rate_e.val((FC.RC_TUNING.RC_RATE * 1000).toFixed(0));
                rc_rate_yaw_e.val((FC.RC_TUNING.rcYawRate * 1000).toFixed(0));
                rc_pitch_expo_e.val((FC.RC_TUNING.RC_PITCH_EXPO * 100).toFixed(0));
                rc_expo_e.val((FC.RC_TUNING.RC_EXPO * 100).toFixed(0));
                rc_yaw_expo_e.val((FC.RC_TUNING.RC_YAW_EXPO * 100).toFixed(0));
            } else {
                rcRateDefault = (370).toFixed(0);
                rateDefault = (80).toFixed(0);
                expoDefault = (50).toFixed(0);
            }

            break;

        case FC.RATES_TYPE.KISS:
            rcRateLabel.text(i18n.getMessage("pidTuningRcRate"));
            rateLabel.text(i18n.getMessage("pidTuningRcRateRaceflight"));
            rcExpoLabel.text(i18n.getMessage("pidTuningRcExpoKISS"));

            rateMax = 0.99;

            break;

        case FC.RATES_TYPE.ACTUAL:
            rcRateLabel.text(i18n.getMessage("pidTuningRcRateActual"));
            rateLabel.text(i18n.getMessage("pidTuningRateQuickRates"));
            rcExpoLabel.text(i18n.getMessage("pidTuningRcExpoRaceflight"));

            rateMin = 10;
            rateMax = 2000;
            rateStep = 10;
            rcRateMax = 2000;
            rcRateMin = 10;
            rcRateStep = 10;

            if (sameType) {
                pitch_rate_e.val((FC.RC_TUNING.pitch_rate * 1000).toFixed(0));
                roll_rate_e.val((FC.RC_TUNING.roll_rate * 1000).toFixed(0));
                yaw_rate_e.val((FC.RC_TUNING.yaw_rate * 1000).toFixed(0));
                rc_rate_pitch_e.val((FC.RC_TUNING.rcPitchRate * 1000).toFixed(0));
                rc_rate_e.val((FC.RC_TUNING.RC_RATE * 1000).toFixed(0));
                rc_rate_yaw_e.val((FC.RC_TUNING.rcYawRate * 1000).toFixed(0));
            } else {
                rcRateDefault = (70).toFixed(0);
                rateDefault = (670).toFixed(0);
                expoDefault = (0).toFixed(2);
            }

            break;

        case FC.RATES_TYPE.QUICKRATES:
            rcRateLabel.text(i18n.getMessage("pidTuningRcRate"));
            rateLabel.text(i18n.getMessage("pidTuningRateQuickRates"));
            rcExpoLabel.text(i18n.getMessage("pidTuningRcExpoRaceflight"));

            rateMin = 10;
            rateMax = 2000;
            rateStep = 10;

            if (sameType) {
                pitch_rate_e.val((FC.RC_TUNING.pitch_rate * 1000).toFixed(0));
                roll_rate_e.val((FC.RC_TUNING.roll_rate * 1000).toFixed(0));
                yaw_rate_e.val((FC.RC_TUNING.yaw_rate * 1000).toFixed(0));
            } else {
                rateDefault = (670).toFixed(0);
            }

            break;

        // add future rates types here
        default: // BetaFlight
            rcRateLabel.text(i18n.getMessage("pidTuningRcRate"));
            rateLabel.text(i18n.getMessage("pidTuningRate"));
            rcExpoLabel.text(i18n.getMessage("pidTuningRcExpo"));
            centerSensitivyLabel.text(i18n.getMessage("pidTuningAcroCenterSensitiviy"));
            break;
    }

    const rc_rate_input_c = $('#pid-tuning input[class="rc_rate_input"]');
    const rate_input_c = $('#pid-tuning input[class="rate_input"]');
    const expo_input_c = $('#pid-tuning input[class="expo_input"]');

    if (!sameType) {
        rate_input_c.val(rateDefault);
        rc_rate_input_c.val(rcRateDefault);
        expo_input_c.val(expoDefault);
    }

    rc_rate_input_c.attr({ max: rcRateMax, min: rcRateMin, step: rcRateStep }).change();
    rate_input_c.attr({ max: rateMax, min: rateMin, step: rateStep }).change();
    expo_input_c.attr({ max: expoMax, min: expoMin, step: expoStep }).change();

    if (sameType) {
        self.setDirty(false);
    }
};

pid_tuning.changeRatesTypeLogo = function () {
    const self = this;

    const ratesLogoElement = $('.rates_type img[id="ratesLogo"]');

    switch (self.currentRatesType) {
        case FC.RATES_TYPE.RACEFLIGHT:
            ratesLogoElement.attr("src", "./images/rate_logos/raceflight.svg");

            break;

        case FC.RATES_TYPE.KISS:
            ratesLogoElement.attr("src", "./images/rate_logos/kiss.svg");

            break;

        case FC.RATES_TYPE.ACTUAL:
            ratesLogoElement.attr("src", "./images/rate_logos/actual.svg");

            break;

        case FC.RATES_TYPE.QUICKRATES:
            ratesLogoElement.attr("src", "./images/rate_logos/quickrates.svg");

            break;

        // add future rates types here
        default: // BetaFlight
            ratesLogoElement.attr("src", "./images/rate_logos/betaflight.svg");

            break;
    }
};

pid_tuning.expertModeChanged = function (expertModeEnabled) {
    TuningSliders.setExpertMode(expertModeEnabled);
};

pid_tuning.sliderOnScroll = function (slider) {
    slider.parent().on("input wheel", function (e) {
        if (slider.prop("disabled")) {
            return;
        }

        if (!(e.originalEvent?.deltaY && e.originalEvent?.altKey)) {
            return;
        }

        e.preventDefault();

        const step = parseFloat(slider.attr("step"));
        const delta = e.originalEvent.deltaY > 0 ? -step : step;
        const preScrollSliderValue = isInt(slider.val()) ? parseInt(slider.val()) : parseFloat(slider.val());
        const sliderValue = (Math.floor(preScrollSliderValue * 100) + Math.floor(delta * 100)) / 100;

        slider.val(sliderValue);
        slider.trigger("input");
        slider.trigger("change");
    });
};

TABS.pid_tuning = pid_tuning;
export { pid_tuning };
