import bpy
import re
import os
from . import fn

class GPEXP_OT_mute_toggle_output_nodes(bpy.types.Operator):
    bl_idname = "gp.mute_toggle_output_nodes"
    bl_label = "Mute Toggle output nodes"
    bl_description = "Mute / Unmute all output nodes"
    bl_options = {"REGISTER"}

    mute : bpy.props.BoolProperty(default=True, options={'SKIP_SAVE'})

    def execute(self, context):
        ct = 0
        for n in context.scene.node_tree.nodes:
            if n.type != 'OUTPUT_FILE':
                continue
            n.mute = self.mute
            ct += 1

        state = 'muted' if self.mute else 'unmuted'
        self.report({"INFO"}, f'{ct} nodes {state}')
        return {"FINISHED"}

class GPEXP_OT_number_outputs(bpy.types.Operator):
    bl_idname = "gp.number_outputs"
    bl_label = "Number Outputs"
    bl_description = "(Re)Number the outputs to have ordered file by name in export directories\
        \nCtrl+Clic : Delete numbering"
    bl_options = {"REGISTER"}

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

    mode : bpy.props.StringProperty(default='SELECTED', options={'SKIP_SAVE'})
    clear : bpy.props.BoolProperty(default=False, options={'SKIP_SAVE'})

    def invoke(self, context, event):
        # use clear with Ctrl + Click 
        if event.ctrl:
            self.clear = True
        return self.execute(context)

    def execute(self, context):
        scn = context.scene

        ct = 0
        nodes = scn.node_tree.nodes
        for fo in nodes:
            if fo.type != 'OUTPUT_FILE':
                continue
            if self.mode == 'SELECTED' and not fo.select:
                continue
            # print(f'numbering {fo.name}')
            ct += 1
            if self.clear:
                fn.delete_numbering(fo)
            else:
                fn.renumber_keep_existing(fo)

        txt = 'de-numbered' if self.clear else 're-numbered'
        if ct:
            self.report({'INFO'}, f'{ct} output nodes {txt}')
        else:
            self.report({'ERROR'}, f'No output nodes {txt}')

        return {"FINISHED"}

class GPEXP_OT_set_output_node_format(bpy.types.Operator):
    bl_idname = "gp.set_output_node_format"
    bl_label = "Set output format from active"
    bl_description = "Change all selected output node to match active output node format"
    bl_options = {"REGISTER"}

    mute : bpy.props.BoolProperty(default=True, options={'SKIP_SAVE'})

    def execute(self, context):
        nodes = context.scene.node_tree.nodes
        if not nodes.active or nodes.active.type != 'OUTPUT_FILE':
            self.report({"ERROR"}, f'Active node should be an output file to use as reference for output format')
            return {"CANCELLED"}

        ref = nodes.active

        # file_format = ref.format.file_format
        # color_mode = ref.format.color_mode
        # color_depth = ref.format.color_depth
        # compression = ref.format.compression

        ct = 0
        for n in nodes:
            if n.type != 'OUTPUT_FILE' or n == ref or not n.select:
                continue

            for attr in dir(ref.format):
                if attr.startswith('__') or attr in {'rna_type','bl_rna', 'view_settings', 'display_settings','stereo_3d_format'}: # views_format
                    continue
                try:
                    setattr(n.format, attr, getattr(ref.format, attr))
                except Exception as e:
                    print(f"can't set attribute : {attr}")

            # n.format.file_format = file_format
            # n.format.color_mode = color_mode
            # n.format.color_depth = color_depth
            # n.format.compression = compression

            ct += 1

        # state = 'muted' if self.mute else 'unmuted'
        self.report({"INFO"}, f'{ct} output format copied from {ref.name}')
        return {"FINISHED"}



def out_norm(x):
    a = x.group(1) if x.group(1) else ''
    b = x.group(2) if x.group(2) else ''
    c = x.group(3) if x.group(3) else ''
    d = x.group(4) if x.group(4) else ''
    e = x.group(5) if x.group(5) else ''
    return f'{a}{b}{fn.normalize(c)}{d}{e}'


