from esphome import automation
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import (
    CONF_ALPHA,
    CONF_BLUE,
    CONF_BRIGHTNESS,
    CONF_COLD_WHITE,
    CONF_COLOR_BRIGHTNESS,
    CONF_COLOR_MODE,
    CONF_COLOR_TEMPERATURE,
    CONF_COLORS,
    CONF_DURATION,
    CONF_GREEN,
    CONF_INTENSITY,
    CONF_LAMBDA,
    CONF_MAX_BRIGHTNESS,
    CONF_MIN_BRIGHTNESS,
    CONF_NAME,
    CONF_NUM_LEDS,
    CONF_RANDOM,
    CONF_RED,
    CONF_SEQUENCE,
    CONF_SPEED,
    CONF_STATE,
    CONF_TRANSITION_LENGTH,
    CONF_UPDATE_INTERVAL,
    CONF_WARM_WHITE,
    CONF_WHITE,
    CONF_WIDTH,
)
from esphome.cpp_generator import MockObjClass
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
from esphome.util import Registry

from .types import (
    COLOR_MODES,
    AddressableColorWipeEffect,
    AddressableColorWipeEffectColor,
    AddressableFireworksEffect,
    AddressableFlickerEffect,
    AddressableLambdaLightEffect,
    AddressableLightRef,
    AddressableRainbowLightEffect,
    AddressableRandomTwinkleEffect,
    AddressableScanEffect,
    AddressableTwinkleEffect,
    AutomationLightEffect,
    Color,
    ColorMode,
    FlickerLightEffect,
    LambdaLightEffect,
    LightColorValues,
    PulseLightEffect,
    RandomLightEffect,
    StrobeLightEffect,
    StrobeLightEffectColor,
)

CONF_ADD_LED_INTERVAL = "add_led_interval"
CONF_REVERSE = "reverse"
CONF_GRADIENT = "gradient"
CONF_MOVE_INTERVAL = "move_interval"
CONF_SCAN_WIDTH = "scan_width"
CONF_TWINKLE_PROBABILITY = "twinkle_probability"
CONF_PROGRESS_INTERVAL = "progress_interval"
CONF_SPARK_PROBABILITY = "spark_probability"
CONF_USE_RANDOM_COLOR = "use_random_color"
CONF_FADE_OUT_RATE = "fade_out_rate"
CONF_STROBE = "strobe"
CONF_FLICKER = "flicker"
CONF_ADDRESSABLE_LAMBDA = "addressable_lambda"
CONF_ADDRESSABLE_RAINBOW = "addressable_rainbow"
CONF_ADDRESSABLE_COLOR_WIPE = "addressable_color_wipe"
CONF_ADDRESSABLE_SCAN = "addressable_scan"
CONF_ADDRESSABLE_TWINKLE = "addressable_twinkle"
CONF_ADDRESSABLE_RANDOM_TWINKLE = "addressable_random_twinkle"
CONF_ADDRESSABLE_FIREWORKS = "addressable_fireworks"
CONF_ADDRESSABLE_FLICKER = "addressable_flicker"
CONF_AUTOMATION = "automation"
CONF_ON_LENGTH = "on_length"
CONF_OFF_LENGTH = "off_length"

BINARY_EFFECTS = []
MONOCHROMATIC_EFFECTS = []
RGB_EFFECTS = []
ADDRESSABLE_EFFECTS = []

EFFECTS_REGISTRY = Registry()


def register_effect(
    name: str,
    effect_type: MockObjClass,
    default_name: str,
    schema: cv.Schema | dict,
    *extra_validators,
):
    schema = schema if isinstance(schema, cv.Schema) else cv.Schema(schema)
    schema = schema.extend(
        {
            cv.Optional(CONF_NAME, default=default_name): cv.string_strict,
        }
    )
    validator = cv.All(schema, *extra_validators)
    return EFFECTS_REGISTRY.register(name, effect_type, validator)


def register_binary_effect(
    name: str,
    effect_type: MockObjClass,
    default_name: str,
    schema: cv.Schema | dict,
    *extra_validators,
):
    # binary effect can be used for all lights
    BINARY_EFFECTS.append(name)
    MONOCHROMATIC_EFFECTS.append(name)
    RGB_EFFECTS.append(name)
    ADDRESSABLE_EFFECTS.append(name)

    return register_effect(name, effect_type, default_name, schema, *extra_validators)


