# THIS FILE IS A PART OF VCStudio
# PYTHON 3

###############################################################################

# In order for Multiuser to function. VCStudio should have a process on the 
# background can access to all the data, that will talk to the Multiuser
# server.

###############################################################################


##################### IMPLEMENTED FEATURES LIST ###############################

# [V] List of users     |   Important to know the usernames of all users
# [V] Assets            |   Important to have up to date assets everywhere
# [ ] Shots             |   Important to have up to date shots / and their assets
# [ ] Rendering         |   Ability to render on a separate machine.
# [ ] Story             |   Real time Sync of the story edito and the script writer
# [ ] Analytics         |   Real time Sync of History and Schedules.
# [ ] Messages          |   A little messaging system in the Multiuser window.

###############################################################################

import os
import sys
import time
import json
import socket
import random
import hashlib
import datetime
import threading
import subprocess

from UI import UI_elements
from settings import talk
from network import insure

time_format = "%Y/%m/%d-%H:%M:%S"

def client(win):
    
    ###########################################################################
    
    # This function is the thing that we need. It's going to run in it's own
    # thread. Separatelly from the rest of the program.
    
    ###########################################################################
   
    while True:
        try:
            # So the first thing that we want to do is to listen for a server
            # broadcasting himself. And when found connect to that server.

            connect(win)
            
            # Then it's going to ask us that project are we in and our username.
            
            server = win.multiuser["server"]
            
            # Then as soon as we are connected. We want to start the main loop. I know
            # that some stuff should be done early. But I guess it's better to do them
            # in the main loop. So basically the server will request a bunch of stuff. 
            # And when it sends no valid request. Or something like KEEP ALIVE then it's
            # a turn for as to send requests to server. Or nothing. The communication 
            # happens all the time.
            
            while win.multiuser["server"]:
            
                request = insure.recv(server)
                
                if request == "yours":
                    
                    ############################################################    
                    #                   WE REQUEST FROM SERVER                 #
                    ############################################################
                    
                    if win.multiuser["curs"]:
                        
                        # The get asset function
                        
                        get(win, list(win.multiuser["curs"].keys())[0], 
                            win.multiuser["curs"][list(win.multiuser["curs"].keys())[0]])
                        
                        try:
                            del win.multiuser["curs"][list(win.multiuser["curs"].keys())[0]]
                        except:
                            pass
                    
                    elif win.multiuser["request"]:
                        insure.send(server, win.multiuser["request"])
                        win.multiuser["request"] = []
                    
                    
                    elif not win.multiuser["users"]:
                        insure.send(server, "users")
                        win.multiuser["users"] = insure.recv(server)
                    
                    elif win.cur and win.cur != win.multiuser["last_request"]:
                        
                        win.multiuser["last_request"] = win.cur
                        
                        # If we are currently at some asset or shot. We want to
                        # send to the server the current state of the folder
                        # if such exists. So if we have the latest one. Every
                        # body else could send us a give request. But if somebody
                        # has the latest and it's not us. We could send them
                        # the get request. 
                        
                        # First step will be to check whitch cur are we in.
                        if win.url == "assets":
                            t = "/dev"
                        elif win.url == "script":
                            t = "/rnd"
                        else:
                            t = ""
                        
                        # Then we need to see if there is a folder to begin 
                        # with. Some scenes and some shots have no folder.
                        
                        if not os.path.exists(win.project+t+win.cur):
                            insure.send(server, "yours")
                        else:
                            
                            # If there is a folder let's get the timestamp.
                            timestamp = getfoldertime(win.project+t+win.cur)
                        
                            insure.send(server, ["at", t+win.cur, timestamp])
                    
                    else:
                        
                        insure.send(server, "yours")
                        
                        if win.url not in ["assets", "script", "analytics"]:
                            win.multiuser["last_request"] = ""
                            win.cur = ""
                else:   
                    
                    ############################################################    
                    #                  SERVER REQUESTS FROM US                 #
                    ############################################################
                    
                    
                    if request == "story":
                        
                        if not win.multiuser["story_check"]:
                            storytime = "1997/07/30-00:00:00"
                            win.multiuser["story_check"] = True
                        else:
                            storytime = gettime(win.project+"/pln/story.vcss")
                        
                        selectedtmp = win.story["selected"]
                        tmppointers = win.story["pointers"]
                        tmpcamera   = win.story["camera"]
                        
                        insure.send(server, [win.story, storytime])
                        
                        story = insure.recv(server)
                        
                        win.story = story[1].copy()
                        win.story["selected"] = selectedtmp
                        win.story["camera"]   = tmpcamera
                        win.story["pointers"] = tmppointers
                        
                        if story[0] in win.multiuser["users"]:
                            win.multiuser["users"][story[0]]["camera"] = story[1]["camera"]
                        

                    
                    elif request == "users":
                        win.multiuser["users"] = {}
                        
                        
                    elif request == "analytics":
                        if not win.multiuser["analytics_check"]:
                            analyticstime = "1997/07/30-00:00:00"
                            win.multiuser["analytics_check"] = True
                        else:
                            analyticstime = datetime.datetime.strftime(datetime.datetime.now(), time_format)
                        insure.send(server, [win.analytics, analyticstime])
                        
                        win.analytics = insure.recv(server)

                    elif request == "assets":
                        
                        assets = list_all_assets(win)
                        insure.send(server, assets)
                        new_assets = insure.recv(server)
                        
                        
                        for asset in new_assets:
                            
                            if asset not in assets or assets[asset][1] < new_assets[asset][1]:
                                
                                # If a given asset is not on our system or
                                # being updated on another system. We want to
                                # call for get() function. To get the new
                                # and up to date version of the asset. And if
                                # doesn't exist get the asset.
                                
                                # But it's better to do one by one.
                                try:
                                    win.multiuser["curs"]["/dev"+asset] = new_assets[asset][0]
                                except:
                                    pass
                        
                                
                    elif request[0] == "give":
                        give(win, request[1], server)            
                    
                    elif request[1] == "at":
                        print(request)
                        try:
                            # If we need an update let's update
                            if getfoldertime(win.project+request[2]) < request[3]:
                                win.multiuser["curs"][request[2]] = request[0]
                            # Else tell the world that we are the newest ones
                            elif getfoldertime(win.project+request[2]) > request[3]:
                                insure.send(server, ["at", request[2], getfoldertime(win.project+request[2])])
                                insure.recv(server)
                        except:
                            pass
                    
                    elif request[0] == "messages":
                        win.multiuser["messages"] = request[1]
                        win.multiuser["unread"] += 1 
                        win.scroll["multiuser_messages"] = 0-500*len(request[1])
                    
                    
                    insure.send(server, "yours")
                    
        except Exception as e:
            #raise()
            win.multiuser["server"] = False
            print("Connection Multiuser Error | "+str(e)+" | Line: "+str(sys.exc_info()[-1].tb_lineno))

