from os import error
import bpy
import re

from bpy.types import Operator
from bpy.props import StringProperty, BoolProperty, EnumProperty
from bpy.app.handlers import persistent
from .utils import get_addon_prefs, is_vector_close


# --- OPS ---

# PATTERN = r'([A-Z]{2})?_?([A-Z]{2})?_?(.*)' # bad ! match whithout separator
# pattern = r'(?:(^[A-Z]{2})_)?(?:([A-Z]{2})_)?(.*)' # matching only two letter
# pattern = r'^([A-Z]{2}_)?([A-Z]{2}_)?(.*)' # matching letters with separator

# pattern = r'^([A-Z]{1,6}_)?([A-Z]{1,6}_)?(.*)' # matching capital letters from one to six
# pattern = r'^([A-Z]{1,6}_)?([A-Z]{1,6}_)?(.*?)(_[A-Z]{2})?$' # 2 letter suffix
# pattern = r'^(?P<tag>[A-Z]{1,6}_)?(?P<tag2>[A-Z]{1,6}_)?(?P<name>.*?)(?P<sfix>_[A-Z]{2})?$' # named
# pattern = r'^(?P<grp>-\s)?(?P<tag>[A-Z]{2}_)?(?P<tag2>[A-Z]{1,6}_)?(?P<name>.*?)(?P<sfix>_[A-Z]{2})?$' # group start '  - '
# PATTERN = r'^(?P<grp>-\s)?(?P<tag>[A-Z]{2}_)?(?P<tag2>[A-Z]{1,6}_)?(?P<name>.*?)(?P<sfix>_[A-Z]{2})?(?P<inc>\.\d{3})?$' # numering
PATTERN = r'^(?P<grp>-\s)?(?P<tag>[A-Z]{2}_)?(?P<name>.*?)(?P<sfix>_[A-Z]{2})?(?P<inc>\.\d{3})?$' # numering


def layer_name_build(layer, prefix='', desc='', suffix=''):
    '''GET a layer and argument to build and assign name
    return new name
    '''


    prefs = get_addon_prefs()
    sep = prefs.separator
    name = old = layer.info

    pattern = PATTERN.replace('_', sep) # set separator

    res = re.search(pattern, name.strip())

    # prefix -> tag
    # prefix2 -> tag2
    # desc -> name
    # suffix -> sfix
    grp = '' if res.group('grp') is None else res.group('grp')
    tag = '' if res.group('tag') is None else res.group('tag')
    # tag2 = '' if res.group('tag2') is None else res.group('tag2')
    name = '' if res.group('name') is None else res.group('name')
    sfix = '' if res.group('sfix') is None else res.group('sfix')
    inc = '' if res.group('inc') is None else res.group('inc')

    if grp:
        grp = '  ' + grp # name is strip(), so grp first spaces are gones.

    if prefix:
        if prefix == 'prefixkillcode':
            tag = ''
        else:
            tag = prefix.upper().strip() + sep
    # if prefix2:
    #     tag2 = prefix2.upper().strip() + sep
    if desc:
        name = desc

    if suffix:
        if suffix == 'suffixkillcode':
            sfix = ''
        else:
            sfix = sep + suffix.upper().strip()

    # check if name is available without the increment ending
    new = f'{grp}{tag}{name}{sfix}'

    layer.info = new

    ## update name in modifier targets
    if old != new:
        # find objects using this GP datablock
        for ob_user in [o for o in bpy.data.objects if o.data == layer.id_data]: # bpy.context.scene.objects
            # maybe a more elegant way exists to find all objects users ?

            # update Gpencil modifier targets
            for mod in ob_user.grease_pencil_modifiers:
                if not hasattr(mod, 'layer'):
                    continue
                if mod.layer == old:
                    mod.layer = new

