import importlib
import logging
import pkgutil

from esphome import pins
import esphome.codegen as cg
from esphome.components import display
from esphome.components.const import (
    BYTE_ORDER_BIG,
    BYTE_ORDER_LITTLE,
    CONF_BYTE_ORDER,
    CONF_DRAW_ROUNDING,
)
from esphome.components.display import CONF_SHOW_TEST_CARD
from esphome.components.esp32 import VARIANT_ESP32P4, only_on_variant
from esphome.components.mipi import (
    COLOR_ORDERS,
    CONF_COLOR_DEPTH,
    CONF_HSYNC_BACK_PORCH,
    CONF_HSYNC_FRONT_PORCH,
    CONF_HSYNC_PULSE_WIDTH,
    CONF_PCLK_FREQUENCY,
    CONF_PIXEL_MODE,
    CONF_USE_AXIS_FLIPS,
    CONF_VSYNC_BACK_PORCH,
    CONF_VSYNC_FRONT_PORCH,
    CONF_VSYNC_PULSE_WIDTH,
    MODE_BGR,
    PIXEL_MODE_16BIT,
    PIXEL_MODE_24BIT,
    DriverChip,
    dimension_schema,
    get_color_depth,
    map_sequence,
    power_of_two,
    requires_buffer,
)
import esphome.config_validation as cv
from esphome.const import (
    CONF_COLOR_ORDER,
    CONF_DIMENSIONS,
    CONF_DISABLED,
    CONF_ENABLE_PIN,
    CONF_ID,
    CONF_INIT_SEQUENCE,
    CONF_INVERT_COLORS,
    CONF_LAMBDA,
    CONF_MIRROR_X,
    CONF_MIRROR_Y,
    CONF_MODEL,
    CONF_RESET_PIN,
    CONF_ROTATION,
    CONF_SWAP_XY,
    CONF_TRANSFORM,
    CONF_WIDTH,
)
from esphome.final_validate import full_config

from . import mipi_dsi_ns, models

# Currently only ESP32-P4 is supported, so esp_ldo and psram are required
DEPENDENCIES = ["esp32", "esp_ldo", "psram"]
DOMAIN = "mipi_dsi"

LOGGER = logging.getLogger(DOMAIN)

MIPI_DSI = mipi_dsi_ns.class_("MIPI_DSI", display.Display, cg.Component)
ColorOrder = display.display_ns.enum("ColorMode")
ColorBitness = display.display_ns.enum("ColorBitness")

CONF_LANE_BIT_RATE = "lane_bit_rate"
CONF_LANES = "lanes"

DriverChip("CUSTOM")

# Import all models dynamically from the models package

for module_info in pkgutil.iter_modules(models.__path__):
    importlib.import_module(f".models.{module_info.name}", package=__package__)

MODELS = DriverChip.get_models()

COLOR_DEPTHS = {
    16: ColorBitness.COLOR_BITNESS_565,
    24: ColorBitness.COLOR_BITNESS_888,
}


def model_schema(config):
    model = MODELS[config[CONF_MODEL].upper()]
    model.defaults[CONF_SWAP_XY] = cv.UNDEFINED
    transform = cv.Any(
        cv.Schema(
            {
                cv.Required(CONF_MIRROR_X): cv.boolean,
                cv.Required(CONF_MIRROR_Y): cv.boolean,
                cv.Optional(CONF_SWAP_XY): cv.invalid(
                    "Axis swapping not supported by DSI displays"
                ),
            }
        ),
        cv.one_of(CONF_DISABLED, lower=True),
    )
    # CUSTOM model will need to provide a custom init sequence
    iseqconf = (
        cv.Required(CONF_INIT_SEQUENCE)
        if model.initsequence is None
        else cv.Optional(CONF_INIT_SEQUENCE)
    )
    # Dimensions are optional if the model has a default width
    cv_dimensions = cv.Optional if model.get_default(CONF_WIDTH) else cv.Required
    pixel_modes = (PIXEL_MODE_16BIT, PIXEL_MODE_24BIT, "16", "24")
    schema = display.FULL_DISPLAY_SCHEMA.extend(
        {
            model.option(CONF_RESET_PIN, cv.UNDEFINED): pins.gpio_output_pin_schema,
            cv.GenerateID(): cv.declare_id(MIPI_DSI),
            cv_dimensions(CONF_DIMENSIONS): dimension_schema(
                model.get_default(CONF_DRAW_ROUNDING, 1)
            ),
            model.option(CONF_ENABLE_PIN, cv.UNDEFINED): cv.ensure_list(
                pins.gpio_output_pin_schema
            ),
            model.option(CONF_COLOR_ORDER, MODE_BGR): cv.enum(COLOR_ORDERS, upper=True),
            model.option(CONF_DRAW_ROUNDING, 2): power_of_two,
            model.option(CONF_PIXEL_MODE, PIXEL_MODE_16BIT): cv.one_of(
                *pixel_modes, lower=True
            ),
            model.option(CONF_TRANSFORM, cv.UNDEFINED): transform,
            cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True),
            model.option(CONF_INVERT_COLORS, False): cv.boolean,
            model.option(CONF_COLOR_DEPTH, "16"): cv.one_of(
                *[str(d) for d in COLOR_DEPTHS],
                *[f"{d}bit" for d in COLOR_DEPTHS],
                lower=True,
            ),
            model.option(CONF_USE_AXIS_FLIPS, True): cv.boolean,
            model.option(CONF_PCLK_FREQUENCY, "40MHz"): cv.All(
                cv.frequency, cv.Range(min=4e6, max=100e6)
            ),
            model.option(CONF_LANES, 2): cv.int_range(1, 2),
            model.option(CONF_LANE_BIT_RATE, None): cv.All(
                cv.bps, cv.Range(min=100e6, max=3200e6)
            ),
            iseqconf: cv.ensure_list(map_sequence),
            model.option(CONF_BYTE_ORDER, BYTE_ORDER_LITTLE): cv.one_of(
                BYTE_ORDER_LITTLE, BYTE_ORDER_BIG, lower=True
            ),
            model.option(CONF_HSYNC_PULSE_WIDTH): cv.int_,
            model.option(CONF_HSYNC_BACK_PORCH): cv.int_,
            model.option(CONF_HSYNC_FRONT_PORCH): cv.int_,
            model.option(CONF_VSYNC_PULSE_WIDTH): cv.int_,
            model.option(CONF_VSYNC_BACK_PORCH): cv.int_,
            model.option(CONF_VSYNC_FRONT_PORCH): cv.int_,
        }
    )
    return cv.All(
        schema,
        cv.only_on_esp32,
        only_on_variant(supported=[VARIANT_ESP32P4]),
    )


