import operator
from functools import reduce

from esphome.const import (
    CONF_INPUT,
    CONF_MODE,
    CONF_NUMBER,
    CONF_OPEN_DRAIN,
    CONF_OUTPUT,
    CONF_PULLDOWN,
    CONF_PULLUP,
)
from esphome.util import SimpleRegistry
from esphome.core import CORE


PIN_SCHEMA_REGISTRY = SimpleRegistry()


def _set_mode(value, default_mode):
    import esphome.config_validation as cv

    if CONF_MODE not in value:
        return {**value, CONF_MODE: default_mode}
    mode = value[CONF_MODE]
    if not isinstance(mode, str):
        return value
    # mode is a string, try parsing it like arduino pin modes
    PIN_MODES = {
        "INPUT": {
            CONF_INPUT: True,
        },
        "OUTPUT": {
            CONF_OUTPUT: True,
        },
        "INPUT_PULLUP": {
            CONF_INPUT: True,
            CONF_PULLUP: True,
        },
        "OUTPUT_OPEN_DRAIN": {
            CONF_OUTPUT: True,
            CONF_OPEN_DRAIN: True,
        },
        "INPUT_PULLDOWN_16": {
            CONF_INPUT: True,
            CONF_PULLDOWN: True,
        },
        "INPUT_PULLDOWN": {
            CONF_INPUT: True,
            CONF_PULLDOWN: True,
        },
        "INPUT_OUTPUT_OPEN_DRAIN": {
            CONF_INPUT: True,
            CONF_OUTPUT: True,
            CONF_OPEN_DRAIN: True,
        },
    }
    if mode.upper() not in PIN_MODES:
        raise cv.Invalid(f"Unknown pin mode {mode}", [CONF_MODE])
    return {**value, CONF_MODE: PIN_MODES[mode.upper()]}


def _schema_creator(default_mode, internal: bool = False):
    def validator(value):
        if not isinstance(value, dict):
            return validator({CONF_NUMBER: value})
        value = _set_mode(value, default_mode)
        if not internal:
            for key, entry in PIN_SCHEMA_REGISTRY.items():
                if key != CORE.target_platform and key in value:
                    return entry[1](value)
        return PIN_SCHEMA_REGISTRY[CORE.target_platform][1](value)

    return validator


def _internal_number_creator(mode):
    def validator(value):
        value_d = {CONF_NUMBER: value}
        value_d = _set_mode(value_d, mode)
        return PIN_SCHEMA_REGISTRY[CORE.target_platform][1](value_d)[CONF_NUMBER]

    return validator


def gpio_flags_expr(mode):
    """Convert the given mode dict to a gpio Flags expression"""
    import esphome.codegen as cg

    FLAGS_MAPPING = {
        CONF_INPUT: cg.gpio_Flags.FLAG_INPUT,
        CONF_OUTPUT: cg.gpio_Flags.FLAG_OUTPUT,
        CONF_OPEN_DRAIN: cg.gpio_Flags.FLAG_OPEN_DRAIN,
        CONF_PULLUP: cg.gpio_Flags.FLAG_PULLUP,
        CONF_PULLDOWN: cg.gpio_Flags.FLAG_PULLDOWN,
    }
    active_flags = [v for k, v in FLAGS_MAPPING.items() if mode.get(k)]
    if not active_flags:
        return cg.gpio_Flags.FLAG_NONE

    return reduce(operator.or_, active_flags)


gpio_pin_schema = _schema_creator
internal_gpio_pin_number = _internal_number_creator
gpio_output_pin_schema = _schema_creator(
    {
        CONF_OUTPUT: True,
    }
)
gpio_input_pin_schema = _schema_creator(
    {
        CONF_INPUT: True,
    }
)
gpio_input_pullup_pin_schema = _schema_creator(
    {
        CONF_INPUT: True,
        CONF_PULLUP: True,
    }
)
internal_gpio_output_pin_schema = _schema_creator(
    {
        CONF_OUTPUT: True,
    },
    internal=True,
)
internal_gpio_output_pin_number = _internal_number_creator({CONF_OUTPUT: True})
internal_gpio_input_pin_schema = _schema_creator(
    {
        CONF_INPUT: True,
    },
    internal=True,
)
internal_gpio_input_pin_number = _internal_number_creator({CONF_INPUT: True})
internal_gpio_input_pullup_pin_schema = _schema_creator(
    {
        CONF_INPUT: True,
        CONF_PULLUP: True,
    },
    internal=True,
)
internal_gpio_input_pullup_pin_number = _internal_number_creator(
    {
        CONF_INPUT: True,
        CONF_PULLUP: True,
    }
)
