import imgui
import glfw
import os
from OpenGL.GL import *
from imgui.integrations.glfw import GlfwRenderer
from threading import Thread, Lock
from get_hmd_info import get_software_version, get_board_serial, get_hmd_serial, get_left_oled_serial, get_right_oled_serial
from mic_test import connect_mic, get_mic_over_time
from fan_test import set_fan_speed, get_fan_speed
from duty_test import set_duty_cycle
from audio_test import listen_for_devices
from audio_test import check_for_headphones
from audio_test import play_audio
from rgb_led_test import set_rgb_led
from enum import Enum, auto
import time
import numpy as np
from localized_string_provider import get_string, StringKey, Language
from get_usbc_flip import get_usbc_flip, USBC_Flip_State
import subprocess
from mes_logger import mes_add
from mes_logger import get_firmware_version
import logging

custom_font = None
custom_font_small = None
custom_font_large = None
MAX_ATTEMPTS = 20

current_language = Language.CHINESE

# MES values
MES_SW_VER = "0.0.1"
uut_start = 0
uut_stop = 0

class MicState(Enum):
    Idle = auto()
    CouldntConnect = auto()
    Baseline = auto()
    Testing = auto()
    Failed = auto()
    Passed = auto()

class FanState(Enum):
    Idle = auto()
    CouldntConnect = auto()
    Running = auto()
    Failed = auto()
    Passed = auto()

class DutyState(Enum):
    Idle = auto()
    CouldntConnect = auto()
    Running = auto()
    Finished = auto()
    Passed = auto()
    Failed = auto()

class AudioState(Enum):
    Idle = auto()
    CouldntConnect = auto()
    Waiting = auto()
    Ready = auto()
    Finished = auto()
    Passed = auto()
    Failed = auto()

class RgbLedState(Enum):
    Idle = auto()
    CouldntConnect = auto()
    Running = auto()
    Finished = auto()
    Passed = auto()
    Failed = auto()

class DisplayPictureState(Enum):
    Idle = auto()
    CouldntConnect = auto()
    Running = auto()
    Finished = auto()
    Passed = auto()
    Failed = auto()

class VerticalStripeState(Enum):
    Idle = auto()
    CouldntConnect = auto()
    Running = auto()
    Waiting = auto()
    Closing = auto()
    Finished = auto()
    Passed = auto()
    Failed = auto()

class UsbFlipState(Enum):
    Idle = auto()
    CouldntConnect = auto()
    Running = auto()
    Finished = auto()
    Passed = auto()
    Failed = auto()

