"""
Device discovery functions for Zigbee Home Automation.

For more details about this component, please refer to the documentation at
https://home-assistant.io/components/zha/
"""

import logging

from homeassistant import const as ha_const
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR
from homeassistant.components.sensor import DOMAIN as SENSOR
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_send
from .channels import (
    AttributeListeningChannel, EventRelayChannel, ZDOChannel
)
from .channels.registry import ZIGBEE_CHANNEL_REGISTRY
from .const import (
    CONF_DEVICE_CONFIG, COMPONENTS, ZHA_DISCOVERY_NEW, DATA_ZHA,
    SENSOR_TYPE, UNKNOWN, GENERIC
)
from .registries import (
    BINARY_SENSOR_TYPES, CHANNEL_ONLY_CLUSTERS, EVENT_RELAY_CLUSTERS,
    SENSOR_TYPES, DEVICE_CLASS, COMPONENT_CLUSTERS,
    SINGLE_INPUT_CLUSTER_DEVICE_CLASS, SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS,
    OUTPUT_CHANNEL_ONLY_CLUSTERS, REMOTE_DEVICE_TYPES
)

_LOGGER = logging.getLogger(__name__)


@callback
def async_process_endpoint(
        hass, config, endpoint_id, endpoint, discovery_infos, device,
        zha_device, is_new_join):
    """Process an endpoint on a zigpy device."""
    import zigpy.profiles

    if endpoint_id == 0:  # ZDO
        _async_create_cluster_channel(
            endpoint,
            zha_device,
            is_new_join,
            channel_class=ZDOChannel
        )
        return

    component = None
    profile_clusters = []
    device_key = "{}-{}".format(device.ieee, endpoint_id)
    node_config = {}
    if CONF_DEVICE_CONFIG in config:
        node_config = config[CONF_DEVICE_CONFIG].get(
            device_key, {}
        )

    if endpoint.profile_id in zigpy.profiles.PROFILES:
        if DEVICE_CLASS.get(endpoint.profile_id, {}).get(
                endpoint.device_type, None):
            profile_info = DEVICE_CLASS[endpoint.profile_id]
            component = profile_info[endpoint.device_type]

    if ha_const.CONF_TYPE in node_config:
        component = node_config[ha_const.CONF_TYPE]

    if component and component in COMPONENTS and \
            component in COMPONENT_CLUSTERS:
        profile_clusters = COMPONENT_CLUSTERS[component]
        if profile_clusters:
            profile_match = _async_handle_profile_match(
                hass, endpoint, profile_clusters, zha_device,
                component, device_key, is_new_join)
            discovery_infos.append(profile_match)

    discovery_infos.extend(_async_handle_single_cluster_matches(
        hass,
        endpoint,
        zha_device,
        profile_clusters,
        device_key,
        is_new_join
    ))


@callback
def _async_create_cluster_channel(cluster, zha_device, is_new_join,
                                  channels=None, channel_class=None):
    """Create a cluster channel and attach it to a device."""
    # really ugly hack to deal with xiaomi using the door lock cluster
    # incorrectly.
    if hasattr(cluster, 'ep_attribute') and \
            cluster.ep_attribute == 'multistate_input':
        channel_class = AttributeListeningChannel
    # end of ugly hack
    if channel_class is None:
        channel_class = ZIGBEE_CHANNEL_REGISTRY.get(cluster.cluster_id,
                                                    AttributeListeningChannel)
    channel = channel_class(cluster, zha_device)
    zha_device.add_cluster_channel(channel)
    if channels is not None:
        channels.append(channel)


@callback
def async_dispatch_discovery_info(hass, is_new_join, discovery_info):
    """Dispatch or store discovery information."""
    if not discovery_info['channels']:
        _LOGGER.warning(
            "there are no channels in the discovery info: %s", discovery_info)
        return
    component = discovery_info['component']
    if is_new_join:
        async_dispatcher_send(
            hass,
            ZHA_DISCOVERY_NEW.format(component),
            discovery_info
        )
    else:
        hass.data[DATA_ZHA][component][discovery_info['unique_id']] = \
            discovery_info


