"""OLED display manager for the Pi Multi-FX Pedal.

Controls a 128x64 SSD1306 OLED via I2C to show:
- Current preset name, bank, and status
- Bypass status and tuner mode
- Active FX chain with per-block status
- Parameter values on edit
"""

from __future__ import annotations

import logging
from dataclasses import dataclass, field
from typing import Optional

logger = logging.getLogger(__name__)

# Display dimensions
DISPLAY_W = 128
DISPLAY_H = 64

# Layout constants
MARGIN = 2
LINE_H = 10        # 8px font + 2px spacing
HEADER_H = 10      # Top status bar height
FOOTER_Y = 56      # Bottom status line Y

# Font sizes for PIL
FONT_SMALL = 8
FONT_NORMAL = 10
FONT_LARGE = 16

# Tuner display geometry
TUNER_CENTER_X = 64
TUNER_CENTER_Y = 32
TUNER_NOTE_RADIUS = 20
TUNER_NOTE_FONT = 24
TUNER_CENT_FONT = 10


@dataclass
class DisplayState:
    """What to show on the display."""
    mode: str = "preset"        # preset, tuner, fx_edit, settings
    preset_name: str = ""
    bank_name: str = ""
    bypassed: bool = False
    bypass_led_state: bool = False
    fx_active: list[str] = field(default_factory=list)
    fx_bypass_states: dict[str, bool] = field(default_factory=dict)
    tuner_note: str = ""
    tuner_cents: int = 0
    param_name: str = ""
    param_value: float = 0.0


def _import_display_driver():
    """Import the SSD1306 display library gracefully."""
    try:
        import board
        import busio
        import adafruit_ssd1306
        from PIL import Image, ImageDraw, ImageFont
        return (adafruit_ssd1306, Image, ImageDraw, ImageFont)
    except (ImportError, NotImplementedError):
        return None