def register_monochromatic_effect(
    name: str,
    effect_type: MockObjClass,
    default_name: str,
    schema: cv.Schema | dict,
    *extra_validators,
):
    # monochromatic effect can be used for all lights expect binary
    MONOCHROMATIC_EFFECTS.append(name)
    RGB_EFFECTS.append(name)
    ADDRESSABLE_EFFECTS.append(name)

    return register_effect(name, effect_type, default_name, schema, *extra_validators)


def register_rgb_effect(
    name: str,
    effect_type: MockObjClass,
    default_name: str,
    schema: cv.Schema | dict,
    *extra_validators,
):
    # RGB effect can be used for RGB and addressable lights
    RGB_EFFECTS.append(name)
    ADDRESSABLE_EFFECTS.append(name)

    return register_effect(name, effect_type, default_name, schema, *extra_validators)


def register_addressable_effect(
    name: str,
    effect_type: MockObjClass,
    default_name: str,
    schema: cv.Schema | dict,
    *extra_validators,
):
    # addressable effect can be used only in addressable
    ADDRESSABLE_EFFECTS.append(name)

    return register_effect(name, effect_type, default_name, schema, *extra_validators)


@register_binary_effect(
    "lambda",
    LambdaLightEffect,
    "Lambda",
    {
        cv.Required(CONF_LAMBDA): cv.lambda_,
        cv.Optional(CONF_UPDATE_INTERVAL, default="0ms"): cv.update_interval,
    },
)
async def lambda_effect_to_code(config, effect_id):
    lambda_ = await cg.process_lambda(
        config[CONF_LAMBDA], [(bool, "initial_run")], return_type=cg.void
    )
    return cg.new_Pvariable(
        effect_id, config[CONF_NAME], lambda_, config[CONF_UPDATE_INTERVAL]
    )


@register_binary_effect(
    "automation",
    AutomationLightEffect,
    "Automation",
    {
        cv.Required(CONF_SEQUENCE): automation.validate_automation(single=True),
    },
)
async def automation_effect_to_code(config, effect_id):
    var = cg.new_Pvariable(effect_id, config[CONF_NAME])
    await automation.build_automation(var.get_trig(), [], config[CONF_SEQUENCE])
    return var


@register_monochromatic_effect(
    "pulse",
    PulseLightEffect,
    "Pulse",
    {
        cv.Optional(CONF_TRANSITION_LENGTH, default="1s"): cv.Any(
            cv.positive_time_period_milliseconds,
            cv.Schema(
                {
                    cv.Required(CONF_ON_LENGTH): cv.positive_time_period_milliseconds,
                    cv.Required(CONF_OFF_LENGTH): cv.positive_time_period_milliseconds,
                }
            ),
        ),
        cv.Optional(
            CONF_UPDATE_INTERVAL, default="1s"
        ): cv.positive_time_period_milliseconds,
        cv.Optional(CONF_MIN_BRIGHTNESS, default="0%"): cv.percentage,
        cv.Optional(CONF_MAX_BRIGHTNESS, default="100%"): cv.percentage,
    },
)
async def pulse_effect_to_code(config, effect_id):
    effect = cg.new_Pvariable(effect_id, config[CONF_NAME])
    if isinstance(config[CONF_TRANSITION_LENGTH], dict):
        cg.add(
            effect.set_transition_on_length(
                config[CONF_TRANSITION_LENGTH][CONF_ON_LENGTH]
            )
        )
        cg.add(
            effect.set_transition_off_length(
                config[CONF_TRANSITION_LENGTH][CONF_OFF_LENGTH]
            )
        )
    else:
        transition_length = config[CONF_TRANSITION_LENGTH]
        cg.add(effect.set_transition_on_length(transition_length))
        cg.add(effect.set_transition_off_length(transition_length))
    cg.add(effect.set_update_interval(config[CONF_UPDATE_INTERVAL]))
    cg.add(
        effect.set_min_max_brightness(
            config[CONF_MIN_BRIGHTNESS], config[CONF_MAX_BRIGHTNESS]
        )
    )
    return effect


