# ====================== BEGIN GPL LICENSE BLOCK ======================
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License
#  as published by the Free Software Foundation; either version 2
#  of the License, or (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software Foundation,
#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ======================= END GPL LICENSE BLOCK ========================

# <pep8 compliant>

# ########################################
# Render Cube Map
#
# Dalai Felinto
# --
# blendernetwork.org/dalai-felinto
# www.dalaifelinto.com
#
# Original code:
# Rio de Janeiro, September 2015
#
# Latest update:
# Rio de Janeiro, July 2016
# ########################################

import bpy

from bpy.app.handlers import persistent

from bpy.types import (
        Operator,
        Panel,
        )

from bpy.props import (
        BoolProperty,
        )

bl_info = {
    "name": "Cube Map",
    "author": "Dalai Felinto",
    "version": (1, 0),
    "blender": (2, 80, 0),
    "location": "Render Panel",
    "description": "",
    "warning": "Needs Updating",
    "doc_url": "https://github.com/dfelinto/render_cube_map",
    "tracker_url": "",
    "category": "Render"}


# ############################################################
# Global Check
# ############################################################

def do_run(cube_map, use_force):
    if not (cube_map.use_cube_map or use_force):
        return False

    if cube_map.is_enabled and not use_force:
        return False

    return True


# ############################################################
# Callbacks
# ############################################################

class NodeTree:
    def __init__(self, scene):
        self._use_nodes = scene.use_nodes
        self._use_compositing = scene.render.use_compositing

        self._nodes_mute = {}
        self._scene = scene

        self._scene.render.use_compositing = True

        if not self._use_nodes:
            scene.use_nodes = True
            self._muteNodes()
        else:
            self._storeNodes()
            self._muteNodes()

    def _storeNodes(self):
        """
        store the existent nodes and if they are muted
        """
        nodes = self._scene.node_tree.nodes
        for node in nodes:
            self._nodes_mute[hash(node)] = node.mute

    def _muteNodes(self):
        """
        mute all the existent nodes
        """
        nodes = self._scene.node_tree.nodes
        for node in nodes:
            node.mute = True

    def cleanupScene(self):
        """
        remove all the new nodes, and unmute original ones
        """
        scene = self._scene
        scene.use_nodes = self._use_nodes
        scene.render.use_compositing = self._use_compositing

        self._cleanNodes()
        self._unMuteNodes()

    def _cleanNodes(self):
        """
        remove all the nodes created temporarily
        """
        nodes = self._scene.node_tree.nodes
        to_del = []
        keys = self._nodes_mute.keys()

        for node in nodes:
            if hash(node) not in keys:
                to_del.append(node)

        for node in to_del:
            nodes.remove(node)

    def _unMuteNodes(self):
        """
        unmute all the existent nodes
        """
        nodes = self._scene.node_tree.nodes
        for node in nodes:
            node.mute = self._nodes_mute[hash(node)]


class View:
    def __init__(self, name, euler_rotation):
        self._name = name
        self._collection = None
        self._scene = None
        self._scene_camera = None
        self._node = None
        self._camera = None
        self._euler_rotation = euler_rotation

    def setScene(self, scene):
        scene.name = self._name
        self._scene = scene

        scene.cube_map.use_cube_map = False
        scene.render.use_compositing = False

        self._setFilepath()

    def _setFilepath(self):
        import os

        filepath = self._scene.render.filepath

        dirname = os.path.dirname(filepath)
        basename = os.path.basename(filepath)

        path = os.path.join(dirname, "{0}{1}".format(self._name, basename))
        self._scene.render.filepath = path

    def setNode(self, node, links, node_output):
        node.name = self._name
        node.label = self._name
        node.scene = self._scene
        self._node = node

        # TODO if there were nodetrees, duplicate them here

        # connect to output
        _input = node_output.layer_slots.new(self._name)
        links.new(node.outputs[0], _input)

    def setCamera(self, data, loc, zed):
        self._scene_camera = self._scene.camera

        self._camera = bpy.data.objects.new(self._name, data)
        self._collection.objects.link(self._camera)

        rotation = self._euler_rotation.copy()
        rotation.z += zed

        self._camera.rotation_euler = rotation
        self._camera.location = loc

        # change scene camera
        self._scene.camera = self._camera

    def resetCamera(self):
        self._collection.objects.unlink(self._camera)
        bpy.data.objects.remove(self._camera)
        self._camera = None

    @property
    def scene(self):
        return self._scene

    @property
    def name(self):
        return self._name