def getfoldertime(path):
    
    # I noticed a problem with getting a last modification time for a directory.
    # It is not nearly the same time as the contents inside that directory. And
    # when for example I have /dev/chr/Moria folder. And I want to check is this
    # asset is newer here or somebody has a more up to date version. It checks
    # for only the time of the folder /dev/chr/Moria and not the contents of
    # that folder. Which is not cool. I want to get the newest time from all the
    # files and folders inside it. Including in the case of assets the 
    # /ast/chr/Moria.blend files. So this is why you see this function here.
    
    
    
    if os.path.isdir(path):
        
        # So basically we are doing it only if it's actually a directory. In
        # case there is an error. I didn't check. But who knows.
        
        # We need to get the full list of subdirectories and files. 
        
        allstuff = []
        
        if "/dev/" in path and os.path.exists(path[:path.find("/dev")]+"/ast"+path[path.find("/dev")+4:]+".blend"):
            allstuff.append(gettime(path[:path.find("/dev")]+"/ast"+path[path.find("/dev")+4:]+".blend"))
        
        for i in os.walk(path):
            if i[-1]:
                for b in i[-1]:
                    allstuff.append(gettime(i[0]+"/"+b))
            else:
                allstuff.append(gettime(i[0]))
        
        # Now let's find the biggest one
        biggest = allstuff[0]
        for i in allstuff:
            if i > biggest:
                biggest = i
        
        return biggest
        
    else:
        return gettime(path)

def gettime(path):
    
    # This function will get a pretty time for a given path. The last change
    # to the file or the folder recorded by the os.
    time_format = "%Y/%m/%d-%H:%M:%S"
    timestamp = os.path.getmtime(path)
    timestamp = datetime.datetime.fromtimestamp(timestamp)
    timestamp = datetime.datetime.strftime(timestamp, time_format)
    return timestamp
    
def connect(win):
    
    # This is going to be a function that connects to the server and makes a
    # handshake
    
    while not win.multiuser["server"]:
        
        # So the first step will be to listen for multiuser. 
        
        data = ""
        
        try:
            sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 
            sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            sock.bind(("255.255.255.255", 54545))
            
            
            data, addr = sock.recvfrom(1024)
            data = data.decode('utf8')

            sock.close()
            
        except:
            pass
        
        # If any data revieved. It's not nessesarily the server. So let's read it
        
        if data.startswith("VCStudio MULTIUSER SERVER"):
            try:
                data, ip, project = data.split(" | ")
            except:
                continue
            
            # So now we know the ip of the server. And the name of a project
            # that it's hosting. We need to check that our name is the same
            # and if yes. Connect to the server.
            
            if win.analytics["name"] == project:
                
                server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                server.connect((ip, 64646))
                win.multiuser["server"] = server
                win.multiuser["userid"] = str(server.getsockname()[0])+":"+str(server.getsockname()[1])
                
                insure.recv(server)
                insure.send(server, win.settings["Username"])
                insure.recv(server)
                insure.send(server, win.analytics["name"])
                
                print("Connected to Multiuser as: "+win.multiuser["userid"])
                
                # Adiing myself into the list of all users
                win.multiuser["users"][win.multiuser["userid"]] = {"username":win.settings["Username"], "camera":[0,0]}

