"""
TODO:

Might be nice to store specific presets of RenderOptions, for specific final
choices for rendering stimuli for a given experiment.

FOR NOW, this is not going to be a mapped class...

"""

# Imports
import os
import sys
import math as bnp
from .. import utils as bvpu
from ..options import config

try:
    import bpy
    import mathutils as bmu
    is_blender = True
except ImportError:
    is_blender = False

RENDER_DIR = os.path.expanduser(config.get('path', 'render_dir'))
bvp_basedir = os.path.abspath(os.path.join(os.path.dirname(__file__),
                                           '../', '../'))
render_file = os.path.abspath(os.path.join(bvp_basedir, 'Scripts',
                                           'BlenderRender.py'))

# The "type" input for compositor node creation has been arbitrarily changed
# numerous times throughout Blender API development. This is EXTREMELY
# IRRITATING. Nonetheless, the format may change again, so I've collected
# all the node type IDs here and use the variables below

if sys.platform == 'darwin':
    # print('Mac computer node names!')
    # RLayerNodeX = 'R_LAYERS'
    # CompositorNodeX = 'COMPOSITE'
    # OutputFileNodeX = 'OUTPUT_FILE'
    # ViewerNodeX = 'VIEWER'
    # SepRGBANodeX = 'CompositorNodeSepRGBA'
    # CombRGBANodeX = 'CompositorNodeCombRGBA'
    # IDmaskNodeX = 'ID_MASK'
    # MathNodeX = 'CompositorNodeMath'
    RLayerNode = 'CompositorNodeRLayers'
    CompositorNode = 'CompositorNodeComposite'
    OutputFileNode = 'CompositorNodeOutputFile'
    ViewerNode = 'CompositorNodeViewer'
    SepRGBANode = 'CompositorNodeSepRGBA'
    CombRGBANode = 'CompositorNodeCombRGBA'
    IDmaskNode = 'CompositorNodeIDMask'
    MathNode = 'CompositorNodeMath'

else:
    RLayerNode = 'CompositorNodeRLayers'
    CompositorNode = 'CompositorNodeComposite'
    OutputFileNode = 'CompositorNodeOutputFile'
    ViewerNode = 'CompositorNodeViewer'
    SepRGBANode = 'CompositorNodeSepRGBA'
    CombRGBANode = 'CompositorNodeCombRGBA'
    IDmaskNode = 'CompositorNodeIDMask'
    MathNode = 'CompositorNodeMath'


