# GNU GPL v3 or later
# ( C ) J.Y.Amihud ( blenderdumbass ) 2024

import os
import sys
import zlib
import json
import time
import threading
import subprocess

import Settings
from Common import *
from Multiplayer_Shared import *


from http.server import BaseHTTPRequestHandler, HTTPServer

class handler(BaseHTTPRequestHandler):
    
    def do_POST(self):

        data = self.Recieve()
        response = ParseRequest(data)
        self.Send(response)
        
    def Send(self, data):

        # Compressing
        data = json.dumps(data)
        data = data.encode("utf-8")
        data = zlib.compress(data)
        
        # Sending
        self.send_response(200)
        self.send_header("Content-type","application/zip")
        self.end_headers()
        self.wfile.write(data)

    def Recieve(self):

        length = int(self.headers.get('content-length'))
        data = self.rfile.read(length)

        data = zlib.decompress(data)
        data = json.loads(data)
        return data

    def log_message(self, format, *args):
        # I don't want any logs.
        return

def ParseRequest(data):

    resp = {}
    
    for key in data:

        payload = data[key]
        
        if "login" == key:
            resp[key] = Login(payload)

        
        elif "change-ownership" == key:
            resp[key] = ChangeOwnership(payload)
            
        elif "scene" == key:
            resp[key] = Scene(data)

        elif "vision" == key:
            pass

        else:
            print(key, payload)


    userId = data.get("login", {}).get("userId")
    room   = Safe(data.get("login", {}).get("room"))
    messages = LoadData("messages", {}, room)
    if messages.get(userId):
        resp["message"] = messages[userId].pop(0)
    SaveData("messages", messages, room)
            
    return resp

def Login(data):

    resp = {}
    newuser = False
    
    # Checking userId 
    if not data.get("userId"):
        resp["userId"] = RandomString()
        

    # Checking room
    if not data.get("room"):
        resp["room"] = "MainRoom"
    elif data.get("room") != Safe(data.get("room", "")):
        resp["room"] = Safe(data["room"])

    data["timestamp"] = time.time()
        
    # Storring users data.

    users = LoadData("users", {})

    if data.get("userId") and data.get("userId") not in users:
        newuser = True
    
    if data.get("userId"):
        users[data["userId"]] = data
        
    SaveData("users", users)

    if newuser:
        userId = data.get("userId")
        name   = data.get("username")
        room   = Safe(data.get("room"))
        NotifyOthers(userId, name+" joined game.", room)
        print(Format(userId), "JOINED GAME!")
        
    return resp

def ChangeOwnership(data):

    print(Format(data.get("userId")), "GRABBED", Format(data.get("netId")))
        
    objects = LoadData("objects", {})
    if data.get("netId") in objects:
        obj = objects[data.get("netId")]
        obj["ownerId"] = data.get("userId")

        SaveData("objects", objects)
        
        # Changing ownership in chunk
        addr = obj.get("chunk")
        room = Safe(obj.get("room"))
        chunk = LoadData(addr, {}, room)
        for o in chunk:
            if o.get("netId") == data.get("netId"):
                o["ownerId"] = data.get("userId")
        SaveData(addr, chunk, room)
        
        return True
    else:
        print(Format(data.get("netId")),"netId:",data.get("netId"), "NOT FOUND, WHILE CHANGING OWNERSHIP!")
        return False

    
    
