# -*- 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>


"""
Parse the results of a DAAP response based on the documentation
available on http://tapjam.net/daap/
"""

from twisted.internet import task, defer

import struct, re



# Byte-Code mapped to the decode type
dataTypes = {
    1:'b',        # byte
    2:'B',        # unsigned byte
    3:'h',        # short
    4:'H',        # unsigned short
    5:'i',        # integer
    6:'ui',       # unsigned integer
    7:'q',        # long
    8:'Q',        # unsigned long
    9:'s',        # string
    10:'I',       # timestamp
    11:'version', # version
    12:'c',       # container
    }

class NotEnoughData(Exception):
    pass

class DaapParser(object):
    """
    A DaapParser provides different ways to parse the response you get on daap
    request.
    """

    def __init__(self):
        # minimals for the first requests like server-info and content-codes
        self._code_to_type = {'msst': 'i', 'mcnm' : 's', 'mcty' : 'h',
                'mcna': 's', 'msix': 'b', 'msau': 'b', 'mslr': 'b', 'mspi': 'b',
                'msbr': 'b', 'msub': 'b', 'msex': 'b', 'msup': 'b', 'msrs': 'b',
                'mstt': 'i', 'apro': 'version', 'mpro': 'version', 'msal': 'b',
                'msqy': 'b', 'mstm': 'i', 'msdc': 'i', 'mlid': 'i', 'musr': 'i'}

    def parse_chunk(self, data):
        """
        unpack one chunk of data and decode the value, if it is not a container.

        @param data:    the data to read
        @type data:     C{str}
        @return:        the code_name, the (decoded) value and the rest of the data
        @rtype:         C{tuple} containing 3 strings

        @raises NotEnoughData: if the size for value is longer than the rest of
        the data that is left
        """
        # first 4 bytes are the code, the second 4 are
        # telling us the size of data so that we can actually split it off
        code, size = struct.unpack('!4sI', data[:8])
        end = size+8
        if end > len(data):
            raise NotEnoughData(code)

        value = self._decode(code, data[8:end])

        rest = data[end:]
        return (code, value, rest)

    def simple_parse(self, data):
        """
        Parse the full data simple into a dictionary with key:value. This is
        usefull for simple answers (as the login or the server info).

        @param data:    the full data to read
        @type data:     C{str}
        @return:        code mapped to the value           
        @rtype:         C{dict}

        """
        # first part is always a container
        code, data, nothing = self.parse_chunk(data)
        result = {}
        while len(data) >= 8:
            code, value, data = self.parse_chunk(data)
            result[code] = value
        return result

    def parse_mdcl(self, data):
        """
        parse *one* mdcl entry and return the three values (decoded)
        @return:    mcna, mcnm, mcty
        @rtype:     C{tuple}
        """
        # an mdcl is always three code, value things
        code, mcna, rest = self.parse_chunk(data)
        code, mcnm, rest = self.parse_chunk(rest)
        code, mcty, rest = self.parse_chunk(rest)

        # add it to the dictionary, if it is not yet there
        if not mcna in self._code_to_type:
            self._code_to_type[mcna] = dataTypes[mcty]

        return (mcna, mcnm, mcty)

    def parse_to_model(self, data, model):
        result_dfr = defer.Deferred()
        dfr = task.coiterate(self._parse_to_model(data, model, dfr=result_dfr))
        return result_dfr

    def _parse_to_model(self, data, model, dfr=None):
        """
        Parse the given chunk of data into the given model by using the
        models.mappings, model.container and model.container_items.

        This is a very complex method you could use to parse any given chunk of
        data you get over a http request to the given model. It is also able to
        find listitems and parse the values. It then wraps the values into a new
        created C{model.container_items} Model by calling the same method with
        the value and the model again. After wards it appends it to the list
        specified in model.container.

        @param data:    the chunk of data to parse into the model
        @type data:     C{str}
        @param model:   The Model to parse it into
        @type model:    L{elisa.plugins.daap.models.DaapModel}

        @return:        the tail of the data it was not able to parse
        in case it's missing some data to be able to unpack it. You should keep
        this data and reuse it for a new request, when you have more data.
        @rtype:         C{str}

        @raises NotImplementedError: when a container is found but the given
        model doesn't support containers.
        """

        item_klass = None
        if model.container:
            # this is a container able model
            item_klass = model.container_items
            container = getattr(model, model.container)

        while len(data) >= 8:
            try:
                key, value, data = self.parse_chunk(data)
            except NotEnoughData, e:
                if e.message == 'mlcl':
                    # we just ignore it because we might not have all the data
                    # we want to have but we can still start on with the subitems
                    data = data[8:]
                    yield data
                    continue

                # obviously we miss data to be able to parse it proper. Give the
                # rest we have back to the caller so that he can decide what
                # should happen next (e.g. read the next chunk of data)
                break

            if key == 'mlit':
                # this is a list item, create it, parse it and append it
                if not item_klass:
                    # we have a list for a non container able model, raise an
                    # Exception to inform the developer about it
                    msg = "Found container data, but %s does not support" \
                          " containers." % model
                    raise NotImplementedError(msg)

                item = item_klass()

                for value in self._parse_to_model(value, item):
                    yield value

                container.append(item)

            elif key == 'mlcl':
                # we are at the toplevel of a list, lets parse it
                data = value
            else:
                if key in model.mappings:
                    setattr(model, model.mappings[key], value)
            yield data

        # return the rest
        if dfr:
            dfr.callback(data)
        else:
            yield data

    def _decode(self, code, data):
        
        if not code in self._code_to_type or not len(data):
            # unknown type or no data return raw data
            return data

        type = self._code_to_type[code]

        if type == 'c':
            # it is a container, return raw data
            return data

        if type == 's':
            # handle strings differently
            return struct.unpack('!%ss' % len(data), data)[0]
        elif type == 'version':
            # handle versions differently
            return float("%s.%s" % struct.unpack('!HH', data))

        # unpack everything else according to the type
        return struct.unpack('!%s' % type, data)[0]
