# THIS FILE IS A PART OF VCStudio
# PYTHON 3

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

# This file will handle rendering on the background. For more info about how
# it all works. See:
#                   network/network_renders.py

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

import os
import sys
import json
import time
import signal
import socket
import datetime
from subprocess import *

# Following 2 functions are copied from the code of Blender-Organizer. How cool
# that python2 and python3 can share so much code.

def getnumstr(num):
    
    # This function turns numbers like 1 or 20 into numbers like 0001 or 0020
    
    s = ""
    for i in range(4-len(str(num))):
        s = s + "0"
    
    return s+str(num)

def getfileoutput(num, FORMAT):
    
    # Function gives an output of a file. From the current frame that's rendering.
    # instead of having frame 1 and format EXR it will give you 0001.exr
    
    s = getnumstr(num)
    
    if FORMAT == "JPEG":
        s = s + ".jpg"
    else:
        s = s + "." + FORMAT.lower()
        
    return s

def output(string):
    
    # This function will act similar to python's print. But rather then just
    # printing the string to the terminal. It will also send a signal to the
    # VCStudio. And anyone who listening. 
    
    string = str(string)
    
    print(string)
    
    # for i in range(500):    
    #     cs1 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    #     cs1.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    #     cs1.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
    #     cs1.sendto(bytes(string, 'utf-8'), ('127.0.0.1', 54545))

# Now to make it all work the script that will lanuch this script will give it
# an agrument of a project location. Which we might need to use for rendering

project = ""
blender = ""

if len(sys.argv) > 1:
    project = sys.argv[1]
    blender = sys.argv[2]

if not project or not blender:
    exit()

output(project)
output(blender)

# Now that we have the project. Let's get data about the current renders that 
# are setup for rendering.

def get_active_renders():
    
    # This tiny function will gives us the list of current files set to render
    # at any moment these will be needed.    
        
    active_renders = []
    try:
        active_renders = open(project+"/set/active_renders.data")
        active_renders = active_renders.read()
        active_renders = active_renders.split("\n")
    except:
        pass

    return active_renders

def remove_active_render(render):
    
    # This function will edit the active renders and remove the current
    # render from the list.
    
    active_renders = open(project+"/set/active_renders.data")
    active_renders = active_renders.read()
    active_renders = active_renders.split("\n")
    
    s = open(project+"/set/active_renders.data", "w")
    
    for i in active_renders:
        if i != render and i:
            s.write(i+"\n")
    
    s.close()

active_renders = get_active_renders()

# Now I think we also need to get the data from the files. So to speak read their json
# render settings to be able to know what are our start and end frames, format and such.

def read_settings(filename):
    
    # This function will read data from the various render settings.
    
    folder = filename[:filename.rfind("/")]+"/extra"
    savefile = folder+filename[filename.rfind("/"):]+".json"
    
    data = {}
    try:
        with open(project+savefile) as json_file: 
            data = json.load(json_file) 
    except Exception as e:
        output(e)
    
    return data

def get_runtime_data(project):

    # This will read a specific .json file from the system.

    template = {"to_render":True,
                "current_progress":"Fra: 69 | Mem: 420 |Sample: 69/420"}
    try:
        with open(project+"/render_runtime.json") as json_file: 
            data = json.load(json_file)
    except:
        data = template.copy()

    return data

def save_runtime_data(data, project):
    with open(project+"/render_runtime.json", 'w') as fp:
        json.dump(data, fp, indent=4)


runtime = get_runtime_data(project)
        
# Now let's start the main loop. I'm pretty sure that there will be tons of bugs
# at this stage. So please look at the following code carefully. 

# What I want to do is always read the first line and render it. By the end
# delete the first line from the render list. Remove it from a file. Tho the
# removal could happen at any time anywhere. A user could remove the line
# manually. Or delete the file from the renders in the VCStudio. It doesn't
# matter. This file should be abborted and the next one should start rendering
# to do this. I need to write some clever algorithm.

