#!/usr/bin/env python3
# Administration function of light version of 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/>.

import os
import socket
import sys
import threading
import time
from modules.shared.cid import CID
from modules.shared.time_priority_queue import TimePriorityQueue
from modules.shared.ini1aintercept import INI1AIntercept
from modules.sockets.li_socket_manager import LISocketManager
from modules.sockets.li import acceptConnection, ptptAsClient, acceptTCPLIConnection, \
    acceptTCPLIConnectionSimple
from modules.tools.config import ReadConfig, configError, parseServerTcpIfcConfig
import modules.tools.log as log

# Global variables representing the queues
QUEUE_SIZE = 0 # Infinite
wiq = TimePriorityQueue("afwiq.content")
aiq = TimePriorityQueue("afaiq.content")
run = True
# Time period in which interception is configured in the system before it
# should really start and also the time period in which the interception is
# configured in the system even though it should have already finished
INTERCEPTION_STANDBY_SECONDS = 10

# LIIDs that have already been added. They can't be reused so that the system
# is can't be confused by duplicate LIID. LIID should be unique anyway.
seenLIIDs = set([])

def main(argv):
    # Setup logging
    log.setupLogging(argv[1:])
    global logger
    logger = log.createFileLogger("AF_logger", "af.log")
    # Read configuration
    ini1a = parseServerTcpIfcConfig("ini1a", "af.ini")
    hi1 = parseServerTcpIfcConfig("hi1", "af.ini")
    # Prepare interfaces
    sm = LISocketManager(
        (
        ),
        ("afaiq", "afwiq"),
        (
            ("ini1a",) + ini1a,
            ("hi1",) + hi1,
        ),
        ()
    )
    # Prepare subprocesses
    wiqThread = threading.Thread(target = queueThread, args=(wiq, "afwiq"))
    wiqThread.daemon = True
    wiqThread.start()
    aiqThread = threading.Thread(target = queueThread, args=(aiq, "afaiq"))
    aiqThread.daemon = True
    aiqThread.start()
    # Run
    sm.tryInterfaces(argv[0])
    logEvent("Administration function started")
    sm.mainLoop(globals())
    # Close sockets
    sm.closeSockets()
    global run
    # Stop the queue thread
    run = False
    wiq.push(time.mktime(time.localtime()), None)
    wiqThread.join()
    aiq.push(time.mktime(time.localtime()), None)
    aiqThread.join()
    logEvent("Administration function finished")

def queueThread(queue, socketName):
    """ Manages currently active interceptions """
    while True: # Infinite loop
        interception = queue.pop()
        if not run:
            break
        s = ptptAsClient(socketName)
        s.send(interception)

def brokenSocketINI1A(s, sm):
    """ Handles situation when socket connection fails

    Returns if the AF should continue
    """
    logEvent("AF: broken connection %s" % str(s.getpeername()), "error")
    return True

def processMessageINI1A(msg, sm, s):
    """ Handler for processing messages received through INI1a

    msg Received message
    sm Socket manager
    s Socket that has received the message

    Returns if the AF should continue
    """
    log.emptyMessageHandler(msg)
    return True

def processRequestINI1A(s, sm):
    """ Process new request from INI1A interface

    s Server socket
    sm Socket manager
    """
    newiri = acceptTCPLIConnection(s, sm, "ini1a")
    # Read configuration
    af_config = ReadAFConfig()
    for hi1int in aiq.getContent():
        try:
            cid = CID("uid 0", str(hi1int.getNID().getValue()), \
                0 , af_config.get("CID", "DCC"))
            logEvent("AF: configuring new IRI probe for interception: %s" % (str(hi1int,)))
            newiri.send(("new_intercept", "iri-iif", INI1AIntercept( \
                cid, *hi1int.getINI1AInformation())))
        except configError as e:
            logEvent("AF: Cannot start interception: Error in configuration file %s" % str(e), "critical")
            return
        except Exception as e:
            log.unhandledException("AF: ", e)
            logEvent("AF: Interception not added correctly: %s" % str(e), "critical")
        logEvent("AF: Configuration for interception %s done" % (str(hi1int,)))
    return True