"""
def layer_name_build(layer, prefix='', prefix2='', desc='', suffix=''):
    '''GET a layer and infos to build name
    Can take one or two prefix and description/name of the layer)
    '''


    prefs = get_addon_prefs()
    sep = prefs.separator
    name = layer.info

    pattern = pattern.replace('_', sep) # set separator

    res = re.search(pattern, name.strip())
    p1 = '' if res.group(1) is None else res.group(1)
    p2 = '' if res.group(2) is None else res.group(2)
    p3 = '' if res.group(3) is None else res.group(3)
    p4 = '' if res.group(4) is None else res.group(4)

    if prefix:
        if prefix == 'prefixkillcode':
            p1 = ''
        else:
            p1 = prefix.upper().strip() + sep

    if prefix2:
        p2 = prefix2.upper().strip() + sep

    if desc:
        p3 = desc

    if suffix:
        if suffix == 'suffixkillcode':
            p4 = ''
        else:
            p4 = sep + suffix.upper().strip()

    new = f'{p1}{p2}{p3}{p4}'
    layer.info = new
"""

## multi-prefix solution (Caps letters)
class GPTB_OT_layer_name_build(Operator):
    bl_idname = "gp.layer_name_build"
    bl_label = "Layer Name Build"
    bl_description = "Change prefix of layer name"
    bl_options = {"REGISTER", "UNDO"}

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

    prefix : StringProperty(default='', options={'SKIP_SAVE'})
    # prefix2 : StringProperty(default='', options={'SKIP_SAVE'})
    desc : StringProperty(default='', options={'SKIP_SAVE'})
    suffix : StringProperty(default='', options={'SKIP_SAVE'})
    tooltip : StringProperty(default='', options={'SKIP_SAVE'})

    @classmethod
    def description(cls, context, properties):
        tag = properties.prefix if properties.prefix else properties.suffix
        if properties.tooltip:
            return f"Use prefix: {tag} ({properties.tooltip})"
        else:
            return f"Use prefix: {tag}"

    def execute(self, context):
        ob = context.object
        gpl = ob.data.layers
        act = gpl.active
        if not act:
            self.report({'ERROR'}, 'no layer active')
            return {"CANCELLED"}

        layer_name_build(act, prefix=self.prefix, desc=self.desc, suffix=self.suffix)

        ## Deactivate multi-selection on layer !
        ## somethimes it affect a random layer that is still considered selected
        # for l in gpl:
        #     if l.select or l == act:
        #         layer_name_build(l, prefix=self.prefix, desc=self.desc, suffix=self.suffix)

        return {"FINISHED"}


def grp_toggle(l, mode='TOGGLE'):
    '''take mode in (TOGGLE, GROUP, UNGROUP) '''
    grp_item_id = '  - '
    res = re.search(r'^(\s{1,3}-\s{0,3})(.*)', l.info)
    if not res and mode in ('TOGGLE', 'GROUP'):
         # No gpr : add group prefix after stripping all space and dash
        l.info = grp_item_id + l.info.lstrip(' -')

    elif res and mode in ('TOGGLE', 'UNGROUP'):
        # found : delete group prefix
        l.info = res.group(2)


class GPTB_OT_layer_group_toggle(Operator):
    bl_idname = "gp.layer_group_toggle"
    bl_label = "Group Toggle"
    bl_description = "Group or ungroup a layer"
    bl_options = {"REGISTER", "UNDO"}

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

    # group : StringProperty(default='', options={'SKIP_SAVE'})

    def execute(self, context):
        ob = context.object
        gpl = ob.data.layers
        act = gpl.active
        if not act:
            self.report({'ERROR'}, 'no layer active')
            return {"CANCELLED"}
        for l in gpl:
            if l.select or l == act:
                grp_toggle(l)
        return {"FINISHED"}

class GPTB_OT_layer_new_group(Operator):
    bl_idname = "gp.layer_new_group"
    bl_label = "New Group"
    bl_description = "Create a group from active layer"
    bl_options = {"REGISTER", "UNDO"}

    def execute(self, context):
        ob = context.object
        gpl = ob.data.layers
        act = gpl.active
        if not act:
            self.report({'ERROR'}, 'no layer active')
            return {"CANCELLED"}

        res = re.search(PATTERN, act.info)
        if not res:
            self.report({'ERROR'}, 'Could not create a group name, create a layer manually')
            return {"CANCELLED"}

        name = res.group('name').strip(' -')
        if not name:
            self.report({'ERROR'}, f'No name found in {act.info}')
            return {"CANCELLED"}

        if name in [l.info.strip(' -') for l in gpl]:
            self.report({'WARNING'}, f'Name already exists: {act.info}')
            return {"FINISHED"}

        grp_toggle(act, mode='GROUP')
        n = gpl.new(name, set_active=False)
        n.use_onion_skinning = n.use_lights = False
        n.hide = True
        n.opacity = 0
        return {"FINISHED"}