@persistent
def cube_map_render_init(scene, use_force=False):
    """
    setup the cube map settings for all the render frames
    """
    from mathutils import Euler
    from math import pi
    half_pi = pi * 0.5

    cube_map = scene.cube_map

    if not do_run(cube_map, use_force):
        return

    main_scene = scene
    hashes = [hash(scene) for scene in bpy.data.scenes]

    views_raw = (
            (
                'NORTH_',
                Euler((half_pi, 0.0,  0.0)),
                cube_map.use_view_north,
                ),
            (
                'SOUTH_',
                Euler((half_pi, 0.0, pi)),
                cube_map.use_view_south,
                ),
            (
                'WEST_',
                Euler((half_pi, 0.0, half_pi)),
                cube_map.use_view_west,
                ),
            (
                'EAST_',
                Euler((half_pi, 0.0, -half_pi)),
                cube_map.use_view_east,
                ),
            (
                'ZENITH_',
                Euler((pi, 0.0, 0.0)),
                cube_map.use_view_zenith,
                ),
            (
                'NADIR_',
                Euler((0.0, 0.0, 0.0)),
                cube_map.use_view_nadir,
                ),
            )

    views = [
            View(name, euler) for (name, euler, use) in views_raw
            if use or not cube_map.is_advanced]

    for view in views:
        # create a scene per view
        # XXX : line below crashes Blender 2.80
        bpy.ops.scene.new(type='LINK_COPY')
        scene = [
                scene for scene in bpy.data.scenes if
                hash(scene) not in hashes][0]

        # mark the scene to remove it afterwards
        scene.cube_map.is_temporary = True

        hashes.append(hash(scene))
        view.setScene(scene)
        # have Dalai to look at this?
        view._collection = bpy.context.collection  # XXX TODO better fix

    # create a scene from scratch
    node_tree_data = NodeTree(main_scene)

    # created the necessary nodetrees there
    node_tree = main_scene.node_tree

    # output node
    node_output = node_tree.nodes.new('CompositorNodeOutputFile')
    node_output.inputs.clear()

    for view in views:
        node = node_tree.nodes.new('CompositorNodeRLayers')
        view.setNode(node, node_tree.links, node_output)

    # globals
    bpy.cube_map_node_tree_data = node_tree_data
    bpy.cube_map_views = views


# ############################################################
# Cameras Setup
# ############################################################

@persistent
def cube_map_render_pre(scene, use_force=False):

    if not do_run(scene.cube_map, use_force):
        return

    from math import radians

    camera = scene.camera
    data = camera.data.copy()

    data.lens_unit = 'FOV'
    data.angle = radians(90)
    data.type = 'PERSP'

    mat = camera.matrix_world

    loc = mat.to_translation()
    rot = mat.to_euler()
    zed = rot.z

    views = bpy.cube_map_views

    for view in views:
        view.setCamera(data, loc, zed)


@persistent
def cube_map_render_post(scene, use_force=False):

    if not do_run(scene.cube_map, use_force):
        return

    views = bpy.cube_map_views

    for view in views:
        view.resetCamera()


# ############################################################
# Clean-Up
# ############################################################

@persistent
def cube_map_render_cancel(scene):
    cube_map_cleanup(scene)


@persistent
def cube_map_render_complete(scene):
    cube_map_cleanup(scene)


def cube_map_cleanup(scene, use_force=False):
    """
    remove all the temporary data created for the cube map
    """

    if not do_run(scene.cube_map, use_force):
        return

    bpy.cube_map_node_tree_data.cleanupScene()
    del bpy.cube_map_node_tree_data
    del bpy.cube_map_views

    bpy.app.handlers.scene_update_post.append(cube_map_post_update_cleanup)


def cube_map_post_update_cleanup(scene):
    """
    delay removal of scenes (otherwise we get a crash)
    """
    scenes_temp = [
            scene for scene in bpy.data.scenes if
            scene.cube_map.is_temporary]

    if not scenes_temp:
        bpy.app.handlers.scene_update_post.remove(cube_map_post_update_cleanup)

    else:
        scenes_temp[0].user_clear()
        try:
            bpy.data.scenes.remove(scenes_temp[0], do_unlink=False)
        except TypeError:
            bpy.data.scenes.remove(scenes_temp[0])


# ############################################################
# Setup Operator
# ############################################################

