import sys
import socket
import subprocess
import os, time, datetime
import glob
import string
import math
import json
import signal 

### SET THIS DATA TO YOUR LIKING 
 
# IRC connection
HOST = "naamio.fi.eu.synirc.net"
#HOST = "irc.gamesurge.net"
PORT = 6667
NICK = "BlitzBot"
IDENT = "blitzbot"
REALNAME = "Dominions 4 BlitzBot"
CHANNEL = "#dom4goons"
#CHANNEL = "#dominions"

# Dominions directories
DOMDIR = "/home/antti/Dom4/dom4_linux/"
DOMDATA = "/home/antti/dominions4/"
# Change to dom4_x86 or dom4_x64 (depending on system) on Linux 
DOMAPPNAME = "dom4_x86"
  
# Deadlines before games can be killed by other users without password
SHORT_HOST_DEADLINE = 60
LONG_HOST_DEADLINE = 2880
LONG_HOST_PASSWORD = "DEFAULTPASSWORDISAWESOME" 

# Available ports. Python ranges don't include the number specified in the second argument
# You can (I think, it isn't tested) combine different ranges with set(range1 + range2)
PORTS = range(45050, 45054)

# Your IP. This is technically not used for any function, but since it's not that simple to
# find out it automatically when you're behind a router, it's manual.
OWNIP = "130.234.189.10"

# Acceptable Dominions 4 args
WHITELIST = ['--hosttime', '--minutes', '--hours', '--noquickhost', '--uploadtime', '--uploadmaxp', '--team', '--mapfile', '--research', '--hofsize', '--indepstr', '--magicsites', '--eventrarity', '--richness', '--resources', '--supplies', '--masterpass', '--startprov', '--renaming', '--scoregraphs', '--nonationinfo', '--era', '--enablemod', '--teamgame', '--noartrest', '--clustered', '--conqall', '--thrones', '--requiredap', '--totalvp', '--capitalvp', '--requiredvp', '--summervp']
MULTI = ['--enablemod', '--team']

### DO NOT CHANGE THINGS UNDER THIS
gamepasswords = {}
commands = {}

### COMMANDS
class Help:
    def main(self, root, sender, message):
        root.send("PRIVMSG %s :See http://koti.kapsi.fi/elmokki/dom4/blitzbot_help.txt for help." % (sender))
commands['help'] = Help()

class GameStatus:
    def main(self, root, sender, message):
        args = str.split(message)
        arg = "no argument"
        if(len(args) > 1):
            arg = args[1]
        root.send("PRIVMSG %s :%s" % (sender, servermanager.gamestatus(arg)))
commands['gamestatus'] = GameStatus()

class ListStuff:
    def main(self, root, sender, message):
        args = str.split(message)
        arg = "no argument"
        if(len(args) > 1):
            arg = args[1]
        stuff = listThings(arg)
        root.send("PRIVMSG %s :%s" % (sender, stuff))
commands['list'] = ListStuff()

class Servers:
    def main(self, root, sender, message):
        servermanager.removeDeadServers()
        stri = "SERVERS: "
        for server in servermanager.servers:
            if(server.process is not None):
                stri = stri + " " + server.game + " "
            else:
                stri = stri + " FREE SERVER "
            stri = stri + "(" + OWNIP + ":" + str(server.port)
            if(server.process is not None):
                stri = stri + ", hosted %s ago" % getTime(servermanager.getLastHost(server.game))
            if(isTooOld(server)):
                stri = stri + " - STOPPABLE"
            stri = stri + "), "
            stri = stri.rstrip(", ")
        root.send("PRIVMSG %s :%s" % (sender, stri))
commands['servers'] = Servers()


class RefreshGame:
    def main(self, root, sender, message):
        args = str.split(message)
        arg = ""
        if(len(args) < 2):
            root.send("PRIVMSG %s :Please specify a game name." % (sender))
            return()
        pw = ""
        if(len(args) > 2):
            pw = args[2]
        arg = args[1]
        if(not servermanager.containsGame(arg)):
            root.send("PRIVMSG %s :Game %s is not currently being hosted." % (sender, arg))
        else:
            game = servermanager.getGame(arg)
            if(game.password == pw or game.password == ""):
               servermanager.refreshGame(arg, game.password)
               root.send("PRIVMSG %s :The request to refresh %s was executed." % (sender, arg))
            else:
               root.send("PRIVMSG %s :Wrong password." % (sender))
