'''
session.py
===============================
The session support for Enlight
'''

import socket, time, threading, logging
import random
import string

logging.basicConfig(level=logging.DEBUG, format='%(asctime)s:%(levelname)s:%(threadName)s: %(message)s', datefmt='%Y.%m.%d %H:%M:%S')  # %Y.%m.%d %H:%M:%S

USED_SESSION_IDS = []

VERSION = "1.1.2"

## Role defs
HOST = 0
USER = 1
ADMIN = 2
VALID_ROLES = [HOST, USER, ADMIN]

def get_local_ip(no = 0):
    """
    Gets the local ip address

    :int no: ID of the ip adress
    """
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    s.connect(("8.8.8.8", 80))
    ipAddr = s.getsockname()[no]
    s.close()
    return(ipAddr)

def get_random_alphanumeric_string(length):
    """
    Returns a random alphanumeric string

    :int length: The length of the output string
    """
    letters_and_digits = string.ascii_letters + string.digits
    result_str = ''.join((random.choice(letters_and_digits) for i in range(length)))
    return(result_str)

class enlightSession():
    """
    The main session class

    :string name: The session name (not needed)
    :string role: The role of the local instance (defaults to HOST, can be HOST or USER)
    """
    def __init__(self, name = "", role = HOST, passcode = ""):
        self.sessionName = name
        self.__activ__  = False

        self.__my_ip__ = get_local_ip()
        logging.info("My IP is: " + self.__my_ip__)

        ## For HOST role
        self.sessionId = None
        self.__server__ = None
        self.__server_thread__ = None
        self.sessionpasscode = passcode
        self.members = []
        self.allowJoin = False
        self.__direct_socket__ = None
        self.__direct_thread__ = None

        ## For USER and ADMIN role
        self.sessionId = None
        self.__client__ = None
        self.__client_thread__ = None
        self.allOnlineSessions = {}
        self.connected = False
        self.__direct_socket__ = None
        
        self.__role__ = role

        if(self.sessionpasscode == ""):
            self.passcodeSet = "0"
        else:
            self.passcodeSet = "1"

        if(self.__role__ not in VALID_ROLES):
            self.__role__ = HOST
            logging.warning("No vaild session role was set, using HOST as default")
        
        if(self.__role__ == HOST and name == ""):
            logging.warning("Role is set to HOST, but no session name was given.")
            self.sessionName = "Unnamed session"
        
    
    def initConnection(self):
        """
        Starts the main dicovery/connction method(s)
        """
        global USED_SESSION_IDS
        if(self.__role__ == HOST):
            self.sessionId = get_random_alphanumeric_string(24)
            while(self.sessionId in USED_SESSION_IDS):
                self.sessionId = get_random_alphanumeric_string(24)
            logging.info("My session id is " + self.sessionId)
            USED_SESSION_IDS.append(self.sessionId)
            self.__server__ = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            self.__server__.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
            self.__server__.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            self.__activ__ = True
            self.allowJoin = True
            logging.info("Starting server thread")
            self.__server_thread__ = threading.Thread(target=self.serverMain, args=(), name="Discovery server")
            self.__server_thread__.start()

            self.__direct_socket__ = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            # self.__direct_socket__.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
            self.__direct_socket__.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            self.__direct_socket__.bind(("", 5589))
            self.__direct_thread__ = threading.Thread(target=self.lightSearcherMain, args=(), name="Inbound session server")
            self.__direct_thread__.start()

        elif(self.__role__ == USER or self.__role__ == ADMIN):
            self.__client__ = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)#, socket.IPPROTO_UDP)  # UDP
            self.__client__.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
            self.__client__.bind(("", 37020))

            logging.info("Starting lighthouse thread")
            self.__activ__  = True
            self.__direct_socket__ = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.__direct_socket__.bind(("", 5589))
            self.__client_thread__ = threading.Thread(target=self.lighthouseMain, args=(), name="Lighthouse server")
            self.__client_thread__.start()
            self.__direct_thread__ = threading.Thread(target=self.lightSearcherDirectMain, args=(), name="Inbound session direct server")
            self.__direct_thread__.start()
            self.__trying_to_connect__ = False
            

    def lightSearcherMain(self):
        """
        The main thread for clients to connect
        """
        logging.info("Inbound connection handler started")
        self.__direct_socket__.listen()
        while self.allowJoin: 
            try:
                conn, addr = self.__direct_socket__.accept()
                data = conn.recv(1024)
                conn.sendall(b"CONNECTION INT OK")
                logging.info("Got inbound connection with data: " + str(data) + " from " + str(addr))
            except OSError:
                logging.warning("Client socket, failure. Session maybe not avaiable anymore?")
            
        logging.info("Inbound connection handler stopped")

    def lightSearcherDirectMain(self):
        """
        The Handler for direct comuncation between HOST and USER
        """
        while self.allowJoin: 
            try:
                conn, addr = self.__direct_socket__.accept()
                data = conn.recv(1024)
                if(self.__trying_to_connect__ != False):
                    conn.sendall(b"ACK")
                logging.info("Got inbound connection with data: " + str(data) + " from " + str(addr))
            except OSError:
                logging.warning("Client socket, failure. Session maybe not avaiable anymore?")

    def lighthouseMain(self):
        """
        The main thread for searching/finding sessions
        """
        while self.__activ__:
            try:
                data, addr = self.__client__.recvfrom(1024)
            except OSError:
                logging.warning("Client socket, failure. Session maybe not avaiable anymore?")
            else:
                data = data.decode("utf-8")
                data = data.replace("|", "")
                proc = data.split(";")

                logging.info("Got broadcast from " + str(addr) + " with data " + str(proc))
                if(proc[3] != VERSION): #if(proc[3] == VERSION):
                    logging.warning("Software versions are not compatible. ABORT")
                else:
                    ## More data handling
                    if(proc[2] not in self.allOnlineSessions):
                        proc.append(addr)
                        self.allOnlineSessions[proc[2]] = proc
                        logging.info("Found new session named " + proc[1])

    def clearAllSessions(self):
        """
        Clears all discoverd sessions 
        """
        if(self.__role__ != HOST):
            logging.info("Clearing all know session")
            self.allOnlineSessions = {}

    def serverMain(self):
        """
        Main Discovery server
        """
        logging.info("Discovery server started")
        # Set a timeout so the socket does not block
        # indefinitely when trying to receive data.
        self.__server__.settimeout(0.2)
        message = b"SESSION;" + self.sessionName.encode("utf-8") + b";" + self.sessionId.encode("utf-8") + b";"  + VERSION.encode("utf-8") + b";" + self.passcodeSet.encode("utf-8") + b";" + str(len(self.members)).encode("utf-8") + b"|"

        while self.allowJoin:
            ipChoped = get_local_ip().split(".")
            ipWithBroadEnding = ipChoped[0] + "." + ipChoped[1] + "." + ipChoped[2] + ".255"
            self.__server__.sendto(message, (ipWithBroadEnding, 37020))    # <broadcast>  192.168.178.60 255.255.255.255

            logging.info("Sent discovery broadcast")
            time.sleep(1)
        logging.info("Discovery server stopped")

    def getSessionMembers(self):
        """
        Get all session members
        :list members: A list of all Session members
        """
        return(self.members)
    
    def getSessionId(self):
        """
        Get the local session id
        """
        return(self.sessionId)

    def join(self, sessionID):
        """
        Join a remote session
        """
        if(not self.connected):
            try:
                data = self.allOnlineSessions[sessionID]
            except KeyError:
                logging.error("an unknow/undiscorvered session ID was given")
                return(-2)
            else:
                try:
                   # print((data[len(data)-1][0], 5589))
                    ## Try to open a socket to the ip/port
                    self.__direct_socket__ = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # UDP
                    self.__direct_socket__.connect((data[len(data)-1][0], 5589))
                    
                    self.__direct_socket__.sendall(b'CONNECT')
                    self.__trying_to_connect__ = sessionID
                    logging.info("Sent JOIN intent to session host")
                except ConnectionRefusedError:
                    logging.warning("Connection refused by session host")
                    return(-1)
                else:
                    return(1)
        else:
            logging.warning("join was called, while there is a session activ")
            return(-2)

    def leave(self):
        """
        Leaves the session, will take at least two seconds
        """
        if(self.__role__ == USER or self.__role__ == ADMIN):
            ## TODO: Send leave command to host
            self.__activ__  = False
            time.sleep(2)
            self.__client__.shutdown(socket.SHUT_RDWR)
            self.__client__.close()
            logging.info("Closed Lighthouse port")
        else:
            logging.warning("leave was called, without the role set to USER or ADMIN. Did you mean .stopSession?")

    def stopSession(self):
        """
        Stops the session as a HOST
        """
        if(self.__role__ == HOST):
            self.allowJoin = False
            self.__activ__ = False
            # TODO: Kick all users
            self.members = []
            self.__server_thread__ = None
            self.__server__.shutdown(socket.SHUT_RDWR)
            self.__server__.close()
            #self.__direct_socket__.shutdown(socket.SHUT_RDWR)
            self.__direct_socket__.close()
            logging.info("Closed Discovery server port")
        else:
            logging.warning("stopSession was called, without the role set to HOST. Did you mean .leave?")



def testModule():
    """
    Tests the session server
    """
    testSession = enlightSession("TestSession", role = HOST)
    userSession = enlightSession("myLocalSession", role = USER)
    userSession.initConnection()
    testSession.initConnection()
    time.sleep(1)
    userSession.join(testSession.getSessionId())
    time.sleep(8)
    testSession.stopSession()
    userSession.leave()

def testOnlyServer():
    """
    Tests the session server without client
    """
    testSession = enlightSession("TestSession", role = HOST)
    testSession.initConnection()
    time.sleep(30)
    testSession.stopSession()

if __name__ == "__main__":
    testOnlyServer()
 