import logging

from esphome import automation, pins
import esphome.codegen as cg
from esphome.components import i2c
from esphome.components.esp32 import add_idf_component, add_idf_sdkconfig_option
from esphome.components.psram import DOMAIN as psram_domain
import esphome.config_validation as cv
from esphome.const import (
    CONF_BRIGHTNESS,
    CONF_CONTRAST,
    CONF_DATA_PINS,
    CONF_FREQUENCY,
    CONF_I2C,
    CONF_I2C_ID,
    CONF_ID,
    CONF_PIN,
    CONF_RESET_PIN,
    CONF_RESOLUTION,
    CONF_SCL,
    CONF_SDA,
    CONF_TRIGGER_ID,
    CONF_VSYNC_PIN,
)
from esphome.core import CORE
from esphome.core.entity_helpers import setup_entity
import esphome.final_validate as fv
from esphome.types import ConfigType

_LOGGER = logging.getLogger(__name__)

AUTO_LOAD = ["camera"]
DEPENDENCIES = ["esp32"]

esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera")
ESP32Camera = esp32_camera_ns.class_("ESP32Camera", cg.PollingComponent, cg.EntityBase)
ESP32CameraImageData = esp32_camera_ns.struct("CameraImageData")
# Triggers
ESP32CameraImageTrigger = esp32_camera_ns.class_(
    "ESP32CameraImageTrigger", automation.Trigger.template()
)
ESP32CameraStreamStartTrigger = esp32_camera_ns.class_(
    "ESP32CameraStreamStartTrigger",
    automation.Trigger.template(),
)
ESP32CameraStreamStopTrigger = esp32_camera_ns.class_(
    "ESP32CameraStreamStopTrigger",
    automation.Trigger.template(),
)
ESP32CameraFrameSize = esp32_camera_ns.enum("ESP32CameraFrameSize")
FRAME_SIZES = {
    "160X120": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_160X120,
    "QQVGA": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_160X120,
    "176X144": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_176X144,
    "QCIF": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_176X144,
    "240X176": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_240X176,
    "HQVGA": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_240X176,
    "320X240": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_320X240,
    "QVGA": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_320X240,
    "400X296": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_400X296,
    "CIF": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_400X296,
    "640X480": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_640X480,
    "VGA": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_640X480,
    "800X600": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_800X600,
    "SVGA": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_800X600,
    "1024X768": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_1024X768,
    "XGA": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_1024X768,
    "1280X1024": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_1280X1024,
    "SXGA": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_1280X1024,
    "1600X1200": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_1600X1200,
    "UXGA": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_1600X1200,
    "1920X1080": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_1920X1080,
    "FHD": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_1920X1080,
    "720X1280": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_720X1280,
    "PHD": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_720X1280,
    "864X1536": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_864X1536,
    "P3MP": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_864X1536,
    "2048X1536": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_2048X1536,
    "QXGA": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_2048X1536,
    "2560X1440": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_2560X1440,
    "QHD": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_2560X1440,
    "2560X1600": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_2560X1600,
    "WQXGA": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_2560X1600,
    "1080X1920": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_1080X1920,
    "PFHD": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_1080X1920,
    "2560X1920": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_2560X1920,
    "QSXGA": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_2560X1920,
}
ESP32CameraPixelFormat = esp32_camera_ns.enum("ESP32CameraPixelFormat")
PIXEL_FORMATS = {
    "RGB565": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_RGB565,
    "YUV422": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_YUV422,
    "YUV420": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_YUV420,
    "GRAYSCALE": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_GRAYSCALE,
    "JPEG": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_JPEG,
    "RGB888": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_RGB888,
    "RAW": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_RAW,
    "RGB444": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_RGB444,
    "RGB555": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_RGB555,
}
ESP32GainControlMode = esp32_camera_ns.enum("ESP32GainControlMode")
ENUM_GAIN_CONTROL_MODE = {
    "MANUAL": ESP32GainControlMode.ESP32_GC_MODE_MANU,
    "AUTO": ESP32GainControlMode.ESP32_GC_MODE_AUTO,
}
ESP32AgcGainCeiling = esp32_camera_ns.enum("ESP32AgcGainCeiling")
ENUM_GAIN_CEILING = {
    "2X": ESP32AgcGainCeiling.ESP32_GAINCEILING_2X,
    "4X": ESP32AgcGainCeiling.ESP32_GAINCEILING_4X,
    "8X": ESP32AgcGainCeiling.ESP32_GAINCEILING_8X,
    "16X": ESP32AgcGainCeiling.ESP32_GAINCEILING_16X,
    "32X": ESP32AgcGainCeiling.ESP32_GAINCEILING_32X,
    "64X": ESP32AgcGainCeiling.ESP32_GAINCEILING_64X,
    "128X": ESP32AgcGainCeiling.ESP32_GAINCEILING_128X,
}
ESP32WhiteBalanceMode = esp32_camera_ns.enum("ESP32WhiteBalanceMode")
ENUM_WB_MODE = {
    "AUTO": ESP32WhiteBalanceMode.ESP32_WB_MODE_AUTO,
    "SUNNY": ESP32WhiteBalanceMode.ESP32_WB_MODE_SUNNY,
    "CLOUDY": ESP32WhiteBalanceMode.ESP32_WB_MODE_CLOUDY,
    "OFFICE": ESP32WhiteBalanceMode.ESP32_WB_MODE_OFFICE,
    "HOME": ESP32WhiteBalanceMode.ESP32_WB_MODE_HOME,
}
ESP32SpecialEffect = esp32_camera_ns.enum("ESP32SpecialEffect")
ENUM_SPECIAL_EFFECT = {
    "NONE": ESP32SpecialEffect.ESP32_SPECIAL_EFFECT_NONE,
    "NEGATIVE": ESP32SpecialEffect.ESP32_SPECIAL_EFFECT_NEGATIVE,
    "GRAYSCALE": ESP32SpecialEffect.ESP32_SPECIAL_EFFECT_GRAYSCALE,
    "RED_TINT": ESP32SpecialEffect.ESP32_SPECIAL_EFFECT_RED_TINT,
    "GREEN_TINT": ESP32SpecialEffect.ESP32_SPECIAL_EFFECT_GREEN_TINT,
    "BLUE_TINT": ESP32SpecialEffect.ESP32_SPECIAL_EFFECT_BLUE_TINT,
    "SEPIA": ESP32SpecialEffect.ESP32_SPECIAL_EFFECT_SEPIA,
}