def _config_schema(config):
    config = cv.Schema(
        {
            cv.Required(CONF_MODEL): cv.one_of(*MODELS, upper=True),
        },
        extra=cv.ALLOW_EXTRA,
    )(config)
    return model_schema(config)(config)


def _final_validate(config):
    global_config = full_config.get()

    from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN

    if not requires_buffer(config) and LVGL_DOMAIN not in global_config:
        # If no drawing methods are configured, and LVGL is not enabled, show a test card
        config[CONF_SHOW_TEST_CARD] = True
    return config


CONFIG_SCHEMA = _config_schema
FINAL_VALIDATE_SCHEMA = _final_validate


async def to_code(config):
    model = MODELS[config[CONF_MODEL].upper()]
    color_depth = COLOR_DEPTHS[get_color_depth(config)]
    pixel_mode = int(config[CONF_PIXEL_MODE].removesuffix("bit"))
    width, height, _offset_width, _offset_height = model.get_dimensions(config)
    var = cg.new_Pvariable(config[CONF_ID], width, height, color_depth, pixel_mode)

    sequence = model.get_sequence(config)
    cg.add(var.set_model(config[CONF_MODEL]))
    cg.add(var.set_init_sequence(sequence))
    cg.add(var.set_invert_colors(config[CONF_INVERT_COLORS]))
    cg.add(var.set_hsync_pulse_width(config[CONF_HSYNC_PULSE_WIDTH]))
    cg.add(var.set_hsync_back_porch(config[CONF_HSYNC_BACK_PORCH]))
    cg.add(var.set_hsync_front_porch(config[CONF_HSYNC_FRONT_PORCH]))
    cg.add(var.set_vsync_pulse_width(config[CONF_VSYNC_PULSE_WIDTH]))
    cg.add(var.set_vsync_back_porch(config[CONF_VSYNC_BACK_PORCH]))
    cg.add(var.set_vsync_front_porch(config[CONF_VSYNC_FRONT_PORCH]))
    cg.add(var.set_pclk_frequency(config[CONF_PCLK_FREQUENCY] / 1.0e6))
    cg.add(var.set_lanes(int(config[CONF_LANES])))
    cg.add(var.set_lane_bit_rate(config[CONF_LANE_BIT_RATE] / 1.0e6))
    if reset_pin := config.get(CONF_RESET_PIN):
        reset = await cg.gpio_pin_expression(reset_pin)
        cg.add(var.set_reset_pin(reset))
    if enable_pin := config.get(CONF_ENABLE_PIN):
        enable = [await cg.gpio_pin_expression(pin) for pin in enable_pin]
        cg.add(var.set_enable_pins(enable))

    if model.rotation_as_transform(config):
        config[CONF_ROTATION] = 0
    await display.register_display(var, config)
    if lamb := config.get(CONF_LAMBDA):
        lambda_ = await cg.process_lambda(
            lamb, [(display.DisplayRef, "it")], return_type=cg.void
        )
        cg.add(var.set_writer(lambda_))