class RenderOptions(object):
    """Class for storing render options for a scene."""
    def __init__(self, blender_params=None, bvp_params=None):
        """Initialize rendering options for scenes in BVP framework.

        Parameters
        ----------
        blender_params : dict
            directly updates any blender scene.render params for the scene
        bvp_params : dict
            establishes more complex BVP options (whether to initialize node setup 
            for different render passes [some work, others don't], base path for 
            render directory, and which file to use to render BVP scenes).
            fields : defaults are as follows:
                ### --- Basic file / rendering stuff --- ###
                Type : 'FirstFrame', # other options :  All, FirstAndLastFrame, 'every4th'
                RenderFile : os.path.join(bvp.__path__[0], 'Scripts', 'BlenderRender.py') # File to call to render scenes
                BasePath : '/auto/k1/mark/Desktop/BlenderTemp/', # Base render path (TO DO: replace with bvp config settings)
                
                ### --- Render passes --- ###
                Image : True # Render RGB(A) images or not (alpha layer or not determined by blender_params )
                ObjectMasks : False # Render masks for all BVP objects in scene (working)
                Zdepth : False # Render Z depth pass 
                Normals : False, # Not yet implemented
                
                ### --- Work-in-progress render passes (NOT working as of 2015.05) --- ###
                Contours : False, #Freestyle, yet to be implemented
                Motion : False, # Render motion (ground truth optical flow) pass
                Voxels : False # Create voxelized version of each scene 
                Axes : False, # based on N.Cornea code, for now 
                Clay : False, # All shape, no material / texture (over-ride w/ plain [clay] material) lighting??

        Notes
        -----
        RenderOptions does not directly modify a scene's file path; it only provides the base file (parent directory) for all rendering.
        Scene's "apply_opts" function should be the only function to modify with bpy.context.scene.filepath (!!) (2012.03.12)

        """
        
        self.use_freestyle = False # May need some clever if statement here - checking on version of blender
        if self.use_freestyle:
            pass
            # Freestyle settings. Not used yet as of 2016.07
            #FSlineThickness = 3.0
            #FSlineCol = (0.0, 0.0, 0.0)            
        self.use_antialiasing = True
        self.antialiasing_samples = '8'
        self.use_edge_enhance = False
        self.use_raytrace = True
        self.use_compositing = True
        self.use_textures = True
        self.use_sss = False
        self.use_shadows = True
        self.use_envmaps = False
        # File size
        self.engine='BLENDER_RENDER'
        self.resolution_x = 512
        self.resolution_y = 512
        self.resolution_percentage = 100
        self.tile_x = 64 # More?
        self.tile_y = 64 # More?
        # Fields not in bpy.data.scene.render class:
        # Image settings: File format and color mode
        self.image_settings = dict(color_mode='RGBA',
                                   file_format='PNG')
        
        self.DefaultLayerOpts = {
            'layers':tuple([True]*20), 
            'use_zmask':False, 
            'use_all_z':False, 
            'use_solid':True, # Necessary for almost everything
            'use_halo':False, 
            'use_ztransp':False, 
            'use_sky':False, 
            'use_edge_enhance':False, 
            'use_strand':False, 
            'use_freestyle':False, 
            'use_pass_combined':False, 
            'use_pass_z':False, 
            'use_pass_vector':False, 
            'use_pass_normal':False, 
            'use_pass_uv':False, 
            'use_pass_mist':False, 
            'use_pass_object_index':False, 
            'use_pass_color':False, 
            'use_pass_diffuse':False, 
            'use_pass_specular':False, 
            'use_pass_shadow':False, 
            'use_pass_emit':False, 
            'use_pass_ambient_occlusion':False, 
            'use_pass_environment':False, 
            'use_pass_indirect':False, 
            'use_pass_reflection':False, 
            'use_pass_refraction':False, 
            }
        self.BVPopts = {
            # BVP specific rendering options
            "Image":True, 
            "Voxels":False, # Not yet implemented reliably. 
            "ObjectMasks":False, 
            "Motion":False, # Not yet implemented reliably. Buggy AF
            "Zdepth":False, 
            "Contours":False, #Freestyle, yet to be implemented
            "Axes":False, # based on N.Cornea code, for now - still unfinished
            "Normals":False,
            "Clay":False, # Not yet implemented - All shape, no material / texture (over-ride w/ plain [clay] material) lighting??
            "Type":'FirstFrame', # other options: "All", "FirstAndLastFrame", 'every4th'
            "RenderFile":render_file, 
            "BasePath":RENDER_DIR, 
            }
        # Disallow updates that add fields
        self.__dict__ = bvpu.basics.fixedKeyDict(self.__dict__)
        # Update defaults w/ inputs
        if not bvp_params is None:
            # TO DO: Change this variable name. Big change, tho.
            self.BVPopts.update(bvp_params)
        if not blender_params is None:
            # TO DO: Clean this shit up. Sloppy organization. 
            if 'DefaultLayerOpts' in blender_params.keys():
                DefaultLayerOpts = blender_params.pop('DefaultLayerOpts')
                self.DefaultLayerOpts.update(DefaultLayerOpts)
            self.__dict__.update(blender_params)
    def __repr__(self):
        S = 'Class "RenderOptions":\n'+self.__dict__.__repr__()
        return S
    
    def apply_opts(self, scn=None):
        if scn is None:
            # Get current scene if input not supplied
            scn = bpy.context.scene
        # Backwards compatibility:
        if not 'Voxels' in self.BVPopts:
            self.BVPopts['Voxels'] = False
        if not 'Motion' in self.BVPopts:
            self.BVPopts['Motion'] = False
        scn.use_nodes = True
        # Set only first layer to be active
        if bpy.app.version < (2, 80, 0):
            scn.layers = [True]+[False]*19
        else:
            # NEED TO FIGURE OUT WHAT TO DO HERE
            pass 
        
        # Get all non-function attributes
        ToSet = [x for x in self.__dict__.keys() if not hasattr(self.__dict__[x], '__call__') and not x in ['BVPopts', 'DefaultLayerOpts', 'image_settings']]
        for s in ToSet:
            try:
                setattr(scn.render, s, self.__dict__[s]) # define __getattr__ or whatever so self[s] works
            except:
                print('Unable to set attribute %s!'%s)
        # Set image settings:
        scn.render.image_settings.file_format = self.image_settings['file_format']
        scn.render.image_settings.color_mode = self.image_settings['color_mode']

        # Re-set all nodes and render layers
        for n in scn.node_tree.nodes:
            scn.node_tree.nodes.remove(n)
        old_render_layers = bpy.context.scene.render.layers.keys()
        bpy.ops.scene.render_layer_add()
        for ii in range(len(old_render_layers)):
            bpy.context.scene.render.layers.active_index = 0
            bpy.ops.scene.render_layer_remove()
        # Rename newly-added layer (with default properties) to default name
        # (Doing it in this order assures Blender won't make it, e.g., RenderLayer.001
        # if a layer named RenderLayer already exists)
        bpy.context.scene.render.layers[0].name = 'RenderLayer'
        # Add basic node setup:
        RL = scn.node_tree.nodes.new(type=RLayerNode)
        compositor_output = scn.node_tree.nodes.new(type=CompositorNode)
        scn.node_tree.links.new(RL.outputs['Image'], compositor_output.inputs['Image'])
        scn.node_tree.links.new(RL.outputs['Alpha'], compositor_output.inputs['Alpha'])
        # Decide whether we're only rendering one type of output:
        single_output = sum([self.BVPopts['Image'], self.BVPopts['ObjectMasks'], self.BVPopts['Zdepth'], 
                        self.BVPopts['Contours'], self.BVPopts['Axes'], self.BVPopts['Normals']])==1
        if bpy.app.version < (2, 80, 0):
            update = scn.update
        else:
            update = bpy.context.view_layer.update
        # Add compositor nodes for optional outputs:
        if self.BVPopts['Voxels']:
            self.SetUpVoxelization()
            update()
            return # Special case! no other node-based options can be applied!
        if self.BVPopts['ObjectMasks']:
            self.add_object_masks(single_output=single_output)
        if self.BVPopts['Motion']:
            self.add_motion(single_output=single_output)
        if self.BVPopts['Zdepth']:
            self.add_depth(single_output=single_output)
        if self.BVPopts['Contours']:
            raise NotImplementedError('Not ready yet!')
        if self.BVPopts['Axes']:
            raise NotImplementedError('Not ready yet!')
        if self.BVPopts['Normals']:
            self.add_normals(single_output=single_output)
        if self.BVPopts['Clay']:
            raise NotImplementedError('Not ready yet!')
            #self.AddClayLayerNodes(Is_RenderOnlyClay=single_output)
        if not self.BVPopts['Image']:
            # Switch all properties from one of the file output nodes to the composite output
            # Grab a node
            aa = [N for N in scn.node_tree.nodes if N.type=='OUTPUT_FILE']
            print([a.type for a in scn.node_tree.nodes])
            output = aa[0]
            # Find input to this node
            Lnk = [L for L in scn.node_tree.links if L.to_node == output][0]
            Input = Lnk.from_socket
            # Remove all input to composite node:
            NodeComposite = [N for N in scn.node_tree.nodes if N.type=='COMPOSITE'][0]
            L = [L for L in scn.node_tree.links if L.to_node==NodeComposite]
            for ll in L:
                scn.node_tree.links.remove(ll)
            # Make link from input to file output to composite output:
            scn.node_tree.links.new(Input, NodeComposite.inputs['Image'])
            # Update Scene info to reflect node info:
            scn.render.filepath = output.base_path+output.file_slots[0].path
            scn.render.image_settings.file_format = output.format.file_format
            # Get rid of old file output
            scn.node_tree.nodes.remove(output)
            # Get rid of render layer that renders image:
            RL = scn.render.layers['RenderLayer']
            scn.render.layers.remove(RL)
            # Turn off raytracing??

        update()
    """
    Notes on nodes: The following functions add various types of compositor nodes to a scene in Blender.
    These allow output of other image files that represent other "meta-information" (e.g. Z depth, 
    normals, etc) that is separate from the pixel-by-pixel color / luminance information in standard images. 
    To add nodes: NewNode = nt.nodes.new(type=NodeType) 
    See top of code for list of node types used.
    """
    def add_object_masks(self, scn=None, single_output=False):
        """Adds compositor nodes to render out object masks.

        Parameters
        ----------
        scn : bpy.data.scene | None (default=None)
            Leave as default (None) for now. Placeholder for future code updates.
        single_output : bool
            Whether to render ONLY masks.

        Notes
        -----
        The current implementation relies on objects being linked into Blender scene (without creating proxies), or being 
        mesh objects. Older versions of the code filtered objects by whether or not they had any parent object. The old 
        way may be useful, if object insertion methods change.
        IMPORTANT: 
        If scenes are created with many objects off-camera, this code will create a mask for EVERY off-scene object. 
        These masks will not be in the scene, but blender will render an image (an all-black image) for each and 
        every one of them.
        """
        if not scn:
            scn = bpy.context.scene
        scn.use_nodes = True
        scn.render.use_compositing = True
        ########################################################################
        ### --- First: Allocate all objects' pass indices (and groups??) --- ### 
        ########################################################################
        DisallowedNames = ['BG_', 'CamTar', 'Shadow_'] # Also constraint objects...
        Ob = [o for o in bpy.context.scene.objects if not any([x in o.name for x in DisallowedNames])]
        PassCt = 1
        to_skip = []
        for o in Ob:
            if o.name in to_skip:
                continue
            # Check for dupli groups:
            if o.type=='EMPTY':
                if o.dupli_group:
                    o.pass_index = PassCt
                    for po in o.dupli_group.objects:
                        po.pass_index = PassCt
                    bvpu.blender.set_layers(o, [0, PassCt])
                    PassCt +=1
            # Check for mesh objects:
            elif o.type in ('MESH', 'CURVE'):
                print('assigning pass index %d to %s'%(PassCt, o.name))