camera_fb_location_t = cg.global_ns.enum("camera_fb_location_t")
ENUM_FB_LOCATION = {
    "PSRAM": cg.global_ns.CAMERA_FB_IN_PSRAM,
    "DRAM": cg.global_ns.CAMERA_FB_IN_DRAM,
}

# pin assignment
CONF_HREF_PIN = "href_pin"
CONF_PIXEL_CLOCK_PIN = "pixel_clock_pin"
CONF_EXTERNAL_CLOCK = "external_clock"
CONF_I2C_PINS = "i2c_pins"
CONF_POWER_DOWN_PIN = "power_down_pin"
# image
CONF_PIXEL_FORMAT = "pixel_format"
CONF_JPEG_QUALITY = "jpeg_quality"
CONF_VERTICAL_FLIP = "vertical_flip"
CONF_HORIZONTAL_MIRROR = "horizontal_mirror"
CONF_SATURATION = "saturation"
CONF_SPECIAL_EFFECT = "special_effect"
# exposure
CONF_AEC_MODE = "aec_mode"
CONF_AEC2 = "aec2"
CONF_AE_LEVEL = "ae_level"
CONF_AEC_VALUE = "aec_value"
# gains
CONF_AGC_MODE = "agc_mode"
CONF_AGC_VALUE = "agc_value"
CONF_AGC_GAIN_CEILING = "agc_gain_ceiling"
# white balance
CONF_WB_MODE = "wb_mode"
# test pattern
CONF_TEST_PATTERN = "test_pattern"
# framerates
CONF_MAX_FRAMERATE = "max_framerate"
CONF_IDLE_FRAMERATE = "idle_framerate"
# frame buffer
CONF_FRAME_BUFFER_COUNT = "frame_buffer_count"
CONF_FRAME_BUFFER_LOCATION = "frame_buffer_location"