@register_monochromatic_effect(
    "random",
    RandomLightEffect,
    "Random",
    {
        cv.Optional(
            CONF_TRANSITION_LENGTH, default="7.5s"
        ): cv.positive_time_period_milliseconds,
        cv.Optional(
            CONF_UPDATE_INTERVAL, default="10s"
        ): cv.positive_time_period_milliseconds,
    },
)
async def random_effect_to_code(config, effect_id):
    effect = cg.new_Pvariable(effect_id, config[CONF_NAME])
    cg.add(effect.set_transition_length(config[CONF_TRANSITION_LENGTH]))
    cg.add(effect.set_update_interval(config[CONF_UPDATE_INTERVAL]))
    return effect


@register_binary_effect(
    "strobe",
    StrobeLightEffect,
    "Strobe",
    {
        cv.Optional(
            CONF_COLORS,
            default=[
                {CONF_STATE: True, CONF_DURATION: "0.5s"},
                {CONF_STATE: False, CONF_DURATION: "0.5s"},
            ],
        ): cv.All(
            cv.ensure_list(
                cv.Schema(
                    {
                        cv.Optional(CONF_STATE, default=True): cv.boolean,
                        cv.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage,
                        cv.Optional(CONF_COLOR_MODE): cv.enum(
                            COLOR_MODES, upper=True, space="_"
                        ),
                        cv.Optional(CONF_COLOR_BRIGHTNESS, default=1.0): cv.percentage,
                        cv.Optional(CONF_RED, default=1.0): cv.percentage,
                        cv.Optional(CONF_GREEN, default=1.0): cv.percentage,
                        cv.Optional(CONF_BLUE, default=1.0): cv.percentage,
                        cv.Optional(CONF_WHITE, default=1.0): cv.percentage,
                        cv.Optional(CONF_COLOR_TEMPERATURE): cv.color_temperature,
                        cv.Optional(CONF_COLD_WHITE, default=1.0): cv.percentage,
                        cv.Optional(CONF_WARM_WHITE, default=1.0): cv.percentage,
                        cv.Required(
                            CONF_DURATION
                        ): cv.positive_time_period_milliseconds,
                        cv.Optional(
                            CONF_TRANSITION_LENGTH, default="0s"
                        ): cv.positive_time_period_milliseconds,
                    }
                ),
                cv.has_at_least_one_key(
                    CONF_STATE,
                    CONF_BRIGHTNESS,
                    CONF_COLOR_MODE,
                    CONF_COLOR_BRIGHTNESS,
                    CONF_RED,
                    CONF_GREEN,
                    CONF_BLUE,
                    CONF_WHITE,
                    CONF_COLOR_TEMPERATURE,
                    CONF_COLD_WHITE,
                    CONF_WARM_WHITE,
                ),
            ),
            cv.Length(min=2),
        ),
    },
)
async def strobe_effect_to_code(config, effect_id):
    var = cg.new_Pvariable(effect_id, config[CONF_NAME])
    colors = [
        cg.StructInitializer(
            StrobeLightEffectColor,
            (
                "color",
                LightColorValues(
                    color.get(CONF_COLOR_MODE, ColorMode.UNKNOWN),
                    color[CONF_STATE],
                    color[CONF_BRIGHTNESS],
                    color[CONF_COLOR_BRIGHTNESS],
                    color[CONF_RED],
                    color[CONF_GREEN],
                    color[CONF_BLUE],
                    color[CONF_WHITE],
                    color.get(CONF_COLOR_TEMPERATURE, 0.0),
                    color[CONF_COLD_WHITE],
                    color[CONF_WARM_WHITE],
                ),
            ),
            ("duration", color[CONF_DURATION]),
            ("transition_length", color[CONF_TRANSITION_LENGTH]),
        )
        for color in config.get(CONF_COLORS, [])
    ]
    cg.add(var.set_colors(colors))
    return var


@register_monochromatic_effect(
    "flicker",
    FlickerLightEffect,
    "Flicker",
    {
        cv.Optional(CONF_ALPHA, default=0.95): cv.percentage,
        cv.Optional(CONF_INTENSITY, default=0.015): cv.percentage,
    },
)
async def flicker_effect_to_code(config, effect_id):
    var = cg.new_Pvariable(effect_id, config[CONF_NAME])
    cg.add(var.set_alpha(config[CONF_ALPHA]))
    cg.add(var.set_intensity(config[CONF_INTENSITY]))
    return var


