# -*- coding: utf-8 -*-
# Moovida - Home multimedia server
# Copyright (C) 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: Fernando Casanova <fcasanova@fluendo.com>

"""
Provide access to resources served by LastFM over HTTP.
"""

from elisa.core.components.resource_provider import ResourceProvider
from elisa.core.media_uri import MediaUri, quote
from elisa.core.utils.cancellable_defer import CancellableDeferred, \
                                               CancelledError

from elisa.plugins.http_client.http_client import ElisaAdvancedHttpClient,\
                                                   ElisaHttpClient
from twisted.web2 import responsecode
from twisted.web2.stream import BufferedStream



from elisa.plugins.lastfm.models import LastFMAlbumModel
from elisa.plugins.lastfm.key import LASTFMWS_KEY

from elisa.plugins.base.models.media import RawDataModel
from elisa.plugins.base.models.image import ImageModel

from elisa.core.utils import defer
from twisted.internet import task

import re
from xml.dom import minidom

LASTFMWS_SERVER = 'ws.audioscrobbler.com'
IMG_SERVER = 'userserve-ak.last.fm'


def get_lastfm_albumgetinfo_url(artist, album_name):
    """
    Function returning the full URL to Last.FM API's query
    for information about an album.

    @param artist: the name of the artist of the album
    @type artist:  C{unicode}
    @param album_name: the name of the album
    @type album_name:  C{unicode}

    @return:         a string containing the full URL of the album info
    @rtype :         C{unicode}
    """
    base_query = 'http://' + LASTFMWS_SERVER + '/2.0/?'
    operation = 'method=album.getinfo'
    access_key = '&api_key=%s' % LASTFMWS_KEY
    query = '&artist=%s' % quote(artist.lower().strip())
    query += '&album=%s' % quote(album_name.lower().strip())

    full_query = base_query + operation + access_key + query
    return full_query