## does not match the right thing yet
class GPEXP_OT_normalize_outnames(bpy.types.Operator):
    bl_idname = "gp.normalize_outnames"
    bl_label = "Normalize Output names"
    bl_description = "Normalize output names with lowercase and replace dash to underscore"
    bl_options = {"REGISTER"}

    mute : bpy.props.BoolProperty(default=True, options={'SKIP_SAVE'})

    def execute(self, context):
        nodes = context.scene.node_tree.nodes

        reslash = re.compile('\\/')
        ct = 0
        for n in nodes:
            if n.type != 'OUTPUT_FILE' or not n.select:
                continue
            # Normalize last part of the file out names
            base_path_l = reslash.split(n.base_path)
            base_path_l[-1] = fn.normalize(base_path_l[-1])
            n.base_path = '/'.join(base_path_l)

            for fs in n.file_slots:
                fp = fs.path
                fp_l = reslash.split(fp)
                for i, part in enumerate(fp_l):
                    fp_l[1] = re.sub(r'(^\d{3}_)?([A-Z]{2}_)?(.*?)(_[A-Z]{2})?(_)?', out_norm, part)

                fs.path = '/'.join(fp_l)

            ct += 1

        # state = 'muted' if self.mute else 'unmuted'
        self.report({"INFO"}, f'{ct} output nodes normalized')
        return {"FINISHED"}


class GPEXP_OT_enable_all_viewlayers(bpy.types.Operator):
    bl_idname = "gp.enable_all_viewlayers"
    bl_label = "Enable All Viewlayers"
    bl_description = "Enable all View layers except those named 'exclude' 'View Layer'"
    bl_options = {"REGISTER"}

    def execute(self, context):
        scn = context.scene

        vl_list = [vl for vl in scn.view_layers if not vl.use and vl.name not in {'View Layer', 'exclude'}]
        for v in vl_list:
            v.use = True

        self.report({"INFO"}, f'{len(vl_list)} ViewLayers Reactivated')
        return {"FINISHED"}

class GPEXP_OT_activate_only_selected_layers(bpy.types.Operator):
    bl_idname = "gp.activate_only_selected_layers"
    bl_label = "Activate Only Selected Layers"
    bl_description = "Activate only selected node view layer , excluding all others"
    bl_options = {"REGISTER"}

    def execute(self, context):
        scn = context.scene

        nodes = scn.node_tree.nodes

        rlayers_nodes = [n for n in nodes if n.select and n.type == 'R_LAYERS']
        vls = [scn.view_layers.get(n.layer) for n in rlayers_nodes if scn.view_layers.get(n.layer)]
        for v in scn.view_layers:
            v.use = v in vls

        self.report({"INFO"}, f'Now only {len(vls)} viewlayer active (/{len(scn.view_layers)})')
        return {"FINISHED"}


### TODO reset scene settings (set settings )

class GPEXP_OT_reset_render_settings(bpy.types.Operator):
    bl_idname = "gp.reset_render_settings"
    bl_label = "Reset Render Settings"
    bl_description = "Reset render settings on all scene, disabling AA nodes when there is no Merge nodegroup"
    bl_options = {"REGISTER"}

    def execute(self, context):
        for scn in bpy.data.scenes:
            if scn.name == 'Scene':
                # don't touch original scene
                continue

            # set a unique preview output
            # - avoid possible write/sync overlap (point to tmp on linux ?)
            # - allow to monitor output of a scene and possibly use Overwrite

            if scn.render.filepath.startswith('//render/preview/'):
                scn.render.filepath = f'//render/preview/{bpy.path.clean_name(scn.name.lower())}/preview_'
                print(f'Scene {scn.name}: change output to {scn.render.filepath}')

            if not scn.use_nodes:
                continue

            # set the settings depending on merges node presences
            use_native_aa = True
            for n in scn.node_tree.nodes:
                if n.name.startswith('merge_NG_'):
                    use_native_aa = False
                    break

            if scn.gp_render_settings.use_aa != use_native_aa:
                print(f'Scene {scn.name}: changed scene AA settings, native AA = {use_native_aa}')
            fn.scene_aa(scene=scn, toggle=use_native_aa)

            # set propertie on scn to reflect changes (without triggering update)
            scn.gp_render_settings['use_aa'] = use_native_aa

        return {"FINISHED"}

class GPEXP_PG_selectable_prop(bpy.types.PropertyGroup):
    node_name: bpy.props.StringProperty(name="Node Name")
    node_label: bpy.props.StringProperty(name="Node Label")
    name: bpy.props.StringProperty(name="Name or Path")
    socket_name: bpy.props.StringProperty(name="Source socket Name") # Source socket name as reference
    select: bpy.props.BoolProperty(name="Selected", default=True)
    is_linked: bpy.props.BoolProperty(name="Linked", default=False)
    # is_valid: bpy.props.BoolProperty(name="Valid", default=True)
    
    ## extra output naming options
    name_from_node: bpy.props.StringProperty(name="Name From Node")
    name_from_node_and_socket: bpy.props.StringProperty(name="Name From Node And Socket")