@register_addressable_effect(
    "addressable_lambda",
    AddressableLambdaLightEffect,
    "Addressable Lambda",
    {
        cv.Required(CONF_LAMBDA): cv.lambda_,
        cv.Optional(
            CONF_UPDATE_INTERVAL, default="0ms"
        ): cv.positive_time_period_milliseconds,
    },
)
async def addressable_lambda_effect_to_code(config, effect_id):
    args = [
        (AddressableLightRef, "it"),
        (Color, "current_color"),
        (bool, "initial_run"),
    ]
    lambda_ = await cg.process_lambda(config[CONF_LAMBDA], args, return_type=cg.void)
    return cg.new_Pvariable(
        effect_id, config[CONF_NAME], lambda_, config[CONF_UPDATE_INTERVAL]
    )


@register_addressable_effect(
    "addressable_rainbow",
    AddressableRainbowLightEffect,
    "Rainbow",
    {
        cv.Optional(CONF_SPEED, default=10): cv.uint32_t,
        cv.Optional(CONF_WIDTH, default=50): cv.int_range(min=1, max=65535),
    },
)
async def addressable_rainbow_effect_to_code(config, effect_id):
    var = cg.new_Pvariable(effect_id, config[CONF_NAME])
    cg.add(var.set_speed(config[CONF_SPEED]))
    cg.add(var.set_width(config[CONF_WIDTH]))
    return var


@register_addressable_effect(
    "addressable_color_wipe",
    AddressableColorWipeEffect,
    "Color Wipe",
    {
        cv.Optional(
            CONF_COLORS, default=[{CONF_NUM_LEDS: 1, CONF_RANDOM: True}]
        ): cv.ensure_list(
            {
                cv.Optional(CONF_RED, default=1.0): cv.percentage,
                cv.Optional(CONF_GREEN, default=1.0): cv.percentage,
                cv.Optional(CONF_BLUE, default=1.0): cv.percentage,
                cv.Optional(CONF_WHITE, default=1.0): cv.percentage,
                cv.Optional(CONF_RANDOM, default=False): cv.boolean,
                cv.Required(CONF_NUM_LEDS): cv.All(cv.uint32_t, cv.Range(min=1)),
                cv.Optional(CONF_GRADIENT, default=False): cv.boolean,
            }
        ),
        cv.Optional(
            CONF_ADD_LED_INTERVAL, default="0.1s"
        ): cv.positive_time_period_milliseconds,
        cv.Optional(CONF_REVERSE, default=False): cv.boolean,
    },
)
async def addressable_color_wipe_effect_to_code(config, effect_id):
    var = cg.new_Pvariable(effect_id, config[CONF_NAME])
    cg.add(var.set_add_led_interval(config[CONF_ADD_LED_INTERVAL]))
    cg.add(var.set_reverse(config[CONF_REVERSE]))
    colors = [
        cg.StructInitializer(
            AddressableColorWipeEffectColor,
            ("r", int(round(color[CONF_RED] * 255))),
            ("g", int(round(color[CONF_GREEN] * 255))),
            ("b", int(round(color[CONF_BLUE] * 255))),
            ("w", int(round(color[CONF_WHITE] * 255))),
            ("random", color[CONF_RANDOM]),
            ("num_leds", color[CONF_NUM_LEDS]),
            ("gradient", color[CONF_GRADIENT]),
        )
        for color in config.get(CONF_COLORS, [])
    ]
    cg.add(var.set_colors(colors))
    return var


@register_addressable_effect(
    "addressable_scan",
    AddressableScanEffect,
    "Scan",
    {
        cv.Optional(
            CONF_MOVE_INTERVAL, default="0.1s"
        ): cv.positive_time_period_milliseconds,
        cv.Optional(CONF_SCAN_WIDTH, default=1): cv.int_range(min=1),
    },
)
async def addressable_scan_effect_to_code(config, effect_id):
    var = cg.new_Pvariable(effect_id, config[CONF_NAME])
    cg.add(var.set_move_interval(config[CONF_MOVE_INTERVAL]))
    cg.add(var.set_scan_width(config[CONF_SCAN_WIDTH]))
    return var


