# -*- 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: Alessandro Decina <alessandro@fluendo.com>

import time

from twisted.internet import reactor
from twisted.internet.error import ConnectionDone
from twisted.internet.task import LoopingCall
from twisted.internet.protocol import ServerFactory, ClientFactory
from twisted.protocols.amp import AMP, Argument, Box, Command, \
        Integer, String, _objectsToStrings, _stringsToObjects, parseString
from twisted.python.reflect import qual, namedAny, accumulateClassList

class UnionMeta(type):
    def __new__(cls, name, bases, dic):
        type_map = dic['type_map'] = {}
        newtype = type.__new__(cls, name, bases, dic)
        types = []
        accumulateClassList(newtype, 'types', types)
        for typ in types:
            type_map[typ.__class__.__name__] = typ

        return newtype

class Union(Argument):
    __metaclass__ = UnionMeta

    # the list of possible types
    types = []

    def fromStringProto(self, inString, proto):
        box = parseString(inString)[0]
        type_name = box['type']
        typ = self.type_map[type_name]
        subargs = (('type', String()), ('value', typ))
        obj = _stringsToObjects(box, subargs, proto)
        return obj['value']

    def toStringProto(self, inObjects, proto):
        typ, value = inObjects
        type_name = typ.__class__.__name__
        subargs = (('type', String()), ('value', typ))
        objects = {'type': type_name, 'value': value}
        string = _objectsToStrings(objects, subargs, Box(), proto)
        string = string.serialize()
        return string

class Ping(Command):
    arguments = []
    response = [('cookie', Integer()), ('pong', String())]

class HeartBeatAMP(AMP):
    ping_period = 3
    ping_timeout = 2

    def __init__(self, cookie):
        AMP.__init__(self)
        self._cookie = cookie
        self._peer_cookie = None
        self._ping_loop_call = LoopingCall(self._periodicPing)
        self._ping_timeout_call = None
        self._last_box_timestamp = None

    def connectionLost(self, reason):
        AMP.connectionLost(self, reason)
        self._cancelTimeout()
        if self._ping_loop_call.running:
            self._ping_loop_call.stop()

    def _startPeriodicPing(self):
        self._ping_loop_call.start(self.ping_period)

    def _pingCb(self, response):
        if self._peer_cookie is None:
            self._peer_cookie = response['cookie']
        self._cancelTimeout()
        self._last_box_timestamp = time.time()

    def _pingEb(self, failure):
        # The peer closed the connection. It's fine, the rest will happen in
        # connectionLost.
        failure.trap(ConnectionDone)

    def _periodicPing(self):
        now = time.time()
        if self._last_box_timestamp is None:
            diff = self.ping_period
        else:
            diff = now - self._last_box_timestamp
        
        if diff < self.ping_period:
            return

        dfr = self.callRemote(Ping, cookie=self._cookie)
        self._startTimeout()
        dfr.addCallback(self._pingCb)
        dfr.addErrback(self._pingEb)

    def _startTimeout(self):
        if self._ping_timeout_call is None:
            self._ping_timeout_call = reactor.callLater(self.ping_timeout,
                    self._pingTimeout)
        else:
            self._ping_timeout_call.reset(self.ping_timeout)

    def _cancelTimeout(self):
        if self._ping_timeout_call is not None:
            self._ping_timeout_call.cancel()
            self._ping_timeout_call = None

    def _pingTimeout(self):
        self._ping_timeout_call = None
        self.connectionDied()

    def ping(self):
        return {'cookie': self._cookie, 'pong': 'pong'}
    Ping.responder(ping)

    def connectionDied(self):
        """
        Called when a ping timeout occurred.
        """
        self.transport.loseConnection()

class MasterProtocol(HeartBeatAMP):
    def __init__(self):
        cookie = newCookie()
        HeartBeatAMP.__init__(self, cookie)

    def connectionMade(self):
        HeartBeatAMP.connectionMade(self)

        # the first ping is handled a bit differently as it's our protocol
        # initializer. Before the first pong response we don't have the cookie,
        # hence we can't setup a timeout and call
        # self.master._slaveDead(cookie). 
        # This means that the interval between the slave process is spawned and
        # the slave answers the first ping needs to be guarded with a timeout in
        # an higher layer (ie. the Master)
        dfr = self.callRemote(Ping)
        dfr.addCallback(self._firstPongCb)
        dfr.addErrback(self._pingEb)

    def connectionLost(self, reason):
        HeartBeatAMP.connectionLost(self, reason)

        # check if the connection was lost before the first pong (and so the
        # cookie is still None and self.factory.master._slaveStarted hasn't been
        # called yet)
        if self._peer_cookie is not None:
            self.factory.master._slaveDead(self._peer_cookie)

    def connectionDied(self):
        HeartBeatAMP.connectionDied(self)
        if self._peer_cookie is not None:
            self.factory.master._slaveDead(self._peer_cookie)

    def _firstPongCb(self, response):
        self._peer_cookie = response['cookie']
        self.factory.master._slaveStarted(self._peer_cookie, self)
        self._startPeriodicPing()
    
    def ping(self):
        return {'cookie': self._cookie, 'pong': 'pong'}
    Ping.responder(ping)

class MasterFactory(ServerFactory):
    protocol = MasterProtocol

    def __init__(self, master):
        # ServerFactory has no __init__
        self.master = master

    def buildProtocol(self, addr):
        protocol = ServerFactory.buildProtocol(self, addr)
        return protocol

class SlaveProtocol(HeartBeatAMP):
    def connectionMade(self):
        HeartBeatAMP.connectionMade(self)
        self._startPeriodicPing()

    def connectionLost(self, reason):
        # a slave dies when it looses the connection with its master
        reactor.stop()

class SlaveFactory(ClientFactory):
    protocol = SlaveProtocol

    def __init__(self, cookie):
        if isinstance(cookie, basestring):
            # the cookie is 'Slave-%d'
            cookie = int(cookie[6:])
        self._cookie = cookie

    def buildProtocol(self, addr):
        protocol = self.protocol(self._cookie)
        protocol.factory = self
        return protocol

_cookie = 0
def newCookie():
    global _cookie
    cookie = _cookie
    _cookie += 1

    return cookie