commands['refresh'] = RefreshGame()

class KillGame:
    def main(self, root, sender, message):
        args = str.split(message)
        arg = ""
        if(len(args) > 1):
            arg = args[1]
            if(not servermanager.containsGame(arg)):
                root.send("PRIVMSG %s :Game %s is not currently being hosted." % (sender, arg))
            else:
                game = servermanager.getGame(arg)
                if(game is not None):
                    pw = ""
                    if(len(args) > 2):
                        pw = args[2]
                        
                    if(game.password == pw or isTooOld(game) or game.password == ""):
                            servermanager.killGame(arg)    
                            root.send("PRIVMSG %s :The request to kill %s was executed." % (sender, arg))
                    else:
                         root.send("PRIVMSG %s :Wrong password for game %s and the server is too fresh to be shut down (It has a safe time of %s minutes after each hosting)." % (sender, arg, str(game.hostdeadline)))
                else:
                         root.send("PRIVMSG %s :Game %s could not be found." % (sender, arg))


        else:
            root.send("PRIVMSG %s :Please specify a game name." % (sender))
commands['stopgame'] = KillGame()
 
 
 
 
class HostGameCommand:
    def main(self, root, sender, message):

        message = sanitize(message)
        if(servermanager.getNewServer() is None):
            root.send("PRIVMSG %s :No free servers. Try killing them (or checking server list with servers to see if that frees up a crashed server.)" % (sender))
            return()
  
        largs = {}
        largs["--era"] = "2"
        margs = []
        
        gotmap = False
        
        password = ""
        name = ""
        longhost = False
        
        args = str.split(message)
        for i in range(1, len(args)):
            if(args[i] == "-password"):
                if(i < len(args) - 1 and not args[i+1].startswith("-")):
                    password = args[i+1]
                else:
                    root.send("PRIVMSG %s :Unable to set password (unset or starts with -)" % (sender))
                    return()
            elif(args[i] == "-name"):
                if(i < len(args) - 1 and not args[i+1].startswith("-")):
                    name = args[i+1]
                else:
                    root.send("PRIVMSG %s :Unable to set name (unset or starts with -)" % (sender))
                    return()
            elif(args[i] == "-longhost"):
                if(i < len(args) - 1 and not args[i+1].startswith("-")):
                    if(args[i+1] == LONG_HOST_PASSWORD):
                        longhost = True
                    else:
                        root.send("PRIVMSG %s :Invalid password for long host time: %s" % (sender, str(args[i+1])))
                        return()
                else:
                    root.send("PRIVMSG %s :Invalid argument for -longhost (unset or starts with -)" % (sender))
                    return()
            elif(args[i].startswith("-") and args[i] in WHITELIST):
                argset = ""
                for j in range(i+1, len(args)):
                    if(args[j].startswith("-")):
                        break
                    argset = argset + args[j] + " "
                
                ## Check if maps/mods can be found
                if(args[i] == "--mapfile" and argset is not ""):
                    if(os.path.isfile(DOMDIR + "/maps/" + args[i+1]) or os.path.isfile(DOMDATA + "/maps/" + args[i+1])):
                        derp = sanitize(argset)
                        if(len(derp) > 0):
                           print("-> MAP FILE: " + args[i + 1])
                           gotmap = True
                    else:
                        root.send("PRIVMSG %s :Invalid map file: %s." % (sender, argset))
                        return()    
                elif(args[i] == "--enablemod" and argset is not ""):
                    if(os.path.isfile(DOMDATA + "/mods/" + args[i+1])):
                        derp = sanitize(argset)
                        if(len(derp) > 0):
                           print("-> MOD FILE: " + args[i + 1])
                    else:
                        root.send("PRIVMSG %s :Invalid mod file: %s." % (sender, argset))
                        return()
                            
                if(args[i] not in MULTI):   
                    largs[args[i]] = argset.strip() 
                else:
                    margs.append(args[i] + " " + argset.strip()) 
                    
            elif(args[i].startswith("-")):
                root.send("PRIVMSG %s :Invalid command: %s." % (sender, str(args[i])))
                return()
             
        if(name == ""):
            root.send("PRIVMSG %s :%s" % (sender, "Invalid name."))
            return()
        name = sanitize(name)
        print("Succesful validation of host command")
        text = servermanager.hostNewGame(largs, margs, name, password, longhost)  
        root.send("PRIVMSG %s :%s" % (sender, text))