class LastFMResourceProvider(ResourceProvider):

    """
    A resource provider that implements the GET method for use on the LastFM
    Web Services API (see http://www.last.fm/api/intro for details).

    The GET method allows to retrieve lists of items containing the results to
    a search query to the LastFM Web Services, or directly an image from the
    LastFM image server.
    """

    # Queries to the LastFM Web Services API
    lastfmws_uri = 'http://' + LASTFMWS_SERVER + '/2.0/.*'
    lastfmws_re = re.compile(lastfmws_uri)
    # Queries to the LastFM image server (for album covers)
    img_uri = 'http://' + IMG_SERVER + '/serve/.*'
    img_re = re.compile(img_uri)

    supported_uri = lastfmws_uri + '|' + img_uri

    def initialize(self):
        def _parent_initialized(result):
            self._lastfmws_client = ElisaHttpClient(host=LASTFMWS_SERVER, pipeline=False)
            # Dirty hack: LastFM API servers close the connection after each
            # request because they use HTTP 1.0. Our HTTP client
            # doesn't know how to handle HTTP 1.0.
            # What we do not even try to pipeline requests so that our HTTP client
            # becomes HTTP 1.0 compliant. The GETs are, however, sent in HTTP 1.1
            # FIXME: obviously, this problem needs to be adressed in a much
            # cleaner way...
            self._lastfmws_client.pipeline_queue = [] # Emulate pipelining
            self._img_client = ElisaAdvancedHttpClient(host=IMG_SERVER)
            return result

        dfr = super(LastFMResourceProvider, self).initialize()
        dfr.addCallback(_parent_initialized)
        return dfr

    def clean(self):
        # Close all open HTTP connections
        lastfmws_close_dfr = self._lastfmws_client.close()
        img_close_dfr = self._img_client.close()
        dfr = defer.DeferredList([lastfmws_close_dfr, img_close_dfr],
                                  consumeErrors=True)

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

        dfr.addCallback(parent_clean)
        return dfr

    def _response_read(self, response, model, url):
        # Parse the response and populate the model accordingly
        if isinstance(model, LastFMAlbumModel):
            self.debug('Response received : PARSING API XML data')
            dom = minidom.parseString(response)

            lfmitem = dom.getElementsByTagName('lfm')
            if not lfmitem:
                return model

            if lfmitem[0].getAttribute('status') == 'failed':
                error = lfmitem[0].getElementsByTagName('error')[0]
                code = error.getAttribute('code')
                msg = error.firstChild.nodeValue
                self.warning('LastFM API returned an error code %s : %s' %\
                   (code, msg))
                self.warning('LastFM API returned an error for %s' % url)
                return defer.fail(ValueError('%s: %s' % (code, msg)))

            # LastFM doesn't return a list of albums, but a single album
            album_node = lfmitem[0].getElementsByTagName('album')[0]

            # Here we could retrieve a lot of information but we decided to
            # retrieve the basically innocuous id field.
            # BTW, we don't use it at all.
            model.id = album_node.getElementsByTagName('id')[0].firstChild.nodeValue

            # Let's see if the album_node contains album covers
            images = album_node.getElementsByTagName('image')
            sizeValue = {'small' : 1,
                         'medium' : 2,
                         'large' : 3,
                         'extralarge' : 4}
            imagesURL = {}
            for image in images:
                size = sizeValue[image.getAttribute('size')]
                imagesURL[size] = image.firstChild.nodeValue

            if len(imagesURL) > 0:
                highest_resolution_index = max(imagesURL.keys())
                model.cover = ImageModel()
                model.cover.references.append(
                    MediaUri(imagesURL[highest_resolution_index]))

        elif isinstance(model, RawDataModel):
            self.debug('Response received : RETURNING IMAGE')
            model.data = response
            model.size = len(response)

        return model


    def get(self, uri, context_model=None):
        """
        GET request to the LastFM servers.

        It accepts the following types of URLs:

          - http://ws.audioscrobbler.com/2.0/.* : query to the LastFM Web
            Services API, returns a single album information item in XML.

          - http://userserve-ak.last.fm/serve/.* : query to the LastFM image
            server for e.g. an audio album cover, returns the image data
            (L{elisa.plugins.base.models.media.RawDataModel}).

        The contextual model is currently not used.
        """
        url = str(uri)
        self.debug('GET for URL %s' % url)

        # Select the correct HTTP client to target
        if self.lastfmws_re.match(url) is not None:
            http_client = self._lastfmws_client
            result_model = LastFMAlbumModel()
            result_model.id = url
        elif self.img_re.match(url) is not None:
            http_client = self._img_client
            result_model = RawDataModel()

        # Here we emulate pipelining by filling an internal request queue,
        # because the image servers do not support pipelining
        def _api_request_queued(response, client):
            client.pipeline_queue.pop(0)
            if client.pipeline_queue:
                return _api_queue_next_request(response, client)
            else:
                return defer.succeed(None)

        def _api_queue_next_request(response, client):
            try:
                url, deferred, result_model = client.pipeline_queue[0]
            except IndexError:
                # No requests to queue
                return defer.succeed(None)
            if deferred.called:
                # The deferred has been cancelled, ignore the request and go on
                # with the next one in the queue
                client.pipeline_queue.pop(0)
                return _api_queue_next_request(response, client)
            request_dfr = client.request(url)
            request_dfr.addCallback(request_done, result_model, url)
            request_dfr.chainDeferred(deferred)
            # We want to queue a request even if the previous one fails
            request_dfr.addBoth(_api_request_queued, client)
            return request_dfr

        def _cancel_request(deferred):
            deferred.errback(CancelledError('Cancelled request'))

        def request_done(response, model, url):
            self.debug('Request Done')
            if response.code == responsecode.OK:
                # Read the response stream
                read_dfr = BufferedStream(response.stream).readExactly()
                read_dfr.addCallback(self._response_read, model, url)
                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))
            elif response.code == responsecode.BAD_REQUEST:
                # This is the result code when the API returns an error
                # We still want to parse this, so we send it to _response_read
                read_dfr = BufferedStream(response.stream).readExactly()
                read_dfr.addCallback(self._response_read, model, url)
                return read_dfr
            else:
                # Other HTTP response code
                return defer.fail(Exception('Received an %d HTTP response code' % response.code))

        if hasattr(http_client, 'pipeline_queue'):
            # API client, fake pipelining
            request_dfr = CancellableDeferred(canceller=_cancel_request)
            http_client.pipeline_queue.append((url, request_dfr, result_model))
            if len(http_client.pipeline_queue) == 1:
                _api_queue_next_request(None, http_client)
        else:
            # Normal case, the server supports pipelining
            request_dfr = http_client.request(url)
            request_dfr.addCallback(request_done, result_model, url)
        return (result_model, request_dfr)