#-## SELECTION MANAGEMENT ##-#

def activate_channel_group_color(context):
    if not context.preferences.edit.use_anim_channel_group_colors:
        context.preferences.edit.use_anim_channel_group_colors = True

def refresh_areas():
    for area in bpy.context.screen.areas:
        area.tag_redraw()

def build_layers_targets_from_dopesheet(context):
    '''Return all selected layers on context GP dopesheet according to seelction and filters'''
    ob = context.object
    gpl = context.object.data.layers
    act = gpl.active
    dopeset = context.space_data.dopesheet


    if dopeset.show_only_selected:
        pool = [o for o in context.selected_objects if o.type == 'GPENCIL']
    else:
        pool = [o for o in context.scene.objects if o.type == 'GPENCIL']
        if not dopeset.show_hidden:
            pool = [o for o in pool if o.visible_get()]

    layer_pool = [l for o in pool for l in o.data.layers]
    layer_pool = list(set(layer_pool)) # remove dupli-layers from same data source with

    # apply search filter
    if dopeset.filter_text:
        layer_pool = [l for l in layer_pool if (dopeset.filter_text.lower() in l.info.lower()) ^ dopeset.use_filter_invert]

    return layer_pool

def build_dope_gp_list(layer_list):
    '''Take a list of GP layers return a dict with pairs {gp data : own layer list}'''
    from collections import defaultdict
    gps = defaultdict(list)
    for l in layer_list:
        gps[l.id_data].append(l)
    return gps

class GPTB_OT_select_set_same_prefix(Operator):
    bl_idname = "gp.select_same_prefix"
    bl_label = "Select Same Prefix"
    bl_description = "Select layers that have the same prefix as active\nSet with ctrl+clic"
    bl_options = {"REGISTER", "UNDO"}

    @classmethod
    def poll(cls, context):
        return context.object and context.object.type == 'GPENCIL'

    mode : EnumProperty(default='SELECT', options={'SKIP_SAVE'},
        items=(
                ('SELECT', "Select", "Select layer with same prefix as active"),
                ('SET', "Set", "Set prefix on selected layer to the same as active"),
            ),
        )

    def invoke(self, context, event):
        if event.ctrl:
            self.mode = 'SET'
        return self.execute(context)

    def execute(self, context):
        prefs = get_addon_prefs()
        sep = prefs.separator # '_'

        gp = context.object.data
        act = gp.layers.active

        pool = build_layers_targets_from_dopesheet(context)
        if not pool:
            self.report({'ERROR'}, 'No layers found in current GP dopesheet')
            return {"CANCELLED"}

        gp_dic = build_dope_gp_list(pool)
        if not act:
            # Check in other displayed layer if there is an active one
            for gp, _layer_list in gp_dic.items():
                if gp.layers.active:
                    # overwrite gp variable at the same time
                    act = gp.layers.active
                    break
            if not act:
                self.report({'ERROR'}, 'No active layer to base action')
                return {"CANCELLED"}

        print(f'Select/Set ref layer: {gp.name} > {gp.layers.active.info}')

        res = re.search(PATTERN, act.info)
        if not res:
            self.report({'ERROR'}, f'Error scanning {act.info}')
            return {"CANCELLED"}

        namespace = res.group('tag')
        if not namespace:
            self.report({'WARNING'}, f'No prefix detected in {act.info} with separator {sep}')
            return {"CANCELLED"}

        if self.mode == 'SELECT':
            ## with split
            # namespace = act.info.split(sep,1)[0]
            # namespace_bool_list = [l.info.split(sep,1)[0] == namespace for l in gpl]

            ## with reg # only active
            # namespace_bool_list = [l.info.split(sep,1)[0] + sep == namespace for l in gpl]
            # gpl.foreach_set('select', namespace_bool_list)

            ## don't work Need Foreach set per gp
            # for l in pool:
            #     l.select = l.info.split(sep,1)[0] + sep == namespace

            for gp, layers in gp_dic.items():
                # check namespace + restrict selection to visible layers according to filters
                # TODO : Should use the regex pattern to detect and compare r.group('tag')
                namespace_bool_list = [(l in layers) and (l.info.split(sep,1)[0] + sep == namespace) for l in gp.layers]
                gp.layers.foreach_set('select', namespace_bool_list)

        elif self.mode == 'SET':
            for l in pool:
                if not l.select or l == act:
                    continue
                layer_name_build(l, prefix=namespace.strip(sep))

        refresh_areas()
        return {"FINISHED"}