commands['host'] = HostGameCommand()
 
 
### MISC FUNCTIONS
def isTooOld(server):
    if(not server in servermanager.servers):
        return(True)
    elif(server == None or server.game == "" or server.process == None):
        return(False)
    else:
        dir = DOMDATA + "/savedgames/" + server.game + "/"
        lasthost = server.created
        if(len(glob.glob(dir + "*.trn")) > 0):
            lasthost = os.path.getmtime(glob.glob(dir + "*.trn")[0])
        d = time.time() - lasthost
 
        if(d > server.hostdeadline * 60):
            return(True)
        else:
            return(False)
         
def listThings(arg):
    stri = "";
    if(arg == "mods" or arg == "maps"):
        dirl = "";
        end = "";
        if(arg == "maps"):
            dirl = "/maps/"
            end = ".map"
        elif(arg == "mods"):
            dirl = "/mods/"
            end = ".dm"
        dirl = DOMDATA + dirl
        stri = "Listing " + arg + ": "
        for file in glob.glob(dirl + "*" + end):
            stri = stri + os.path.basename(file) + ", "
        stri = stri + "DOES NOT CONTAIN DEFAULT MAPS/MODS"
    elif(arg == "games" or arg == "saves"):
        dirl = DOMDATA + "/savedgames/"
        stri = "Available games: "
        for dirl2 in os.listdir(DOMDATA + "/savedgames/"):
            if(os.path.isfile(dirl + dirl2 + "/ftherlnd")):
                stri = stri + dirl2 + ", "
    else:
        stri = "Please specify an argument (maps/mods/games)"
    return(stri)

def isInDict(name, dicta):
   for key in dicta:
      if(key.lower() == name.lower()):
         return True
   return False
 
def sanitize(tmpstr):
   valid_chars = "-_.() %s%s" % (string.ascii_letters, string.digits)
   return("".join(c for c in tmpstr if c in valid_chars))

def getTime(dtime):
   stri = ""
   if(dtime.days > 0):
      stri = stri + str(dtime.days) + "d "

   secs = dtime.seconds
   if(secs >= 60):
      multi = math.floor(secs/60)
      secs = secs - multi*60
      if(multi >= 60):
         multi2 = math.floor(multi/60)
         stri = stri + str(multi2) + "h "
         multi = multi - multi2*60
         
      stri = stri + str(multi) + "m "
      
   if(secs > 0):
      stri = stri + str(secs) + "s "
   return(stri.strip())   
   
   
def containsStart(list, start):
    for i in list:
        if(i.startswith(start)):
            return(True)
    return(False)
        
## Server class
class Server:
    hostdeadline = SHORT_HOST_DEADLINE
    password = ""
    process = None
    game = ""
    created = None
    def __init__(self, port):
        self.port = port
        
