"""
quibot.py — Programa principal del robot QuiBot H2O.
Inicialitza tots els mòduls, executa el homing i arrenca els threads.
Equivalent a QuiBot.ino del codi Arduino/ESP32.

Threads permanents:
  - task_read_blocks   → llegeix blocs i executa accions (aquest fitxer)
  - task_read_gestures → llegeix gestos i executa accions (aquest fitxer)
  - _stepper_loop      → genera polsos STEP dels motors (motion.py)
  - _task_update_leds  → parpelleig dels LEDs (eyes.py)
  - _poll_loop         → polling del sensor de gestos (gesture.py)
"""

import time
import threading
import signal
import sys
import pigpio

from motion import (
    motion_setup, motion_cleanup,
    arms_home, syringe_home,
    task_move_to, task_rotate, task_take_or_leave_something, task_idle,
    distance_to_object,
    CROSSING, TAKE, LEAVE, CW, CCW,
)
from blocks import (
    blocks_setup,
    read_block_color, servo_move_to,
    OPEN_POSITION, EJECT_POSITION,
    BK, RD, GN, BU, YE, OG, VT,
)
from eyes import (
    eyes_setup, eyes_cleanup,
    eyes_turn_on, eyes_turn_off,
    eyes_gesture_mode_on, eyes_gesture_mode_off,
    EYES_OPEN, EYES_FW, EYES_DOWN,
    RED, GREEN, BLUE, YELLOW, ORANGE, CYAN,
)
from gesture import (
    gesture_setup, gesture_cleanup,
    read_gesture,
    GS_NONE, GS_FORWARD, GS_LEFT, GS_RIGHT,
    GS_UP, GS_DOWN, GS_CLOCKWISE, GS_ANTICLOCKWISE, GS_WAVE,
)

# ==================
# Colors addicionals (equivalents CRGB del C++)
# ==================

GRAY     = (128, 128, 128)   # CRGB::Gray     → estat normal
DARK_RED = (139,   0,   0)   # CRGB::DarkRed  → avançar

# ==================
# Timeouts
# ==================

INSERT_BLOCK_MS = 2.0   # s — espera que l'infant insereixi el bloc
EJECT_BLOCK_MS  = 2.0   # s — espera que el bloc caigui
CHECK_COLOR_MS  = 1.0   # s — interval entre lectures de color

# ==================
# Estat global
# ==================

_pi: pigpio.pi = None

# Mutex que evita que tasca de blocs i tasca de gestos llancin accions simultànies.
# En el C++ original compartien TaskHandle sense mutex explícit (possible race condition).
# Aquí ho fem correctament.
_action_lock = threading.Lock()

_shutdown_event      = threading.Event()
_gesture_mode_active = False          # False = mode blocs, True = mode gestos
_mode_lock           = threading.Lock()


# ==================
# Helper d'execució d'accions
# ==================

def _execute_action(fn, *args):
    """
    Adquireix el lock d'acció, executa fn(*args) de forma bloquejant i l'allibera.
    Garanteix que mai s'executen dues accions de moviment simultànies.
    """
    with _action_lock:
        fn(*args)


# ==================
# Tasca de blocs
# ==================

def task_read_blocks():
    """
    Llegeix blocs contínuament, executa l'acció corresponent al color i expulsa el bloc.
    Equivalent a task_read_blocks() del FreeRTOS.
    """
    while not _shutdown_event.is_set():
        eyes_state = False
        eyes_turn_on(EYES_OPEN, GRAY)

        # Obre el servo per permetre la inserció del bloc
        servo_move_to(OPEN_POSITION)
        time.sleep(INSERT_BLOCK_MS)

        # Espera que hi hagi un bloc i llegeix el seu color
        color_id = BK
        while not _shutdown_event.is_set():
            if distance_to_object() < 80:
                # Objecte a menys de 80mm — esperem que es retiri
                if not eyes_state:
                    eyes_turn_on(EYES_DOWN, GRAY)
                    eyes_state = True
            else:
                color_id = read_block_color()
                if eyes_state:
                    eyes_turn_on(EYES_OPEN, GRAY, 1, False)
                    eyes_state = False
                if color_id != BK:
                    break
            time.sleep(CHECK_COLOR_MS)

        # Si estem en mode gestos, ignora el bloc
        with _mode_lock:
            if _gesture_mode_active:
                continue

        # Executa l'acció corresponent al color del bloc
        if color_id == RD:
            eyes_turn_on(EYES_FW, DARK_RED, 2)
            time.sleep(1.0)
            _execute_action(task_move_to, CROSSING)

        elif color_id == GN:
            eyes_turn_on(EYES_OPEN, GREEN, 2)
            time.sleep(1.0)
            _execute_action(task_rotate, CW)

        elif color_id == BU:
            eyes_turn_on(EYES_OPEN, BLUE, 2)
            time.sleep(1.0)
            _execute_action(task_rotate, CCW)

        elif color_id == YE:
            eyes_turn_on(EYES_OPEN, YELLOW, 2)
            time.sleep(1.0)
            _execute_action(task_take_or_leave_something, TAKE)

        elif color_id == OG:
            eyes_turn_on(EYES_OPEN, ORANGE, 2)
            time.sleep(1.0)
            _execute_action(task_take_or_leave_something, LEAVE)

        elif color_id == VT:
            _execute_action(task_idle)

        eyes_turn_on(EYES_OPEN, GRAY)

        # Expulsa el bloc
        servo_move_to(EJECT_POSITION)
        time.sleep(EJECT_BLOCK_MS)