class mmi_gui:
    def __init__(self):
        self.software_version = ""
        self.serial_number = ""
        self.hmd_serial_number = ""
        self.left_oled_serial_number = ""
        self.right_oled_serial_number = ""
        self.hid_mutex = Lock()
        self.mic_mutex = Lock()
        self.running_swver = False
        self.running_serial = False
        self.mic_state = MicState.Idle
        self.mic_hits = 0
        self.fan_state = FanState.Idle
        self.current_fan_speed = 0
        self.fan_speed_results = [0,0,0,0]
        self.duty_state = DutyState.Idle
        self.current_duty_cycle = 0
        self.audio_state = AudioState.Idle
        self.rgb_led_state = RgbLedState.Idle
        self.display_picture_state = DisplayPictureState.Idle
        self.vertical_stripe_state = VerticalStripeState.Idle
        self.usb_flip_state = UsbFlipState.Idle
        self.testing_mode = False
        self.logger = logging.getLogger("MMI")
        self.file_handler = logging.FileHandler("MMI.log")
        self.yaml_fw_version = '0'  # FW ver read from MES YAML, should match HMD FW ver

    def get_software_version_runnable(self):
        self.hid_mutex.acquire() # don't use HID unless it's free
        try:
            self.software_version = get_software_version()
            self.logger.info("Firmware version: " + self.software_version)
        except:
            self.software_version = "no device"
        self.hid_mutex.release()
        self.running_swver = False

    def get_software_version(self):
        if(not self.running_swver):
            self.running_swver = True
            self.t_swver = Thread(target=self.get_software_version_runnable)
            self.t_swver.start()

    def get_serial_number_runnable(self):
        self.hid_mutex.acquire() # don't use HID unless it's free
        try:
            self.serial_number = get_board_serial()
            self.hmd_serial_number = get_hmd_serial()
            self.left_oled_serial_number = get_left_oled_serial()
            self.right_oled_serial_number = get_right_oled_serial()
            self.logger.info("Board serial: " + self.serial_number)
            self.logger.info("HMD serial: " + self.hmd_serial_number)
            self.logger.info("Left OLED serial: " + self.left_oled_serial_number)
            self.logger.info("Right OLED serial: " + self.right_oled_serial_number)
        except:
            self.serial_number = "no device"
        self.hid_mutex.release()
        self.running_serial = False

    def get_serial_number(self):
        if(not self.running_serial):
            self.running_serial = True
            self.t_serial = Thread(target=self.get_serial_number_runnable)
            self.t_serial.start()

    def mic_test_runnable(self):
        num_quiet_samples = 8
        sample_length = 200 # milliseconds
        test_length = 10 # seconds
        hit_threshold_multiplier = 10 # peak must be 10x the average quiet peak value to count

        self.mic_mutex.acquire()
        try:
            bmic = connect_mic()
            if(bmic is None):
                self.mic_state = MicState.CouldntConnect
                self.mic_mutex.release()
                return
        except:
            self.mic_state = MicState.CouldntConnect
            self.mic_mutex.release()
            return

        self.mic_state = MicState.Baseline
        # collect baseline samples. the quiet room.
        # records for about 2 seconds and averages 
        # the highest peaks
        quiet_samples_l = np.zeros((num_quiet_samples,))
        quiet_samples_r = np.zeros((num_quiet_samples,))
        for i in range(num_quiet_samples):
            mdat = get_mic_over_time(bmic, sample_length)
            quiet_samples_l[i] = np.max(np.power(mdat[0,:],2))
            quiet_samples_r[i] = np.max(np.power(mdat[1,:],2))

        avg_l = np.mean(quiet_samples_l)
        avg_r = np.mean(quiet_samples_r)

        self.mic_state = MicState.Testing
            
        start_time = time.monotonic_ns()
        stop_time = start_time + test_length*1e9 
        while(time.monotonic_ns() < stop_time):
            # grab a short segment of audio
            mdat = get_mic_over_time(bmic, sample_length)
            # find if peaks occurred
            lpeaks = np.mean(np.power(mdat[0,:].reshape(-1,48),2), 1) # downsample by averaging 48 samples
            rpeaks = np.mean(np.power(mdat[1,:].reshape(-1,48),2), 1)
            if((np.max(lpeaks) > avg_l*hit_threshold_multiplier) and (np.max(rpeaks) > avg_r*hit_threshold_multiplier)):
                self.mic_hits = self.mic_hits + 1
            if(self.mic_hits >= 5):
                self.mic_state = MicState.Passed
                self.mic_mutex.release()
                return
            
        self.mic_state = MicState.Failed
        self.mic_mutex.release()

    def run_mic_test(self):
        if(self.mic_state == MicState.Idle):
            self.t_mic = Thread(target=self.mic_test_runnable)
            self.t_mic.start()

    def set_and_get_fan(self, new_speed:int) -> int:
        set_fan_speed(new_speed)
        time.sleep(0.5) # wait for accel / decel
        speed_accumulator = 0
        for i in range(5):
            # collect 5 speed samples
            self.current_fan_speed = get_fan_speed(250)
            if(self.current_fan_speed >= 0): 
                # -1 is an error, so only count zero or positive values
                speed_accumulator = speed_accumulator + self.current_fan_speed
        return int(speed_accumulator / 5)

    def fan_test_runnable(self):
        self.fan_speed_results = [0,0,0,0]

        self.hid_mutex.acquire()
        try:
            # First set speed to zero (fan stopped)
            self.fan_state = FanState.Running
            self.fan_speed_results[0] = self.set_and_get_fan(0)
            if(self.fan_state == FanState.CouldntConnect):
                self.hid_mutex.release()
                return
            # Then try 3 different speeds
            self.fan_speed_results[1] = self.set_and_get_fan(20)
            if(self.fan_state == FanState.CouldntConnect):
                self.hid_mutex.release()
                return
            self.fan_speed_results[2] = self.set_and_get_fan(50)
            if(self.fan_state == FanState.CouldntConnect):
                self.hid_mutex.release()
                return
            self.fan_speed_results[3] = self.set_and_get_fan(80)
            if(self.fan_state == FanState.CouldntConnect):
                self.hid_mutex.release()
                return

            # Check if values are sane. Should probably set bounds, but for now
            # just make sure each higher power setting has a faster speed
            if((self.fan_speed_results[3] > self.fan_speed_results[2]) and 
            (self.fan_speed_results[2] > self.fan_speed_results[1]) and
            (self.fan_speed_results[1] > self.fan_speed_results[0])):
                self.fan_state = FanState.Passed
            else:
                self.fan_state = FanState.Failed

            set_fan_speed(0)
        except:
            self.fan_state = FanState.CouldntConnect
        self.hid_mutex.release()

    def duty_test_runnable(self):
        self.hid_mutex.acquire()
        self.duty_state = DutyState.Running
        # Start the displays using direct_mode_dx12.exe demo program
        os.environ["DM_VENDOR_ID"] = "0x2709" # set the desired EDID vendor ID to Bigscreen's

        # start direct mode with "dsc_present_test"
        dm_args = ['-dsc_present_test','0','0','1','0','0x11','4','128', '0xB88E2C']
        # other arguments:
        #   display 0 (which should be the only direct mode display attached, the Beyond)
        #   mode 0 (most likely 75Hz, unless an edid switch has been issued and then it would be 90Hz)
        #   present type 1 (event, rather than 2: queued) not sure what this does but choosing event works so ¯\_(ツ)_/¯
        #   notify selection 0 (not needed, other option is 2: needed) 
        #   DSC version 0x11 (version 1.1)
        #   DSC slice count 4
        #   DSC bits per pixel 128 (actually bits-per-pixel x16, so the real bpp is 8)
        #   RGB color (kinda gold'ish) of the background. Center triangle is always white
        subprocess_path = "directmode/direct_mode_dx12_color.exe"
        if os.path.exists("_Internal/") is True:
            subprocess_path = "_Internal/" + subprocess_path
        proc = subprocess.Popen([subprocess_path] + dm_args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                                stdin=subprocess.PIPE)
        # wait a moment for the displays to turn on
        time.sleep(5)
        try:
            # Set low duty cycle (10%)
            for attempt in range(MAX_ATTEMPTS):
                success = set_duty_cycle(100)
                if success:
                    break  # Exit the loop if successful
                elif attempt < MAX_ATTEMPTS - 1:
                    time.sleep(0.5)
                    pass
                else:
                    self.duty_state = DutyState.CouldntConnect
                    self.hid_mutex.release()
                    return
            time.sleep(3)
            # Set high duty cycle (50%)
            for attempt in range(MAX_ATTEMPTS):
                success = set_duty_cycle(512)
                if success:
                    break  # Exit the loop if successful
                elif attempt < MAX_ATTEMPTS - 1:
                    time.sleep(0.5)
                    pass
                else:
                    self.duty_state = DutyState.CouldntConnect
                    self.hid_mutex.release()
                    return
            time.sleep(3)
            # Set duty cycle back to default (26%)
            for attempt in range(MAX_ATTEMPTS):
                success = set_duty_cycle(266)
                if success:
                    break  # Exit the loop if successful
                elif attempt < MAX_ATTEMPTS - 1:
                    time.sleep(0.5)
                    pass
                else:
                    self.duty_state = DutyState.CouldntConnect
                    self.hid_mutex.release()
                    return
            time.sleep(3)
            self.duty_state = DutyState.Finished
        except:
            self.duty_state = DutyState.CouldntConnect

        # quit the process
        proc.communicate(b'q\n') # this also waits for the process to stop
        

        self.hid_mutex.release()

    def audio_test_runnable(self):
        self.hid_mutex.acquire()
        try:
            self.audio_state = AudioState.Waiting
            # Disabling the listen for headphones to be inserted, causes permission issues on CM PC
            # listen_for_devices()
            # while check_for_headphones() is False:
            #     # wait for headphones to be inserted
            #     pass
            self.audio_state = AudioState.Ready
        except:
            self.audio_state = AudioState.CouldntConnect
        self.hid_mutex.release()

    def rgb_led_test_runnable(self):
        self.hid_mutex.acquire()
        try:
            self.rgb_led_state = RgbLedState.Running
            set_rgb_led(255,0,0)
            time.sleep(1)
            set_rgb_led(0,255,0)
            time.sleep(1)
            set_rgb_led(0,0,255)
            time.sleep(1)
            self.rgb_led_state = RgbLedState.Finished
        except:
            self.rgb_led_state = RgbLedState.CouldntConnect
        self.hid_mutex.release()

    def display_picture_test_runnable(self):
        self.hid_mutex.acquire()
        os.environ["DM_VENDOR_ID"] = "0x2709" # set the desired EDID vendor ID to Bigscreen's
        dm_args_red = ['-dsc_present_test','0','0','1','0','0x11','4','128', '0xAA0000']
        try:
            self.display_picture_state = DisplayPictureState.Running

            subprocess_path = "directmode/direct_mode_dx12_color.exe"
            if os.path.exists("_Internal/") is True:
                subprocess_path = "_Internal/" + subprocess_path
            proc = subprocess.Popen([subprocess_path] + dm_args_red, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                        stdin=subprocess.PIPE)
            time.sleep(6)
            proc.stdin.write(b'G\n')
            proc.stdin.flush()
            time.sleep(2)
            proc.stdin.write(b'B\n')
            proc.stdin.flush()
            time.sleep(2)
            proc.communicate(b'q\n')
            # open_unity()
            # set_red_picture()
            # time.sleep(1)
            # set_green_picture()
            # time.sleep(1)
            # set_blue_picture()
            # time.sleep(1)
            # close_unity()
            self.display_picture_state = DisplayPictureState.Finished
        except:
            self.display_picture_state = DisplayPictureState.CouldntConnect
        self.hid_mutex.release()

    def vertical_stripe_test_runnable(self):
        self.hid_mutex.acquire()
        dm_args_vspattern = ['-vspattern']
        try:
            self.vertical_stripe_state = VerticalStripeState.Running

            subprocess_path = "directmode/direct_mode_dx12_color.exe"
            if os.path.exists("_Internal/") is True:
                subprocess_path = "_Internal/" + subprocess_path
            proc = subprocess.Popen([subprocess_path] + dm_args_vspattern, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                        stdin=subprocess.PIPE)
            # delay for startup
            time.sleep(5)
            # show the close button
            self.vertical_stripe_state = VerticalStripeState.Waiting

            while(self.vertical_stripe_state == VerticalStripeState.Waiting):
                time.sleep(0.005) # idle cycles while waiting for operator to click close
            proc.communicate(b'q\n')
            self.vertical_stripe_state = VerticalStripeState.Finished
        except:
            self.vertical_stripe_state = VerticalStripeState.CouldntConnect
        self.hid_mutex.release()

    def usb_flip_test_runnable(self):
        self.hid_mutex.acquire()
        self.usb_flip_state = UsbFlipState.Running
        has_been_flipped = False

        # First, find the starting state
        res = get_usbc_flip()

        # Wait for state to be different from starting state
        while has_been_flipped is False:
            flip_check = get_usbc_flip()
            if flip_check != res and flip_check != USBC_Flip_State.Invalid:
                has_been_flipped = True
            time.sleep(1)

        self.usb_flip_state = UsbFlipState.Passed
        self.hid_mutex.release()

    def run_fan_test(self):
        if(self.fan_state == FanState.Idle):
            self.t_fan = Thread(target = self.fan_test_runnable)
            self.t_fan.start()

    def run_duty_test(self):
        if(self.duty_state == DutyState.Idle):
            self.t_duty = Thread(target = self.duty_test_runnable)
            self.t_duty.start()

    def run_display_picture_test(self):
        if(self.display_picture_state == DisplayPictureState.Idle):
            self.t_display_picture = Thread(target = self.display_picture_test_runnable)
            self.t_display_picture.start()

    def run_vertical_stripe_test(self):
        if(self.vertical_stripe_state == VerticalStripeState.Idle):
            self.t_display_vs = Thread(target=self.vertical_stripe_test_runnable)
            self.t_display_vs.start()

    def run_audio_test(self):
        if(self.audio_state == AudioState.Idle):
            self.t_audio = Thread(target = self.audio_test_runnable)
            self.t_audio.start()

    def run_rgb_led_test(self):
        if(self.rgb_led_state == RgbLedState.Idle):
            self.t_rgb_led = Thread(target = self.rgb_led_test_runnable)
            self.t_rgb_led.start()

    def run_usb_flip_test(self):
        if(self.usb_flip_state == UsbFlipState.Idle):
            self.t_usb_flip = Thread(target = self.usb_flip_test_runnable)
            self.t_usb_flip.start()

    def run_gui(self):
        global uut_start, uut_stop
        
        # Font stack safety - ensure we start with a clean state
        font_stack_depth = 0
        
        try:
            imgui.push_font(custom_font)  # Add default font to base of stack
            font_stack_depth += 1

            if self.testing_mode:
                self.run_sequential_tests()

            imgui.push_font(custom_font_small)
            font_stack_depth += 1
            if(self.serial_number == "" or self.hmd_serial_number == "" or self.left_oled_serial_number == "" or self.right_oled_serial_number == ""):
                self.get_serial_number()
            if(self.software_version == ""):
                self.get_software_version()

            serial_string = get_string(StringKey.BOARD_SERIAL, current_language) + self.serial_number
            imgui.text_colored(serial_string,0.75,0.75,0.75,1)
            imgui.same_line()
            version_string = get_string(StringKey.SOFTWARE_VERSION, current_language) + self.software_version
            imgui.text_colored(version_string,0.75,0.75,0.75,1)
            imgui.same_line()
            hmd_serial_string = get_string(StringKey.HMD_SERIAL, current_language) + self.hmd_serial_number
            imgui.text_colored(hmd_serial_string,0.75,0.75,0.75,1)

            if self.yaml_fw_version == '0':
                self.yaml_fw_version = get_firmware_version()

            if self.yaml_fw_version != self.software_version and self.software_version != "no device" and self.software_version != "error":
                imgui.same_line()
                imgui.text_colored(get_string(StringKey.FW_VERSION_MISMATCH, current_language), 1, 0, 0, 1)

            if imgui.button(get_string(StringKey.REFRESH, current_language)):
                self.get_serial_number()
                self.get_software_version()

            imgui.pop_font()

            imgui.separator()

            if imgui.button("Start All") and not self.testing_mode:
                self.testing_mode = True
                uut_start = time.time()
                self.start_next_test()

            imgui.separator()

            if(self.mic_state == MicState.Idle):
                if(imgui.button(get_string(StringKey.RUN_MICROPHONE_TEST, current_language))):
                    self.run_mic_test()
            else:
                if(self.mic_state == MicState.Baseline or self.mic_state == MicState.Testing):
                    # give the GUI thread a rest so the mic thread can run
                    time.sleep(0.25)

                # mic test status or results
                if(self.mic_state == MicState.Baseline):
                    imgui.text_colored(get_string(StringKey.COLLECT_SAMPLES, current_language), 1, .7, .7, 1)
                if(self.mic_state == MicState.CouldntConnect):
                    imgui.text_colored(get_string(StringKey.ERROR_CONNECT, current_language),1,0,0,1)
                if(self.mic_state == MicState.Testing):
                    imgui.text_colored(get_string(StringKey.MAKE_NOISES, current_language), 1, 1, 0, 1)
                    imgui.text_colored(get_string(StringKey.HITS_RECEIVED, current_language) +
                                       ": {}".format(self.mic_hits), 1, 1, 0, 1)
                if(self.mic_state == MicState.Passed):
                    imgui.text_colored(get_string(StringKey.TEST_PASSED, current_language), 0, 1, 0, 1)
                if(self.mic_state == MicState.Failed):
                    imgui.text_colored(get_string(StringKey.TEST_FAILED, current_language), 1, .3, .3, 1)
                    imgui.same_line()
                    if imgui.button("Retry"):
                        self.mic_state = MicState.Idle
                        self.mic_hits = 0

            imgui.separator()

            if(self.fan_state == FanState.Idle):
                if(imgui.button(get_string(StringKey.RUN_FAN_TEST, current_language))):
                    self.run_fan_test()
            else:
                if(self.fan_state == FanState.CouldntConnect):
                    imgui.text_colored(get_string(StringKey.ERROR_CONNECT, current_language),1,0,0,1)
                else:
                    if(self.fan_state == FanState.Running):
                        imgui.text(get_string(StringKey.TEST_RUNNING, current_language))
                    imgui.push_font(custom_font_small)
                    imgui.begin_table("fan_results_table",5)
                    imgui.table_setup_column(get_string(StringKey.FAN_POWER, current_language))
                    imgui.table_setup_column("0%")
                    imgui.table_setup_column("20%")
                    imgui.table_setup_column("50%")
                    imgui.table_setup_column("80%")
                    imgui.table_headers_row()
                    imgui.table_next_row()
                    imgui.table_next_column()
                    imgui.text(get_string(StringKey.FAN_SPEED, current_language))
                    for i in range(4):
                        imgui.table_next_column()
                        imgui.text("{} rpm".format(self.fan_speed_results[i]))
                    imgui.end_table()
                    imgui.pop_font()
                    if(self.fan_state == FanState.Passed):
                        imgui.text_colored(get_string(StringKey.TEST_PASSED, current_language),.3,1,.3,1)
                    if(self.fan_state == FanState.Failed):
                        imgui.text_colored(get_string(StringKey.TEST_FAILED, current_language), 1, .3, .3, 1)
                        imgui.same_line()
                        if imgui.button(get_string(StringKey.RETRY, current_language)):
                            self.fan_state = FanState.Idle

            imgui.separator()

            if self.duty_state == DutyState.Idle:
                if imgui.button(get_string(StringKey.RUN_BRIGHTNESS_TEST, current_language)):
                    self.run_duty_test()
            else:
                if self.duty_state == DutyState.CouldntConnect:
                    imgui.text_colored(get_string(StringKey.ERROR_CONNECT, current_language), 1, 0, 0, 1)
                if self.duty_state == DutyState.Running:
                    imgui.text(get_string(StringKey.TEST_RUNNING, current_language))
                if self.duty_state == DutyState.Finished:
                    if not imgui.is_popup_open("Result"):
                        imgui.open_popup("Result")

                    if imgui.begin_popup_modal("Result"):
                        imgui.text(get_string(StringKey.BRIGHTNESS_CHANGE_PROMPT, current_language))

                        if imgui.button(get_string(StringKey.AFFIRMATIVE, current_language)):
                            imgui.close_current_popup()
                            self.duty_state = DutyState.Passed

                        elif imgui.button(get_string(StringKey.NEGATIVE, current_language)):
                            imgui.close_current_popup()
                            self.duty_state = DutyState.Failed

                        imgui.end_popup()
                if self.duty_state == DutyState.Passed:
                    imgui.text_colored(get_string(StringKey.TEST_PASSED, current_language), .3, 1, .3, 1)
                if self.duty_state == DutyState.Failed:
                    imgui.text_colored(get_string(StringKey.TEST_FAILED, current_language), 1, 0, 0, 1)
                    imgui.same_line()
                    if imgui.button(get_string(StringKey.RETRY, current_language)):
                        self.duty_state = DutyState.Idle

            imgui.separator()

            if(self.audio_state == AudioState.Idle):
                if(imgui.button(get_string(StringKey.RUN_AUDIO_TEST, current_language))):
                    self.run_audio_test()

                # Disabled warning to have headphones unplugged; listen_for_devices() causes permission issues on CM PC
                # imgui.push_font(custom_font_small)
                # imgui.same_line()
                # imgui.text_colored(get_string(StringKey.HEADPHONE_WARNING, current_language), 1, 0, 0, 1)
                # imgui.pop_font()
            else:
                if self.audio_state == AudioState.CouldntConnect:
                    imgui.text_colored(get_string(StringKey.ERROR_CONNECT, current_language), 1, 0, 0, 1)
                if(self.audio_state == AudioState.Waiting):
                    imgui.text(get_string(StringKey.INSERT_HEADPHONES, current_language))
                if(self.audio_state == AudioState.Ready):
                    imgui.text_colored(get_string(StringKey.AUDIO_TEST_READY, current_language), 1, 1, 0, 1)
                    if (imgui.button(get_string(StringKey.PLAY_AUDIO, current_language))):
                        try:
                            play_audio()
                            self.audio_state = AudioState.Finished
                        except Exception as e:
                            self.logger.error(f"Audio playback failed: {e}")
                            # Don't change state to Finished if audio failed
                            # Let user try again
                if(self.audio_state == AudioState.Finished):
                    if not imgui.is_popup_open("Result"):
                        imgui.open_popup("Result")

                    if imgui.begin_popup_modal("Result"):
                        imgui.text(get_string(StringKey.HEAR_AUDIO_PROMPT, current_language))

                        if imgui.button(get_string(StringKey.AFFIRMATIVE, current_language)):
                            imgui.close_current_popup()
                            self.audio_state = AudioState.Passed

                        elif imgui.button(get_string(StringKey.NEGATIVE, current_language)):
                            imgui.close_current_popup()
                            self.audio_state = AudioState.Failed

                        imgui.end_popup()
                if self.audio_state == AudioState.Passed:
                    imgui.text_colored(get_string(StringKey.TEST_PASSED, current_language), .3, 1, .3, 1)
                if self.audio_state == AudioState.Failed:
                    imgui.text_colored(get_string(StringKey.TEST_FAILED, current_language), 1, 0, 0, 1)
                    imgui.same_line()
                    if imgui.button(get_string(StringKey.RETRY, current_language)):
                        self.audio_state = AudioState.Idle

            imgui.separator()

            if(self.rgb_led_state == RgbLedState.Idle):
                if(imgui.button(get_string(StringKey.RUN_RGB_LED_TEST, current_language))):
                    self.run_rgb_led_test()
            else:
                if self.rgb_led_state == RgbLedState.CouldntConnect:
                    imgui.text_colored(get_string(StringKey.ERROR_CONNECT, current_language), 1, 0, 0, 1)
                if(self.rgb_led_state == RgbLedState.Running):
                    imgui.text(get_string(StringKey.TEST_RUNNING, current_language))
                if self.rgb_led_state == RgbLedState.Finished:
                    if not imgui.is_popup_open("Result"):
                        imgui.open_popup("Result")

                    if imgui.begin_popup_modal("Result"):
                        imgui.text(get_string(StringKey.LED_COLOR_PROMPT, current_language))

                        if imgui.button(get_string(StringKey.AFFIRMATIVE, current_language)):
                            imgui.close_current_popup()
                            self.rgb_led_state = RgbLedState.Passed

                        elif imgui.button(get_string(StringKey.NEGATIVE, current_language)):
                            imgui.close_current_popup()
                            self.rgb_led_state = RgbLedState.Failed

                        imgui.end_popup()
                if self.rgb_led_state == RgbLedState.Passed:
                        imgui.text_colored(get_string(StringKey.TEST_PASSED, current_language), .3, 1, .3, 1)
                if self.rgb_led_state == RgbLedState.Failed:
                        imgui.text_colored(get_string(StringKey.TEST_FAILED, current_language), 1, 0, 0, 1)
                        imgui.same_line()
                        if imgui.button(get_string(StringKey.RETRY, current_language)):
                            self.rgb_led_state = RgbLedState.Idle

            imgui.separator()

            if(self.display_picture_state == DisplayPictureState.Idle):
                if(imgui.button(get_string(StringKey.RUN_DISPLAY_PICTURE_TEST, current_language))):
                    self.run_display_picture_test()
            else:
                if self.display_picture_state == DisplayPictureState.CouldntConnect:
                    imgui.text_colored(get_string(StringKey.ERROR_CONNECT, current_language), 1, 0, 0, 1)
                if(self.display_picture_state == DisplayPictureState.Running):
                    imgui.text(get_string(StringKey.TEST_RUNNING, current_language))
                if(self.display_picture_state == DisplayPictureState.Finished):
                    if not imgui.is_popup_open("Result"):
                        imgui.open_popup("Result")

                    if imgui.begin_popup_modal("Result"):
                        imgui.text(get_string(StringKey.DISPLAY_COLOR_PROMPT, current_language))

                        if imgui.button(get_string(StringKey.AFFIRMATIVE, current_language)):
                            imgui.close_current_popup()
                            self.display_picture_state = DisplayPictureState.Passed

                        elif imgui.button(get_string(StringKey.NEGATIVE, current_language)):
                            imgui.close_current_popup()
                            self.display_picture_state = DisplayPictureState.Failed

                        imgui.end_popup()
                if self.display_picture_state == DisplayPictureState.Passed:
                    imgui.text_colored(get_string(StringKey.TEST_PASSED, current_language), .3, 1, .3, 1)
                if self.display_picture_state == DisplayPictureState.Failed:
                    imgui.text_colored(get_string(StringKey.TEST_FAILED, current_language), 1, 0, 0, 1)
                    imgui.same_line()
                    if imgui.button(get_string(StringKey.RETRY, current_language)):
                        self.display_picture_state = DisplayPictureState.Idle

            imgui.separator()

            if self.vertical_stripe_state == VerticalStripeState.Idle:
                if imgui.button(get_string(StringKey.RUN_VERTICAL_STRIPE_TEST, current_language)):
                    self.run_vertical_stripe_test()
            else:
                if self.vertical_stripe_state == VerticalStripeState.CouldntConnect:
                    imgui.text_colored(get_string(StringKey.ERROR_CONNECT, current_language), 1, 0, 0, 1)
                if self.vertical_stripe_state == VerticalStripeState.Running:
                    imgui.text(get_string(StringKey.TEST_RUNNING, current_language))
                if self.vertical_stripe_state == VerticalStripeState.Waiting:
                    if not imgui.is_popup_open("Waiting##VST"):
                        imgui.open_popup("Waiting##VST")
                    if imgui.begin_popup_modal("Waiting##VST"):
                        if imgui.button(get_string(StringKey.CLOSE_VS_TEST, current_language)):
                            imgui.close_current_popup()
                            self.vertical_stripe_state = VerticalStripeState.Closing
                        imgui.end_popup()
                if self.vertical_stripe_state == VerticalStripeState.Finished:
                    if not imgui.is_popup_open("Result"):
                        imgui.open_popup("Result")

                    if imgui.begin_popup_modal("Result"):
                        imgui.text(get_string(StringKey.VERTICAL_STRIPE_PROMPT, current_language))

                        if imgui.button(get_string(StringKey.NEGATIVE, current_language)):
                            imgui.close_current_popup()
                            self.vertical_stripe_state = VerticalStripeState.Passed

                        elif imgui.button(get_string(StringKey.AFFIRMATIVE, current_language)):
                            imgui.close_current_popup()
                            self.vertical_stripe_state = VerticalStripeState.Failed

                        imgui.end_popup()
                if self.vertical_stripe_state == VerticalStripeState.Passed:
                    imgui.text_colored(get_string(StringKey.TEST_PASSED, current_language), .3, 1, .3, 1)
                if self.vertical_stripe_state == VerticalStripeState.Failed:
                    imgui.text_colored(get_string(StringKey.TEST_FAILED, current_language), 1, 0, 0, 1)
                    imgui.same_line()
                    if imgui.button(get_string(StringKey.RETRY, current_language)):
                        self.vertical_stripe_state = VerticalStripeState.Idle

            imgui.separator()

            if(self.usb_flip_state == UsbFlipState.Idle):
                if imgui.button(get_string(StringKey.RUN_USB_FLIP_TEST, current_language)):
                    self.run_usb_flip_test()
            else:
                if self.usb_flip_state == UsbFlipState.CouldntConnect:
                    imgui.text_colored(get_string(StringKey.ERROR_CONNECT, current_language), 1, 0, 0, 1)
                if self.usb_flip_state == UsbFlipState.Running:
                    imgui.text(get_string(StringKey.FLIP_CABLE, current_language))
                if self.usb_flip_state == UsbFlipState.Passed:
                    imgui.text_colored(get_string(StringKey.TEST_PASSED, current_language), .3, 1, .3, 1)
                if self.usb_flip_state == UsbFlipState.Failed:
                    imgui.text_colored(get_string(StringKey.TEST_FAILED, current_language), 1, 0, 0, 1)
                    imgui.same_line()
                    if imgui.button(get_string(StringKey.RETRY, current_language)):
                        self.usb_flip_state = UsbFlipState.Idle

            imgui.separator()

            imgui.push_font(custom_font_small)
            if imgui.button(get_string(StringKey.RESET_ALL, current_language)):
                # Only allow reset if no tests are running
                if not self.hid_mutex.locked():
                    self.testing_mode = False
                    self.mic_hits = 0
                    self.mic_state = MicState.Idle
                    self.fan_state = FanState.Idle
                    self.duty_state = DutyState.Idle
                    self.audio_state = AudioState.Idle
                    self.rgb_led_state = RgbLedState.Idle
                    self.display_picture_state = DisplayPictureState.Idle
                    self.vertical_stripe_state = VerticalStripeState.Idle
                    self.usb_flip_state = UsbFlipState.Idle
                    self.send_mes_log()

            imgui.pop_font()

            imgui.pop_font()

        except Exception as e:
            self.logger.error(f"Critical GUI error: {e}")
            # The run_gui method already handles font stack balancing

    def load_ui(self):
        global custom_font, custom_font_large, custom_font_small
        window_width = 600
        window_height = 550
        if not glfw.init():
            print("Could not initialize OpenGL context")
            exit()
        glfw.window_hint(glfw.RESIZABLE, 0)
        glfw_window = glfw.create_window(window_width, window_height, "MMI Test", None, None)

        if not glfw_window:
            print("Could not initialize glfw window")
            exit()

        glfw.make_context_current(glfw_window)
        glViewport(0, 0, window_width, window_height)
        glEnable(GL_BLEND)
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
        glMatrixMode(GL_PROJECTION)
        glLoadIdentity()
        glOrtho(0, window_width, window_height, 0, 0, 1)
        imgui.create_context()
        imgui.get_io().display_size = [window_width, window_height]

        # Load assets
        asset_path = "assets/"
        if os.path.exists("_internal/") is True:  # If we're in a Pyinstaller build, find assets in the _internal folder
            asset_path = "_internal/" + asset_path

        custom_font = imgui.get_io().fonts.add_font_from_file_ttf(asset_path + "fonts/NotoSansSC-Regular.ttf", 32.0, None,
                                                    imgui.get_io().fonts.get_glyph_ranges_chinese())
        custom_font_small = imgui.get_io().fonts.add_font_from_file_ttf(asset_path + "fonts/NotoSansSC-Regular.ttf", 16.0, None,
                                                    imgui.get_io().fonts.get_glyph_ranges_chinese())
        custom_font_large = imgui.get_io().fonts.add_font_from_file_ttf(asset_path + "fonts/NotoSansSC-Regular.ttf", 48.0, None,
                                                    imgui.get_io().fonts.get_glyph_ranges_chinese())
        imgui.get_io().fonts.get_tex_data_as_rgba32()

        imgui_renderer = GlfwRenderer(glfw_window)

        while not glfw.window_should_close(glfw_window):
            glClearColor(1.0, 1.0, 1.0, 1.0)
            glClear(GL_COLOR_BUFFER_BIT)
            imgui.new_frame()
            imgui.set_next_window_size(imgui.get_io().display_size.x,
                                       imgui.get_io().display_size.y)
            imgui.set_next_window_position(0, 0)
            imgui.begin(get_string(StringKey.TEST_TITLE, current_language), True, imgui.WINDOW_NO_COLLAPSE | imgui.WINDOW_NO_RESIZE)

            try:
                self.run_gui()
            except Exception as e:
                self.logger.info(e)

            imgui.end()
            imgui.end_frame()
            imgui_renderer.process_inputs()
            imgui.render()
            imgui_renderer.render(imgui.get_draw_data())

            glfw.poll_events()
            if glfw.get_key(glfw_window, glfw.KEY_ESCAPE):
                break

            glfw.swap_buffers(glfw_window)

        glfw.terminate()

    def start_next_test(self):
        global uut_start, uut_stop
        if self.mic_state == MicState.Idle:
            self.run_mic_test()
        elif self.fan_state == FanState.Idle and self.mic_state == MicState.Passed:
            self.run_fan_test()
        elif self.duty_state == DutyState.Idle and self.fan_state == FanState.Passed:
            self.run_duty_test()
            time.sleep(1)
        elif self.audio_state == AudioState.Idle and self.duty_state == DutyState.Passed:
            self.run_audio_test()
        elif self.rgb_led_state == RgbLedState.Idle and self.audio_state == AudioState.Passed:
            self.run_rgb_led_test()
        elif self.display_picture_state == DisplayPictureState.Idle and self.rgb_led_state == RgbLedState.Passed:
            self.run_display_picture_test()
        elif self.vertical_stripe_state == VerticalStripeState.Idle and self.display_picture_state == DisplayPictureState.Passed:
            self.run_vertical_stripe_test()
        elif self.usb_flip_state == UsbFlipState.Idle and self.vertical_stripe_state == VerticalStripeState.Passed:
            self.run_usb_flip_test()
        elif self.usb_flip_state == UsbFlipState.Passed:
            self.testing_mode = False
            uut_stop = time.time()

            # Log final results to MES
            self.send_mes_log()

    def run_sequential_tests(self):
        global uut_start, uut_stop
        # If any test is running, wait for it to finish
        if self.mic_state in [MicState.Baseline, MicState.Testing] or \
                self.fan_state == FanState.Running or \
                self.duty_state in [DutyState.Running, DutyState.Finished] or \
                self.audio_state in [AudioState.Waiting, AudioState.Ready, AudioState.Finished] or \
                self.rgb_led_state in [RgbLedState.Running, RgbLedState.Finished] or \
                self.display_picture_state in [DisplayPictureState.Running, DisplayPictureState.Finished] or \
                self.vertical_stripe_state in [VerticalStripeState.Running, VerticalStripeState.Waiting, VerticalStripeState.Finished] or \
                self.usb_flip_state in [UsbFlipState.Running, UsbFlipState.Finished]:
            return

        if self.mic_state in [MicState.Passed, MicState.Failed]:
            self.start_next_test()
        elif self.fan_state in [FanState.Passed, FanState.Failed]:
            self.start_next_test()
        elif self.duty_state in [DutyState.Passed, DutyState.Failed]:
            self.start_next_test()
        elif self.audio_state in [AudioState.Passed, AudioState.Failed]:
            self.start_next_test()
        elif self.rgb_led_state in [RgbLedState.Passed, RgbLedState.Failed]:
            self.start_next_test()
        elif self.display_picture_state in [DisplayPictureState.Passed, DisplayPictureState.Failed]:
            self.start_next_test()
        elif self.vertical_stripe_state in [VerticalStripeState.Passed, VerticalStripeState.Failed]:
            self.start_next_test()
        elif self.usb_flip_state in [UsbFlipState.Passed, UsbFlipState.Failed]:
            self.testing_mode = False

    def initialize_logging(self):
        self.logger.setLevel(logging.INFO)
        self.logger.handlers = []
        self.file_handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
        self.logger.addHandler(self.file_handler)

    def send_mes_log(self):
        mes_string = ""
        if self.mic_state == MicState.Passed and \
                self.fan_state == FanState.Passed and \
                self.duty_state == DutyState.Passed and \
                self.audio_state == AudioState.Passed and \
                self.rgb_led_state == RgbLedState.Passed and \
                self.display_picture_state == DisplayPictureState.Passed and \
                self.vertical_stripe_state == VerticalStripeState.Passed and \
                self.usb_flip_state == UsbFlipState.Passed:
            mes_string = mes_add(self.hmd_serial_number, 'PASS', uut_start, uut_stop,
                                 MES_SW_VER)
        else:
            mes_string = mes_add(self.hmd_serial_number, 'FAIL', uut_start, uut_stop,
                                 MES_SW_VER)

        self.logger.info("Writing MES...")
        self.logger.info(mes_string)


if __name__ == '__main__':
    mgui = mmi_gui()
    mgui.initialize_logging()
    mgui.load_ui()