# stream trigger
CONF_ON_STREAM_START = "on_stream_start"
CONF_ON_STREAM_STOP = "on_stream_stop"
CONF_ON_IMAGE = "on_image"

camera_range_param = cv.int_range(min=-2, max=2)


def validate_fb_location_(value):
    validator = cv.enum(ENUM_FB_LOCATION, upper=True)
    if value.lower() == psram_domain:
        validator = cv.All(validator, cv.requires_component(psram_domain))
    return validator(value)


def validate_jpeg_quality(config: ConfigType) -> ConfigType:
    quality = config.get(CONF_JPEG_QUALITY)
    pixel_format = config.get(CONF_PIXEL_FORMAT, "JPEG")

    if quality == 0:
        # Set default JPEG quality if not specified for backwards compatibility
        if pixel_format == "JPEG":
            config[CONF_JPEG_QUALITY] = 10
        # For pixel formats other than JPEG, the valid 0 means no conversion
    elif quality < 6 or quality > 63:
        raise cv.Invalid(f"jpeg_quality must be between 6 and 63, got {quality}")

    return config


CONFIG_SCHEMA = cv.All(
    cv.ENTITY_BASE_SCHEMA.extend(
        {
            cv.GenerateID(): cv.declare_id(ESP32Camera),
            # pin assignment
            cv.Required(CONF_DATA_PINS): cv.All(
                [pins.internal_gpio_input_pin_number], cv.Length(min=8, max=8)
            ),
            cv.Required(CONF_VSYNC_PIN): pins.internal_gpio_input_pin_number,
            cv.Required(CONF_HREF_PIN): pins.internal_gpio_input_pin_number,
            cv.Required(CONF_PIXEL_CLOCK_PIN): pins.internal_gpio_input_pin_number,
            cv.Required(CONF_EXTERNAL_CLOCK): cv.Schema(
                {
                    cv.Required(CONF_PIN): pins.internal_gpio_input_pin_number,
                    cv.Optional(CONF_FREQUENCY, default="20MHz"): cv.All(
                        cv.frequency, cv.float_range(min=8e6, max=20e6)
                    ),
                }
            ),
            cv.Optional(CONF_I2C_PINS): cv.Schema(
                {
                    cv.Required(CONF_SDA): pins.internal_gpio_output_pin_number,
                    cv.Required(CONF_SCL): pins.internal_gpio_output_pin_number,
                }
            ),
            cv.Optional(CONF_I2C_ID): cv.Any(
                cv.use_id(i2c.InternalI2CBus),
                msg="I2C bus must be an internal ESP32 I2C bus",
            ),
            cv.Optional(CONF_RESET_PIN): pins.internal_gpio_output_pin_number,
            cv.Optional(CONF_POWER_DOWN_PIN): pins.internal_gpio_output_pin_number,
            # image
            cv.Optional(CONF_RESOLUTION, default="640X480"): cv.enum(
                FRAME_SIZES, upper=True
            ),
            cv.Optional(CONF_PIXEL_FORMAT, default="JPEG"): cv.enum(
                PIXEL_FORMATS, upper=True
            ),
            cv.Optional(CONF_JPEG_QUALITY, default=0): cv.Any(
                cv.one_of(0), cv.int_range(min=6, max=63)
            ),
            cv.Optional(CONF_CONTRAST, default=0): camera_range_param,
            cv.Optional(CONF_BRIGHTNESS, default=0): camera_range_param,
            cv.Optional(CONF_SATURATION, default=0): camera_range_param,
            cv.Optional(CONF_VERTICAL_FLIP, default=True): cv.boolean,
            cv.Optional(CONF_HORIZONTAL_MIRROR, default=True): cv.boolean,
            cv.Optional(CONF_SPECIAL_EFFECT, default="NONE"): cv.enum(
                ENUM_SPECIAL_EFFECT, upper=True
            ),
            # exposure
            cv.Optional(CONF_AGC_MODE, default="AUTO"): cv.enum(
                ENUM_GAIN_CONTROL_MODE, upper=True
            ),
            cv.Optional(CONF_AEC2, default=False): cv.boolean,
            cv.Optional(CONF_AE_LEVEL, default=0): camera_range_param,
            cv.Optional(CONF_AEC_VALUE, default=300): cv.int_range(min=0, max=1200),
            # gains
            cv.Optional(CONF_AEC_MODE, default="AUTO"): cv.enum(
                ENUM_GAIN_CONTROL_MODE, upper=True
            ),
            cv.Optional(CONF_AGC_VALUE, default=0): cv.int_range(min=0, max=30),
            cv.Optional(CONF_AGC_GAIN_CEILING, default="2X"): cv.enum(
                ENUM_GAIN_CEILING, upper=True
            ),
            # white balance
            cv.Optional(CONF_WB_MODE, default="AUTO"): cv.enum(
                ENUM_WB_MODE, upper=True
            ),
            # test pattern
            cv.Optional(CONF_TEST_PATTERN, default=False): cv.boolean,
            # framerates
            cv.Optional(CONF_MAX_FRAMERATE, default="10 fps"): cv.All(
                cv.framerate, cv.Range(min=0, min_included=False, max=60)
            ),
            cv.Optional(CONF_IDLE_FRAMERATE, default="0.1 fps"): cv.All(
                cv.framerate, cv.Range(min=0, max=1)
            ),
            cv.Optional(CONF_FRAME_BUFFER_COUNT, default=1): cv.int_range(min=1, max=2),
            cv.Optional(
                CONF_FRAME_BUFFER_LOCATION, default="PSRAM"
            ): validate_fb_location_,
            cv.Optional(CONF_ON_STREAM_START): automation.validate_automation(
                {
                    cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
                        ESP32CameraStreamStartTrigger
                    ),
                }
            ),
            cv.Optional(CONF_ON_STREAM_STOP): automation.validate_automation(
                {
                    cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
                        ESP32CameraStreamStopTrigger
                    ),
                }
            ),
            cv.Optional(CONF_ON_IMAGE): automation.validate_automation(
                {
                    cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
                        ESP32CameraImageTrigger
                    ),
                }
            ),
        }
    ).extend(cv.COMPONENT_SCHEMA),
    validate_jpeg_quality,
    cv.has_exactly_one_key(CONF_I2C_PINS, CONF_I2C_ID),
)


