"""
homeassistant.components.groups
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Provides functionality to group devices that can be turned on or off.
"""

import homeassistant as ha
from homeassistant.helpers import generate_entity_id
from homeassistant.helpers.entity import Entity
import homeassistant.util as util
from homeassistant.const import (
    ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, STATE_ON, STATE_OFF,
    STATE_HOME, STATE_NOT_HOME, STATE_UNKNOWN, ATTR_HIDDEN)

DOMAIN = "group"
DEPENDENCIES = []

ENTITY_ID_FORMAT = DOMAIN + ".{}"

ATTR_AUTO = "auto"

# List of ON/OFF state tuples for groupable states
_GROUP_TYPES = [(STATE_ON, STATE_OFF), (STATE_HOME, STATE_NOT_HOME)]


def _get_group_on_off(state):
    """ Determine the group on/off states based on a state. """
    for states in _GROUP_TYPES:
        if state in states:
            return states

    return None, None


def is_on(hass, entity_id):
    """ Returns if the group state is in its ON-state. """
    state = hass.states.get(entity_id)

    if state:
        group_on, _ = _get_group_on_off(state.state)

        # If we found a group_type, compare to ON-state
        return group_on is not None and state.state == group_on

    return False


def expand_entity_ids(hass, entity_ids):
    """ Returns the given list of entity ids and expands group ids into
        the entity ids it represents if found. """
    found_ids = []

    for entity_id in entity_ids:
        if not isinstance(entity_id, str):
            continue

        entity_id = entity_id.lower()

        try:
            # If entity_id points at a group, expand it
            domain, _ = util.split_entity_id(entity_id)

            if domain == DOMAIN:
                found_ids.extend(
                    ent_id for ent_id
                    in get_entity_ids(hass, entity_id)
                    if ent_id not in found_ids)

            else:
                if entity_id not in found_ids:
                    found_ids.append(entity_id)

        except AttributeError:
            # Raised by util.split_entity_id if entity_id is not a string
            pass

    return found_ids


def get_entity_ids(hass, entity_id, domain_filter=None):
    """ Get the entity ids that make up this group. """
    entity_id = entity_id.lower()

    try:
        entity_ids = hass.states.get(entity_id).attributes[ATTR_ENTITY_ID]

        if domain_filter:
            domain_filter = domain_filter.lower()

            return [ent_id for ent_id in entity_ids
                    if ent_id.startswith(domain_filter)]
        else:
            return entity_ids

    except (AttributeError, KeyError):
        # AttributeError if state did not exist
        # KeyError if key did not exist in attributes
        return []


def setup(hass, config):
    """ Sets up all groups found definded in the configuration. """
    for name, entity_ids in config.get(DOMAIN, {}).items():
        # Support old deprecated method - 2/28/2015
        if isinstance(entity_ids, str):
            entity_ids = entity_ids.split(",")

        setup_group(hass, name, entity_ids)

    return True


class Group(object):
    """ Tracks a group of entity ids. """
    # pylint: disable=too-many-instance-attributes

    visibility = Entity.visibility
    _hidden = False

    def __init__(self, hass, name, entity_ids=None, user_defined=True):
        self.hass = hass
        self.name = name
        self.user_defined = user_defined

        self.entity_id = generate_entity_id(ENTITY_ID_FORMAT, name, hass=hass)

        self.tracking = []
        self.group_on, self.group_off = None, None

        if entity_ids is not None:
            self.update_tracked_entity_ids(entity_ids)
        else:
            self.force_update()

    @property
    def state(self):
        """ Return the current state from the group. """
        return self.hass.states.get(self.entity_id)

    @property
    def state_attr(self):
        """ State attributes of this group. """
        return {
            ATTR_ENTITY_ID: self.tracking,
            ATTR_AUTO: not self.user_defined,
            ATTR_FRIENDLY_NAME: self.name,
            ATTR_HIDDEN: self.hidden
        }

    def update_tracked_entity_ids(self, entity_ids):
        """ Update the tracked entity IDs. """
        self.stop()
        self.tracking = tuple(ent_id.lower() for ent_id in entity_ids)
        self.group_on, self.group_off = None, None

        self.force_update()

        self.start()

    def force_update(self):
        """ Query all the tracked states and update group state. """
        for entity_id in self.tracking:
            state = self.hass.states.get(entity_id)

            if state is not None:
                self._update_group_state(state.entity_id, None, state)

        # If parsing the entitys did not result in a state, set UNKNOWN
        if self.state is None:
            self.hass.states.set(
                self.entity_id, STATE_UNKNOWN, self.state_attr)

    def start(self):
        """ Starts the tracking. """
        self.hass.states.track_change(self.tracking, self._update_group_state)

    def stop(self):
        """ Unregisters the group from Home Assistant. """
        self.hass.states.remove(self.entity_id)

        self.hass.bus.remove_listener(
            ha.EVENT_STATE_CHANGED, self._update_group_state)

    def _update_group_state(self, entity_id, old_state, new_state):
        """ Updates the group state based on a state change by
            a tracked entity. """

        # We have not determined type of group yet
        if self.group_on is None:
            self.group_on, self.group_off = _get_group_on_off(new_state.state)

            if self.group_on is not None:
                # New state of the group is going to be based on the first
                # state that we can recognize
                self.hass.states.set(
                    self.entity_id, new_state.state, self.state_attr)

            return

        # There is already a group state
        cur_gr_state = self.hass.states.get(self.entity_id).state
        group_on, group_off = self.group_on, self.group_off

        # if cur_gr_state = OFF and new_state = ON: set ON
        # if cur_gr_state = ON and new_state = OFF: research
        # else: ignore

        if cur_gr_state == group_off and new_state.state == group_on:

            self.hass.states.set(
                self.entity_id, group_on, self.state_attr)

        elif (cur_gr_state == group_on and
              new_state.state == group_off):

            # Check if any of the other states is still on
            if not any(self.hass.states.is_state(ent_id, group_on)
                       for ent_id in self.tracking if entity_id != ent_id):
                self.hass.states.set(
                    self.entity_id, group_off, self.state_attr)

    @property
    def hidden(self):
        """
        Returns the official decision of whether the entity should be hidden.
        Any value set by the user in the configuration file will overwrite
        whatever the component sets for visibility.
        """
        if self.entity_id is not None and \
                self.entity_id.lower() in self.visibility:
            return self.visibility[self.entity_id.lower()] == 'hide'
        else:
            return self._hidden

    @hidden.setter
    def hidden(self, val):
        """ Sets the suggestion for visibility. """
        self._hidden = bool(val)


def setup_group(hass, name, entity_ids, user_defined=True):
    """ Sets up a group state that is the combined state of
        several states. Supports ON/OFF and DEVICE_HOME/DEVICE_NOT_HOME. """

    return Group(hass, name, entity_ids, user_defined)