def Scene(data):

    scene  = data["scene"]
    userId = data.get("login", {}).get("userId")
    room   = Safe(data.get("login", {}).get("room", "MainRoom"))

    # We are not updating scene if there is no userId yet.
    if not userId:
        return

    # Reading scene payload.

    resp = {}
    objects = LoadData("objects", {})
    
    for addr in scene:
        chunk = scene[addr]
       
        for obj in chunk:

            # Some people might want to cheat by inputing
            # values for other users.
            if obj.get("netId"):

                
                obj["ownerId"] = objects.get(obj.get("netId"), {}).get("ownerId")
                if not obj["ownerId"]:
                    obj["ownerId"] = userId
                
                if obj.get("ownerId") and obj.get("ownerId") != userId:
                    chunk.remove(obj)

                

                
    # Saving chunks data.
    
    for addr in scene:
        chunk = scene[addr]
        saved = LoadData(addr, [], room)
        add   = []
        
        # Cleaning up
        for obj in saved:
            if obj.get("netId") not in objects or objects[obj.get("netId")].get("chunk") != addr:
                saved.remove(obj)
                
        ids  = []        
        for obj in saved:
            ids.append(obj.get("netId"))

        for obj in chunk:
            
            # If object is Dani and it has no netId
            # we assume that somebody new joined the
            # room.

            if obj.get("name") == "Dani_Box" and not obj.get("netId"):
                obj["netId"]   = userId
                obj["ownerId"] = userId
                add.append(obj)
                

            elif not obj.get("netId"):
                obj["netId"] = RandomString()
                obj["ownerId"] = userId
                add.append(obj)

            if obj.get("netId") not in ids:
                saved.append(obj)
                
            else:
                saved[ids.index(obj.get("netId"))] = obj

            
            objects[obj["netId"]] = {"chunk":addr,
                                     "name":obj.get("name"),
                                     "timestamp":time.time(),
                                     "ownerId":obj.get("ownerId"),
                                     "room":room}
            
        SaveData(addr, saved, room)

        if addr in data.get("vision", []):
            resp[addr] = []
            for obj in saved:
                if obj.get("ownerId") != userId:
                    resp[addr].append(obj)
            for obj in add:
                resp[addr].append(obj)

            
    SaveData("objects", objects)

        

    return resp

def Notify(userId, message, room):

    messages = LoadData("messages", {}, room)
    if userId not in messages:
        messages[userId] = []
    messages[userId].append(message)
    SaveData("messages", messages, room)

def NotifyOthers(userId, message, room):

    users = LoadData("users", {})
    for user in users:
        if user != userId and users[user].get("room") == room:
            Notify(user, message, room)
    
def SaveData(name, data, room=None):

    # Making folder for server stuff
    folder = Settings.get_settings_folder()+"/server/"
    if room: folder = folder+str(room)+"/"
    try:    os.makedirs(folder)
    except: pass

    # Saving the json
    with open(folder+str(name)+".json", "w") as save:
        json.dump(data, save, indent=4)

def LoadData(name, otherwise=None, room=None):

    try:
        folder = Settings.get_settings_folder()+"/server/"
        if room: folder = folder+str(room)+"/"
        with open(folder+str(name)+".json") as o:
            return json.load(o)
    except: return otherwise
    
def Safe(string):

    # This function stripts strings from any unsafe
    # characters.

    good = "1234567890qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM_"
    new  = ""
    for i in string:
        if i in good: new = new + i
        else:         new = new + "_"

    return new

def Format(netId, room=True):

    objects = LoadData("objects",{})
    users   = LoadData("users",{})

    if netId in users:
        name  = users[netId].get("username", "Unknown")
        room  = users[netId].get("room", "Unknown Room")
        ownerId = netId
        owner = ""
    else:
        name  = objects.get(netId, {}).get("name", "Unknown Object")
        room  = objects.get(netId, {}).get("room", "Unknown Room")
        ownerId = objects.get(netId, {}).get("ownerId")
        owner = users.get(ownerId, {}).get("username", "Unknown")

    string = IDcolor(room)+" "+room+" "+IDcolor(netId)+" "+netId[-4:]+" "+name
    if owner and ownerId:
        string = string + " " + IDcolor(ownerId) + " by "+ownerId[-4:]+" " + owner
    string = string + " " + clr["norm"]
        
    return string



###### RUNNING THE SERVER #####

