# -*- coding: utf-8 -*-
# Moovida - Home multimedia server
# Copyright (C) 2006-2009 Fluendo Embedded S.L. (www.fluendo.com).
# All rights reserved.
#
# This file is available under one of two license agreements.
#
# This file is licensed under the GPL version 3.
# See "LICENSE.GPL" in the root of this distribution including a special
# exception to use Moovida with Fluendo's plugins.
#
# The GPL part of Moovida is also available under a commercial licensing
# agreement from Fluendo.
# See "LICENSE.Moovida" in the root directory of this distribution package
# for details on that license.
#
# Author: Philippe Normand <philippe@fluendo.com>

"""
UPnP Content-Directory client
"""

from twisted.internet import defer, task
from twisted.web2.stream import BufferedStream
from twisted.web2 import responsecode

from coherence.upnp.core import DIDLLite
from coherence.base import Coherence
from coherence.upnp.devices.control_point import ControlPoint

from elisa.core import common
from elisa.core import media_uri
from elisa.core.components.resource_provider import ResourceProvider

from elisa.plugins.base.models.media import RawDataModel
from elisa.plugins.base.models.media import PlayableModel
from elisa.plugins.base.models import audio
from elisa.plugins.coherence import models

from elisa.plugins.http_client.http_client import ElisaAdvancedHttpClient

from urllib2 import urlparse

IMAGE_MAGIC = '_hack-is-image_'