def processRequestHI1(s, sm):
    """ Process new request from HI1 interface

    s Server socket
    sm Socket manager
    """
    conn = acceptTCPLIConnectionSimple(s)
    msgType, *params = conn.blockingReceive()
    handlers = {"new_intercept": newIntercept,
            "delete_intercept": deleteIntercept,
    }
    try:
        h = handlers[msgType]
    except KeyError as e:
        errorText = "Not implemented: AF received %s through hi1" % \
                (str((msgType, params),))
        logEvent(errorText, "critical")
    h(sm, conn, *params)
    conn.close()
    return True

def newIntercept(sm, hi1, hi1int):
    """ Process new interception

    sm Socket manager
    hi1 HI1 interface socket
    hi1int Object that defines interception
    """
    start, end = hi1int.getInterceptionPeriod()
    if end <= time.localtime():
        hi1.send(("Cannot add interception from the past", hi1int))
    elif hi1int.getLIID() in seenLIIDs:
        hi1.send(("Corrupted interception - duplicate LIID", hi1int))
    else:
        hi1.send(("ack", hi1int))
        logEvent("AF: Interception received: %s" % (str(hi1int,)))
        seenLIIDs.add(hi1int.getLIID())
        wiq.push(time.mktime(hi1int.getInterceptionPeriod()[0]) - \
            INTERCEPTION_STANDBY_SECONDS, hi1int)

def deleteIntercept(sm, hi1, liid):
    """ Delete existing interception

    sm Socket manager
    hi1 HI1 interface socket
    liid LIID that identifies the interception to be removed
    """
    def keepOthers(i):
        return i.getLIID() != liid
    hi1.send(("ack", liid))
    logEvent("AF: Removing interception identified by %s from LI system" % (liid))
    wiq.erase(keepOthers)
    active = aiq.erase(keepOthers)
    if active:
        removeActiveInterceptionsFromSystem(sm, liid)
    logEvent("AF: Removal of interception identified by %s done" % (liid))

def removeActiveInterceptionsFromSystem(sm, liid):
    """ Removes active interceptions from other blocks in the system """
    # FIXME ack
    sm.send("ini1a", ("delete_intercept", "iri-iif", liid))

def processRequestAFWIQ(s, sm):
    """ New interception is about to start

    s Server socket
    sm Socket manager
    """
    # Receive data
    conn = acceptConnection(s)
    hi1int = conn.blockingReceive()
    # Read configuration
    af_config = ReadAFConfig()
    # Configure the system
    try:
        cid = CID("uid 0", str(hi1int.getNID().getValue()), \
            0 , af_config.get("CID", "DCC"))
        logEvent("AF: Configuring LI system for interception: %s" % (str(hi1int,)))
        sm.send("ini1a", ("new_intercept", "iri-iif", INI1AIntercept( \
            cid, *hi1int.getINI1AInformation())))
    except configError as e:
        logEvent("AF: Cannot start interception: Error in configuration file %s" % str(e), "critical")
        return
    except Exception as e:
        log.unhandledException("AF: ", e)
        logEvent("AF: Interception not added correctly: %s" % str(e), "critical")
    aiq.push(time.mktime(hi1int.getInterceptionPeriod()[1]) + \
        INTERCEPTION_STANDBY_SECONDS, hi1int)
    logEvent("AF: Configuration for interception %s done" % (str(hi1int,)))

def processRequestAFAIQ(s, sm):
    """ Active interception already ended

    s Server socket
    sm Socket manager
    """
    conn = acceptConnection(s)
    hi1int = conn.blockingReceive()
    liid = hi1int.getLIID()
    logEvent("AF: Removing finished interception %s from LI system" % (str(hi1int,)))
    removeActiveInterceptionsFromSystem(sm, liid)
    logEvent("AF: Removal of finished interception %s done" % (str(hi1int,)))

def ReadAFConfig():
    return ReadConfig("af.ini")

def logEvent(eventText, severity = "info"):
    """ Adds given text to log

    severity - Level of the event

    """
    global logger
    getattr(logger, severity)(eventText)

if __name__ == "__main__":
    try:
        main(sys.argv)
    except Exception as e:
        log.unhandledException("AF", e)