def _final_validate(config):
    # Check psram requirement for non-JPEG formats
    if (
        config.get(CONF_PIXEL_FORMAT, "JPEG") != "JPEG"
        and psram_domain not in CORE.loaded_integrations
    ):
        raise cv.Invalid(
            f"Non-JPEG pixel formats require the '{psram_domain}' component for JPEG conversion"
        )

    if CONF_I2C_PINS not in config:
        return
    fconf = fv.full_config.get()
    if fconf.get(CONF_I2C):
        raise cv.Invalid(
            "The `i2c_pins:` config option is incompatible with an dedicated `i2c:` block, use `i2c_id` instead"
        )
    _LOGGER.warning(
        "The `i2c_pins:` config option is deprecated. Use `i2c_id:` with a dedicated `i2c:` definition instead."
    )


FINAL_VALIDATE_SCHEMA = _final_validate

SETTERS = {
    # pin assignment
    CONF_DATA_PINS: "set_data_pins",
    CONF_VSYNC_PIN: "set_vsync_pin",
    CONF_HREF_PIN: "set_href_pin",
    CONF_PIXEL_CLOCK_PIN: "set_pixel_clock_pin",
    CONF_RESET_PIN: "set_reset_pin",
    CONF_POWER_DOWN_PIN: "set_power_down_pin",
    # image
    CONF_PIXEL_FORMAT: "set_pixel_format",
    CONF_JPEG_QUALITY: "set_jpeg_quality",
    CONF_VERTICAL_FLIP: "set_vertical_flip",
    CONF_HORIZONTAL_MIRROR: "set_horizontal_mirror",
    CONF_CONTRAST: "set_contrast",
    CONF_BRIGHTNESS: "set_brightness",
    CONF_SATURATION: "set_saturation",
    CONF_SPECIAL_EFFECT: "set_special_effect",
    # exposure
    CONF_AEC_MODE: "set_aec_mode",
    CONF_AEC2: "set_aec2",
    CONF_AE_LEVEL: "set_ae_level",
    CONF_AEC_VALUE: "set_aec_value",
    # gains
    CONF_AGC_MODE: "set_agc_mode",
    CONF_AGC_VALUE: "set_agc_value",
    CONF_AGC_GAIN_CEILING: "set_agc_gain_ceiling",
    # white balance
    CONF_WB_MODE: "set_wb_mode",
    # test pattern
    CONF_TEST_PATTERN: "set_test_pattern",
    CONF_FRAME_BUFFER_LOCATION: "set_frame_buffer_location",
}


