"""Support to interface with Sonos players."""
import asyncio
import datetime
import functools as ft
import logging
import socket
import urllib

import async_timeout
import pysonos
from pysonos import alarms
from pysonos.exceptions import SoCoException, SoCoUPnPException
import pysonos.snapshot

from homeassistant.components.media_player import MediaPlayerDevice
from homeassistant.components.media_player.const import (
    ATTR_MEDIA_ENQUEUE,
    MEDIA_TYPE_MUSIC,
    MEDIA_TYPE_PLAYLIST,
    SUPPORT_CLEAR_PLAYLIST,
    SUPPORT_NEXT_TRACK,
    SUPPORT_PAUSE,
    SUPPORT_PLAY,
    SUPPORT_PLAY_MEDIA,
    SUPPORT_PREVIOUS_TRACK,
    SUPPORT_SEEK,
    SUPPORT_SELECT_SOURCE,
    SUPPORT_SHUFFLE_SET,
    SUPPORT_STOP,
    SUPPORT_VOLUME_MUTE,
    SUPPORT_VOLUME_SET,
)
from homeassistant.const import (
    ENTITY_MATCH_ALL,
    STATE_IDLE,
    STATE_PAUSED,
    STATE_PLAYING,
)
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util.dt import utcnow

from . import (
    ATTR_ALARM_ID,
    ATTR_ENABLED,
    ATTR_INCLUDE_LINKED_ZONES,
    ATTR_MASTER,
    ATTR_NIGHT_SOUND,
    ATTR_QUEUE_POSITION,
    ATTR_SLEEP_TIME,
    ATTR_SPEECH_ENHANCE,
    ATTR_TIME,
    ATTR_VOLUME,
    ATTR_WITH_GROUP,
    CONF_ADVERTISE_ADDR,
    CONF_HOSTS,
    CONF_INTERFACE_ADDR,
    DATA_SERVICE_EVENT,
    DOMAIN as SONOS_DOMAIN,
    SERVICE_CLEAR_TIMER,
    SERVICE_JOIN,
    SERVICE_PLAY_QUEUE,
    SERVICE_RESTORE,
    SERVICE_SET_OPTION,
    SERVICE_SET_TIMER,
    SERVICE_SNAPSHOT,
    SERVICE_UNJOIN,
    SERVICE_UPDATE_ALARM,
)

_LOGGER = logging.getLogger(__name__)

SCAN_INTERVAL = 10
DISCOVERY_INTERVAL = 60

SUPPORT_SONOS = (
    SUPPORT_VOLUME_SET
    | SUPPORT_VOLUME_MUTE
    | SUPPORT_PLAY
    | SUPPORT_PAUSE
    | SUPPORT_STOP
    | SUPPORT_SELECT_SOURCE
    | SUPPORT_PREVIOUS_TRACK
    | SUPPORT_NEXT_TRACK
    | SUPPORT_SEEK
    | SUPPORT_PLAY_MEDIA
    | SUPPORT_SHUFFLE_SET
    | SUPPORT_CLEAR_PLAYLIST
)

DATA_SONOS = "sonos_media_player"

SOURCE_LINEIN = "Line-in"
SOURCE_TV = "TV"

ATTR_SONOS_GROUP = "sonos_group"

UPNP_ERRORS_TO_IGNORE = ["701", "711", "712"]


class SonosData:
    """Storage class for platform global data."""

    def __init__(self, hass):
        """Initialize the data."""
        self.entities = []
        self.discovered = []
        self.topology_condition = asyncio.Condition()


async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
    """Set up the Sonos platform. Obsolete."""
    _LOGGER.error(
        "Loading Sonos by media_player platform config is no longer supported"
    )


