import time
import network
import ntptime
import os
from machine import Pin, SPI, SoftSPI, ADC, PWM, SDCard, RTC
from st7789 import Display, color565 as _color565

# ---------------------------------------------------------------------------
# Couleurs prédéfinies (module-level, utilisables sans instance)
# ---------------------------------------------------------------------------
BLACK   = 0x0000
WHITE   = 0xFFFF
RED     = _color565(255, 0,   0)
GREEN   = _color565(0,   255, 0)
BLUE    = _color565(0,   0,   255)
ORANGE  = _color565(255, 140, 0)
CYAN    = _color565(0,   255, 255)
YELLOW  = _color565(255, 220, 0)
MAGENTA = _color565(255, 0,   255)
GRAY    = _color565(128, 128, 128)
DARK    = _color565(32,  32,  32)

class CYD:
    # -----------------------------------------------------------------------
    # Constantes couleurs accessibles via CYD.BLACK, cyd.RED, etc.
    # -----------------------------------------------------------------------
    BLACK   = BLACK
    WHITE   = WHITE
    RED     = RED
    GREEN   = GREEN
    BLUE    = BLUE
    ORANGE  = ORANGE
    CYAN    = CYAN
    YELLOW  = YELLOW
    MAGENTA = MAGENTA
    GRAY    = GRAY
    DARK    = DARK

    def __init__(self):
        # 1. Écran ST7789 (Pins 14/13/12) — HSPI matériel
        self.spi_tft = SPI(1, baudrate=40000000, sck=Pin(14), mosi=Pin(13), miso=Pin(12))
        self.display = Display(self.spi_tft, cs=Pin(15), dc=Pin(2), rst=-1, rotation=270, x_offset=0)

        # 2. Tactile XPT2046 (Pins 25/32/39/33) — SoftSPI pour éviter les conflits
        from xpt2046 import Touch
        self.spi_touch = SoftSPI(baudrate=1000000, sck=Pin(25), mosi=Pin(32), miso=Pin(39))
        self.touch = Touch(self.spi_touch, cs=Pin(33), width=320, height=240, swap_xy=True, invert_x=False)

        # 3. Rétroéclairage PWM (Pin 21)
        self.bl_pwm = PWM(Pin(21), freq=1000)
        self._brightness = 80
        self.backlight(self._brightness)
        
        # --- Ponts d'affichage (Display Bridges) ---
        self.fill_hrect = self.display.fill_hrect
        self.draw_text  = self.display.draw_text
        self.blit       = self.display.blit
        self.clear      = self.display.clear
        self.write_cmd  = self.display.write_cmd


        # 4. LED RGB (Active LOW — 1 = éteint, 0 = allumé)
        # ⚠️ IMPORTANT : Pin 4 est PARTAGÉE entre la LED Rouge et l'amplificateur audio.
        # Ne jamais créer un second objet Pin(4) — utiliser self.red pour les deux rôles.
        self.red   = Pin(4,  Pin.OUT, value=1)  # aussi amp_en (1 = shutdown)
        self.green = Pin(16, Pin.OUT, value=1)
        self.blue  = Pin(17, Pin.OUT, value=1)

        # 5. Capteur de Lumière LDR (Pin 34)
        self.ldr = ADC(Pin(34))
        self.ldr.atten(ADC.ATTN_11DB)

        # 6. Audio (Géré à la demande dans beep() pour la fiabilité)
        self.speaker = None


        # 7. WiFi
        self.wlan = network.WLAN(network.STA_IF)

    # -----------------------------------------------------------------------
    # LED RGB
    # -----------------------------------------------------------------------
    def set_led(self, r=0, g=0, b=0):
        """Contrôle la LED RGB intégrée (Active Low).
        Paramètres : r, g, b — 1 = allumé, 0 = éteint."""
        self.red.value(0 if r else 1)
        self.green.value(0 if g else 1)
        self.blue.value(0 if b else 1)

    # -----------------------------------------------------------------------
    # Capteur de lumière
    # -----------------------------------------------------------------------
    def get_light(self):
        """Lit la valeur brute du LDR (0 = sombre, 4095 = lumineux)."""
        return self.ldr.read()

    # -----------------------------------------------------------------------
    # Rétroéclairage & veille
    # -----------------------------------------------------------------------
    def backlight(self, percent):
        """Règle la luminosité de 0 (éteint) à 100 (maximum)."""
        self._brightness = max(0, min(100, percent))
        self.bl_pwm.duty(int(self._brightness * 1023 / 100))

    def display_sleep(self):
        """Veille matérielle : envoie DISPOFF au ST7789 et coupe le PWM.
        Plus économique que backlight(0) seul."""
        self.display.write_cmd(0x28)  # DISPOFF
        self.bl_pwm.duty(0)

    def display_wake(self, brightness=None):
        """Réveille l'écran (DISPON + restore luminosité).
        brightness : si fourni, fixe une nouvelle luminosité (0-100)."""
        self.display.write_cmd(0x29)  # DISPON
        if brightness is not None:
            self.backlight(brightness)
        else:
            self.backlight(self._brightness)

    # -----------------------------------------------------------------------
    # Audio
    # -----------------------------------------------------------------------
    def beep(self, freq=1000, duration=100):
        """Audio desactive (pas de haut-parleur)."""
        pass

    def play_click(self):
        """Audio desactive (pas de haut-parleur)."""
        pass

    # -----------------------------------------------------------------------
    # WiFi & NTP
    # -----------------------------------------------------------------------
    def scan_wifi(self):
        """Scanne les réseaux alentours. Retourne list[(ssid, rssi)]."""
        self.wlan.active(True)
        networks = self.wlan.scan()
        return [(n[0].decode('utf-8'), n[3]) for n in networks]

    def connect_wifi(self, ssid, password, timeout=15):
        """Se connecte au réseau WiFi. Retourne ifconfig() ou None."""
        self.wlan.active(True)
        if not self.wlan.isconnected():
            print(f"Connexion a {ssid}...")
            self.wlan.connect(ssid, password)
            start = time.time()
            while not self.wlan.isconnected() and (time.time() - start) < timeout:
                time.sleep(1)
        return self.wlan.ifconfig() if self.wlan.isconnected() else None

    def sync_ntp(self, timezone=2):
        """Synchronise l'horloge interne via NTP (nécessite WiFi).
        timezone : décalage en heures (2 = France été, 1 = France hiver)."""
        if not self.wlan.isconnected():
            print("Erreur NTP : WiFi non connecte")
            return False
        try:
            print("Synchronisation NTP...")
            ntptime.settime()
            return True
        except Exception as e:
            print(f"Erreur NTP : {e}")
            return False

    def get_time_string(self, timezone=2):
        """Retourne l'heure au format 'HH:MM:SS' avec correction de fuseau."""
        t = time.time() + (timezone * 3600)
        tm = time.localtime(t)
        return "{:02d}:{:02d}:{:02d}".format(tm[3], tm[4], tm[5])

    # -----------------------------------------------------------------------
    # Carte SD
    # -----------------------------------------------------------------------
    def mount_sd(self):
        """Monte la carte SD sur /sd (bus VSPI, Slot 2 — Pins 18/19/23/5).
        Retourne la liste des fichiers ou None en cas d'échec."""
        try:
            sd = SDCard(slot=2)
            vfs = os.VfsFat(sd)
            os.mount(vfs, "/sd")
            print("Carte SD montee avec succes sur /sd")
            return os.listdir("/sd")
        except Exception as e:
            if "OSRETR_EXIST" in str(e) or "EEXIST" in str(e):
                return os.listdir("/sd")
            print(f"Erreur SD : {e}")
            self.display.draw_text(5, 5, "SD ERROR: " + str(e)[:20], WHITE)
            return None

    # -----------------------------------------------------------------------
    # Affichage — BMP
    # -----------------------------------------------------------------------
    def draw_bmp(self, path, x=0, y=0):
        """Affiche un fichier BMP 24-bit. Silencieux si absent."""
        try:
            with open(path, "rb") as f:
                if f.read(2) != b"BM":
                    return
                f.seek(10)
                pixel_offset = int.from_bytes(f.read(4), "little")
                f.seek(18)
                width  = int.from_bytes(f.read(4), "little")
                height = int.from_bytes(f.read(4), "little")
                f.seek(28)
                bits = int.from_bytes(f.read(2), "little")
                if bits != 24:
                    print(f"Erreur : Seul le BMP 24-bit est supporte")
                    return
                row_size = (width * 3 + 3) & ~3
                f.seek(pixel_offset)
                line_buf = bytearray(width * 2)
                print(f"Affichage BMP {width}x{height}...")
                for row in range(height):
                    curr_y = y + height - 1 - row
                    if curr_y < 0 or curr_y >= self.display.height:
                        f.seek(row_size, 1)
                        continue
                    raw_data = f.read(row_size)
                    for col in range(width):
                        b = raw_data[col * 3]
                        g = raw_data[col * 3 + 1]
                        r = raw_data[col * 3 + 2]
                        res = (r & 0xf8) << 8 | (g & 0xfc) << 3 | b >> 3
                        line_buf[col * 2]     = (res >> 8) & 0xFF
                        line_buf[col * 2 + 1] = res & 0xFF
                    self.display.blit(x, curr_y, width, 1, line_buf)
        except:
            pass # Fichier absent ou corrompu

    # -----------------------------------------------------------------------
    # Affichage — RAW RGB565
    # -----------------------------------------------------------------------
    def draw_raw(self, path, x=0, y=0, w=320, h=240, trans_color=None):
        """Affiche un fichier binaire brut RGB565 Big-Endian.

        trans_color : couleur RGB565 traitée comme transparente (ex: 0x0000).
                      Les pixels de cette couleur exacte sont ignorés (fond visible).
                      Utilisez img_to_raw.py pour générer des fichiers compatibles.
        """
        try:
            # Vérification de la taille du fichier
            expected = w * h * 2
            try:
                actual = os.stat(path)[6]
                if actual != expected:
                    print(f"Erreur RAW: taille {actual}o != attendu {expected}o ({w}x{h})")
                    return
            except OSError:
                pass  # os.stat peut échouer sur certains systèmes de fichiers

            with open(path, "rb") as f:
                # Mode opaque : chunks de 30 lignes pour minimiser les appels f.read/blit
                # Mode transparent : 1 ligne à la fois (logique chroma key)
                lines_per_chunk = 1 if trans_color is not None else 30

                # Mode opaque : chunks de 30 lignes pour minimiser les appels f.read/blit
                # Mode transparent : 1 ligne à la fois (logique chroma key)
                lines_per_chunk = 1 if trans_color is not None else 30

                for curr_y in range(y, y + h, lines_per_chunk):
                    actual_lines = min(lines_per_chunk, y + h - curr_y)
                    data = f.read(w * 2 * actual_lines)
                    if not data:
                        break

                    if trans_color is None:
                        # Mode opaque rapide — un seul blit par chunk
                        self.display.blit(x, curr_y, w, actual_lines, data)
                    else:
                        # Mode chroma key — segments continus de pixels visibles
                        # memoryview évite les copies mémoire pour chaque segment
                        mv = memoryview(data)
                        start_idx = -1
                        for col in range(w):
                            idx = col * 2
                            color = (data[idx] << 8) | data[idx + 1]
                            is_trans = (color == trans_color)

                            if not is_trans and start_idx == -1:
                                start_idx = idx
                            elif is_trans and start_idx != -1:
                                seg_w = (idx - start_idx) // 2
                                self.display.blit(x + (start_idx // 2), curr_y, seg_w, 1, mv[start_idx:idx])
                                start_idx = -1

                        # Dernier segment si la ligne se termine par du visible
                        if start_idx != -1:
                            seg_w = (w * 2 - start_idx) // 2
                            self.display.blit(x + (start_idx // 2), curr_y, seg_w, 1, mv[start_idx:])
        except Exception as e:
            print(f"Erreur RAW ({path}) : {e}")


    def draw_raw_sd(self, filename, x=0, y=0, w=320, h=240, trans_color=None):
        """Affiche un asset RAW embarqué dans la flash en le synchronisant vers la SD.

        filename  : nom du fichier SANS chemin (ex: 'icon.raw').
                    Cherche dans '/<filename>' (flash) et copie vers '/sd/<filename>'.
        Copie uniquement si le fichier est absent ou de taille différente.
        """
        flash_path = "/" + filename
        sd_path    = "/sd/" + filename
        try:
            flash_size = os.stat(flash_path)[6]
            try:
                sd_size = os.stat(sd_path)[6]
                need_copy = (sd_size != flash_size)
            except OSError:
                need_copy = True
            if need_copy:
                print(f"Sync {flash_path} -> {sd_path}...")
                with open(flash_path, "rb") as s, open(sd_path, "wb") as d:
                    d.write(s.read())
                print("Sync OK.")
        except OSError as e:
            print(f"draw_raw_sd: flash introuvable ({e}), tentative SD directe.")
        self.draw_raw(sd_path, x, y, w, h, trans_color)

    # -----------------------------------------------------------------------
    # Couleurs
    # -----------------------------------------------------------------------
    @staticmethod
    def color565(r, g, b):
        """Convertit une couleur RGB888 en valeur RGB565 16-bit.
        Disponible aussi comme fonction module : from cyd import color565"""
        return _color565(r, g, b)


# Expose color565 directement au niveau module pour import pratique :
# from cyd import color565
color565 = _color565
