#! /usr/bin/env python3
# Script that process stdout from pcf and creates IRI messages
#
# Copyright (C) 2013 Libor Polčák
#
# 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/>.

from modules.sockets.li import ptptAsClient

import fileinput
import os
import time
import sys
import xml.etree.ElementTree as ET

similar_skew = {}

def connectToCollector():
    """ Connects to IRI-IIF Collector

    Returns socket
    """
    return ptptAsClient("iricol")

def knownAddressPair(t, a1, a2):
    """ Returns True if the address binding of a1, a2 was already detected by t
    """
    global similar_skew
    try:
        return a2 in similar_skew[t][a1]
    except KeyError:
        return False

def addAddressPair(t, a1, a2):
    """ Stores information about address binding a1, a2 detected by method t
    """
    global similar_skew
    if t not in similar_skew:
        similar_skew[t] = {}
    try:
        similar_skew[t][a1].append(a2)
    except KeyError:
        similar_skew[t][a1] = [a2]
    try:
        similar_skew[t][a2].append(a1)
    except KeyError:
        similar_skew[t][a2] = [a1]

def deleteAddressPair(t, a1, a2):
    """ Deletes address pair a1, a2 detected by method t """
    global similar_skew
    try:
        similar_skew[t][a1].remove(a2)
        similar_skew[t][a2].remove(a1)
    except KeyError:
        return

def deleteAddressKey(detectionType, address):
    """ Deletes inactive address in dictionary by method t """
    global similar_skew
    try:
        del similar_skew[detectionType][address]
    except KeyError:
        return

def checkMissingPairs(s, activity, detectionType, address, pairedAddress):
    """ Checks if all previously known address paires are still valid
    
    @param s Socket for communication with IRI-Collector
    @param detectionType Clock skew detection method name
    @param address The primary address
    @param pairedAddress Addresses now paired with the address

    Sends IRI END messages for all previous address pairs that are no longer valid
    """
    try:
        knownPaired = similar_skew[detectionType][address][:]
    except KeyError:
        return
    for known in knownPaired:
        if known not in pairedAddress:
            deleteAddressPair(detectionType, address, known)
            s.send(createIRI(detectionType, "END", "", (address[0], known[0])))
            #print(createIRI(detectionType, "END", "", (address[0], known[0])))
    if(activity == "inactive"):
        deleteAddressKey(detectionType, address)
        s.send(createIRI(detectionType, "END", "inactive", (address[0], address[0])))
        #print(createIRI(detectionType, "END", "inactive", (address[0], address[0])))

def checkNewPairs(s, detectionType, address, pairedAddress):
    """ Detects newly found address pairs
    
    @param s Socket for communication with IRI-Collector
    @param detectionType Clock skew detection method name
    @param address The primary address
    @param pairedAddress Addresses now paired with the address

    Sends IRI BEGIN and CONTINUE messages for all currently known address pairs for address address
    """
    for other in pairedAddress:
        if not knownAddressPair(detectionType, address, other):
            addAddressPair(detectionType, address, other)
            s.send(createIRI(detectionType, "BEGIN", (address[1],other[1]), (address[0],other[0])))
            #print(createIRI(detectionType, "BEGIN", (address[1],other[1]), (address[0],other[0])))
        else:
            s.send(createIRI(detectionType, "CONTINUE", (address[1],other[1]), (address[0],other[0])))
            #print(createIRI(detectionType, "CONTINUE", (address[1],other[1]), (address[0],other[0])))

def encodeIP(ip):
    """ Encodes IP address NID to be transferred to IRI Core """
    return ("IPv6" if ":" in ip else "IPv4", ip)

def createIRI(detectionType, iriType, textMessage, nidIterable):
    """ Creates IRI message to be sent to IRI-Core """
    nidList = []
    natIPs = []
    for nid in nidIterable:
        if "_" in nid:
            ip, port = nid.split("_")
            nidList.append(("TCP3", nid))
            natIPs.append(encodeIP(ip))
        else:
            nidList.append(encodeIP(nid))
    return ("clockskew_" + detectionType,
            time.time(),
            iriType,
            textMessage,
            nidList,
            natIPs,
            [],
            )

def addSkewFromXML(xmlFileLocation, t, addresses):
    """ Creates (ip, skew) tuples for all given addresses """
    ret = []
    found = 0
    try:
        tree = ET.parse(xmlFileLocation + "/" + t + "/active.xml")
        root = tree.getroot()
    except:
        # could not open XML -> set skew to 0
        for ip in addresses:
            ret.append((ip, 0))
        return ret
    for ip in addresses:
        for node in root.iter('computer'):
            if node.find('address').text == ip:
                ret.append((ip, node.get('skew')))
                found = 1
        if(found == 0):
            ret.append((ip, "NaN"))
        found = 0
    return ret

def updateSkewsInDict(t, addresses):
    """ Updates info in similar_skew if known IP address changed it's skew """
    if t in similar_skew:
        for address in addresses:
            for key, lst in similar_skew[t].items():
                # Set new key if skew is different
                if key[0] == address[0] and key[1] != address[1]:
                    similar_skew[t][address] = lst
                    del similar_skew[t][key]
                # Update skew in lists
                for item in lst:
                    if item[0] == address[0] and item[1] != address[1]:
                        try:
                            lst.remove(item)
                            lst.append(address)
                        except:
                            pass
                

def processInputLine(line, s, xmlFileLocation):
    """ Parses and processes information from one line of pcf output

    Creates IRI messages for IRI-IIF
    """
    try:
        activity, *detectedAddresses = line.split("\t")
        detectionType = detectedAddresses[0]
        detectedAddresses.pop(0)
        ret = addSkewFromXML(xmlFileLocation, detectionType, detectedAddresses)
        updateSkewsInDict(detectionType, ret)
        address, *pairedAddress = ret
    except:
        # Ignore invalid lines
        return
    checkMissingPairs(s, activity, detectionType, address, pairedAddress)
    checkNewPairs(s, detectionType, address, pairedAddress)

def main():
    """ Main body of the program """
    if(len(sys.argv) == 2):
        xmlFileLocation = sys.argv[1]
    else:
        xmlFileLocation = "/opt/slis/www/pcf/www/data"
    s = connectToCollector()
    #s = None
    for line in iter(sys.stdin.readline, ''):
        processInputLine(line.strip(), s, xmlFileLocation)

if __name__ == "__main__":
    main()
