import bpy
import time
import os
import subprocess
import base64
import struct
from . import psy_export

class PsychopathRender(bpy.types.RenderEngine):
    bl_idname = 'PSYCHOPATH_RENDER'
    bl_label = "Psychopath"
    DELAY = 1.0

    @staticmethod
    def _locate_binary():
        addon_prefs = bpy.context.user_preferences.addons[__package__].preferences

        # Use the system preference if its set.
        psy_binary = addon_prefs.filepath_psychopath
        if psy_binary:
            if os.path.exists(psy_binary):
                return psy_binary
            else:
                print("User Preference to psychopath %r NOT FOUND, checking $PATH" % psy_binary)

        # search the path all os's
        psy_binary_default = "psychopath"

        os_path_ls = os.getenv("PATH").split(':') + [""]

        for dir_name in os_path_ls:
            psy_binary = os.path.join(dir_name, psy_binary_default)
            if os.path.exists(psy_binary):
                return psy_binary
        return ""

    def _start_psychopath(self, scene, psy_filepath, use_stdin, crop):
        psy_binary = PsychopathRender._locate_binary()
        if not psy_binary:
            print("Psychopath: could not execute psychopath, possibly Psychopath isn't installed")
            return False

        # Figure out command line options
        args = []
        if crop != None:
            args += ["--crop", str(crop[0]), str(self.size_y - crop[3]), str(crop[2] - 1), str(self.size_y - crop[1] - 1)]
        if use_stdin:
            args += ["--spb", str(scene.psychopath.max_samples_per_bucket), "--serialized_output", "--use_stdin"]
        else:
            args += ["--spb", str(scene.psychopath.max_samples_per_bucket), "--serialized_output", "-i", psy_filepath]

        # Start Rendering!
        try:
            self._process = subprocess.Popen([psy_binary] + args, bufsize=1, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
        except OSError:
            # TODO, report api
            print("Psychopath: could not execute '%s'" % psy_binary)
            import traceback
            traceback.print_exc()
            print ("***-DONE-***")
            return False

        return True

    def _draw_bucket(self, crop, bucket_info, pixels_encoded):
        if crop != None:
            x = bucket_info[0] - crop[0]
            y = self.size_y - bucket_info[3] - crop[1]
        else:
            x = bucket_info[0]
            y = self.size_y - bucket_info[3]
        width = bucket_info[2] - bucket_info[0]
        height = bucket_info[3] - bucket_info[1]

        # Decode pixel data
        pixels = [p for p in struct.iter_unpack("ffff", base64.b64decode(pixels_encoded))]
        pixels_flipped = []
        for i in range(height):
            n = height - i - 1
            pixels_flipped += pixels[n*width:(n+1)*width]

        # Write pixel data to render image
        result = self.begin_result(x, y, width, height)
        lay = result.layers[0].passes["Combined"]
        lay.rect = pixels_flipped
        self.end_result(result)

    def render(self, scene):
        self._process = None
        try:
            self._render(scene)
        except:
            if self._process != None:
                self._process.terminate()
            raise

    def _render(self, scene):
        # has to be called to update the frame on exporting animations
        scene.frame_set(scene.frame_current)

        export_path = scene.psychopath.export_path.strip()
        use_stdin = False

        r = scene.render
        # compute resolution
        self.size_x = int(r.resolution_x * r.resolution_percentage / 100)
        self.size_y = int(r.resolution_y * r.resolution_percentage / 100)

        # Calculate border cropping, if any.
        if scene.render.use_border == True:
            minx = r.resolution_x * scene.render.border_min_x * (r.resolution_percentage / 100)
            miny = r.resolution_y * scene.render.border_min_y * (r.resolution_percentage / 100)
            maxx = r.resolution_x * scene.render.border_max_x * (r.resolution_percentage / 100)
            maxy = r.resolution_y * scene.render.border_max_y * (r.resolution_percentage / 100)
            crop = (int(minx), int(miny), int(maxx), int(maxy))
        else:
            crop = None

        # Are we using an output file or standard in/out?
        if export_path != "":
            export_path += "_%d.psy" % scene.frame_current
        else:
            # We'll write directly to Psychopath's stdin
            use_stdin = True

        if use_stdin:
            # Start rendering
            if not self._start_psychopath(scene, export_path, use_stdin, crop):
                self.update_stats("", "Psychopath: Not found")
                return

            self.update_stats("", "Psychopath: Collecting...")
            # Export to Psychopath's stdin
            if not psy_export.PsychoExporter(self._process.stdin, self, scene).export_psy():
                # Render cancelled in the middle of exporting,
                # so just return.
                self._process.terminate()
                return
            self._process.stdin.write(bytes("__PSY_EOF__", "utf-8"))
            self._process.stdin.flush()
        else:
            # Export to file
            self.update_stats("", "Psychopath: Exporting data from Blender")
            with open(export_path, 'w+b') as f:
                if not psy_export.PsychoExporter(f, self, scene).export_psy():
                    # Render cancelled in the middle of exporting,
                    # so just return.
                    return

            # Start rendering
            self.update_stats("", "Psychopath: Rendering from %s" % export_path)
            if not self._start_psychopath(scene, export_path, use_stdin, crop):
                self.update_stats("", "Psychopath: Not found")
                return

        self.update_stats("", "Psychopath: Building")

        # If we can, make the render process's stdout non-blocking.  The
        # benefit of this is that canceling the render won't block waiting
        # for the next piece of input.
        try:
            import fcntl
            fd = self._process.stdout.fileno()
            fl = fcntl.fcntl(fd, fcntl.F_GETFL)
            fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
        except:
            print("NOTE: Can't make Psychopath's stdout non-blocking, so canceling renders may take a moment to respond.")

        # Process output from rendering process
        reached_first_bucket = False
        output = b""
        render_process_finished = False
        all_output_consumed = False
        while not (render_process_finished and all_output_consumed):
            if self._process.poll() != None:
                render_process_finished = True

            # Check for render cancel
            if self.test_break():
                self._process.terminate()
                break

            # Get render output from stdin
            tmp = self._process.stdout.read1(2**16)
            if len(tmp) == 0:
                time.sleep(0.0001) # Don't spin on the CPU
                if render_process_finished:
                    all_output_consumed = True
                continue
            output += tmp
            outputs = output.split(b'DIV\n')

            # Skip render process output until we hit the first bucket.
            # (The stuff before it is just informational printouts.)
            if not reached_first_bucket:
                if len(outputs) > 1:
                    reached_first_bucket = True
                    outputs = outputs[1:]
                else:
                    continue

            self.update_stats("", "Psychopath: Rendering")

            # Clear output buffer, since it's all in 'outputs' now.
            output = b""

            # Process buckets
            for bucket in outputs:
                if len(bucket) == 0:
                    continue

                if bucket[-11:] == b'BUCKET_END\n':
                    # Parse bucket text
                    contents = bucket.split(b'\n')
                    percentage = contents[0]
                    bucket_info = [int(i) for i in contents[1].split(b' ')]
                    pixels = contents[2]

                    # Draw the bucket
                    self._draw_bucket(crop, bucket_info, pixels)

                    # Update render progress bar
                    try:
                        progress = float(percentage[:-1])
                    except ValueError:
                        pass
                    finally:
                        self.update_progress(progress/100)
                else:
                    output += bucket

def register():
    bpy.utils.register_class(PsychopathRender)

def unregister():
    bpy.utils.unregister_class(PsychopathRender)