@callback
def _async_handle_profile_match(hass, endpoint, profile_clusters, zha_device,
                                component, device_key, is_new_join):
    """Dispatch a profile match to the appropriate HA component."""
    in_clusters = [endpoint.in_clusters[c]
                   for c in profile_clusters
                   if c in endpoint.in_clusters]
    out_clusters = [endpoint.out_clusters[c]
                    for c in profile_clusters
                    if c in endpoint.out_clusters]

    channels = []

    for cluster in in_clusters:
        _async_create_cluster_channel(
            cluster, zha_device, is_new_join, channels=channels)

    for cluster in out_clusters:
        _async_create_cluster_channel(
            cluster, zha_device, is_new_join, channels=channels)

    discovery_info = {
        'unique_id': device_key,
        'zha_device': zha_device,
        'channels': channels,
        'component': component
    }

    if component == BINARY_SENSOR:
        discovery_info.update({SENSOR_TYPE: UNKNOWN})
        for cluster_id in profile_clusters:
            if cluster_id in BINARY_SENSOR_TYPES:
                discovery_info.update({
                    SENSOR_TYPE: BINARY_SENSOR_TYPES.get(
                        cluster_id, UNKNOWN)
                })
                break

    return discovery_info


@callback
def _async_handle_single_cluster_matches(hass, endpoint, zha_device,
                                         profile_clusters, device_key,
                                         is_new_join):
    """Dispatch single cluster matches to HA components."""
    from zigpy.zcl.clusters.general import OnOff, PowerConfiguration
    cluster_matches = []
    cluster_match_results = []
    matched_power_configuration = False
    for cluster in endpoint.in_clusters.values():
        if cluster.cluster_id in CHANNEL_ONLY_CLUSTERS:
            cluster_match_results.append(
                _async_handle_channel_only_cluster_match(
                    zha_device,
                    cluster,
                    is_new_join,
                ))
            continue

        if cluster.cluster_id not in profile_clusters:
            # Only create one battery sensor per device
            if cluster.cluster_id == PowerConfiguration.cluster_id and \
                    (zha_device.is_mains_powered or
                     matched_power_configuration):
                continue
            elif cluster.cluster_id == PowerConfiguration.cluster_id and not \
                    zha_device.is_mains_powered:
                matched_power_configuration = True
            cluster_match_results.append(_async_handle_single_cluster_match(
                hass,
                zha_device,
                cluster,
                device_key,
                SINGLE_INPUT_CLUSTER_DEVICE_CLASS,
                is_new_join,
            ))

    for cluster in endpoint.out_clusters.values():
        if cluster.cluster_id in OUTPUT_CHANNEL_ONLY_CLUSTERS:
            cluster_match_results.append(
                _async_handle_channel_only_cluster_match(
                    zha_device,
                    cluster,
                    is_new_join,
                ))
            continue

        device_type = cluster.endpoint.device_type
        profile_id = cluster.endpoint.profile_id

        if cluster.cluster_id not in profile_clusters:
            # prevent remotes and controllers from getting entities
            if not (cluster.cluster_id == OnOff.cluster_id and profile_id in
                    REMOTE_DEVICE_TYPES and device_type in
                    REMOTE_DEVICE_TYPES[profile_id]):
                cluster_match_results.append(
                    _async_handle_single_cluster_match(
                        hass,
                        zha_device,
                        cluster,
                        device_key,
                        SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS,
                        is_new_join,
                    )
                )

        if cluster.cluster_id in EVENT_RELAY_CLUSTERS:
            _async_create_cluster_channel(
                cluster,
                zha_device,
                is_new_join,
                channel_class=EventRelayChannel
            )

    for cluster_match in cluster_match_results:
        if cluster_match is not None:
            cluster_matches.append(cluster_match)
    return cluster_matches


@callback
def _async_handle_channel_only_cluster_match(
        zha_device, cluster, is_new_join):
    """Handle a channel only cluster match."""
    _async_create_cluster_channel(cluster, zha_device, is_new_join)


@callback
def _async_handle_single_cluster_match(hass, zha_device, cluster, device_key,
                                       device_classes, is_new_join):
    """Dispatch a single cluster match to a HA component."""
    component = None  # sub_component = None
    for cluster_type, candidate_component in device_classes.items():
        if isinstance(cluster_type, int):
            if cluster.cluster_id == cluster_type:
                component = candidate_component
        elif isinstance(cluster, cluster_type):
            component = candidate_component
            break

    if component is None or component not in COMPONENTS:
        return
    channels = []
    _async_create_cluster_channel(cluster, zha_device, is_new_join,
                                  channels=channels)

    cluster_key = "{}-{}".format(device_key, cluster.cluster_id)
    discovery_info = {
        'unique_id': cluster_key,
        'zha_device': zha_device,
        'channels': channels,
        'entity_suffix': '_{}'.format(cluster.cluster_id),
        'component': component
    }

    if component == SENSOR:
        discovery_info.update({
            SENSOR_TYPE: SENSOR_TYPES.get(cluster.cluster_id, GENERIC)
        })
    if component == BINARY_SENSOR:
        discovery_info.update({
            SENSOR_TYPE: BINARY_SENSOR_TYPES.get(cluster.cluster_id, UNKNOWN)
        })

    return discovery_info