# ==================
# Tasca de gestos
# ==================

def task_read_gestures():
    """
    Llegeix gestos contínuament.
    GS_WAVE activa/desactiva el mode gestos.
    Equivalent a task_read_gestures() del FreeRTOS.
    """
    gesture_mode_active = False

    while not _shutdown_event.is_set():
        gesture_id = read_gesture()

        # WAVE: toggle entre mode blocs i mode gestos
        if gesture_id == GS_WAVE:
            with _mode_lock:
                _gesture_mode_active = not _gesture_mode_active
                active = _gesture_mode_active
            if active:
                eyes_gesture_mode_on()
                print("Gesture mode ON")
            else:
                eyes_gesture_mode_off()
                print("Gesture mode OFF")
            time.sleep(1.0)
            continue

        with _mode_lock:
            active = _gesture_mode_active
        if not active or gesture_id == GS_NONE:
            time.sleep(0.1)
            continue

        # Executa l'acció corresponent al gest
        if gesture_id == GS_FORWARD:
            eyes_turn_on(EYES_FW, DARK_RED, 2)
            _execute_action(task_move_to, CROSSING)

        elif gesture_id == GS_RIGHT:
            eyes_turn_on(EYES_OPEN, GREEN, 2)
            _execute_action(task_rotate, CW)

        elif gesture_id == GS_LEFT:
            eyes_turn_on(EYES_OPEN, BLUE, 2)
            _execute_action(task_rotate, CCW)

        elif gesture_id == GS_UP:
            eyes_turn_on(EYES_OPEN, YELLOW, 2)
            _execute_action(task_take_or_leave_something, TAKE)

        elif gesture_id == GS_DOWN:
            eyes_turn_on(EYES_OPEN, ORANGE, 2)
            _execute_action(task_take_or_leave_something, LEAVE)

        elif gesture_id in (GS_CLOCKWISE, GS_ANTICLOCKWISE):
            _execute_action(task_idle)

        eyes_turn_on(EYES_OPEN, CYAN)
        time.sleep(0.5)


# ==================
# Shutdown
# ==================

def _shutdown(sig, frame):
    print("\nAturant QuiBot...")
    _shutdown_event.set()


# ==================
# Main
# ==================

def main():
    global _pi

    # Connecta amb pigpiod (ha d'estar en marxa amb: sudo pigpiod -s 1)
    _pi = pigpio.pi()
    if not _pi.connected:
        print("ERROR: No s'ha pogut connectar a pigpiod. Executa: sudo pigpiod -s 1")
        sys.exit(1)

    # Inicialitza tots els mòduls
    blocks_setup(_pi)
    motion_setup(_pi)
    eyes_setup(_pi)
    gesture_setup()

    # Homing (bloquejant)
    arms_home()
    syringe_home()

    # Registra els senyals de sortida
    signal.signal(signal.SIGINT,  _shutdown)
    signal.signal(signal.SIGTERM, _shutdown)

    # Arrenca els threads principals
    t_blocks   = threading.Thread(target=task_read_blocks,   daemon=True, name="blocks")
    t_gestures = threading.Thread(target=task_read_gestures, daemon=True, name="gestures")

    t_blocks.start()
    t_gestures.start()

    print("QuiBot llest.")

    # Espera senyal de sortida
    _shutdown_event.wait()

    # Cleanup ordenat
    motion_cleanup()
    eyes_cleanup()
    gesture_cleanup()
    _pi.stop()
    print("QuiBot aturat.")


if __name__ == "__main__":
    main()