class GPTB_OT_select_set_same_color(Operator):
    bl_idname = "gp.select_same_color"
    bl_label = "Select Same Color"
    bl_description = "Select layers that have the same color as active\nSet with ctrl+clic"
    bl_options = {"REGISTER", "UNDO"}

    @classmethod
    def poll(cls, context):
        return context.object and context.object.type == 'GPENCIL'

    mode : EnumProperty(default='SELECT', options={'SKIP_SAVE'},
        items=(
                ('SELECT', "Select", "Select layer with same prefix as active"),
                ('SET', "Set", "Set prefix on selected layer to the same as active"),
            ),
        )

    def invoke(self, context, event):
        if event.ctrl:
            self.mode = 'SET'
        return self.execute(context)

    def execute(self, context):
        gp = context.object.data
        act = gp.layers.active

        pool = build_layers_targets_from_dopesheet(context)
        if not pool:
            self.report({'ERROR'}, 'No layers found in current GP dopesheet')
            return {"CANCELLED"}

        gp_dic = build_dope_gp_list(pool)
        if not act:
            # Check in other displayed layer if there is an active one
            for gp, _layer_list in gp_dic.items():
                if gp.layers.active:
                    # overwrite gp variable at the same time
                    act = gp.layers.active
                    break
            if not act:
                self.report({'ERROR'}, 'No active layer to base action')
                return {"CANCELLED"}

        print(f'Select/Set ref layer: {gp.name} > {gp.layers.active.info}')
        color = act.channel_color
        if self.mode == 'SELECT':
            ## NEED FOREACH TO APPLY SELECT

            ## Only on active object
            # same_color_bool = [l.channel_color == act.channel_color for l in gpl]
            # gpl.foreach_set('select', same_color_bool) # only

            # On multiple objects -- don't work, need foreach
            # for l in pool:
            #     print(l.id_data.name, l.info, l.channel_color == act.channel_color)
            #     l.select = l.channel_color == act.channel_color

            """
            gps = []
            for l in pool:
                if l.id_data not in gps:
                    gps.append(l.id_data)
            for gp in gps:
                same_color_bool = [(l in pool) and is_vector_close(l.channel_color, color) for l in gp.layers]
                gp.layers.foreach_set('select', same_color_bool)
             """
            for gp, layers in gp_dic.items():
                # check color and restrict selection to visible layers according to filters
                same_color_bool = [(l in layers) and is_vector_close(l.channel_color, color) for l in gp.layers]
                gp.layers.foreach_set('select', same_color_bool)

        elif self.mode == 'SET':
            activate_channel_group_color(context)
            for l in pool: # only on active object use gpl
                if not l.select or l == act:
                    continue
                l.channel_color = color

        refresh_areas()
        return {"FINISHED"}


def replace_layer_name(target, replacement, selected_only=True, prefix_only=True, regex=False):
    prefs = get_addon_prefs()
    sep = prefs.separator
    if not target:
        return

    gpl = bpy.context.object.data.layers

    if selected_only:
        lays = [l for l in gpl if l.select] # exclude : l.info != 'background'
    else:
        lays = [l for l in gpl] # exclude : if l.info != 'background'

    ct = 0
    for l in lays:
        old = l.info
        if regex:
            new = re.sub(target, replacement, l.info)
            if old != new:
                l.info = new
                print('rename:', old, '-->', new)
                ct += 1
            continue

        if prefix_only:
            if not sep in l.info:
                # only if separator exists
                continue
            splited = l.info.split(sep)
            prefix = splited[0]
            new_prefix = prefix.replace(target, replacement)
            if prefix != new_prefix:
                splited[0] = new_prefix
                l.info = sep.join(splited)
                print('rename:', old, '-->', l.info)
                ct += 1

        else:
            new = l.info.replace(target, replacement)
            if old != new:
                l.info = new
                print('rename:', old, '-->', new)
                ct += 1
    return ct