### GAME MANAGER
class ServerManager:
    servers = []
    def __init__(self):
        for port in PORTS:
            newserver = Server(port)
            self.servers.append(newserver)
        self.loadDBs()
        
    def containsGame(self, name):
        for server in self.servers:
            if(server.game == name):
                return(True)
        return(False)
        
    def getGame(self, name):
        for server in self.servers:
            if(server.game == name):
                return(server)
        return(None)
        
    def refreshGame(self, name, password):
      game = self.getGame(name)
      longhost = False
      if(game.hostdeadline > SHORT_HOST_DEADLINE):
         longhost = True
           
      self.killGame(name) 
      serv = None
      for server in self.servers:
         if(server.port == game.port):
            serv = server
            
      if(serv == None):
         print("FAILURE TO REFRESH: NO SERVER FOUND")
         return()
      self.hostNewGameWS([], {}, name, password, longhost, serv)
      print("Refresh is a success!")

    def getLastHost(self, name):
        dir = DOMDATA + "/savedgames/" + name + "/"
        if(len(glob.glob(dir + "*.trn"))  > 0):
              files = glob.glob(dir + "*.trn")
              statbuf = os.stat(files[0])
              lasthost = os.path.getmtime(files[0])


              lasthost = time.time() - lasthost
              lasthost = datetime.timedelta(seconds=lasthost)  
              return(lasthost)
            
        if(self.containsGame(name)):
            serv = self.getGame(name)
            lasthost = datetime.timedelta(seconds=time.time() - serv.created)
            return(lasthost)
            
        return(0)
      
    def gamestatus(self, name):
        self.removeDeadServers()
        tmpstring = ""
        dir = DOMDATA + "/savedgames/" + name + "/"

        if(len(glob.glob(dir + "*.trn"))  > 0):
            lasthost = self.getLastHost(name)
            files = glob.glob(dir + "*.trn")
            maxplr = len(files)
            readyplr = 0;
            pfiles = glob.glob(dir + "*.2h")
            for i, v in enumerate(pfiles):
              filetime = time.time() - os.path.getmtime(v)
              if(filetime < lasthost.seconds):
                  readyplr = readyplr + 1
    
            tmpstring = str(readyplr) + "/" + str(maxplr) + " players have submitted their turn. Last hosting: " + getTime(lasthost) + " ago. "

        	          
        if(self.containsGame(name)):
            serv = self.getGame(name)
            tmpstring = tmpstring + "Hosted at " + OWNIP + ":" + str(serv.port) + "."
         		
        if(tmpstring == ""):
          return("No game named " + name + " was found.")
        else:
          tmpstring = "Game status for " + name + ": " + tmpstring
          return(tmpstring.strip())    
        
    def killGame(self, name):
        game = servermanager.getGame(name)
        if(game is not None):
            if(game.process.poll() == None):
                try:
                    print("KILLING!")
                    game.process.kill()
                    ##os.kill(game.process.pid)
                except:
                    print("Failure to terminate process of game " + name + ". This might not be a problem as it might just already be dead.")
            port = game.port
            servermanager.servers.remove(game)
            servermanager.servers.append(Server(port))
            self.saveDBs()
        else:
            print("No game found?!") 
            
    def getNewServer(self):
        for server in self.servers:
            if(server.process is None):
                return(server)
        return(None)
        
    def saveDBs(self):
        if(len(gamepasswords) > 0):
            with open("pws.txt", mode = 'w', encoding = 'utf-8') as outfile:
                json.dump(gamepasswords, outfile)

                
    def loadDBs(self):
        if(os.path.isfile("pws.txt")):
            print("Loading game passwords...")
            with open("pws.txt", mode = 'r', encoding = 'utf-8') as infile:
                gamepasswords = json.load(infile)

    
    def removeDeadServers(self):
        for server in self.servers:
            if(server.process is not None and server.process.poll() != None):
                self.killGame(server.game)

        
        
    def hostNewGame(self, largs, margs, name, password, longhost):
      oldserver = self.getNewServer()
      if(oldserver == None):
         print("Failure to retrieve an empty server. This is a failsafe that should not trigger, but might if there's traffic.")
         return()
      msg = self.hostNewGameWS(largs, margs, name, password, longhost, oldserver)
      return(msg)
           
    def hostNewGameWS(self, largs, margs, name, password, longhost, oldserver):
        print("TRYING TO HOST A GAME")
        self.removeDeadServers()
        restartOld = False;
        
        error = False;
        text = ""
        for server in self.servers:
            if(server.game is not "" and server.game == name):
                text = "There's a game named %s currently in the hosting phase: %s " % (name, str(server.game))
                error = True
                
        if(error == True):
            return(text)
            
        testt = isInDict("--mapfile", largs)
        
        if(os.path.isfile(DOMDATA + "/savedgames/" + name + "/ftherlnd")):
            if(not isInDict(name, gamepasswords)):
                restartOld = True
            elif(gamepasswords[name] == password):
                restartOld = True
            else:
                print("PW WAS ..%s.." % gamepasswords[name])
                print("TRIED PW ..%s.." % password)
                return("Wrong password to restart %s." % name)
        else:
            if(not isInDict("--mapfile", largs)):
                return("No map was specified.")
                
        if(restartOld):
            margs = []
            largs = {}
        
        server = Server(oldserver.port) 
        
        server.game = name
        server.password = password
        server.created = time.time()
        if(longhost):
            server.hostdeadline = LONG_HOST_DEADLINE
        
        launch = DOMAPPNAME + " --tcpserver --textonly --port " + str(server.port) + " "
        for key in largs:
            launch = launch + key + " " + largs[key] + " "
        for string in margs:
            launch = launch + string + " "
        
        launch = launch + name
        
        print("LAUNCH A GAME WITH: " + launch)
            

        flaunch = DOMDIR + "/" + launch
        handle = subprocess.Popen(flaunch.split())    
        
        server.process = handle
        
        print(server.game + " @ port " + str(server.port) + "! PID: " + str(server.process.pid))
        
        self.servers.remove(oldserver)
        self.servers.append(server)
        
        if(password is not ""):
            gamepasswords[name] = password
        self.saveDBs()
        
        tmpstring = "Game " + name + " is up at " + OWNIP + ":" + str(server.port) + "!"
        if(password != ""):
            tmpstring = tmpstring + " REMEMBER YOUR GAME PASSWORD: " + password
        else:
            tmpstring = tmpstring + " No password was set."
        return(tmpstring)    
        
        
                
