from esphome import automation
from esphome.automation import maybe_simple_id
import esphome.codegen as cg
from esphome.components import audio
import esphome.config_validation as cv
from esphome.const import (
    CONF_BITS_PER_SAMPLE,
    CONF_CHANNELS,
    CONF_GAIN_FACTOR,
    CONF_ID,
    CONF_MICROPHONE,
    CONF_ON_DATA,
    CONF_TRIGGER_ID,
)
from esphome.core import CORE
from esphome.coroutine import CoroPriority, coroutine_with_priority

AUTO_LOAD = ["audio"]
CODEOWNERS = ["@jesserockz", "@kahrendt"]

IS_PLATFORM_COMPONENT = True

microphone_ns = cg.esphome_ns.namespace("microphone")

Microphone = microphone_ns.class_("Microphone")
MicrophoneSource = microphone_ns.class_("MicrophoneSource")

CaptureAction = microphone_ns.class_(
    "CaptureAction", automation.Action, cg.Parented.template(Microphone)
)
StopCaptureAction = microphone_ns.class_(
    "StopCaptureAction", automation.Action, cg.Parented.template(Microphone)
)
MuteAction = microphone_ns.class_(
    "MuteAction", automation.Action, cg.Parented.template(Microphone)
)
UnmuteAction = microphone_ns.class_(
    "UnmuteAction", automation.Action, cg.Parented.template(Microphone)
)


DataTrigger = microphone_ns.class_(
    "DataTrigger",
    automation.Trigger.template(cg.std_vector.template(cg.uint8).operator("ref")),
)

IsCapturingCondition = microphone_ns.class_(
    "IsCapturingCondition", automation.Condition
)
IsMutedCondition = microphone_ns.class_("IsMutedCondition", automation.Condition)


async def setup_microphone_core_(var, config):
    for conf in config.get(CONF_ON_DATA, []):
        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
        await automation.build_automation(
            trigger,
            [(cg.std_vector.template(cg.uint8).operator("ref").operator("const"), "x")],
            conf,
        )


async def register_microphone(var, config):
    if not CORE.has_id(config[CONF_ID]):
        var = cg.Pvariable(config[CONF_ID], var)
    await setup_microphone_core_(var, config)


MICROPHONE_SCHEMA = cv.Schema.extend(audio.AUDIO_COMPONENT_SCHEMA).extend(
    {
        cv.Optional(CONF_ON_DATA): automation.validate_automation(
            {
                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DataTrigger),
            }
        ),
    }
)


MICROPHONE_ACTION_SCHEMA = maybe_simple_id({cv.GenerateID(): cv.use_id(Microphone)})


def microphone_source_schema(
    min_bits_per_sample: int = 16,
    max_bits_per_sample: int = 16,
    min_channels: int = 1,
    max_channels: int = 1,
):
    """Schema for a microphone source

    Components requesting microphone data should use this schema instead of accessing a microphone directly.

    Args:
      min_bits_per_sample (int, optional): Minimum number of bits per sample the requesting component supports. Defaults to 16.
      max_bits_per_sample (int, optional): Maximum number of bits per sample the requesting component supports. Defaults to 16.
      min_channels (int, optional): Minimum number of channels the requesting component supports. Defaults to 1.
      max_channels (int, optional): Maximum number of channels the requesting component supports. Defaults to 1.
    """

    def _validate_unique_channels(config):
        if len(config) != len(set(config)):
            raise cv.Invalid("Channels must be unique")
        return config

    return cv.All(
        automation.maybe_conf(
            CONF_MICROPHONE,
            {
                cv.GenerateID(CONF_ID): cv.declare_id(MicrophoneSource),
                cv.GenerateID(CONF_MICROPHONE): cv.use_id(Microphone),
                cv.Optional(CONF_BITS_PER_SAMPLE, default=16): cv.int_range(
                    min_bits_per_sample, max_bits_per_sample
                ),
                cv.Optional(CONF_CHANNELS, default="0"): cv.All(
                    cv.ensure_list(cv.int_range(0, 7)),
                    cv.Length(min=min_channels, max=max_channels),
                    _validate_unique_channels,
                ),
                cv.Optional(CONF_GAIN_FACTOR, default="1"): cv.int_range(1, 64),
            },
        ),
    )


def final_validate_microphone_source_schema(
    component_name: str, sample_rate: int = cv.UNDEFINED
):
    """Validates that the microphone source can provide audio in the correct format. In particular it validates the sample rate and the enabled channels.

    Note that:
      - MicrophoneSource class automatically handles converting bits per sample, so no need to validate
      - microphone_source_schema already validates that channels are unique and specifies the max number of channels the component supports

    Args:
        component_name (str): The name of the component requesting mic audio
        sample_rate (int, optional): The sample rate the component requesting mic audio requires
    """

    def _validate_audio_compatability(config):
        if sample_rate is not cv.UNDEFINED:
            # Issues require changing the microphone configuration
            #  - Verifies sample rates match
            audio.final_validate_audio_schema(
                component_name,
                audio_device=CONF_MICROPHONE,
                sample_rate=sample_rate,
                audio_device_issue=True,
            )(config)

        # Issues require changing the MicrophoneSource configuration
        # - Verifies that each of the enabled channels are available
        audio.final_validate_audio_schema(
            component_name,
            audio_device=CONF_MICROPHONE,
            enabled_channels=config[CONF_CHANNELS],
            audio_device_issue=False,
        )(config)

        return config

    return _validate_audio_compatability


async def microphone_source_to_code(config, passive=False):
    """Creates a MicrophoneSource variable for codegen.

    Setting passive to true makes the MicrophoneSource never start/stop the microphone, but only receives audio when another component has actively started the Microphone. If false, then the microphone needs to be explicitly started/stopped.

    Args:
        config (Schema): Created with `microphone_source_schema` specifying bits per sample, channels, and gain factor
        passive (bool): Enable passive mode for the MicrophoneSource
    """
    mic = await cg.get_variable(config[CONF_MICROPHONE])
    mic_source = cg.new_Pvariable(
        config[CONF_ID],
        mic,
        config[CONF_BITS_PER_SAMPLE],
        config[CONF_GAIN_FACTOR],
        passive,
    )
    for channel in config[CONF_CHANNELS]:
        cg.add(mic_source.add_channel(channel))
    return mic_source


async def microphone_action(config, action_id, template_arg, args):
    var = cg.new_Pvariable(action_id, template_arg)
    await cg.register_parented(var, config[CONF_ID])
    return var


automation.register_action(
    "microphone.capture",
    CaptureAction,
    MICROPHONE_ACTION_SCHEMA,
    synchronous=True,
)(microphone_action)

automation.register_action(
    "microphone.stop_capture",
    StopCaptureAction,
    MICROPHONE_ACTION_SCHEMA,
    synchronous=True,
)(microphone_action)

automation.register_action(
    "microphone.mute", MuteAction, MICROPHONE_ACTION_SCHEMA, synchronous=True
)(microphone_action)
automation.register_action(
    "microphone.unmute", UnmuteAction, MICROPHONE_ACTION_SCHEMA, synchronous=True
)(microphone_action)

automation.register_condition(
    "microphone.is_capturing", IsCapturingCondition, MICROPHONE_ACTION_SCHEMA
)(microphone_action)
automation.register_condition(
    "microphone.is_muted", IsMutedCondition, MICROPHONE_ACTION_SCHEMA
)(microphone_action)


@coroutine_with_priority(CoroPriority.CORE)
async def to_code(config):
    cg.add_global(microphone_ns.using)
    cg.add_define("USE_MICROPHONE")
