# -*- coding: utf-8 -*-
#
# papyon - a python client library for Msn
#
# Copyright (C) 2007 Ali Sabil <ali.sabil@gmail.com>
# Copyright (C) 2007 Johann Prieur <johann.prieur@gmail.com>
# Copyright (C) 2008 Richard Spiers <richard.spiers@gmail.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

"""P2P
This module contains the classes needed to engage in a peer to peer transfer
with a contact.
    @group MSNObject: MSNObjectStore, MSNObject, MSNObjectType
    @sort: MSNObjectStore, MSNObject, MSNObjectType"""
from msnp2p.filetransfer import FileTransferSession
from msnp2p.msnobject import MSNObjectSession
from msnp2p.webcam import WebcamSession
from msnp2p import EufGuid, ApplicationID
from msnp2p.exceptions import ParseError
from profile import NetworkID, Contact, Profile

import papyon.util.element_tree as ElementTree
import papyon.util.string_io as StringIO

import gobject
import xml.sax.saxutils as xml
import urllib
import base64
import hashlib
import logging

__all__ = ['MSNObjectType', 'MSNObject', 'MSNObjectStore',
           'FileTransferManager', 'WebcamHandler']

logger = logging.getLogger('papyon.p2p')

class MSNObjectType(object):
    """Represent the various MSNObject types"""

    CUSTOM_EMOTICON = 2
    "Custom smiley"
    DISPLAY_PICTURE = 3
    "Display picture"
    BACKGROUND_PICTURE = 5
    "Background picture"
    DYNAMIC_DISPLAY_PICTURE = 7
    "Dynamic display picture"
    WINK = 8
    "Wink"
    VOICE_CLIP = 11
    "Void clip"
    SAVED_STATE_PROPERTY = 12
    "Saved state property"
    LOCATION = 14
    "Location"

def _decode_shad(shad, warning=True):
    try:
        shad = base64.b64decode(shad)
    except TypeError:
        # See fd.o#27672 for details on this workaround.
        if ' ' in shad:
            parts = shad.split(' ')

            # Try the first part.
            shad = _decode_shad(parts[0], False)

            # Try the second part.
            if shad is None:
                shad = _decode_shad(parts[1], False)

        else:
            # Only display this warning if we're not in a nested call otherwise the
            # warning will be confusing.
            if warning:
                logger.warning("Invalid SHA1D in MSNObject: %s" % shad)
            shad = None

    return shad

