# -*- 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.
#
# Authors:
#   Benjamin Kampmann <benjamin@fluendo.com>

"""
A Connection, parsing and model creation system for DAAP
"""


from elisa.plugins.daap.daap_parser import DaapParser, NotEnoughData
from elisa.plugins.http_client.http_client import ElisaHttpClient
from twisted.web2.stream import BufferedStream
from twisted.web2 import responsecode

from elisa.plugins.daap.models import DaapServerInfoModel

from elisa.plugins.http_client.extern.client_http import ClientRequest

import base64

class LoginFailed(Exception):
    """
    Raised when the login failed
    """
    pass

class ErrorInResponse(Exception):
    """
    Raised when the client receives an HTTP error code from the server
    """
    pass

DEFAULT_SONG_TAGS = ','.join(["dmap.itemid","dmap.itemname","dmap.itemkind",
                              "daap.songalbum","daap.songartist",
                              "daap.songformat","daap.songgenre",
                              "daap.songsize","daap.songtime",
                              "daap.songtracknumber"])

class DaapConnection(object):
    """
    A DaapConnection holds the connection to a daap server (on one port) and
    allows you to make requests with the corresponding models. Internally it
    using the L{elisa.plugins.daap.daap_parser.DaapParser} to parse the data
    into models.
    """

    def __init__(self, server='localhost', port=3689):
        self._client = ElisaHttpClient(server, port)
        self._parser = DaapParser()
        self._server_info = DaapServerInfoModel()
        self.session_id = None
        self.revision_id = None

    def login(self, password=None):
        """
        Try to log into the server. Has to be called before trying to make
        request otherwise the request probably fails.

        @raises LoginFailed: in case the login failed.

        @rtype: L{twisted.internet.defer.Deferred}
        """
        def got_revision(revision):
            if not revision['mstt'] == responsecode.OK:
                raise LoginFailed(revision['mstt'])
            self.revision_id = revision['musr']
            return True

        def got_login(login):
            if login['mstt'] != responsecode.OK:
                # status_code
                raise LoginFailed(login['mstt'])

            self.session_id = login['mlid']
            request_str = "/update?session-id=%s&revision-number=1"

            return self._request_and_fullread(request_str %
                    self.session_id).addCallback(
                    self._parser.simple_parse).addCallback(got_revision)

        def error_received(failure):
            if failure.type == ErrorInResponse:
                raise LoginFailed(failure.value)
            return failure

            
        def do_request(result):
            if self._server_info.login_required or password:
                if not password:
                    raise LoginFailed('No password given')

                # authentication is in bae64
                auth = base64.encodestring( '%s:%s'%('user', password) )[:-1]
                request = ClientRequest('GET', '/login', None, None)
                request.headers.setRawHeaders('Authorization', ["Basic %s" % auth])
            else:
                request = '/login'

            return self._request_and_fullread(request).addCallback(
                    self._parser.simple_parse).addCallbacks(got_login,
                    error_received)

        if not self._server_info.server_name:
            # we have to do the server_request and content codes first
            return self._request_and_parse_full('/server-info',
                    self._server_info).addCallback(lambda x:
                    self._request_content_codes()).addCallback(do_request)

        # do the request
        return do_request(None)

    def request(self, uri, model):
        """
        request the C{uri} and wrap the data in the C{model}. The parameters
        'session-id' and 'revision-id' are overwritten with the values from
        inside this class.

        You need to be logged in before trying to request anything.

        FIXME: deferred returned by this method is not cancellable.

        @type uri: L{elisa.core.media_uri.MediaUri}
        @type model: L{elisa.plugins.daap.models.DaapModel}
        @rtype L{elisa.twisted.internet.defer.Deferred}
        """

        uri.set_param('session-id', self.session_id)
        uri.set_param('revision-id', self.revision_id)

        if uri.filename in ('items', 'containers') and uri.get_param('meta') == '':
            uri.set_param('meta', DEFAULT_SONG_TAGS)

        return self._request_and_parse_full(uri, model)
        
    def _internal_request(self, request):
        if isinstance(request, ClientRequest):
            return self._client.request_full(request)
        else:
            return self._client.request(request)
        
    def _request_and_fullread(self, request): 
        def got_response(response):
            if response.code != responsecode.OK:
                raise ErrorInResponse(response) 

            # got a response: read it fully
            return BufferedStream(response.stream).readExactly()

        return self._internal_request(request).addCallback(got_response)


    def _request_and_parse_full(self, request, model):

        def data_received(result, stream, first):

            def load_next_chunk(data):
                dfr = stream.read()
                if isinstance(dfr, basestring):
                    return data_received(data + dfr, stream, first)
                elif not dfr:
                    return model
                else:
                    dfr.addCallback(data_received, stream, first)
                    return dfr

            if not result:
                # reading done
                return model

            # merge with what is left from before
            data = result
            if len(data) <= 8:
                # we need at least to have the code and size to parse
                # something otherwise we have to wait for the next chunk
                return load_next_chunk(data)

            if first:
                first = False
                # the first one is always a container and we don't want to wait
                # until everything is there so we simple drop the parsing of the
                # first tag and size
                data = data[8:]
                if len(data) <= 8:
                    # nothing remains, lets wait for more data
                    return load_next_chunk(data)

            # parse the data and load next chunk
            dfr = self._parser.parse_to_model(data, model)
            dfr.addCallback(load_next_chunk)
            return dfr


        def got_response(response):
            if response.code != responsecode.OK:
                raise ErrorInResponse(response)

            # got a response: parse it
            stream = response.stream
            return stream.read().addCallback(data_received, stream, True)

        return self._internal_request(request).addCallback(got_response)

    def _request_content_codes(self):
        def got_data(data):
            code, value, nothing = self._parser.parse_chunk(data)
            status, value, rest = self._parser.parse_chunk(value)

            while len(rest) >= 8:
                mdcl, value, rest = self._parser.parse_chunk(rest)
                self._parser.parse_mdcl(value)

        return self._request_and_fullread('/content-codes').addCallback(got_data)