class DisplayController:
    """Manages the OLED display.

    In production, uses adafruit-circuitpython-ssd1306 over I2C with PIL.
    In dev/testing, logs display output.
    """

    def __init__(self, i2c_bus: int = 1, i2c_addr: int = 0x3C):
        self._i2c_bus = i2c_bus
        self._i2c_addr = i2c_addr
        self._state = DisplayState()
        self._initialized = False

        # Hardware handles
        self._display = None
        self._image: Optional["Image"] = None
        self._draw: Optional["ImageDraw"] = None
        self._fonts: dict[str, "ImageFont"] = {}

        # Display behaviour settings
        self._auto_dim = True
        self._screen_saver = "5m"

    def initialize(self) -> bool:
        """Initialize the OLED display hardware.

        Returns False if no display is connected (dev mode).
        """
        try:
            driver_pkg = _import_display_driver()
            if driver_pkg is None:
                self._initialized = False
                logger.info("Display libraries not available — running in headless mode")
                return False

            adafruit_ssd1306, Image, ImageDraw, ImageFont = driver_pkg

            import board
            import busio

            i2c = busio.I2C(board.SCL, board.SDA)
            self._display = adafruit_ssd1306.SSD1306_I2C(
                DISPLAY_W, DISPLAY_H, i2c, addr=self._i2c_addr
            )

            # Create an image buffer
            self._image = Image.new("1", (DISPLAY_W, DISPLAY_H))
            self._draw = ImageDraw.Draw(self._image)

            # Load default font — use PIL default bitmap font
            self._fonts["small"] = ImageFont.load_default()
            self._fonts["normal"] = ImageFont.load_default()
            self._fonts["large"] = ImageFont.load_default()

            self._initialized = True
            logger.info("Display initialized (128x64 OLED @ 0x%02x)", self._i2c_addr)

            # Show splash
            self._splash()
            return True

        except Exception as e:
            logger.info("No display detected — running in headless mode: %s", e)
            self._initialized = False
            return False

    def update(self, state: DisplayState) -> None:
        """Update the display with new state and re-render."""
        self._state = state
        if not self._initialized:
            logger.debug(
                "Display [%s]: %s | bypass=%s | FX=%s | tuner=%s %+d",
                state.mode,
                state.preset_name or state.bank_name or "",
                "BYP" if state.bypassed else "ON",
                ", ".join(state.fx_active) or "—",
                state.tuner_note or "—",
                state.tuner_cents,
            )
        else:
            self._render()

    def _render(self) -> None:
        """Render current state to the OLED using the PIL buffer."""
        if not self._initialized or self._draw is None or self._image is None:
            return

        draw = self._draw
        img = self._image

        # Clear the buffer (white background — SSD1306 white=1)
        draw.rectangle((0, 0, DISPLAY_W - 1, DISPLAY_H - 1), fill=0, outline=0)

        if self._state.mode == "tuner":
            self._render_tuner(draw)
        elif self._state.mode == "preset":
            self._render_preset(draw)
        elif self._state.mode == "fx_edit":
            self._render_fx_edit(draw)
        elif self._state.mode == "settings":
            self._render_settings(draw)
        else:
            self._render_preset(draw)

        # Blit to hardware
        self._display.image(img)
        self._display.show()

    # --- Rendering modes ---

    def _render_preset(self, draw) -> None:
        """Render preset mode layout."""
        s = self._state
        font = self._fonts.get("normal")

        # Header bar — bank name
        bank_text = f"[{s.bank_name}]" if s.bank_name else ""
        draw.text((MARGIN, MARGIN), bank_text, fill=255, font=font)

        # Bypass indicator top-right
        bypass_text = "BYP" if s.bypassed else "ACTIVE"
        bypass_x = DISPLAY_W - MARGIN - (len(bypass_text) * 6)
        draw.text((bypass_x, MARGIN), bypass_text, fill=255, font=font)

        # Preset name — large, centered-ish
        preset_y = HEADER_H + 4
        draw.text((MARGIN, preset_y), s.preset_name or "—", fill=255, font=self._fonts.get("large"))

        # Active FX chain (wrap if needed)
        fx_y = preset_y + 20
        if s.fx_active:
            parts = []
            for fx in s.fx_active:
                bypassed = s.fx_bypass_states.get(fx, False)
                if bypassed:
                    parts.append(f"[{fx}]")
                else:
                    parts.append(fx)
            fx_line = " ".join(parts)
            # Truncate to fit display width
            max_chars = DISPLAY_W // 6
            if len(fx_line) > max_chars:
                fx_line = fx_line[:max_chars - 3] + "..."
            draw.text((MARGIN, fx_y), fx_line, fill=255, font=font)

        # Footer — preset number or info
        draw.text((MARGIN, FOOTER_Y), s.mode.upper(), fill=255, font=font)

    def _render_tuner(self, draw) -> None:
        """Render tuner mode — note name + cents indicator."""
        s = self._state
        font_normal = self._fonts.get("normal")

        # "TUNER" header
        draw.text((MARGIN, MARGIN), "TUNER", fill=255, font=font_normal)

        # Large note name in center
        note = s.tuner_note or "--"
        draw.text(
            (TUNER_CENTER_X - len(note) * 7, TUNER_CENTER_Y - 8),
            note,
            fill=255,
            font=self._fonts.get("large"),
        )

        # Cents indicator bar
        cents = max(-50, min(50, s.tuner_cents))
        bar_center_x = TUNER_CENTER_X
        bar_y = TUNER_CENTER_Y + 16
        bar_w = 40
        bar_h = 4

        # Background bar
        draw.rectangle(
            (bar_center_x - bar_w // 2, bar_y, bar_center_x + bar_w // 2, bar_y + bar_h),
            fill=0, outline=255,
        )

        # Indicator position
        pos = int((cents + 50) / 100 * bar_w) - bar_w // 2
        indicator_x = bar_center_x + pos
        draw.rectangle(
            (indicator_x - 2, bar_y - 1, indicator_x + 2, bar_y + bar_h + 1),
            fill=255,
        )

        # Cents text
        draw.text(
            (MARGIN, FOOTER_Y),
            f"{cents:+d} cents",
            fill=255,
            font=font_normal,
        )

    def _render_fx_edit(self, draw) -> None:
        """Render FX parameter edit mode."""
        s = self._state
        font = self._fonts.get("normal")

        draw.text((MARGIN, MARGIN), f"EDIT: {s.param_name}", fill=255, font=font)

        # Parameter value bar
        val = max(0.0, min(1.0, s.param_value))
        bar_x = MARGIN
        bar_y = 20
        bar_w = DISPLAY_W - 2 * MARGIN
        bar_h = 8

        draw.rectangle((bar_x, bar_y, bar_x + bar_w, bar_y + bar_h), fill=0, outline=255)
        fill_w = int(bar_w * val)
        draw.rectangle((bar_x, bar_y, bar_x + fill_w, bar_y + bar_h), fill=255)

        # Value text
        draw.text((MARGIN, bar_y + 12), f"{val:.2f}", fill=255, font=font)

    def _render_settings(self, draw) -> None:
        """Render settings mode."""
        s = self._state
        font = self._fonts.get("normal")
        draw.text((MARGIN, MARGIN), "SETTINGS", fill=255, font=font)
        draw.text((MARGIN, 20), s.param_name or "", fill=255, font=font)

    def _splash(self) -> None:
        """Show boot splash screen."""
        if not self._initialized or self._draw is None or self._image is None:
            return
        draw = self._draw
        draw.rectangle((0, 0, DISPLAY_W - 1, DISPLAY_H - 1), fill=0)
        draw.text((24, 24), "Pi Multi-FX", fill=255, font=self._fonts.get("large"))
        draw.text((28, 44), "Booting...", fill=255, font=self._fonts.get("normal"))
        self._display.image(self._image)
        self._display.show()

    def clear(self) -> None:
        """Clear the display."""
        if self._initialized and self._display is not None:
            self._display.fill(0)
            self._display.show()

    def set_auto_dim(self, enabled: bool) -> None:
        """Enable or disable automatic display dimming after idle."""
        self._auto_dim = enabled
        logger.info("Display auto-dim %s", "enabled" if enabled else "disabled")

    def set_screen_saver(self, mode: str) -> None:
        """Set screen saver timeout mode (off, 1m, 5m, 15m, 30m)."""
        self._screen_saver = mode
        logger.info("Display screen saver set to %s", mode)