@register_addressable_effect(
    "addressable_twinkle",
    AddressableTwinkleEffect,
    "Twinkle",
    {
        cv.Optional(CONF_TWINKLE_PROBABILITY, default="5%"): cv.percentage,
        cv.Optional(
            CONF_PROGRESS_INTERVAL, default="4ms"
        ): cv.positive_time_period_milliseconds,
    },
)
async def addressable_twinkle_effect_to_code(config, effect_id):
    var = cg.new_Pvariable(effect_id, config[CONF_NAME])
    cg.add(var.set_twinkle_probability(config[CONF_TWINKLE_PROBABILITY]))
    cg.add(var.set_progress_interval(config[CONF_PROGRESS_INTERVAL]))
    return var


@register_addressable_effect(
    "addressable_random_twinkle",
    AddressableRandomTwinkleEffect,
    "Random Twinkle",
    {
        cv.Optional(CONF_TWINKLE_PROBABILITY, default="5%"): cv.percentage,
        cv.Optional(
            CONF_PROGRESS_INTERVAL, default="32ms"
        ): cv.positive_time_period_milliseconds,
    },
)
async def addressable_random_twinkle_effect_to_code(config, effect_id):
    var = cg.new_Pvariable(effect_id, config[CONF_NAME])
    cg.add(var.set_twinkle_probability(config[CONF_TWINKLE_PROBABILITY]))
    cg.add(var.set_progress_interval(config[CONF_PROGRESS_INTERVAL]))
    return var


@register_addressable_effect(
    "addressable_fireworks",
    AddressableFireworksEffect,
    "Fireworks",
    {
        cv.Optional(
            CONF_UPDATE_INTERVAL, default="32ms"
        ): cv.positive_time_period_milliseconds,
        cv.Optional(CONF_SPARK_PROBABILITY, default="10%"): cv.percentage,
        cv.Optional(CONF_USE_RANDOM_COLOR, default=False): cv.boolean,
        cv.Optional(CONF_FADE_OUT_RATE, default=120): cv.uint8_t,
    },
)
async def addressable_fireworks_effect_to_code(config, effect_id):
    var = cg.new_Pvariable(effect_id, config[CONF_NAME])
    cg.add(var.set_update_interval(config[CONF_UPDATE_INTERVAL]))
    cg.add(var.set_spark_probability(config[CONF_SPARK_PROBABILITY]))
    cg.add(var.set_use_random_color(config[CONF_USE_RANDOM_COLOR]))
    cg.add(var.set_fade_out_rate(config[CONF_FADE_OUT_RATE]))
    return var


@register_addressable_effect(
    "addressable_flicker",
    AddressableFlickerEffect,
    "Addressable Flicker",
    {
        cv.Optional(
            CONF_UPDATE_INTERVAL, default="16ms"
        ): cv.positive_time_period_milliseconds,
        cv.Optional(CONF_INTENSITY, default="5%"): cv.percentage,
    },
)
async def addressable_flicker_effect_to_code(config, effect_id):
    var = cg.new_Pvariable(effect_id, config[CONF_NAME])
    cg.add(var.set_update_interval(config[CONF_UPDATE_INTERVAL]))
    cg.add(var.set_intensity(config[CONF_INTENSITY]))
    return var


def validate_effects(allowed_effects):
    @schema_extractor("effects")
    def validator(value):
        if value == SCHEMA_EXTRACT:
            return (allowed_effects, EFFECTS_REGISTRY)

        value = cv.validate_registry("effect", EFFECTS_REGISTRY)(value)
        errors = []
        names = set()
        for i, x in enumerate(value):
            key = next(it for it in x)
            if key not in allowed_effects:
                errors.append(
                    cv.Invalid(
                        f"The effect '{key}' is not allowed for this light type",
                        [i],
                    )
                )
                continue
            name = x[key][CONF_NAME]
            if name in names:
                errors.append(
                    cv.Invalid(
                        f"Found the effect name '{name}' twice. All effects must have unique names",
                        [i],
                    )
                )
                continue
            names.add(name)
        if errors:
            raise cv.MultipleInvalid(errors)
        return value

    return validator