class GPTB_OT_rename_gp_layer(Operator):
    '''rename GP layers based on a search and replace'''
    bl_idname = "gp.rename_gp_layers"
    bl_label = "Rename Gp Layers"
    bl_description = "Search/Replace string in all GP layers"

    @classmethod
    def poll(cls, context):
        return context.object and context.object.type == 'GPENCIL'

    find: StringProperty(name="Find", description="Name to replace", default="", maxlen=0, options={'ANIMATABLE'}, subtype='NONE')
    replace: StringProperty(name="Repl", description="New name placed", default="", maxlen=0, options={'ANIMATABLE'}, subtype='NONE')
    selected: BoolProperty(name="Selected Only", description="Affect only selected layers", default=False)
    prefix: BoolProperty(name="Prefix Only", description="Affect only prefix of name (skip layer without separator in name)", default=False)
    use_regex: BoolProperty(name="Regex", description="use regular expression (advanced), equivalent to python re.sub()", default=False)

    def execute(self, context):
        count = replace_layer_name(self.find, self.replace, selected_only=self.selected, prefix_only=self.prefix, regex=self.use_regex)
        if count:
            mess = str(count) + ' layers renamed'
            self.report({'INFO'}, mess)
        else:
            self.report({'WARNING'}, 'No text found !')

        return{'FINISHED'}

    def invoke(self, context, event):
        return context.window_manager.invoke_props_dialog(self)

    def draw(self, context):
        layout = self.layout
        row = layout.row()
        row_a= row.row()
        row_a.prop(self, "selected")

        row_b= row.row()
        row_b.prop(self, "prefix")
        row_c= row.row()

        row_c.prop(self, "use_regex")
        row_b.active = not self.use_regex

        layout.prop(self, "find")
        layout.prop(self, "replace")


## --- UI layer panel---

def layer_name_builder_ui(self, context):
    '''appended to DATA_PT_gpencil_layers'''

    prefs = get_addon_prefs()
    if not prefs.show_prefix_buttons:
        return
    if not len(prefs.prefixes.namespaces) and not len(prefs.suffixes.namespaces):
        return

    layout = self.layout
    # {'EDIT_GPENCIL', 'PAINT_GPENCIL','SCULPT_GPENCIL','WEIGHT_GPENCIL', 'VERTEX_GPENCIL'}
    # layout.separator()
    col = layout.column()


    line_limit = 8

    if len(prefs.prefixes.namespaces):
        ct = 0
        # can't use enumerate cause there can be hided prefix
        for namespace in prefs.prefixes.namespaces:
            if namespace.hide:
                continue
            if ct % line_limit == 0:
                row = col.row(align=True)
            ct += 1
            op = row.operator("gp.layer_name_build", text=namespace.tag)
            op.prefix = namespace.tag
            op.tooltip = namespace.name

        if ct > 0:
            row.operator("gp.layer_name_build", text='', icon='X').prefix = 'prefixkillcode'

    ## old single string prefix method
    """
    if prefs.prefixes:
        p = prefs.prefixes.split(',')
        for i, prefix in enumerate(all_prefixes):
            if i % line_limit == 0:
                row = col.row(align=True)
            row.operator("gp.layer_name_build", text=prefix.upper() ).prefix = prefix
        row.operator("gp.layer_name_build", text='', icon='X').prefix = 'prefixkillcode'
    ## secondary prefix ?


    if prefs.suffixes:
        all_suffixes = prefs.suffixes.split(',')
        for i, suffix in enumerate(all_suffixes):
            if i % line_limit == 0:
                row = col.row(align=True)
            row.operator("gp.layer_name_build", text=suffix.upper() ).suffix = suffix
        row.operator("gp.layer_name_build", text='', icon='X').suffix = 'suffixkillcode'
    """

    ## name (description of layer content)
    row = col.row(align=True)
    row.prop(context.scene.gptoolprops, 'layer_name', text='')

    ## mimic groups using dash (disabled for now)
    # row.operator("gp.layer_new_group", text='', icon='COLLECTION_NEW')
    # row.operator("gp.layer_group_toggle", text='', icon='OUTLINER_OB_GROUP_INSTANCE')
    ## no need for desc ops, already trigerred from update
    # row.operator("gp.layer_name_build", text='', icon='EVENT_RETURN').desc = context.scene.gptoolprops.layer_name

    if len(prefs.suffixes.namespaces):
        ct = 0
        # can't use enumerate cause there can be hided prefix
        for namespace in prefs.suffixes.namespaces:
            if namespace.hide:
                continue
            if ct % line_limit == 0:
                row = col.row(align=True)
            ct += 1
            op = row.operator("gp.layer_name_build", text=namespace.tag)
            op.suffix = namespace.tag
            op.tooltip = namespace.name

        if ct > 0:
            row.operator("gp.layer_name_build", text='', icon='X').suffix = 'suffixkillcode'

