"""Helpers for components that manage entities."""
from threading import Lock

from homeassistant.bootstrap import prepare_setup_platform
from homeassistant.components import group
from homeassistant.const import (
    ATTR_ENTITY_ID, CONF_SCAN_INTERVAL, CONF_ENTITY_NAMESPACE,
    DEVICE_DEFAULT_NAME)
from homeassistant.helpers import config_per_platform, discovery
from homeassistant.helpers.entity import generate_entity_id
from homeassistant.helpers.event import track_utc_time_change
from homeassistant.helpers.service import extract_entity_ids

DEFAULT_SCAN_INTERVAL = 15


class EntityComponent(object):
    """Helper class that will help a component manage its entities."""

    # pylint: disable=too-many-instance-attributes
    # pylint: disable=too-many-arguments
    def __init__(self, logger, domain, hass,
                 scan_interval=DEFAULT_SCAN_INTERVAL, group_name=None):
        """Initialize an entity component."""
        self.logger = logger
        self.hass = hass

        self.domain = domain
        self.entity_id_format = domain + '.{}'
        self.scan_interval = scan_interval
        self.group_name = group_name

        self.entities = {}
        self.group = None
        self.is_polling = False

        self.config = None
        self.lock = Lock()

        self.add_entities = EntityPlatform(self, self.scan_interval,
                                           None).add_entities

    def setup(self, config):
        """Set up a full entity component.

        Loads the platforms from the config and will listen for supported
        discovered platforms.
        """
        self.config = config

        # Look in config for Domain, Domain 2, Domain 3 etc and load them
        for p_type, p_config in config_per_platform(config, self.domain):
            self._setup_platform(p_type, p_config)

        # Generic discovery listener for loading platform dynamically
        # Refer to: homeassistant.components.discovery.load_platform()
        def component_platform_discovered(platform, info):
            """Callback to load a platform."""
            self._setup_platform(platform, {}, info)

        discovery.listen_platform(self.hass, self.domain,
                                  component_platform_discovered)

    def extract_from_service(self, service):
        """Extract all known entities from a service call.

        Will return all entities if no entities specified in call.
        Will return an empty list if entities specified but unknown.
        """
        with self.lock:
            if ATTR_ENTITY_ID not in service.data:
                return list(self.entities.values())

            return [self.entities[entity_id] for entity_id
                    in extract_entity_ids(self.hass, service)
                    if entity_id in self.entities]

    def _setup_platform(self, platform_type, platform_config,
                        discovery_info=None):
        """Setup a platform for this component."""
        platform = prepare_setup_platform(
            self.hass, self.config, self.domain, platform_type)

        if platform is None:
            return

        # Config > Platform > Component
        scan_interval = platform_config.get(
            CONF_SCAN_INTERVAL,
            getattr(platform, 'SCAN_INTERVAL', self.scan_interval))
        entity_namespace = platform_config.get(CONF_ENTITY_NAMESPACE)

        try:
            platform.setup_platform(
                self.hass, platform_config,
                EntityPlatform(self, scan_interval,
                               entity_namespace).add_entities,
                discovery_info)

            self.hass.config.components.append(
                '{}.{}'.format(self.domain, platform_type))
        except Exception:  # pylint: disable=broad-except
            self.logger.exception(
                'Error while setting up platform %s', platform_type)

    def add_entity(self, entity, platform=None):
        """Add entity to component."""
        if entity is None or entity in self.entities.values():
            return False

        entity.hass = self.hass

        if getattr(entity, 'entity_id', None) is None:
            object_id = entity.name or DEVICE_DEFAULT_NAME

            if platform is not None and platform.entity_namespace is not None:
                object_id = '{} {}'.format(platform.entity_namespace,
                                           object_id)

            entity.entity_id = generate_entity_id(
                self.entity_id_format, object_id,
                self.entities.keys())

        self.entities[entity.entity_id] = entity
        entity.update_ha_state()

        return True

    def update_group(self):
        """Set up and/or update component group."""
        if self.group is None and self.group_name is not None:
            self.group = group.Group(self.hass, self.group_name,
                                     user_defined=False)

        if self.group is not None:
            self.group.update_tracked_entity_ids(self.entities.keys())


class EntityPlatform(object):
    """Keep track of entities for a single platform."""

    # pylint: disable=too-few-public-methods
    def __init__(self, component, scan_interval, entity_namespace):
        """Initalize the entity platform."""
        self.component = component
        self.scan_interval = scan_interval
        self.entity_namespace = entity_namespace
        self.platform_entities = []
        self.is_polling = False

    def add_entities(self, new_entities):
        """Add entities for a single platform."""
        with self.component.lock:
            for entity in new_entities:
                if self.component.add_entity(entity, self):
                    self.platform_entities.append(entity)

            self.component.update_group()

            if self.is_polling or \
               not any(entity.should_poll for entity
                       in self.platform_entities):
                return

            self.is_polling = True

            track_utc_time_change(
                self.component.hass, self._update_entity_states,
                second=range(0, 60, self.scan_interval))

    def _update_entity_states(self, now):
        """Update the states of all the polling entities."""
        with self.component.lock:
            # We copy the entities because new entities might be detected
            # during state update causing deadlocks.
            entities = list(entity for entity in self.platform_entities
                            if entity.should_poll)

        for entity in entities:
            entity.update_ha_state(True)