async def async_setup_entry(hass, config_entry, async_add_entities):
    """Set up Sonos from a config entry."""
    if DATA_SONOS not in hass.data:
        hass.data[DATA_SONOS] = SonosData(hass)

    config = hass.data[SONOS_DOMAIN].get("media_player", {})
    _LOGGER.debug("Reached async_setup_entry, config=%s", config)

    advertise_addr = config.get(CONF_ADVERTISE_ADDR)
    if advertise_addr:
        pysonos.config.EVENT_ADVERTISE_IP = advertise_addr

    def _discovery(now=None):
        """Discover players from network or configuration."""
        hosts = config.get(CONF_HOSTS)

        def _discovered_player(soco):
            """Handle a (re)discovered player."""
            try:
                _LOGGER.debug("Reached _discovered_player, soco=%s", soco)

                if soco not in hass.data[DATA_SONOS].discovered:
                    _LOGGER.debug("Adding new entity")
                    hass.data[DATA_SONOS].discovered.append(soco)
                    hass.add_job(async_add_entities, [SonosEntity(soco)])
                else:
                    entity = _get_entity_from_soco_uid(hass, soco.uid)
                    if entity:
                        _LOGGER.debug("Seen %s", entity)
                        hass.add_job(entity.async_seen())
            except SoCoException as ex:
                _LOGGER.debug("SoCoException, ex=%s", ex)

        if hosts:
            for host in hosts:
                try:
                    _LOGGER.debug("Testing %s", host)
                    player = pysonos.SoCo(socket.gethostbyname(host))
                    if player.is_visible:
                        # Make sure that the player is available
                        _ = player.volume

                        _discovered_player(player)
                except (OSError, SoCoException) as ex:
                    _LOGGER.debug("Exception %s", ex)
                    if now is None:
                        _LOGGER.warning("Failed to initialize '%s'", host)

            _LOGGER.debug("Tested all hosts")
            hass.helpers.event.call_later(DISCOVERY_INTERVAL, _discovery)
        else:
            _LOGGER.debug("Starting discovery thread")
            pysonos.discover_thread(
                _discovered_player,
                interval=DISCOVERY_INTERVAL,
                interface_addr=config.get(CONF_INTERFACE_ADDR),
            )

    _LOGGER.debug("Adding discovery job")
    hass.async_add_executor_job(_discovery)

    async def async_service_handle(service, data):
        """Handle d
ispatched services."""
        entity_ids = data.get("entity_id")
        entities = hass.data[DATA_SONOS].entities
        if entity_ids and entity_ids != ENTITY_MATCH_ALL:
            entities = [e for e in entities if e.entity_id in 
entity_ids]

        if service == SERVICE_JOIN:
            master = [
                e
                for e in hass.data[DATA_SONOS].entities
                if e.entity_id == data[ATTR_MASTER]
            ]
            if master:
                await SonosEntity.join_multi(hass, master[0], entities)
        elif service == SERVICE_UNJOIN:
            await SonosEntity.unjoin_multi(hass, entities)
        elif service == SERVICE_SNAPSHOT:
            await SonosEntity.snapshot_multi(hass, entities, data[ATTR_WITH_GROUP])
        elif service == SERVICE_RESTORE:
            await SonosEntity.restore_multi(hass, entities, data[ATTR_WITH_GROUP])
        else:
            for entity in entities:
                if service == SERVICE_SET_TIMER:
                    call = entity.set_sleep_timer
                elif service == SERVICE_CLEAR_TIMER:
                    call = entity.clear_sleep_timer
                elif service == SERVICE_UPDATE_ALARM:
                    call = entity.set_alarm
                elif service == SERVICE_SET_OPTION:
                    call = entity.set_option
                elif service == SERVICE_PLAY_QUEUE:
                    call = entity.play_queue

                hass.async_add_executor_job(call, data)

        # We are ready for the next service call
        hass.data[DATA_SERVICE_EVENT].set()

    async_dispatcher_connect(hass, SONOS_DOMAIN, async_service_handle)


class _ProcessSonosEventQueue:
    """Queue like object for dispatching sonos events."""

    def __init__(self, handler):
        """Initialize Sonos event queue."""
        self._handler = handler

    def put(self, item, block=True, timeout=None):
        """Process event."""
        try:
            self._handler(item)
        except SoCoException as ex:
            _LOGGER.warning("Error calling %s: %s", self._handler, ex)


def _get_entity_from_soco_uid(hass, uid):
    """Return SonosEntity from SoCo uid."""
    for entity in hass.data[DATA_SONOS].entities:
        if uid == entity.unique_id:
            return entity
    return None


def soco_error(errorcodes=None):
    """Filter out specified UPnP errors from logs and avoid exceptions."""

    def decorator(funct):
        """Decorate functions."""

        @ft.wraps(funct)
        def wrapper(*args, **kwargs):
            """Wrap for all soco UPnP exception."""
            try:
                return funct(*args, **kwargs)
            except SoCoUPnPException as err:
                if errorcodes and err.error_code in errorcodes:
                    pass
                else:
                    _LOGGER.error("Error on %s with %s", funct.__name__, err)
            except SoCoException as err:
                _LOGGER.error("Error on %s with %s", funct.__name_