"""
main.py

Starts the complete LED wave projector system.

Running this file will:

1. Start the motor at a constant speed.
2. Start the default LED animation.
3. Start the LCD joystick menu.
4. Switch LED animations whenever the user selects a new mode.

Run with:

    sudo python3 main.py

NeoPixels commonly require root access on Raspberry Pi.
"""

import threading
import time
import traceback

import GUI_controller
import LED_controller
import motor_controller

import atexit
import signal
import sys


# ============================================================
# PROJECT SETTINGS
# ============================================================

DEFAULT_LED_MODE = "Ocean"

MOTOR_DIRECTION = 1
MOTOR_SPEED = 0.45


# ============================================================
# LED ANIMATION STATE
# ============================================================

animation_lock = threading.Lock()

current_animation_thread = None
current_stop_event = None
current_mode = None

program_stopping = False


# ============================================================
# LED THREAD FUNCTIONS
# ============================================================

def animation_worker(mode_name, stop_event):
    """
    Run one LED animation inside a background thread.
    """

    try:
        LED_controller.run_mode(
            mode_name=mode_name,
            stop_event=stop_event
        )

    except Exception as error:
        print(f"LED animation error in mode '{mode_name}': {error}")
        traceback.print_exc()

        try:
            LED_controller.off()
        except Exception:
            pass

def emergency_shutdown(signum=None, frame=None):
    """
    Immediately stop the motor and LEDs when the program exits,
    the terminal closes, or systemd stops the service.
    """

    print("\nStopping hardware...")

    try:
        motor_controller.stop()
    except Exception as error:
        print(f"Could not stop motor: {error}")

    try:
        LED_controller.off()
    except Exception:
        pass

    if signum is not None:
        sys.exit(0)

def stop_current_animation():
    """
    Stop the currently running LED animation and wait briefly
    for its thread to exit.
    """

    global current_animation_thread
    global current_stop_event
    global current_mode

    thread_to_stop = None
    event_to_set = None

    with animation_lock:
        thread_to_stop = current_animation_thread
        event_to_set = current_stop_event

        current_animation_thread = None
        current_stop_event = None
        current_mode = None

    if event_to_set is not None:
        event_to_set.set()

    if (
        thread_to_stop is not None
        and thread_to_stop.is_alive()
        and thread_to_stop is not threading.current_thread()
    ):
        thread_to_stop.join(timeout=1.0)

        if thread_to_stop.is_alive():
            print(
                "Warning: previous LED animation did not "
                "stop within one second."
            )


def change_led_mode(mode_name):
    """
    Stop the old LED animation and start the selected one.

    This function is passed to GUI_controller.run_menu().
    """

    global current_animation_thread
    global current_stop_event
    global current_mode

    if program_stopping:
        return

    if mode_name not in LED_controller.MODE_FUNCTIONS:
        print(f"Unknown LED mode requested: {mode_name}")
        return

    with animation_lock:
        if (
            current_mode == mode_name
            and current_animation_thread is not None
            and current_animation_thread.is_alive()
        ):
            print(f"LED mode already playing: {mode_name}")
            return

    print(f"Changing LED mode to: {mode_name}")

    stop_current_animation()

    stop_event = threading.Event()

    animation_thread = threading.Thread(
        target=animation_worker,
        args=(mode_name, stop_event),
        name=f"LED-{mode_name}",
        daemon=True
    )

    with animation_lock:
        current_stop_event = stop_event
        current_animation_thread = animation_thread
        current_mode = mode_name

    animation_thread.start()


# ============================================================
# STARTUP
# ============================================================

def start_motor():
    """
    Start the motor at a constant speed.
    """

    print(
        f"Starting motor at speed {MOTOR_SPEED:.2f}, "
        f"direction {MOTOR_DIRECTION}"
    )

    motor_controller.run_motor(
        direction=MOTOR_DIRECTION,
        speed=MOTOR_SPEED,
        startup_boost=True
    )


def start_system():
    """
    Start all hardware systems.
    """

    print("=" * 50)
    print("LED Wave Projector")
    print("=" * 50)

    start_motor()

    print("Starting LCD menu")

    # run_menu immediately calls change_led_mode() with
    # DEFAULT_LED_MODE, so the default animation starts here.
    GUI_controller.run_menu(
        on_mode_selected=change_led_mode,
        initial_mode=DEFAULT_LED_MODE
    )


# ============================================================
# SHUTDOWN
# ============================================================

def cleanup():
    """
    Stop the motor, LEDs, LCD, and GPIO resources.
    """

    global program_stopping

    if program_stopping:
        return

    program_stopping = True

    print("\nShutting down LED wave projector...")

    try:
        stop_current_animation()
    except Exception as error:
        print(f"Error stopping LED animation: {error}")

    try:
        LED_controller.cleanup()
    except Exception as error:
        print(f"Error cleaning up LEDs: {error}")

    try:
        motor_controller.cleanup()
    except Exception as error:
        print(f"Error cleaning up motor: {error}")

    try:
        GUI_controller.cleanup()
    except Exception as error:
        print(f"Error cleaning up LCD menu: {error}")

    print("Shutdown complete")


# ============================================================
# MAIN
# ============================================================

def main():
    """
    Main entry point for the complete project.
    """

    try:
        start_system()

    except KeyboardInterrupt:
        print("\nCtrl+C received")

    except Exception as error:
        print(f"\nFatal project error: {error}")
        traceback.print_exc()

        try:
            GUI_controller.display_message("System error")
            time.sleep(1)
        except Exception:
            pass

    finally:
        cleanup()


if __name__ == "__main__":
    signal.signal(signal.SIGINT, emergency_shutdown)
    signal.signal(signal.SIGTERM, emergency_shutdown)
    signal.signal(signal.SIGHUP, emergency_shutdown)

    atexit.register(emergency_shutdown)
    main()