def hash_file(f):
    try:
        BLOCKSIZE = 65536
        hasher = hashlib.md5()
        with open(f, 'rb') as afile:
            buf = afile.read(BLOCKSIZE)
            while len(buf) > 0:
                hasher.update(buf)
                buf = afile.read(BLOCKSIZE)
        return str(hasher.hexdigest())
    except:
        return "FOLDER"

        
def list_all_assets(win):
    
    # This function is listing all the asset CURs in the project. Not the shots
    # only the assets. Since we want to have what assets are being created. For
    # shots. It's done using the story editor file. So we don't really need it.
    
    allcurs = {}
    
    for c in ["chr", "veh", "loc", "obj"]:
        
        for i in os.listdir(win.project+"/dev/"+c):
            allcurs["/"+c+"/"+i] = [
                win.multiuser["userid"], 
                getfoldertime(win.project+"/dev/"+c+"/"+i)
                    ]
    
    return allcurs

def get_give_folder_list(project, folder):
    
    # This function will prepare our folder list for get and giv functions.
    
    path = folder
    astblend = path[:path.find("/dev")]+"/ast"+path[path.find("/dev")+4:]+".blend"
    
    fs = []
    
    if os.path.exists(project+astblend):
        fs.append([astblend, hash_file(project+astblend)])
    
    # There might not even be any folder as far as this function concerned
    # there is nothing in it if there is no folder. 
    
    try:
        for f in os.walk(project+folder):
            
            # If files in the folder
            if f[2]:
                for i in f[2]:  
                    fs.append([f[0].replace(project, "")+"/"+i, hash_file(f[0]+"/"+i)])
            # Else just put the folder in
            else:
                fs.append([f[0].replace(project, ""), "FOLDER"])
    
    except Exception as e:
        print("get_give_folder_list(): "+str(e))    
    
    return fs

def get(win, folder, userid):    
    
    # This function will get any folder from any other user using the connection
    # to the server.
    
    print("Trying to get: [", folder, "] From: [",  userid, "]")
    
    server = win.multiuser["server"]
    
    insure.send(server, ["get", userid, folder])
    
    # Next we will recieve the list of file / folders in the directory
    # we are looking for. With the MD5 hash of each. We do not want to 
    # download files that are identical between both machines.
    
    available = insure.recv(server)
    current   = get_give_folder_list(win.project, folder)
    getlist   = []
    
    # Now we need to compare between them to get a list of files we want.
    # Also at this stange we can already make the folders
    
    for f in available:
        if f not in current:
            if f[1] == "FOLDER":
                
                # If it's a folder there is nothing I need to download, we
                # already have the name. So let's just make it.
                try:
                    os.makedirs(win.project+f[0])
                except:
                    pass
                
            else:
                
                # If it's not a folder. And it does not exist. Let's actually
                # get it.
                
                getlist.append(f[0])
    
    # Now we want to send to that other user the "getlist" so he would know.
    # which files to send back to us. This is a bit harder communication then
    # just sending a big object contaning all of the files in it. Because I'm
    # using Json for the complex data structures. It's just not going to be cool
    # because at best it will convert the bytes of the files into strings of
    # text. Which are like 4 to 8 times larger in sizes. And at worst it will
    # fail complitelly. So what I will do is ask the files one by one. The
    # insure script knows how to deal with bytes objects so we are fine.
    
    insure.send(server, getlist)
    
    # Now let's just recieve the files and save them.
    for f in getlist:
        
        # We also want the folder to be make just in case.
        try:
            os.makedirs(win.project+f[:f.rfind("/")])
        except:
            pass
    
        data = open(win.project+f, "wb")
        data.write(insure.recv(server))
        data.close()
        
        insure.send(server, "saved")
        
    # Refrashing peviews for the images and stuff
    win.checklists = {}
    UI_elements.reload_images(win)
    
def give(win, folder, server):
    
    # This function will send to the server any folder that other users might
    # request.
    
    print("Someone wants: [", folder, "]")
    
    # We are going to send the list of files and folder and their hash values
    # to the other client. So it could choose what files does it wants. Not 
    # all files will be changed. So there is no need to copy the entire folder.
    
    insure.send(server, get_give_folder_list(win.project, folder))
    
    
    # The other user will select the files that he needs. Based on what's
    # changed. And will send us the short version of the same list.
    
    getlist = insure.recv(server)
    
    # Now let's send the actuall binaries of the files.
    
    for f in getlist:
        print("sending file:", f)
        data = open(win.project+f, "rb")
        data = data.read()
        insure.send(server, data)
        
        insure.recv(server)
    