while True:
    
    # I know wild. A while with a True. OMG. But I guess it's the only way to
    # insure that it will keep working if there are any current stuff in the
    # list. And will stop if there is absolutelly nothing in the list.
    
    to_break = True # This is our break thing
    
    active_renders = get_active_renders() # And here we update the current list
    
    for render in active_renders:
        
        if render:
            
            # If anything is found. Then we don't break. And will check again
            # on the next go around. This will be deleted from the file by
            # the end of rendering this file.
            
            to_break = False
            
            output(render)
            data = read_settings(render)
            output(data)
            
            # Before we start rendering let's do a couple of things. Mainly
            # create the folders and clean them if user so desires. 
            
            folder = render[:render.rfind("/")]
            
            try:
               
                os.mkdir(project+folder+"/"+data["save_folder"])
            except:
                pass
            
            try:
                if data["clean_folder"]:
                    
                    # Okay so if user so desires. We want to wipe the folder clean.
                    # and so here is the code.
                    
                    for filename in os.listdir(project+folder+"/"+data["save_folder"]):
                        os.remove(project+folder+"/"+data["save_folder"]+"/"+filename)
            except:
                pass
        
            # So we have our data. Let's do the rendering. But now so fast. We need
            # a brand new while loop here. I know wild. But I need to make sure that
            # every frame will be rendered regardless of whether it's crashed ot not.
            
            # So I will look for all frames currently in the folder. And render the
            # first one missing. Always. 
            
            while True:
                to_break2 = True
                
                # We will need to find th
                
                for frame in range(data["start_frame"], data["end_frame"]+1):
                    
                    # I think it's fair to say that some changes to the 
                    
                    cframe = getfileoutput(frame, data["image_format"] )
                    
                    if cframe not in os.listdir(project+folder+"/"+data["save_folder"]):
                        
                        # Basically now we found a missing frame. It could be
                        # what ever. Where-ever. Usually it's every next frame
                        # but the user might delete a frame in the middle. And 
                        # will be a missing frame.
                        
                        output(cframe)
                        to_break2 = False

                        # Starting Render
                        runtime = get_runtime_data(project)
                        runtime["started_rendering"] = time.time()
                        runtime["current_frame"] = frame
                        save_runtime_data(runtime, project)

                        
                        # Now that we found it I think we car actually render it
                        
                        progress = Popen(['stdbuf', '-o0', blender, "-b",
                             project+render, "-o",
                             project+folder+"/"+data["save_folder"]+"/####", "-F",
                             data["image_format"] ,"-f",
                             str(frame)], stdout=PIPE, universal_newlines=True)
                        
                        # Now while the render is not finished. We are going to
                        # outout everything it saying to the outside.
                        
                        # But before we start I want to start counting time. So
                        # we would have accurate analytics of the renders.
                        
                        stf = datetime.datetime.now()
                        
                        line = progress.stdout.readline()[:-1]
                        
                        while line:
                            output("VCStudio : RENDERING : "+render+" : "+line)
                            
                            
                            # Now at this stage i want it to also listen to a
                            # command to stop. I might want my CPU back at any
                            # moment. So let's do this.
                            
                            # UDP_IP = "127.0.0.1"
                            # UDP_PORT = 54545
                            
                            # try:
                            #     sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 
                            #     sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
                            #     sock.bind((UDP_IP, UDP_PORT))
                            #     sock.settimeout(0.05)
                            #     input_line, addr = sock.recvfrom(1024)
                            #     input_line = input_line.decode('utf8')
                            #     sock.close()
                                
                            #     if input_line == "VCStudio : RENDER STOP":
                            #         progress.kill()
                            #         output("VCStudio : CLOSED")
                            #         to_break2 = True
                            #         to_break = True
                            #         exit()
                            # except Exception as e:
                            #     pass

                            runtime = get_runtime_data(project)

                            if not runtime.get("to_render"):
                                try:
                                    #progress.kill()
                                    
                                    output("VCStudio : CLOSED")
                                    to_break2 = True
                                    to_break = True

                                    os.killpg(os.getpgid(progress.pid), signal.SIGKILL)

                                    exit()
                                except Exception as e:
                                    runtime["error"] = str(e)
                                    save_runtime_data(runtime, project)
                                    
                            runtime["current_progress"] = line
                            runtime["current_file"] = render
                            runtime["running"] = int(time.time())
                            runtime["blender_id"] = int(progress.pid)
                            
                            save_runtime_data(runtime, project)
                            
                            line = progress.stdout.readline()[:-1]
                        
                        # Now that the rendering of the frame is finished. I would
                        # like to save the analytics data. We are going to use
                        # microseconds here. 
                        
                        fif = datetime.datetime.now()
                        mil  = fif - stf
                        s = int(mil.seconds)
                        m = int(mil.microseconds)
                        
                        thetime = (s*1000000)+m
                        
                        # Now I want to record the analytics per folder. So 
                        # data from test renders would not be mangled together
                        # with data from final renders and so on.
                        
                        if data["save_folder"] not in data["analytics"]:
                            data["analytics"][data["save_folder"]] = {}
                        
                        data["analytics"][data["save_folder"]][str(frame)] = thetime
                        
                        # And we want to save the file with the analitycs in them
                        
                        thefolderis = render[:render.rfind("/")]+"/extra"
                        savefile = thefolderis+render[render.rfind("/"):]+".json"
                        
                        
                        with open(project+savefile, 'w') as fp:
                            json.dump(data, fp, indent=4)
                            
                        
                if to_break2:
                    break
            
            remove_active_render(render)
            
            break
            
    if to_break:
        break