### ACTUAL BOT LOOP
class BlitzBot:


    def send(self, msg):
        print("-> " + msg)
        msg = msg + "\r\n"
        bitit = bytes(msg, "UTF-8")
        self.s.send(bitit)
  

    def sendwithoutspam(self, msg):
        msg = msg + "\r\n"
        bitit = bytes(msg, "UTF-8")
        self.s.send(bitit)
        
    def run(self):
        last_ping = time.time()
        readbuffer = ""
        joined = False
        gotping = False
        print("CONNECTING...")
        self.s = socket.socket( )
        self.s.connect((HOST, PORT))
        print("CONNECTED!")
         
        self.send("NICK %s" % NICK)
        self.send("USER %s %s bla :%s" % (IDENT, HOST, REALNAME))
        
        while 1:
            if(time.time() - last_ping > 180):
                print("PING TIMEOUT? RESTARTING BOT")
                break

            try:
                readbuffer = readbuffer+self.s.recv(1024).decode("UTF-8")
            except:
                try:
                    readbuffer = readbuffer+self.s.recv(1024).decode("ISO-8859-1")
                except:
                    print("ERROR ENCODING LINE!")
                        
            temp = str.split(readbuffer, "\n")
            readbuffer=temp.pop( )
            
            ## Need to wait until end of messages of the day thing in GameSurge.
            ## Other networks may need some other kind of trigger or removal of the
            ## trigger or whatever
            if(joined == False and gotping == True):
               self.send("JOIN %s" % CHANNEL)
               print("JOINED %s" % CHANNEL)
               joined = True
               
            for line in temp:
                line = str.rstrip(line)
                line = str.split(line)
         
                if(line[0] == "PING"):
                    self.sendwithoutspam("PONG %s" % line[1])
                    last_ping = time.time()
                    gotping = True
                if(line[1] == "NOTICE"):
                    size = len(line)
                    message = ""
                    i = 3
                    while(i < size): 
                       message += line[i] + " "
                       i = i + 1
                    message.lstrip(":")
                    


                if(line[1] == "PRIVMSG"):
                    ### Remove this and the bot answers non-private messages as well. 
                    if(not "#" in line[2]):
                        print("<- %s" % message)
                        sender = ""
                        for char in line[0]:
                            if(char == "!"):
                                break
                            if(char != ":"):
                                sender += char 
                        size = len(line)
                        i = 3
                        message = ""
                        while(i < size): 
                            message += line[i] + " "
                            i = i + 1
                        message.lstrip(":")
                        

                        if(isInDict(line[3].lstrip(":"), commands)):
                            truecommand = line[3].lstrip(":").lower()
                            commands[truecommand].main(self, sender, message )
                        else:
                            self.send("PRIVMSG %s :COMMAND %s NOT FOUND." % (sender, line[3].lstrip(":")))

                
                
                
      
      
## Actual main
servermanager = ServerManager()
bot = BlitzBot()

while(1):
    try:
        bot.run()
        print("BOT HAS BEEN SHUT DOWN! Retrying in 15 seconds.")
        time.sleep(15)      
    except socket.error:
        print("SOCKET EXCEPTION! Retrying in 15 seconds.")
        time.sleep(15)
  