def ArgumentsHandler():

    port = 6969
    ipv6 = "-4"
    
    if "--help" in sys.argv or "-h" in sys.argv:
        print("""
This is the Multiplayer server for Dani's Race.
It is technically an HTTP server that handles
POST requests send by the game. Those requests
are in a JSON format compressed with python's
ZLIB library. There is no webpage, so trying to
access it with a browser will probably spit out
an error.

"""+clr["bold"]+clr["tdyl"]+"""RUN THIS SERVER FROM THE "Scripts" FOLDER!"""+clr["norm"]+"""

--help , -h     : This Help Text.
--port """+clr["tdyl"]+"""<number>"""+clr["norm"]+""" : Sets up a port on which the
                  server will listen.
--ipv6 , -6     : Use IPv6 connection for global network.

""")
        exit()

    if "--port" in sys.argv:
        try:
            port = int(sys.argv[ sys.argv.index("--port")+1 ])
        except:
            print("Didn't specify port number.\nExample ( for port 8080 ): $ python3 Multiplayer_Server.py --port 8080")
            exit()

    if "--ipv6" in sys.argv or "-6" in sys.argv:
        ipv6 = "-6"

    return port, ipv6
    
PORT, IPV6 = ArgumentsHandler()

print(clr["bold"]+"Dani's Race Multiplayer Server!"+clr["norm"])

###### CLEANUPS #####

def CleanUps():

    while True:

        time.sleep(3)
        
        # Cleaning up users
        
        users = LoadData("users", {})
        delete = []
        for userId in users:
            user = users[userId]
            room   = user.get("room")
            name   = user.get("username")

            # If user missing for 10 seconds, he is gone.
            if user.get("timestamp", 0) < time.time() -10:

                NotifyOthers(userId, name+" left the game.", room)
                print(Format(userId), "LEFT GAME!")
                delete.append(userId)

        for i in delete:
            del users[i]

        SaveData("users", users)

        objects = LoadData("objects", {})
        delete = []
        for netId in objects:
            obj = objects[netId]

            # Deleting objects after 5 missing seconds
            if obj.get("timestamp", 0) < time.time() -5:
                delete.append(netId)

        for i in delete:
            del objects[i]

        SaveData("objects", objects)
        

cleanups = threading.Thread(target=CleanUps)
cleanups.daemon = True
cleanups.start()
print("Started cleanups thread.")


print(clr["bold"]+"Starting server:", clr["norm"])

try:
    IP = subprocess.check_output(["hostname", "-I"]).decode("utf-8").split(" ")[0]
except:
    try:
        # Some versions of hostname just have the -i option that acts like -I in
        # other versions.
        IP = subprocess.check_output(["hostname", "-i"]).decode("utf-8").split(" ")[0]
    except:
        IP = clr["tdrd"]+"127.0.0.1"+clr["norm"]
    
if not IP or IP == "\n":
    IP = clr["tdrd"]+"127.0.0.1"+clr["norm"]
print("           Local IP:", clr["bold"],  IP, clr["norm"])

try:
    GLOBALIP = subprocess.check_output(["curl", "-s", IPV6, "ifconfig.co"]).decode("utf-8")[:-1]
    print("          Global IP:", clr["bold"],  GLOBALIP, clr["norm"])
except:
    GLOBALIP = None
    print("          Global IP:",clr["tdrd"]+clr["bold"], "No connection to Global Network.", clr["norm"])

print("               Port:", clr["bold"],  PORT, clr["norm"])
print("     Local Hostname:", clr["bold"],  "http://"+IP+":"+str(PORT), clr["norm"])
if GLOBALIP:
    if IPV6 == "-4":
        print("    Global Hostname:", clr["bold"],  "http://"+GLOBALIP+":"+str(PORT), clr["norm"])
    else:
        print("    Global Hostname:", clr["bold"],  "http://["+GLOBALIP+"]:"+str(PORT), clr["norm"])

print()
serve = HTTPServer(("", PORT), handler)
try:
    serve.serve_forever()
except KeyboardInterrupt:
    print()
    print("Server Exited!")