## --- UI dopesheet ---

def gpencil_dopesheet_header(self, context):
    '''to append in DOPESHEET_HT_header'''
    layout = self.layout
    st = context.space_data
    if st.mode != 'GPENCIL':
        return

    row = layout.row(align=True)
    # row.operator('gp.active_channel_color_to_selected', text='', icon='RESTRICT_COLOR_ON')
    row.operator('gp.select_same_prefix', text='', icon='SYNTAX_OFF') # SORTALPHA, SMALL_CAPS
    row.operator('gp.select_same_color', text='', icon='RESTRICT_COLOR_ON')


## --- UI context menu ---

def gpencil_layer_dropdown_menu(self, context):
    '''to append in GPENCIL_MT_layer_context_menu'''
    self.layout.operator('gp.rename_gp_layers', icon='BORDERMOVE')

## handler and msgbus

def obj_layer_name_callback():
    '''assign layer name properties so user an tweak it'''
    ob = bpy.context.object
    if not ob or ob.type != 'GPENCIL':
        return
    if not ob.data.layers.active:
        return

    ## Set selection to active object ot avoid un-sync selection on Layers stack
    ## (happen when an objet is selected but not active with 'lock object mode')
    for l in ob.data.layers:
        l.select = l == ob.data.layers.active

    res = re.search(PATTERN, ob.data.layers.active.info.strip())
    if not res:
        return
    if not res.group('name'):
        return
    # print('grp:', res.group('grp'))
    # print('tag:', res.group('tag'))
    # print('name:', res.group('name'))
    # print('sfix:', res.group('sfix'))
    # print('inc:', res.group('inc'))
    bpy.context.scene.gptoolprops['layer_name'] = res.group('name')


def subscribe_layer_change():
    subscribe_to = (bpy.types.GreasePencilLayers, "active_index")
    bpy.msgbus.subscribe_rna(
        key=subscribe_to,
        # owner of msgbus subcribe (for clearing later)
        # owner=handle,
        owner=bpy.types.GreasePencil, # <-- can attach to an ID during all it's lifetime...
        # Args passed to callback function (tuple)
        args=(),
        # Callback function for property update
        notify=obj_layer_name_callback,
        options={'PERSISTENT'},
    )

@persistent
def subscribe_layer_change_handler(dummy):
    subscribe_layer_change()

##--- Add layers

class GPTB_PT_layer_name_ui(bpy.types.Panel):
    bl_space_type = 'TOPBAR'  # dummy
    bl_region_type = 'HEADER'
    bl_options = {'INSTANCED'}
    bl_label = 'Layer Rename'
    bl_ui_units_x = 14

    def invoke(self, context, event):
        # all_addons_l = get_modifier_list()
        wm = context.window_manager
        wm.invoke_props_dialog(self) # , width=600
        return {'FINISHED'}

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

        # def row_with_icon(layout, icon):
        #     # Edit first editable button in popup
        #     row = layout.row()
        #     row.activate_init = True
        #     row.label(icon=icon)
        #     return row
        # row = row_with_icon(layout, 'OUTLINER_DATA_GP_LAYER')

        row = layout.row()
        row.activate_init = True
        row.label(icon='OUTLINER_DATA_GP_LAYER')
        row.prop(context.object.data.layers.active, 'info', text='')

def add_layer(context):
    bpy.ops.gpencil.layer_add()
    context.object.data.layers.active.use_lights = False

