# pantalla_e213_v1.py - Controlador para display E-ink Heltec Vision Master E213 Version 1

from machine import Pin, SPI
import time

# ===== CARGA DE FUENTES =====
from fuente_8x8 import PATRONES as PATRONES_8x8
from fuente_12x16 import PATRONES_12x16

# ===== CONFIGURACIÓN DE HARDWARE =====
PIN_DC = 2
PIN_CS = 5
PIN_BUSY = 1
PIN_RST = 3
PIN_VEXT = 18

SPI_ID = 1
PIN_SCK = 4
PIN_MOSI = 6

ANCHO_FISICO = 122
ALTO_FISICO = 250

NEGRO = 1
BLANCO = 0

# ===============================================================
#               CLASE PANTALLA E213
# ===============================================================
class PantallaE213:
    def __init__(self, orientacion=0):

        self.orientacion = orientacion

        self.pin_dc = Pin(PIN_DC, Pin.OUT)
        self.pin_cs = Pin(PIN_CS, Pin.OUT)
        self.pin_busy = Pin(PIN_BUSY, Pin.IN)
        self.pin_rst = Pin(PIN_RST, Pin.OUT)
        self.pin_vext = Pin(PIN_VEXT, Pin.OUT)

        # SPI más rápido y estable
        self.spi = SPI(
            SPI_ID,
            baudrate=4000000,  # DOBLADO (funciona perfecto en ESP32S3)
            polarity=0,
            phase=0,
            bits=8,
            firstbit=SPI.MSB,
            sck=Pin(PIN_SCK),
            mosi=Pin(PIN_MOSI)
        )

        # Tamaño buffer
        self.bytes_por_fila = (ANCHO_FISICO + 7) // 8
        self.buffer_size = self.bytes_por_fila * ALTO_FISICO
        self.buffer = bytearray(self.buffer_size)

        # Dimensiones lógicas
        if orientacion == 0:
            self.ancho = ANCHO_FISICO
            self.alto = ALTO_FISICO
        elif orientacion == 90:
            self.ancho = ALTO_FISICO
            self.alto = ANCHO_FISICO
        else:
            raise ValueError("Orientación debe ser 0 o 90")

        # buffer para envío rápido de comando/dato
        self._buf1 = bytearray(1)

        self.inicializado = False

    # ===============================================================
    #                     FUNCIONES BAJO NIVEL
    # ===============================================================
    def enviar_comando(self, cmd):
        """Envío rápido de comando"""
        self._buf1[0] = cmd
        self.pin_dc.value(0)
        self.pin_cs.value(0)
        self.spi.write(self._buf1)
        self.pin_cs.value(1)

    def enviar_dato(self, dato):
        """Envío rápido de dato"""
        self._buf1[0] = dato
        self.pin_dc.value(1)
        self.pin_cs.value(0)
        self.spi.write(self._buf1)
        self.pin_cs.value(1)

    # ===============================================================
    def _esperar_listo(self, timeout=5000):
        start = time.ticks_ms()

        while True:
            if self.pin_busy.value() == 1:
                return True
            if time.ticks_diff(time.ticks_ms(), start) > timeout:
                return False
            time.sleep_ms(25)

    # ===============================================================
    #                     INICIALIZACIÓN
    # ===============================================================
    def inicializar(self):
        if self.inicializado:
            return

        self.pin_vext.value(1)
        time.sleep_ms(100)

        # Reset rápido
        self.pin_rst.value(0)
        time.sleep_ms(10)
        self.pin_rst.value(1)
        time.sleep_ms(100)

        self._esperar_listo(2000)

        self.enviar_comando(0x00) 
        self.enviar_dato(0x0F)

        self.enviar_comando(0x50)
        self.enviar_dato(0x97)

        self._cargar_luts()

        self.inicializado = True

    # ===============================================================

    def _cargar_luts(self):
        """Carga los LUTs reales del display LCMEN2R13EFC1 (parcial)."""

        LUT_VCOM = [
            0x01, 0x06, 0x03, 0x02, 0x01, 0x01, 0x01,
            0x01, 0x06, 0x02, 0x01, 0x01, 0x01, 0x01,
        ] + [0x00] * 42

        LUT_WW = [
            0x01, 0x06, 0x03, 0x02, 0x81, 0x01, 0x01,
            0x01, 0x06, 0x02, 0x01, 0x01, 0x01, 0x01,
        ] + [0x00] * 42

        LUT_BW = [
            0x01, 0x86, 0x83, 0x82, 0x81, 0x01, 0x01,
            0x01, 0x86, 0x82, 0x01, 0x01, 0x01, 0x01,
        ] + [0x00] * 42

        LUT_WB = [
            0x01, 0x46, 0x43, 0x02, 0x01, 0x01, 0x01,
            0x01, 0x46, 0x42, 0x01, 0x01, 0x01, 0x01,
        ] + [0x00] * 42

        LUT_BB = [
            0x01, 0x06, 0x03, 0x42, 0x41, 0x01, 0x01,
            0x01, 0x06, 0x02, 0x01, 0x01, 0x01, 0x01,
        ] + [0x00] * 42

        luts = {
            0x20: LUT_VCOM,
            0x21: LUT_WW,
            0x22: LUT_BW,
            0x23: LUT_WB,
            0x24: LUT_BB,
        }

        for cmd, tabla in luts.items():
            self.enviar_comando(cmd)
            for v in tabla:
                self.enviar_dato(v)

    # ===============================================================
    #                            DIBUJO
    # ===============================================================
    def dibujar_pixel(self, x, y, color=NEGRO):

        if x < 0 or y < 0 or x >= self.ancho or y >= self.alto:
            return

        # Transformación más rápida
        if self.orientacion == 0:
            xf, yf = x, y
        else:
            xf = y
            yf = ALTO_FISICO - 1 - x

        pos = yf * self.bytes_por_fila + (xf >> 3)
        mask = 1 << (7 - (xf & 7))

        if color:
            self.buffer[pos] |= mask
        else:
            self.buffer[pos] &= ~mask

    # ===============================================================
    def limpiar(self, color=BLANCO):
        v = 0xFF if color else 0x00
        self.buffer[:] = bytes([v]) * self.buffer_size

    # ===============================================================
    #                           TEXTO
    # ===============================================================
    def dibujar_texto(self, txt, x, y, color=NEGRO, tamaño=8):
        if tamaño == 8:
            for c in txt:
                self._char8(c, x, y, color)
                x += 8
        elif tamaño == 16:
            for c in txt:
                self._char16(c, x, y, color)
                x += 12

    def _char8(self, c, x, y, color):
        p = PATRONES_8x8.get(c)
        if not p:
            for i in range(8):
                self.dibujar_pixel(x, y+i, color)
                self.dibujar_pixel(x+7, y+i, color)
            for i in range(8):
                self.dibujar_pixel(x+i, y, color)
                self.dibujar_pixel(x+i, y+7, color)
            return

        for fy, fila in enumerate(p):
            for cx in range(8):
                if fila & (1 << (7 - cx)):
                    self.dibujar_pixel(x + cx, y + fy, color)

    def _char16(self, c, x, y, color):
        p = PATRONES_12x16.get(c)
        if not p:
            for i in range(12):
                self.dibujar_pixel(x+i, y, color)
                self.dibujar_pixel(x+i, y+15, color)
            for i in range(16):
                self.dibujar_pixel(x, y+i, color)
                self.dibujar_pixel(x+11, y+i, color)
            return

        for fy, fila in enumerate(p):
            for cx in range(12):
                if fila & (1 << (15 - cx)):
                    self.dibujar_pixel(x + cx, y + fy, color)

    # ===============================================================
    #                       LINEAS Y RECTANGULOS
    # ===============================================================

    #         Linea horizontal

    def dibujar_linea_horizontal(self, x, y, longitud, color=NEGRO):
        """Dibuja una línea horizontal optimizada (usa escritura por bytes)."""
        if y < 0 or y >= self.alto:
            return

        if x < 0:
            longitud += x
            x = 0
        if x + longitud > self.ancho:
            longitud = self.ancho - x
        if longitud <= 0:
            return

        # Recalculamos posiciones físicas según la orientación
        for i in range(longitud):
            self.dibujar_pixel(x + i, y, color)

    #               Linea vertical

    def dibujar_linea_vertical(self, x, y, longitud, color=NEGRO):
        """Dibuja una línea vertical optimizada."""
        if x < 0 or x >= self.ancho:
            return

        if y < 0:
            longitud += y
            y = 0
        if y + longitud > self.alto:
            longitud = self.alto - y
        if longitud <= 0:
            return

        for i in range(longitud):
            self.dibujar_pixel(x, y + i, color)

    #              Rectangulo

    def dibujar_rectangulo(self, x, y, ancho, alto, color=NEGRO, relleno=False):
        """Dibuja un rectángulo (con o sin relleno)."""
        if ancho <= 0 or alto <= 0:
            return

        if relleno:
            # Relleno por líneas horizontales (más rápido)
            for dy in range(alto):
                self.dibujar_linea_horizontal(x, y + dy, ancho, color)
        else:
            # Bordes (4 líneas)
            self.dibujar_linea_horizontal(x, y, ancho, color)
            self.dibujar_linea_horizontal(x, y + alto - 1, ancho, color)
            self.dibujar_linea_vertical(x, y, alto, color)
            self.dibujar_linea_vertical(x + ancho - 1, y, alto, color)

    # ===============================================================
    #                             ACTUALIZACIÓN
    # ===============================================================
    def actualizar(self):
        self.inicializar()

        # Power on
        self.enviar_comando(0x04)
        self._esperar_listo(3000)

        # OLD DATA (pantalla anterior)
        self.enviar_comando(0x10)
        self.pin_dc.value(1)
        self.pin_cs.value(0)
        self.spi.write(bytes([0xFF]) * len(self.buffer))  # SIEMPRE BLANCO
        self.pin_cs.value(1)

        # NEW DATA (imagen actual)
        self.enviar_comando(0x13)
        self.pin_dc.value(1)
        self.pin_cs.value(0)
        self.spi.write(self.buffer)
        self.pin_cs.value(1)

        # REFRESH
        self.enviar_comando(0x12)
        self._esperar_listo(10000)

        self.inicializado = False

    # ===============================================================
    def dormir(self):
        self.enviar_comando(0x02)
        self._esperar_listo(2000)
        self.enviar_comando(0x07)
        self.enviar_dato(0xA5)
        self.pin_vext.value(0)
        self.inicializado = False