async def to_code(config):
    cg.add_define("USE_CAMERA")
    var = cg.new_Pvariable(config[CONF_ID])
    await setup_entity(var, config, "camera")
    await cg.register_component(var, config)

    for key, setter in SETTERS.items():
        if key in config:
            cg.add(getattr(var, setter)(config[key]))

    extclk = config[CONF_EXTERNAL_CLOCK]
    cg.add(var.set_external_clock(extclk[CONF_PIN], extclk[CONF_FREQUENCY]))
    if i2c_id := config.get(CONF_I2C_ID):
        i2c_hub = await cg.get_variable(i2c_id)
        cg.add(var.set_i2c_id(i2c_hub))
    else:
        i2c_pins = config[CONF_I2C_PINS]
        cg.add(var.set_i2c_pins(i2c_pins[CONF_SDA], i2c_pins[CONF_SCL]))
    cg.add(var.set_max_update_interval(1000 / config[CONF_MAX_FRAMERATE]))
    if config[CONF_IDLE_FRAMERATE] == 0:
        cg.add(var.set_idle_update_interval(0))
    else:
        cg.add(var.set_idle_update_interval(1000 / config[CONF_IDLE_FRAMERATE]))
    cg.add(var.set_frame_buffer_count(config[CONF_FRAME_BUFFER_COUNT]))
    cg.add(var.set_frame_buffer_location(config[CONF_FRAME_BUFFER_LOCATION]))
    cg.add(var.set_frame_size(config[CONF_RESOLUTION]))

    cg.add_define("USE_CAMERA")
    if config[CONF_JPEG_QUALITY] != 0 and config[CONF_PIXEL_FORMAT] != "JPEG":
        cg.add_define("USE_ESP32_CAMERA_JPEG_CONVERSION")

    add_idf_component(name="espressif/esp32-camera", ref="2.1.5")
    add_idf_sdkconfig_option("CONFIG_SCCB_HARDWARE_I2C_DRIVER_NEW", True)
    add_idf_sdkconfig_option("CONFIG_SCCB_HARDWARE_I2C_DRIVER_LEGACY", False)

    for conf in config.get(CONF_ON_STREAM_START, []):
        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
        await automation.build_automation(trigger, [], conf)

    for conf in config.get(CONF_ON_STREAM_STOP, []):
        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
        await automation.build_automation(trigger, [], conf)

    for conf in config.get(CONF_ON_IMAGE, []):
        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
        await automation.build_automation(
            trigger, [(ESP32CameraImageData, "image")], conf
        )