class MSNObject(object):
    "Represents an MSNObject."
    def __init__(self, creator, size, typ, location, friendly,
                 shad=None, shac=None, data=None):
        """Initializer

            @param creator: the creator of this MSNObject
            @type creator: utf-8 encoded string representing the account

            @param size: the total size of the data represented by this MSNObject
            @type size: int

            @param typ: the type of the data
            @type typ: L{MSNObjectType}

            @param location: a filename for the MSNObject
            @type location: utf-8 encoded string

            @param friendly: a friendly name for the MSNObject
            @type friendly: utf-8 encoded string

            @param shad: sha1 digest of the data

            @param shac: sha1 digest of the MSNObject itself

            @param data: file object to the data represented by this MSNObject
            @type data: File
        """
        # Backward compatible with older clients that pass a Contact/Profile
        if type(creator) is Contact or type(creator) is Profile:
            creator = creator.account
        self._creator = creator
        self._size = size
        self._type = typ
        self._location = location
        self._friendly = friendly

        if shad is None:
            if data is None:
                raise NotImplementedError
            shad = self.__compute_data_hash(data)
        self._data_sha = shad
        self.__data = data
        if shac is None:
            shac = self.__compute_checksum()
        self._checksum_sha = shac
        self._repr = None

    def __ne__(self, other):
        return not (self == other)

    def __eq__(self, other):
        if other == None:
            return False
        return other._type == self._type and \
            other._data_sha == self._data_sha

    def __hash__(self):
        return hash(str(self._type) + self._data_sha)

    def __set_data(self, data):
        if self._data_sha != self.__compute_data_hash(data):
            logger.warning("Received data doesn't match the MSNObject data hash.")
            return

        old_pos = data.tell()
        data.seek(0, 2)
        self._size = data.tell()
        data.seek(old_pos, 0)

        self.__data = data
        self._checksum_sha = self.__compute_checksum()
    def __get_data(self):
        return self.__data
    _data = property(__get_data, __set_data)

    @staticmethod
    def parse(client, xml_data):
        data = StringIO.StringIO(xml_data)
        try:
            element = ElementTree.parse(data).getroot().attrib
        except:
            logger.warning('Invalid MSNObject: %s' % xml_data)
            return

        creator = element["Creator"]
        size = int(element["Size"])
        type = int(element["Type"])

        if "Location" in element:
            location = xml.unescape(element["Location"])
        else:
            location = "0"

        if "Friendly" in element:
            friendly = base64.b64decode(xml.unescape(element["Friendly"]))
        else:
            friendly = base64.b64decode('AAA=')

        shad = element.get("SHA1D", None)
        if shad is not None:
            shad = _decode_shad(shad)
        shac = element.get("SHA1C", None)
        if shac is not None:
            try:
                shac = base64.b64decode(shac)
            except TypeError:
                logger.warning("Invalid SHA1C in MSNObject: %s" % shac)
                shac = None

        result = MSNObject(creator, size, type, location, friendly, shad, shac)
        result._repr = xml_data
        return result

    def __compute_data_hash(self, data):
        digest = hashlib.sha1()
        data.seek(0, 0)
        read_data = data.read(1024)
        while len(read_data) > 0:
            digest.update(read_data)
            read_data = data.read(1024)
        data.seek(0, 0)
        return digest.digest()

    def __compute_checksum(self):
        input = "Creator%sSize%sType%sLocation%sFriendly%sSHA1D%s" % \
            (self._creator, str(self._size), str(self._type),\
                 str(self._location), base64.b64encode(self._friendly), \
                 base64.b64encode(self._data_sha))
        return hashlib.sha1(input).hexdigest()

    def __str__(self):
        return self.__repr__()

    def __repr__(self):
        if self._repr is not None:
            return self._repr
        dump = "<msnobj Creator=%s Type=%s SHA1D=%s Size=%s Location=%s Friendly=%s/>" % \
            (xml.quoteattr(self._creator),
                xml.quoteattr(str(self._type)),
                xml.quoteattr(base64.b64encode(self._data_sha)),
                xml.quoteattr(str(self._size)),
                xml.quoteattr(str(self._location)),
                xml.quoteattr(base64.b64encode(self._friendly)))
        return dump


class MSNObjectStore(object):

    def __init__(self, client):
        self._client = client
        self._outgoing_sessions = {} # session => (handle_id, callback, errback)
        self._incoming_sessions = {}
        self._published_objects = set()

    def _can_handle_message (self, message):
        euf_guid = message.body.euf_guid
        if euf_guid == EufGuid.MSN_OBJECT:
            return True
        else:
            return False

    def _handle_message(self, peer, message):
        session = MSNObjectSession(self._client._p2p_session_manager,
                peer, message.body.application_id, message)

        handle_id = session.connect("completed",
                        self._incoming_session_transfer_completed)
        self._incoming_sessions[session] = handle_id
        try:
            msn_object = MSNObject.parse(self._client, session._context)
        except ParseError:
            session.reject()
            return
        for obj in self._published_objects:
            if obj._data_sha == msn_object._data_sha:
                session.accept(obj._data)
                return session
        session.reject()

    def request(self, msn_object, callback, errback=None, peer=None):
        if msn_object._data is not None:
            callback[0](msn_object, *callback[1:])

        if peer is None:
            peer = self._client.address_book.search_contact(msn_object._creator,
                    NetworkID.MSN)

        if msn_object._type == MSNObjectType.CUSTOM_EMOTICON:
            application_id = ApplicationID.CUSTOM_EMOTICON_TRANSFER
        elif msn_object._type == MSNObjectType.DISPLAY_PICTURE:
            application_id = ApplicationID.DISPLAY_PICTURE_TRANSFER
        else:
            raise NotImplementedError

        session = MSNObjectSession(self._client._p2p_session_manager,
                peer, application_id)
        handle_id = session.connect("completed",
                self._outgoing_session_transfer_completed)
        self._outgoing_sessions[session] = \
                (handle_id, callback, errback, msn_object)
        session.invite(repr(msn_object))

    def publish(self, msn_object):
        if msn_object._data is None:
            logger.warning("Trying to publish an empty MSNObject")
        else:
            self._published_objects.add(msn_object)

    def _outgoing_session_transfer_completed(self, session, data):
        handle_id, callback, errback, msn_object = self._outgoing_sessions[session]
        session.disconnect(handle_id)
        msn_object._data = data

        callback[0](msn_object, *callback[1:])
        del self._outgoing_sessions[session]

    def _incoming_session_transfer_completed(self, session, data):
        handle_id = self._incoming_sessions[session]
        session.disconnect(handle_id)
        del self._incoming_sessions[session]


