from typing import Any

from esphome import pins
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import (
    CONF_ANALOG,
    CONF_ID,
    CONF_INPUT,
    CONF_INVERTED,
    CONF_MODE,
    CONF_NUMBER,
    CONF_OPEN_DRAIN,
    CONF_OUTPUT,
    CONF_PULLDOWN,
    CONF_PULLUP,
)
from esphome.core import CORE
from esphome.cpp_generator import MockObj
from esphome.types import ConfigType

from . import boards
from .const import KEY_BOARD, KEY_RP2, rp2_ns

RP2GPIOPin = rp2_ns.class_("RP2GPIOPin", cg.InternalGPIOPin)


def _lookup_pin(value: str) -> int:
    board = CORE.data[KEY_RP2][KEY_BOARD]
    board_pins = boards.RP2_BOARD_PINS.get(board, {})

    while isinstance(board_pins, str):
        board_pins = boards.RP2_BOARD_PINS[board_pins]

    if value in board_pins:
        return board_pins[value]
    if value in boards.RP2_BASE_PINS:
        return boards.RP2_BASE_PINS[value]
    raise cv.Invalid(f"Cannot resolve pin name '{value}' for board {board}.")


def _translate_pin(value: Any) -> int:
    if isinstance(value, dict) or value is None:
        raise cv.Invalid(
            "This variable only supports pin numbers, not full pin schemas "
            "(with inverted and mode)."
        )
    if isinstance(value, int) and not isinstance(value, bool):
        return value
    if not isinstance(value, str):
        raise cv.Invalid(f"Invalid pin number: {value}")
    try:
        return int(value)
    except ValueError:
        pass
    if value.startswith("GPIO"):
        return cv.int_(value[len("GPIO") :].strip())
    return _lookup_pin(value)


def _board_max_virtual_pin(board: str) -> int | None:
    """Get the max CYW43 virtual pin for this board, or None if no virtual pins."""
    return boards.BOARDS.get(board, {}).get("max_virtual_pin")


def validate_gpio_pin(value: Any) -> int:
    value = _translate_pin(value)
    board = CORE.data[KEY_RP2][KEY_BOARD]
    max_virtual = _board_max_virtual_pin(board)
    if max_virtual is not None and boards.CYW43_GPIO_OFFSET <= value <= max_virtual:
        return value
    max_pin = boards.BOARDS.get(board, {}).get("max_pin", boards.DEFAULT_MAX_PIN)
    if value < 0 or value > max_pin:
        raise cv.Invalid(f"Invalid pin number: {value} (max {max_pin} for this board)")
    return value


def validate_supports(value: ConfigType) -> ConfigType:
    board = CORE.data[KEY_RP2][KEY_BOARD]
    if (
        _board_max_virtual_pin(board) is None
        or value[CONF_NUMBER] < boards.CYW43_GPIO_OFFSET
    ):
        return value
    mode = value[CONF_MODE]
    is_input = mode[CONF_INPUT]
    is_output = mode[CONF_OUTPUT]
    is_open_drain = mode[CONF_OPEN_DRAIN]
    is_pullup = mode[CONF_PULLUP]
    is_pulldown = mode[CONF_PULLDOWN]
    if not is_output or is_input or is_open_drain or is_pullup or is_pulldown:
        raise cv.Invalid("Only output mode is supported for CYW43 virtual pins")
    return value


RP2_PIN_SCHEMA = cv.All(
    pins.gpio_base_schema(
        RP2GPIOPin,
        validate_gpio_pin,
        modes=pins.GPIO_STANDARD_MODES + (CONF_ANALOG,),
    ),
    validate_supports,
)


@pins.PIN_SCHEMA_REGISTRY.register("rp2", RP2_PIN_SCHEMA)
async def rp2_pin_to_code(config: ConfigType) -> MockObj:
    var = cg.new_Pvariable(config[CONF_ID])
    num = config[CONF_NUMBER]
    cg.add(var.set_pin(num))
    # Only set if true to avoid bloating setup() function
    # (inverted bit in pin_flags_ bitfield is zero-initialized to false)
    if config[CONF_INVERTED]:
        cg.add(var.set_inverted(True))
    cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE])))
    return var
