"""
gpio_headlight.py — Jetson Orin Nano Super 40-pin GPIO headlight control.

Physical pin 33 on the J26 40-pin header is used for the headlight.
On JetPack 6 this corresponds to sysfs GPIO chip gpio-tegra234 / offset 12.

Brightness levels (0.0–1.0):
  0.0  → headlight off
  0.25 → dim       (e.g. indicator, twilight assist)
  0.50 → medium    (normal dark indoor)
  1.0  → full      (outdoor night, ir_only mode)

Implementation:
  1. Tries Jetson.GPIO PWM (RPi-compatible API, available if jetson-gpio installed)
  2. Falls back to sysfs binary (on/off only) via /sys/class/gpio
  3. If sysfs export fails (permissions / virtual env), logs a warning and no-ops.

Sysfs GPIO path on Orin Nano Super (JetPack 6):
  /sys/class/gpio/gpiochip<N>/  — use the tegra234 chip
  Or the legacy /sys/class/gpio/gpio<number>/ path after export.

Note: udev rule or `sudo` may be needed in production.
  Add to /etc/udev/rules.d/99-jetson-gpio.rules:
    SUBSYSTEM=="gpio", ACTION=="add", RUN+="/bin/chmod 777 /sys/class/gpio"
"""

from __future__ import annotations

import logging
import os
from typing import Optional

_LOG = logging.getLogger(__name__)

# Physical board pin for the headlight
_BOARD_PIN = 33

# Brightness → duty cycle string mapping for sysfs PWM (when available)
_LEVEL_DUTY: dict[str, float] = {
    'off':    0.0,
    'dim':    0.25,
    'medium': 0.50,
    'full':   1.0,
}


class GPIOHeadlight:
    """Controls the GPIO headlight with graceful fallbacks."""

    def __init__(self, pin: int = _BOARD_PIN):
        self._pin = pin
        self._brightness: float = 0.0
        self._backend: str = 'none'
        self._pwm = None
        self._gpio_path: Optional[str] = None
        self._init()

    # ── Public API ────────────────────────────────────────────────────────────

    def set_brightness(self, level: float) -> None:
        """Set headlight brightness in [0.0, 1.0]."""
        level = max(0.0, min(1.0, level))
        if level == self._brightness:
            return
        self._brightness = level
        self._apply(level)

    def set_level(self, name: str) -> None:
        """Set headlight to a named level: 'off', 'dim', 'medium', 'full'."""
        self.set_brightness(_LEVEL_DUTY.get(name, 0.0))

    def off(self) -> None:
        self.set_brightness(0.0)

    def cleanup(self) -> None:
        self.off()
        if self._pwm is not None:
            try:
                self._pwm.stop()
                import Jetson.GPIO as GPIO
                GPIO.cleanup()
            except Exception:
                pass
        elif self._gpio_path:
            self._gpio_write('0')

    # ── Init backends ─────────────────────────────────────────────────────────

    def _init(self) -> None:
        # Try Jetson.GPIO PWM first
        try:
            import Jetson.GPIO as GPIO
            GPIO.setmode(GPIO.BOARD)
            GPIO.setup(self._pin, GPIO.OUT, initial=GPIO.LOW)
            self._pwm = GPIO.PWM(self._pin, 1000)   # 1 kHz PWM
            self._pwm.start(0)
            self._backend = 'jetson_gpio_pwm'
            _LOG.info(f'[headlight] Jetson.GPIO PWM on pin {self._pin}')
            return
        except ImportError:
            _LOG.debug('[headlight] Jetson.GPIO not available')
        except Exception as exc:
            _LOG.warning(f'[headlight] Jetson.GPIO init failed: {exc}')

        # Fall back to sysfs binary
        gpio_num = self._board_pin_to_sysfs(self._pin)
        if gpio_num is not None:
            path = f'/sys/class/gpio/gpio{gpio_num}'
            if self._sysfs_export(gpio_num):
                self._gpio_path = path
                self._backend = 'sysfs'
                _LOG.info(f'[headlight] sysfs GPIO /sys/class/gpio/gpio{gpio_num}')
                return

        _LOG.warning('[headlight] No GPIO backend available — headlight is simulated')
        self._backend = 'sim'

    # ── Apply ─────────────────────────────────────────────────────────────────

    def _apply(self, level: float) -> None:
        if self._backend == 'jetson_gpio_pwm':
            try:
                self._pwm.ChangeDutyCycle(level * 100.0)
            except Exception as exc:
                _LOG.warning(f'[headlight] PWM error: {exc}')
        elif self._backend == 'sysfs':
            self._gpio_write('1' if level > 0 else '0')
        # sim: no-op

    # ── Sysfs helpers ─────────────────────────────────────────────────────────

    @staticmethod
    def _board_pin_to_sysfs(board_pin: int) -> Optional[int]:
        """
        Map physical 40-pin board pin to sysfs GPIO number on Orin Nano Super.
        Only a subset is mapped here; extend as needed.
        """
        # Partial map of Orin Nano Super 40-pin header (physical → sysfs offset)
        _ORIN_MAP: dict[int, int] = {
            7:  106,
            11: 112,
            12: 50,
            13: 108,
            15: 109,
            16: 85,
            18: 110,
            19: 130,
            21: 128,
            22: 111,
            23: 129,
            24: 131,
            26: 132,
            29: 118,
            31: 120,
            32: 122,
            33: 200,   # headlight pin
            35: 116,
            36: 54,
            37: 121,
            38: 117,
            40: 56,
        }
        return _ORIN_MAP.get(board_pin)

    def _sysfs_export(self, gpio_num: int) -> bool:
        export_path = f'/sys/class/gpio/gpio{gpio_num}'
        if not os.path.exists(export_path):
            try:
                with open('/sys/class/gpio/export', 'w') as f:
                    f.write(str(gpio_num))
            except PermissionError:
                _LOG.warning('[headlight] No permission for /sys/class/gpio/export')
                return False
            except OSError as exc:
                _LOG.warning(f'[headlight] sysfs export error: {exc}')
                return False
        try:
            with open(f'{export_path}/direction', 'w') as f:
                f.write('out')
            return True
        except OSError as exc:
            _LOG.warning(f'[headlight] sysfs direction error: {exc}')
            return False

    def _gpio_write(self, value: str) -> None:
        if not self._gpio_path:
            return
        try:
            with open(f'{self._gpio_path}/value', 'w') as f:
                f.write(value)
        except OSError as exc:
            _LOG.warning(f'[headlight] sysfs write error: {exc}')