class CubeMapSetup(Operator):
    """"""
    bl_idname = "render.cube_map_setup"
    bl_label = "Cube Map Render Setup"
    bl_description = ""

    action: bpy.props.EnumProperty(
        description="",
        items=(("SETUP", "Setup", "Created linked scenes and setup cube map"),
               ("RESET", "Reset", "Delete added scenes"),
               ),
        default="SETUP",
        options={'SKIP_SAVE'},
        )

    @classmethod
    def poll(cls, context):
        return True

    def setup(self, window, scene):
        cube_map = scene.cube_map
        cube_map.is_enabled = True

        cube_map_render_init(scene, use_force=True)
        cube_map_render_pre(scene, use_force=True)

        # set initial scene back as the main scene
        window.screen.scene = scene

    def reset(self, scene):
        cube_map = scene.cube_map
        cube_map.is_enabled = False

        cube_map_render_post(scene, use_force=True)
        cube_map_cleanup(scene, use_force=True)

    def invoke(self, context, event):
        scene = context.scene
        cube_map = scene.cube_map

        is_enabled = cube_map.is_enabled

        if self.action == 'RESET':

            if is_enabled:
                if cube_map.is_temporary:
                    self.report(
                            {'ERROR'},
                            "Cannot reset cube map from one of "
                            "the created scenes")

                    return {'CANCELLED'}
                else:
                    self.reset(scene)
                    return {'FINISHED'}
            else:
                self.report({'ERROR'}, "Cube Map render is not setup")
                return {'CANCELLED'}

        else:  # SETUP
            if is_enabled:
                self.report({'ERROR'}, "Cube Map render is already setup")
                return {'CANCELLED'}
            else:
                self.setup(context.window, scene)
                return {'FINISHED'}


# ############################################################
# User Interface
# ############################################################

class RENDER_PT_cube_map(Panel):
    bl_space_type = 'PROPERTIES'
    bl_region_type = 'WINDOW'
    bl_context = "render"
    bl_label = "Cube Map"

    @classmethod
    def poll(cls, context):
        scene = context.scene
        return scene and (scene.render.engine != 'BLENDER_GAME')

    def draw_header(self, context):
        self.layout.prop(context.scene.cube_map, "use_cube_map", text="")

    def draw(self, context):
        layout = self.layout
        col = layout.column()

        scene = context.scene
        cube_map = scene.cube_map

        if not cube_map.is_enabled:
            col.operator(
                    "render.cube_map_setup",
                    text="Scene Setup").action = 'SETUP'
        else:
            col.operator(
                    "render.cube_map_setup",
                    text="Scene Reset", icon="X").action = 'RESET'

        col = layout.column()
        col.active = cube_map.use_cube_map
        col.prop(cube_map, "is_advanced")
        if cube_map.is_advanced:
            box = col.box()
            box.active = cube_map.use_cube_map and cube_map.is_advanced
            row = box.row()
            row.prop(cube_map, "use_view_north")
            row.prop(cube_map, "use_view_west")
            row.prop(cube_map, "use_view_zenith")

            row = box.row()
            row.prop(cube_map, "use_view_south")
            row.prop(cube_map, "use_view_east")
            row.prop(cube_map, "use_view_nadir")


# ############################################################
# Scene Properties
# ############################################################

class CubeMapInfo(bpy.types.PropertyGroup):
    use_cube_map: BoolProperty(
            name="Cube Map",
            default=False,
            )

    is_temporary: BoolProperty(
            name="Temporary",
            default=False,
            )

    is_enabled: BoolProperty(
            name="Enabled",
            default=False,
            )

    # per view settings
    is_advanced: BoolProperty(
            name="Advanced",
            default=False,
            description="Decide which views to render",
            )

    use_view_north: BoolProperty(
            name="North",
            default=True,
            )

    use_view_south: BoolProperty(
            name="South",
            default=True,
            )

    use_view_west: BoolProperty(
            name="West",
            default=True,
            )

    use_view_east: BoolProperty(
            name="East",
            default=True,
            )

    use_view_zenith: BoolProperty(
            name="Zenith",
            default=True,
            )

    use_view_nadir: BoolProperty(
            name="Nadir",
            default=True,
            )


# ############################################################
# Un/Registration
# ############################################################

def register():
    bpy.utils.register_class(CubeMapInfo)
    bpy.utils.register_class(CubeMapSetup)
    bpy.types.Scene.cube_map = bpy.props.PointerProperty(
            name="cube_map",
            type=CubeMapInfo,
            options={'HIDDEN'},
            )

    bpy.utils.register_class(RENDER_PT_cube_map)

    bpy.app.handlers.render_init.append(cube_map_render_init)
    bpy.app.handlers.render_pre.append(cube_map_render_pre)
    bpy.app.handlers.render_post.append(cube_map_render_post)
    bpy.app.handlers.render_cancel.append(cube_map_render_cancel)
    bpy.app.handlers.render_complete.append(cube_map_render_complete)


def unregister():
    bpy.utils.unregister_class(CubeMapInfo)
    bpy.utils.unregister_class(CubeMapSetup)
    bpy.utils.unregister_class(RENDER_PT_cube_map)

    bpy.app.handlers.render_init.remove(cube_map_render_init)
    bpy.app.handlers.render_pre.remove(cube_map_render_pre)
    bpy.app.handlers.render_post.remove(cube_map_render_post)
    bpy.app.handlers.render_cancel.remove(cube_map_render_cancel)
    bpy.app.handlers.render_complete.remove(cube_map_render_complete)

    del bpy.types.Scene.cube_map


if __name__ == '__main__':
    register()