class GPTB_OT_add_gp_layer_with_rename(Operator):
    bl_idname = "gp.add_layer_rename"
    bl_label = "Add Rename GPencil Layer"
    bl_description = "Create a new gp layer with use light toggled off and popup a rename box"
    bl_options = {"REGISTER", "UNDO"}
    @classmethod
    def poll(cls, context):
        return context.object and context.object.type == 'GPENCIL'

    def execute(self, context):
        add_layer(context)
        bpy.ops.wm.call_panel(name="GPTB_PT_layer_name_ui", keep_open = False)
        return {"FINISHED"}

class GPTB_OT_add_gp_layer(Operator):
    bl_idname = "gp.add_layer"
    bl_label = "Add GPencil Layer"
    bl_description = "Create a new gp layer with use light toggled off"
    bl_options = {"REGISTER", "UNDO"}
    @classmethod
    def poll(cls, context):
        return context.object and context.object.type == 'GPENCIL'

    def execute(self, context):
        add_layer(context)
        return {"FINISHED"}


addon_keymaps = []
def register_keymaps():
    if bpy.app.background:
        return
    addon = bpy.context.window_manager.keyconfigs.addon

    ##---# Insert Layers
    ## Insert new gp layer (with no use_light)
    km = addon.keymaps.new(name = "Grease Pencil", space_type = "EMPTY") # global (only paint ?)
    kmi = km.keymap_items.new('gp.add_layer', type='INSERT', value='PRESS')
    addon_keymaps.append((km, kmi))

    ## Insert new gp layer (with no use_light and immediately pop up a box to rename)
    # km = addon.keymaps.new(name = "Grease Pencil", space_type = "EMPTY") # global (only paint ?)
    kmi = km.keymap_items.new('gp.add_layer_rename', type='INSERT', value='PRESS', shift=True)
    addon_keymaps.append((km, kmi))

    ##---# F2 rename calls
    ## Direct rename active layer in Paint mode
    km = addon.keymaps.new(name = "Grease Pencil Stroke Paint Mode", space_type = "EMPTY")
    kmi = km.keymap_items.new('wm.call_panel', type='F2', value='PRESS')
    kmi.properties.name = 'GPTB_PT_layer_name_ui'
    kmi.properties.keep_open = False
    addon_keymaps.append((km, kmi))

    ## Same in edit mode
    km = addon.keymaps.new(name = "Grease Pencil Stroke Edit Mode", space_type = "EMPTY")
    kmi = km.keymap_items.new('wm.call_panel', type='F2', value='PRESS')
    kmi.properties.name = 'GPTB_PT_layer_name_ui'
    kmi.properties.keep_open = False
    addon_keymaps.append((km, kmi))


def unregister_keymaps():
    if bpy.app.background:
        return
    for km, kmi in addon_keymaps:
        km.keymap_items.remove(kmi)
    addon_keymaps.clear()



classes=(
    GPTB_OT_rename_gp_layer,
    GPTB_OT_layer_name_build,
    GPTB_OT_layer_group_toggle,
    GPTB_OT_layer_new_group,
    GPTB_OT_select_set_same_prefix,
    GPTB_OT_select_set_same_color,

    ## Layer add and pop-up rename
    GPTB_PT_layer_name_ui, # pop-up
    GPTB_OT_add_gp_layer_with_rename, # shift+Ins
    GPTB_OT_add_gp_layer, # Ins
)

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

    bpy.types.DATA_PT_gpencil_layers.prepend(layer_name_builder_ui)
    bpy.types.DOPESHEET_HT_header.append(gpencil_dopesheet_header)
    bpy.types.GPENCIL_MT_layer_context_menu.append(gpencil_layer_dropdown_menu)
    bpy.app.handlers.load_post.append(subscribe_layer_change_handler)
    register_keymaps()

    # Directly set msgbus to work at first addon activation
    bpy.app.timers.register(subscribe_layer_change, first_interval=1)

def unregister():
    unregister_keymaps()
    bpy.app.handlers.load_post.remove(subscribe_layer_change_handler)
    bpy.types.GPENCIL_MT_layer_context_menu.remove(gpencil_layer_dropdown_menu)
    bpy.types.DOPESHEET_HT_header.remove(gpencil_dopesheet_header)
    bpy.types.DATA_PT_gpencil_layers.remove(layer_name_builder_ui)

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

    # delete layer index trigger
    bpy.msgbus.clear_by_owner(bpy.types.GreasePencil)