# Socket wrapper to be used in LI system
#
# Copyright (C) 2011 Matěj Grégr, Michal Kajan, Libor Polčák, Vladimír Veselý
# 
# 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 3 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, see <http://www.gnu.org/licenses/>.

""" Defines a socket wrapper class to be used as sockets for communication
inside LI system.
"""

import socket
import sys
import threading

from ..tools.wrapper import Wrapper
from ..tools.time import sleepAtLeast
from ..tools import log
from ..shared.lieval import lieval

class LISocket(Wrapper):
    """ Wrapper around ordinary socket offering aditional methods
    
    Warning: do not use standard function from select package for reading
    from this socket type. Use
    LISocketManager provided in this package instead.
    """

    def __init__(self, socket, liName):
        Wrapper.__init__(self, socket)
        self.__socket = self._wrapped_obj
        self.__buffer = ""
        self.__name = liName
        self.__isClosed = False
        self._isOK = True
        self.__LSend = threading.Lock()

    def send(self, data):
        """ Send data through socket
    
        data Tuple of data to be sent
        """
        with self.__LSend:
            self.__socket.send(bytes(repr(data) + "\n", "utf8"))

    def receive(self):
        """ Receive data from socket. Also provides buffering
    
        Returns received object or None if nothing available
        """
        self.__bufferMessage()
        if not self.hasBufferedMessage():
            return None
        msg, self.__buffer = self.__buffer.split("\n", 1)
        return self._parseMessage(msg)

    def close(self):
        """ Extends """
        self.__isClosed = True
        self.__socket.close()

    def blockingReceive(self):
        """ Receive message from the socket. Function may block.

        s The socket
        """
        msg = None
        while not msg:
            msg = self.receive()
        return msg

    def hasBufferedMessage(self):
        """ Returns True if a message has been buffered and is ready. """
        return "\n" in self.__buffer

    def getName(self):
        """ Returns socket name """
        return self.__name

    def isPtpt(self):
        """ Is the socket point-to-point """
        return True

    def isOK(self):
        """ Returns if the socket is in correct state """
        return self._isOK

    def isClosed(self):
        return self.__isClosed

    def changeSocket(self, other):
        """ Changes encapsulated unix socket

        other Another LI socket
        """
        self._wrapped_obj = other._wrapped_obj
        self.__socket = other._wrapped_obj

    def setNotOK(self):
        """ Changes state to not OK """
        self._isOK = False

    def __bufferMessage(self):
        """ Reads from the socket until a message is finished.

        Also returns when the socket does not contain any unread data.
        """
        reading = True
        while (not self.hasBufferedMessage()) and reading:
            data = str(self.__socket.recv(1024), "utf8")
            reading = len(data) > 0
            self.__buffer += data
        if not self.hasBufferedMessage() and not reading:
            self.setNotOK()

    def _parseMessage(self, msg):
        """ Parse given message and create described object """
        try:
            return lieval(msg)
        except Exception as e:
            log.warning("Unknown message received: %s; %s" % (msg, str(e)))
            return None

    def __repr__(self):
        """ This method aims to simplify debugging """
        return str(self.__class__) + ": " + self.__name + " (" + repr(self._wrapped_obj) + ")"