class GPEXP_OT_connect_selected_to_file_out(bpy.types.Operator):
    bl_idname = "gp.connect_selected_to_file_out"
    bl_label = "Connect Selected To File Output"
    bl_description = "Connect Selected Nodes to a fileoutput node\
        \nIf a fileoutput node is selected, socket are added to it"
    bl_options = {"REGISTER", "UNDO"}

    socket_collection : bpy.props.CollectionProperty(type=GPEXP_PG_selectable_prop)

    show_custom_settings : bpy.props.BoolProperty(
        name='Settings',
        default=False)

    ## enum choice for naming: socket_name, node_name, node_and_socket_name, 
    name_type : bpy.props.EnumProperty(
        name='Output Name From',
        description='Choose the output name\
            \nNode name use Label (Use node name when there is no Label)',
        default='node_and_socket_name',
        items=(
                ('node_and_socket_name', 'Node_Socket Name', 'Use the node name prefix and socket name', 0),
                ('socket_name', 'Socket Name', 'Use the socket name as output name', 1),
                ('node_name', 'Node Name', 'Use the node name as output name', 2),
            )
        )

    # prefix_with_node_name : bpy.props.BoolProperty(
    #     name='Prefix With Node Name',
    #     description='Add the node name as prefix to the output name',
    #     default=False)

    base_path : bpy.props.StringProperty(
        name='Custom base path',
        default='',
        description='Set the base path of created file_output (not if already exists)')
    
    file_format : bpy.props.EnumProperty(
        name='Output Format',
        default='NONE',
        items=(
                ('NONE', 'Default', 'Use default settings'),
                ('OPEN_EXR_MULTILAYER', 'OpenEXR MultiLayer', 'Output image in multilayer OpenEXR format'),
                ('OPEN_EXR', 'OpenEXR', 'Output image in OpenEXR format'),
            )
        )

    exr_codec : bpy.props.EnumProperty(
        name='Codec',
        default='PIZ',
        description='Codec settings for OpenEXR',
        items=(
            ('PIZ', 'PIZ (lossless)', ''),
            ('ZIP', 'ZIP (lossless)', ''),
            ('RLE', 'RLE (lossless)', ''),
            ('ZIPS', 'ZIPS (lossless)', ''),
            ('PXR24', 'Pxr24 (lossy)', ''),
            ('B44', 'B44 (lossy)', ''),
            ('B44A', 'B44A (lossy)', ''),
            ('DWAA', 'DWAA (lossy)', ''),
            ('DWAB', 'DWAB (lossy)', ''),
            ),
        )
    
    color_depth : bpy.props.EnumProperty(
        name='Color Depth',
        default='16',
        description='Bit depth per channel',
        items=(
                # ('8', '8', '8-bit color channels'),
                # ('10', '10', '10-bit color channels'),
                # ('12', '12', '12-bit color channels'),
                ('16', '16 (Half)', '16-bit color channels'),
                ('32', '32 (Full)', '32-bit color channels'),
            ),
        )

    def invoke(self, context, event):
        self.socket_collection.clear()
        if event.ctrl:
            # Direct connect, do not use any options
            self.base_path = ''
            return self.execute(context)

        selected = [n for n in context.scene.node_tree.nodes if n.select and n.type != 'OUTPUT_FILE']
        if not selected:
            self.report({'ERROR'}, 'No render layer nodes selected')
            return {'CANCELLED'}
        for n in selected:
            for o in n.outputs:
                if o.is_unavailable:
                    continue
                item = self.socket_collection.add()
                item.node_name = n.name
                item.node_label = n.label.strip()
                item.socket_name = o.name
                
                ## Set editable names
                item.name = o.name

                ## Store other naming options (cleaned at exec with bpy.path.clean_name)
                node_name = n.label.strip() if n.label.strip() else n.name
                
                ## Change node_name for render layers: scene_viewlayer_name
                if n.type == 'R_LAYERS' and node_name != n.label: # skip if a label is set
                    node_name = f'{n.scene.name}_{n.layer}'
                    # node_name = f'{n.layer}'

                item.name_from_node = node_name

                if len(n.outputs) == 1:
                    ## Only one output, just pick node name, no need to add socket name
                    item.name_from_node_and_socket = node_name
                else:
                    item.name_from_node_and_socket = f'{node_name}_{o.name}'

                ## TODO: rename item.name according to tamplate pairs in preferences (to add later)
                if o.is_linked:
                    item.is_linked = True
                    item.select = False

        return context.window_manager.invoke_props_dialog(self, width=500)

    def draw(self, context):
        layout = self.layout
        box = layout.box()
        expand_icon = 'DISCLOSURE_TRI_DOWN' if self.show_custom_settings else 'DISCLOSURE_TRI_RIGHT'
        box.prop(self, 'show_custom_settings', emboss=False, icon=expand_icon)
        ## Settings
        if self.show_custom_settings:
            box.use_property_split = True
            box.row().prop(self, 'name_type', expand=False)
            box.prop(self, 'base_path')
            col = box.column()
            col.prop(self, 'file_format')
            col.prop(self, 'exr_codec')
            col.row().prop(self, 'color_depth', expand=True)

        ## Node Sockets
        layout.use_property_split = False
        col = layout.column()
        current_node_name = ''
        for item in self.socket_collection:
            if item.node_name != current_node_name:
                current_node_name = item.node_name
                col.separator()
                ## Display node label + name or name only
                if item.node_label:
                    display_node_name = f'{item.node_label} ({item.node_name})'
                else:
                    display_node_name = item.node_name
                    ## A bit dirty: For render layer node, show render layer-node name.
                    if display_node_name.startswith('Render Layer'):
                        rln = context.scene.node_tree.nodes.get(item.node_name)
                        display_node_name = f'{rln.scene.name}/{rln.layer} ({display_node_name})'
                col.label(text=display_node_name, icon='NODE_SEL')

            row = col.row()
            if item.is_linked:
                row.label(text='', icon='LINKED') # NODETREE
            else:
                row.label(text='', icon='BLANK1')
            row.prop(item, 'select', text='')

            display_name = item.socket_name
            if 'crypto' in display_name.lower():
                display_name = f'{display_name} -> 32bit output node'
            row.label(text=display_name)
            row.label(text='', icon='RIGHTARROW')

            if self.name_type == 'socket_name':
                row.prop(item, 'name', text='')
            elif self.name_type == 'node_name':
                row.prop(item, 'name_from_node', text='')
            elif self.name_type == 'node_and_socket_name':
                row.prop(item, 'name_from_node_and_socket', text='')

    def execute(self, context):
        
        # Build exclude dict from selection
        excludes = {}
        remap_names = {}
        ## Old system
        # if len(self.socket_collection):
        #     for item in self.socket_collection:
        #         if not item.select:
        #             # All deselected goes to exclude with {node_name: [socket1, ...]}
        #             excludes.setdefault(item.node_name, []).append(item.name)
        #         elif item.socket_name != item.name:
        #             remap_names[item.socket_name] = item.name
        
        if len(self.socket_collection):
            for item in self.socket_collection:
                final_name = item.name
                ## change name if other options were used
                if self.name_type == 'node_name':
                    final_name = item.name_from_node
                elif self.name_type == 'node_and_socket_name':
                    final_name = item.name_from_node_and_socket
                
                if not item.select:
                    # All deselected goes to exclude with {node_name: [socket1, ...]}
                    excludes.setdefault(item.node_name, []).append(item.socket_name)
                elif item.socket_name != final_name:
                    remap_names.setdefault(item.node_name, {})[item.socket_name] = final_name
                    # remap_names[item.socket_name] = final_name

        ## Handle default file format
        file_ext = self.file_format
        if self.file_format == 'NONE':
            env_file_format = os.environ.get('FILE_FORMAT')
            file_ext = env_file_format if env_file_format else 'OPEN_EXR_MULTILAYER'

        file_format = {
            'file_format' : file_ext,
            'exr_codec' : self.exr_codec,
            'color_depth' : self.color_depth,
        }
        
        scn = context.scene
        nodes = scn.node_tree.nodes
        selected = [n for n in nodes if n.select]
        outfile = next((n for n in selected if n.type == 'OUTPUT_FILE'), None)
        # Exclude output file from 
        selected = [n for n in selected if n.type != 'OUTPUT_FILE']        
        
        # fn.connect_to_file_output(selected, outfile)
        for n in selected:
            fn.connect_to_file_output(n, outfile, base_path=self.base_path, excludes=excludes, remap_names=remap_names, file_format=file_format)
        return {"FINISHED"}

classes=(
GPEXP_OT_mute_toggle_output_nodes,
GPEXP_OT_set_output_node_format,
GPEXP_OT_number_outputs,
GPEXP_OT_enable_all_viewlayers,
GPEXP_OT_activate_only_selected_layers,
GPEXP_OT_reset_render_settings,
GPEXP_PG_selectable_prop,
GPEXP_OT_connect_selected_to_file_out,
# GPEXP_OT_normalize_outnames,
)

def register():
    for cls in classes:
        bpy.utils.register_class(cls)

def unregister():
    for cls in reversed(classes):
        bpy.utils.unregister_class(cls)