class UpnpResource(ResourceProvider):
    """
    Resource provider responsible for UPnP MediaServer browsing, using
    the Coherence UPnP framework.

    This resource_provider doesn't depend on the
    L{elisa.plugins.coherence.coherence_resource} resource_provider
    but it's good to have both if the application needs both UPnP
    discovery and browsing of MediaServers.
    """

    supported_uri = 'upnp://.*'

    def initialize(self):
        self._http_clients = {}
        try:
            self._coherence = Coherence()
        except Exception, coherence_exception:
            error = "Coherence failed to start. Error: %s" % coherence_exception
            return defer.fail(error)
        else:
            self._control_point = ControlPoint(self._coherence)
            return defer.succeed(self)

    def clean(self):
        close_dfrs = [client.close() for client in self._http_clients.values()]
        dfr = defer.DeferredList(close_dfrs, consumeErrors=True)

        def parent_clean(result):
            return super(UpnpResource, self).clean()

        dfr.addCallback(parent_clean)
        return dfr

    def _get_playable_url(self, item):
        playable_url = None
        res = item.res.get_matching(['*:*:*:*'], protocol_type='http-get')
        if len(res) > 0:
            res = res[0]
            remote_protocol,remote_network,remote_content_format,_ = res.protocolInfo.split(':')
            playable_url = res.data
        return playable_url

    def _get_http_client(self, server, port):
        client_id = "%s:%s" % (server, port)
        try:
            client = self._http_clients[client_id]
        except KeyError:
            client = ElisaAdvancedHttpClient(host=server, port=port)
            self._http_clients[client_id] = client
        return client

    def _response_read(self, response, model):
         model.data = response
         model.size = len(response)
         return model

    def _request_done(self, response, model, url):
        if response.code == responsecode.OK:
            # Read the response stream
            read_dfr = BufferedStream(response.stream).readExactly()
            read_dfr.addCallback(self._response_read, model)
            return read_dfr
        elif response.code == responsecode.NOT_FOUND:
            # 404 error code: resource not found
            return defer.fail(IOError('Resource not found at %s' % url))
        else:
            # Other HTTP response code
            return defer.fail(Exception('Received an %d HTTP response code' % response.code))

    def _request_error(self, err):
        self.warning("Error while fetching image data: %s", err)

    def _got_browse_results(self, results, uri, model):

        def load_didl_items(model):
            didl = DIDLLite.DIDLElement.fromString(results['Result'])
            for item in didl.getItems():
                m = models.create_model_from_upnp_class(item.upnp_class)
                is_playable = isinstance(m, models.UpnpItemModel)
                item_uri = media_uri.MediaUri(uri)
                item_uri.fragment = item.id
                item_uri.set_param('is_playable', int(is_playable))
                m.name = unicode(item.title)

                if not is_playable:
                    m.elisa_uri = item_uri
                    m.container_id = item.id
                    m.items_count = int(item.childCount or 0)
                else:
                    m.playable_uri = item_uri
                    playable_url = self._get_playable_url(item)
                    if playable_url:
                        m.playable_model = PlayableModel()
                        m.playable_model.uri = media_uri.MediaUri(playable_url)
                model.items.append(m)
                yield model

        if IMAGE_MAGIC in str(uri):
            self.debug("Fetching image data for uri '%s'" % uri)
            # let's consume the generator!
            [f for f in load_didl_items(model)]
            # an image model should hava only one didl item describing it
            if not model.items:
                return defer.fail(IOError('Resource not found at %s' % uri))
            url = str(model.items[0].playable_model.uri)
            pieces = urlparse.urlsplit(url)[1].split(':')
            server = pieces[0]
            port = 80
            if len(pieces) == 2:
                port = int(pieces[1])
            self.debug("Requesting upnp image from %s at port %d" % (server, port))

            result_model = RawDataModel()
            http_client = self._get_http_client(server, port)
            dfr = http_client.request(url)
            dfr.addCallbacks(self._request_done, self._request_error,
                             callbackArgs=(result_model, url))
        else:
            # using task.coiterate because directories can contain lots of
            # items, so we don't want to wait
            dfr = task.coiterate(load_didl_items(model))
            dfr.addCallback(lambda gen: model)

        return dfr

    def get(self, uri, context_model=None):
        """
        Browse a MediaServer located on given UPnP device and
        service. Device and service ids are specified in the URI like
        this:

        upnp://device_uuid?sid=service_uuid&is_playable=0#container_id

        The container_id is optional. If not specified we assume it is
        equal to 0. The is_playable parameter is set to 1 if the item
        to browse is not representing a directory. The is_playable
        parameter defaults to 0 if it cannot be found in the URI.

        example URI:

        ::

           upnp://1ce86a92-8676-4336-bb6f-a08217aadaf2?sid=10f4322f-8b45-4f79-8659-6f66b59c6a38&is_playable=0#1

        Model returned is always a
        L{elisa.plugins.coherence.models.UpnpModel} instance with
        items list containing various other
        L{elisa.plugins.coherence.models.UpnpContainerModel} and/or
        L{elisa.plugins.coherence.models.UpnpItemModel} depending on
        the container being browsed.
        """
        self.debug("GET %r", uri)

        model = None
        dfr = None
        service = None

        device_uuid = uri.host
        service_uuid = uri.get_params().get('sid')
        is_playable = int(uri.get_params().get('is_playable', '0'))
        if service_uuid:
            device = self._control_point.get_device_with_id(device_uuid)
            if device:
                services = [se for se in device.services
                            if se.get_sid() and se.get_sid().endswith(service_uuid)]
                if services:
                    service = services[0]
                else:
                    self.debug("No service found in %r", device)
            else:
                self.debug("Device with ID %r not found", device_uuid)
        else:
            self.debug("Service ID not found in %r", uri)

        if service:

            # extract container_id from uri's fragment
            container_id = uri.fragment or 0

            model = models.UpnpModel()

            kwargs = {'process_result': False}
            if is_playable:
                kwargs['browse_flag'] = 'BrowseMetadata'

            self.debug("Browsing container with ID: %r, is_playable: %r",
                       container_id, is_playable)
            dfr = service.client.browse(container_id, **kwargs)
            dfr.addCallback(self._got_browse_results, uri, model)

        if not dfr:
            dfr = defer.fail(NotImplementedError())

        return model, dfr