class FileTransferManager(gobject.GObject):

    __gsignals__ = {
            "transfer-requested" : (gobject.SIGNAL_RUN_FIRST,
                gobject.TYPE_NONE,
                (object,))
    }

    def __init__(self, client):
        gobject.GObject.__init__(self)
        self._client = client
        self._sessions = {}

    def _can_handle_message(self, message):
        euf_guid = message.body.euf_guid
        return (euf_guid == EufGuid.FILE_TRANSFER)

    def _handle_message(self, peer, message):
        session = FileTransferSession(self._client._p2p_session_manager,
                peer, message.body.application_id, message)
        self._connect_session(session)
        self.emit("transfer-requested", session)
        return session

    def send(self, peer, filename, size):
        session = FileTransferSession(self._client._p2p_session_manager,
                peer, ApplicationID.FILE_TRANSFER)
        session.invite(filename, size)
        self._connect_session(session)
        return session

    def _on_transfer_completed(self, session, data):
        self._disconnect_session(session)
        del self._sessions[session]

    def _connect_session(self, session):
        handle_id = session.connect("completed", self._on_transfer_completed)
        self._sessions[session] = handle_id

    def _disconnect_session(self, session):
        handle_id = self._sessions[session]
        session.disconnect(handle_id)


class WebcamHandler(gobject.GObject):

    __gsignals__ = {
            "session-created" : (gobject.SIGNAL_RUN_FIRST,
                gobject.TYPE_NONE,
                (object, bool))
    }

    def __init__(self, client):
        gobject.GObject.__init__(self)
        self._client = client
        self._sessions = []

    def _can_handle_message (self, message):
        euf_guid = message.body.euf_guid
        if (euf_guid == EufGuid.MEDIA_SESSION or
            euf_guid == EufGuid.MEDIA_RECEIVE_ONLY):
            return True
        else:
            return False

    def _handle_message (self, peer, message):
        euf_guid = message.body.euf_guid
        if (euf_guid == EufGuid.MEDIA_SESSION):
            producer = False
        elif (euf_guid == EufGuid.MEDIA_RECEIVE_ONLY):
            producer = True

        session = WebcamSession(producer, self._client._p2p_session_manager, \
                                    peer, message.body.euf_guid, message)
        self._sessions.append(session)
        self.emit("session-created", session, producer)
        return session

    def invite(self, peer, producer=True):
        print "Creating New Send Session"
        if producer:
            euf_guid = EufGuid.MEDIA_SESSION
        else:
            euf_guid = EufGuid.MEDIA_RECEIVE_ONLY
        session = WebcamSession(producer, self._client._p2p_session_manager, \
                                    peer, euf_guid)
        self._sessions.append(session)
        session.invite()
        return session