class LIHeartbeatSocket(LISocket):
    """ Extends given socket with heartbeat mechanism """

    # Number of seconds between sending heartbeat messages
    HEARTBEAT_TIMEOUT = 60
    # Check if heartbeat was received every HEARTBEAT_CHECK_FREQ attempt to send
    # heartbeat. Warning: set HEARTBEAT_CHECK_FREQ > 1 orrace conditions may
    # introduce false alert
    HEARTBEAT_CHECK_FREQ = 10000000000

    HEARTBEAT_MSG = "HEARTBEAT"

    def __init__(self, socket, liName):
        LISocket.__init__(self, socket, liName)
        self.__heartbeatCountS = threading.Semaphore()
        self.__heartbeatContinueS = threading.Semaphore()
        self.__run = True
        self.__startHeartbeat()

    def heartbeatReceived(self):
        """ Notifies socket that a heartbeat has been correctly received """
        self.__heartbeatCountS.acquire()
        self.__heartbeatCount += 1
        self.__heartbeatCountS.release()

    def changeSocket(self, other):
        """ Extends """
        self.__heartbeatContinueS.acquire()
        self.__heartbeatContinue = False
        self.__heartbeatContinueS.release()
        self.__threadHeartbeat.join()
        LISocket.changeSocket(self, other)
        self.__startHeartbeat()

    def hasReceivedHeartbeat(self):
        """ Returns if the socket received heartbeat after last call
        
        Thread safe
        """
        self.__heartbeatCountS.acquire()
        c = self.__heartbeatCount
        self.__heartbeatCount = 0
        self.__heartbeatCountS.release()
        return c > 0

    def isOK(self):
        """ Overrides """
        self.__heartbeatContinueS.acquire()
        ok = self.__heartbeatContinue
        self.__heartbeatContinueS.release()
        return ok and self._isOK

    def restartHeartbeat(self):
        """ Restarts hearbeat mechanism

        Use this method when there is no active heartbeating only.
        """
        assert(not self.isOK())
        self.__startHeartbeat()

    def deactivateSocket(self):
        """ Same as self.close() """
        self.close()

    def close(self):
        """ Close the socket """
        self.__run = False
        self.__heartbeatContinueS.acquire()
        self.__heartbeatContinue = False
        self.__heartbeatContinueS.release()
        self.__threadHeartbeat.join()
        LISocket.close(self)

    def __startHeartbeat(self):
        """ Starts a new thread that provides heartbeat mechanism """
        self.__heartbeatCount = 0
        self.__heartbeatContinue = True
        self.__threadHeartbeat = threading.Thread(target = self.__heartbeatingThread, args=())
        self.__threadHeartbeat.daemon = True
        self.__threadHeartbeat.start()

    def __heartbeatingThread(self):
        """ Infinite loop that sends message through given socket in regular interval """
        try:
            while self.isOK():
                for i in range(self.HEARTBEAT_CHECK_FREQ):
                    if not self.__run:
                        break
                    self.send(self.HEARTBEAT_MSG)
                    sleepAtLeast(self.HEARTBEAT_TIMEOUT)
                if not self.hasReceivedHeartbeat():
                    self.__heartbeatContinueS.acquire()
                    self.__heartbeatContinue = False # Signal main thread that HB failed
                    self.__heartbeatContinueS.release()
        except socket.error as e:
            self.__heartbeatContinue = False
            log.warning(self.getName(), ": HeartBeating Thread: ", str(e))
        except Exception as e:
            log.unhandledException(self.getName() + ": HeartBeating Thread: ", e)

    def _parseMessage(self, msg):
        """ Overrides """
        m = LISocket._parseMessage(self, msg)
        # Process Heartbeat
        if m == self.HEARTBEAT_MSG:
            self.heartbeatReceived()
            return None
        return m

class LIServerSocket(Wrapper):
    """ Wraps server socket """

    def __init__(self, socket, liName):
        Wrapper.__init__(self, socket)
        self.__socket = self._wrapped_obj
        self.__name = liName

    def getName(self):
        """ Returns socket name """
        return self.__name

    def isPtpt(self):
        """ Is the socket point-to-point """
        return False

    def isOK(self):
        """ Returns if the socket is in correct state """
        return True

    def isClosed(self):
        return False

    def __repr__(self):
        """ This method aims to simplify debugging """
        return "LIServerSocket " + self.__name + " (" + repr(self._wrapped_obj) + ")"

class TCPSocketWrapper(Wrapper):
    """ Wrapps ordinary LISocket and extends it with more functionality """

    def __init__(self, s):
        """ Constructor

        s
          Already connected socket
        """
        Wrapper.__init__(self, s)
        self.__peername = str(s.getpeername())

    def getpeername(self):
        """ Overrides """
        return self.__peername

    def __repr__(self):
        """ This method aims to simplify debugging """
        return "TCPSocketWrapper (" + repr(self._wrapped_obj) + ")"

class UDPSocketWrapper(Wrapper):
    """ Wrapps UDP socket and create socket compatible with LISocketManager """

    def __init__(self, socket, liName):
        Wrapper.__init__(self, socket)
        self.__socket = self._wrapped_obj
        self.__name = liName

    def getName(self):
        """ Returns socket name """
        return self.__name

    def isPtpt(self):
        """ Is the socket point-to-point """
        return True

    def isOK(self):
        """ Returns if the socket is in correct state """
        return True

    def isClosed(self):
        return False

    def hasBufferedMessage(self):
        """ Returns False because every message is delivered in a separate UDP datagram """
        return False

    def receive(self):
        """ Returns one message """
        return self.__socket.recvfrom(4096)

    def __repr__(self):
        """ This method aims to simplify debugging """
        return "UDPSocketWrapper " + self.__name + " (" + repr(self._wrapped_obj) + ")"
