# SPDX-FileCopyrightText: 2010-2023 Blender Foundation
#
# SPDX-License-Identifier: GPL-2.0-or-later

bl_info = {
    "name": "Copy Attributes Menu",
    "author": "Bassam Kurdali, Fabian Fricke, Adam Wiseman, Demeter Dzadik",
    "version": (0, 6, 0),
    "blender": (3, 4, 0),
    "location": "View3D > Ctrl-C",
    "description": "Copy Attributes Menu",
    "doc_url": "{BLENDER_MANUAL_URL}/addons/interface/copy_attributes.html",
    "category": "Interface",
}

import bpy
from mathutils import Matrix
from bpy.types import (
    Operator,
    Menu,
)
from bpy.props import (
    BoolVectorProperty,
    StringProperty,
)

# First part of the operator Info message
INFO_MESSAGE = "Copy Attributes: "


def build_exec(loopfunc, func):
    """Generator function that returns exec functions for operators """

    def exec_func(self, context):
        loopfunc(self, context, func)
        return {'FINISHED'}
    return exec_func


def build_invoke(loopfunc, func):
    """Generator function that returns invoke functions for operators"""

    def invoke_func(self, context, event):
        loopfunc(self, context, func)
        return {'FINISHED'}
    return invoke_func


def build_op(idname, label, description, fpoll, fexec, finvoke):
    """Generator function that returns the basic operator"""

    class myopic(Operator):
        bl_idname = idname
        bl_label = label
        bl_description = description
        execute = fexec
        poll = fpoll
        invoke = finvoke
    return myopic


def genops(copylist, oplist, prefix, poll_func, loopfunc):
    """Generate ops from the copy list and its associated functions"""
    for op in copylist:
        exec_func = build_exec(loopfunc, op[3])
        invoke_func = build_invoke(loopfunc, op[3])
        opclass = build_op(prefix + op[0], "Copy " + op[1], op[2],
                           poll_func, exec_func, invoke_func)
        oplist.append(opclass)


def generic_copy(source, target, string=""):
    """Copy attributes from source to target that have string in them"""
    for attr in dir(source):
        if attr.find(string) > -1:
            try:
                setattr(target, attr, getattr(source, attr))
            except:
                pass
    return


def getmat(bone, active, context, ignoreparent):
    """Helper function for visual transform copy,
       gets the active transform in bone space
    """
    obj_bone = bone.id_data
    obj_active = active.id_data
    data_bone = obj_bone.data.bones[bone.name]
    # all matrices are in armature space unless commented otherwise
    active_to_selected = obj_bone.matrix_world.inverted() @ obj_active.matrix_world
    active_matrix = active_to_selected @ active.matrix
    otherloc = active_matrix  # final 4x4 mat of target, location.
    bonemat_local = data_bone.matrix_local.copy()  # self rest matrix
    if data_bone.parent:
        parentposemat = obj_bone.pose.bones[data_bone.parent.name].matrix.copy()
        parentbonemat = data_bone.parent.matrix_local.copy()
    else:
        parentposemat = parentbonemat = Matrix()
    if parentbonemat == parentposemat or ignoreparent:
        newmat = bonemat_local.inverted() @ otherloc
    else:
        bonemat = parentbonemat.inverted() @ bonemat_local

        newmat = bonemat.inverted() @ parentposemat.inverted() @ otherloc
    return newmat


def rotcopy(item, mat):
    """Copy rotation to item from matrix mat depending on item.rotation_mode"""
    if item.rotation_mode == 'QUATERNION':
        item.rotation_quaternion = mat.to_3x3().to_quaternion()
    elif item.rotation_mode == 'AXIS_ANGLE':
        rot = mat.to_3x3().to_quaternion().to_axis_angle()    # returns (Vector((x, y, z)), w)
        axis_angle = rot[1], rot[0][0], rot[0][1], rot[0][2]  # convert to w, x, y, z
        item.rotation_axis_angle = axis_angle
    else:
        item.rotation_euler = mat.to_3x3().to_euler(item.rotation_mode)


def pLoopExec(self, context, funk):
    """Loop over selected bones and execute funk on them"""
    active = context.active_pose_bone
    selected = context.selected_pose_bones
    selected.remove(active)
    for bone in selected:
        funk(bone, active, context)


# The following functions are used to copy attributes from active to bone

def pLocLocExec(bone, active, context):
    bone.location = active.location


def pLocRotExec(bone, active, context):
    rotcopy(bone, active.matrix_basis.to_3x3())


def pLocScaExec(bone, active, context):
    bone.scale = active.scale


def pVisLocExec(bone, active, context):
    bone.location = getmat(bone, active, context, False).to_translation()


def pVisRotExec(bone, active, context):
    obj_bone = bone.id_data
    rotcopy(bone, getmat(bone, active,
                         context, not obj_bone.data.bones[bone.name].use_inherit_rotation))


def pVisScaExec(bone, active, context):
    obj_bone = bone.id_data
    bone.scale = getmat(
        bone, active, context,
        obj_bone.data.bones[bone.name].inherit_scale not in {'NONE', 'NONE_LEGACY'}
    ).to_scale()


def pDrwExec(bone, active, context):
    bone.custom_shape = active.custom_shape
    bone.use_custom_shape_bone_size = active.use_custom_shape_bone_size
    bone.custom_shape_translation = active.custom_shape_translation
    bone.custom_shape_rotation_euler = active.custom_shape_rotation_euler
    bone.custom_shape_scale_xyz = active.custom_shape_scale_xyz
    bone.bone.show_wire = active.bone.show_wire


def pLokExec(bone, active, context):
    for index, state in enumerate(active.lock_location):
        bone.lock_location[index] = state
    for index, state in enumerate(active.lock_rotation):
        bone.lock_rotation[index] = state
    bone.lock_rotations_4d = active.lock_rotations_4d
    bone.lock_rotation_w = active.lock_rotation_w
    for index, state in enumerate(active.lock_scale):
        bone.lock_scale[index] = state


def pConExec(bone, active, context):
    for old_constraint in active.constraints.values():
        new_constraint = bone.constraints.new(old_constraint.type)
        generic_copy(old_constraint, new_constraint)


def pIKsExec(bone, active, context):
    generic_copy(active, bone, "ik_")


def pBBonesExec(bone, active, context):
    object = active.id_data
    generic_copy(
        object.data.bones[active.name],
        object.data.bones[bone.name],
        "bbone_")


pose_copies = (
    ('pose_loc_loc', "Local Location",
     "Copy Location from Active to Selected", pLocLocExec),
    ('pose_loc_rot', "Local Rotation",
     "Copy Rotation from Active to Selected", pLocRotExec),
    ('pose_loc_sca', "Local Scale",
     "Copy Scale from Active to Selected", pLocScaExec),
    ('pose_vis_loc', "Visual Location",
     "Copy Location from Active to Selected", pVisLocExec),
    ('pose_vis_rot', "Visual Rotation",
     "Copy Rotation from Active to Selected", pVisRotExec),
    ('pose_vis_sca', "Visual Scale",
     "Copy Scale from Active to Selected", pVisScaExec),
    ('pose_drw', "Bone Shape",
     "Copy Bone Shape from Active to Selected", pDrwExec),
    ('pose_lok', "Protected Transform",
     "Copy Protected Transforms from Active to Selected", pLokExec),
    ('pose_con', "Bone Constraints",
     "Copy Object Constraints from Active to Selected", pConExec),
    ('pose_iks', "IK Limits",
     "Copy IK Limits from Active to Selected", pIKsExec),
    ('bbone_settings', "BBone Settings",
     "Copy BBone Settings from Active to Selected", pBBonesExec),
)

@classmethod
def pose_poll_func(cls, context):
    return(context.mode == 'POSE')


def pose_invoke_func(self, context, event):
    wm = context.window_manager
    wm.invoke_props_dialog(self)
    return {'RUNNING_MODAL'}


CustomPropSelectionBoolsProperty = BoolVectorProperty(
    size=32,
    options={'SKIP_SAVE'}
)

class CopySelection:
    """Base class for copying properties from active to selected based on a selection."""

    selection: CustomPropSelectionBoolsProperty

    def draw_bools(self, button_names):
        """Draws the boolean toggle list with a list of strings for the button texts."""
        layout = self.layout
        for idx, name in enumerate(button_names):
            layout.prop(self, "selection", index=idx, text=name,
                        toggle=True)

def copy_custom_property(source, destination, prop_name):
    """Copy a custom property called prop_name, from source to destination.
    source and destination must be a Blender data type that can hold custom properties.
    For a list of such data types, see:
    https://docs.blender.org/manual/en/latest/files/data_blocks.html#files-data-blocks-custom-properties
    """

    # Create the property.
    destination[prop_name] = source[prop_name]
    # Copy the settings of the property.
    try:
        dst_prop_manager = destination.id_properties_ui(prop_name)
    except TypeError:
        # Python values like lists or dictionaries don't have any settings to copy.
        # They just consist of a value and nothing else.
        return

    src_prop_manager = source.id_properties_ui(prop_name)
    assert src_prop_manager, f'Property "{prop_name}" not found in {source}'

    dst_prop_manager.update_from(src_prop_manager)

    # Copy the Library Overridable flag, which is stored elsewhere.
    prop_rna_path = f'["{prop_name}"]'
    is_lib_overridable = source.is_property_overridable_library(prop_rna_path)
    destination.property_overridable_library_set(prop_rna_path, is_lib_overridable)

class CopyCustomProperties(CopySelection):
    """Base class for copying a selection of custom properties."""

    def copy_selected_custom_props(self, active, selected):
        keys = list(active.keys())
        for item in selected:
            if item == active:
                continue
            for index, is_selected in enumerate(self.selection):
                if is_selected:
                    copy_custom_property(active, item, keys[index])

class CopySelectedBoneCustomProperties(CopyCustomProperties, Operator):
    """Copy Chosen custom properties from active to selected"""
    bl_idname = "pose.copy_selected_custom_props"
    bl_label = "Copy Selected Custom Properties"
    bl_options = {'REGISTER', 'UNDO'}

    poll = pose_poll_func
    invoke = pose_invoke_func

    def draw(self, context):
        self.draw_bools(context.active_pose_bone.keys())

    def execute(self, context):
        self.copy_selected_custom_props(context.active_pose_bone, context.selected_pose_bones)
        return {'FINISHED'}


class CopySelectedPoseConstraints(Operator):
    """Copy Chosen constraints from active to selected"""
    bl_idname = "pose.copy_selected_constraints"
    bl_label = "Copy Selected Constraints"

    selection: BoolVectorProperty(
        size=32,
        options={'SKIP_SAVE'}
    )

    poll = pose_poll_func
    invoke = pose_invoke_func

    def draw(self, context):
        layout = self.layout
        for idx, const in enumerate(context.active_pose_bone.constraints):
            layout.prop(self, "selection", index=idx, text=const.name,
                        toggle=True)

    def execute(self, context):
        active = context.active_pose_bone
        selected = context.selected_pose_bones[:]
        selected.remove(active)
        for bone in selected:
            for index, flag in enumerate(self.selection):
                if flag:
                    bone.constraints.copy(active.constraints[index])
        return {'FINISHED'}


pose_ops = []  # list of pose mode copy operators
genops(pose_copies, pose_ops, "pose.copy_", pose_poll_func, pLoopExec)


class VIEW3D_MT_posecopypopup(Menu):
    bl_label = "Copy Attributes"

    def draw(self, context):
        layout = self.layout
        layout.operator_context = 'INVOKE_REGION_WIN'
        for op in pose_copies:
            layout.operator("pose.copy_" + op[0])
        layout.operator("pose.copy_selected_constraints")
        layout.operator("pose.copy_selected_custom_props")
        layout.operator("pose.copy", text="copy pose")


def obLoopExec(self, context, funk):
    """Loop over selected objects and execute funk on them"""
    active = context.active_object
    selected = context.selected_objects[:]
    selected.remove(active)
    for obj in selected:
        msg = funk(obj, active, context)
    if msg:
        self.report({msg[0]}, INFO_MESSAGE + msg[1])


def world_to_basis(active, ob, context):
    """put world coords of active as basis coords of ob"""
    local = ob.parent.matrix_world.inverted() @ active.matrix_world
    P = ob.matrix_basis @ ob.matrix_local.inverted()
    mat = P @ local
    return(mat)


# The following functions are used to copy attributes from
# active to selected object

def obLoc(ob, active, context):
    ob.location = active.location


def obRot(ob, active, context):
    rotcopy(ob, active.matrix_local.to_3x3())


def obSca(ob, active, context):
    ob.scale = active.scale


def obVisLoc(ob, active, context):
    if ob.parent:
        mat = world_to_basis(active, ob, context)
        ob.location = mat.to_translation()
    else:
        ob.location = active.matrix_world.to_translation()
    return('INFO', "Object location copied")


def obVisRot(ob, active, context):
    if ob.parent:
        mat = world_to_basis(active, ob, context)
        rotcopy(ob, mat.to_3x3())
    else:
        rotcopy(ob, active.matrix_world.to_3x3())
    return('INFO', "Object rotation copied")


def obVisSca(ob, active, context):
    if ob.parent:
        mat = world_to_basis(active, ob, context)
        ob.scale = mat.to_scale()
    else:
        ob.scale = active.matrix_world.to_scale()
    return('INFO', "Object scale copied")


def obDrw(ob, active, context):
    ob.display_type = active.display_type
    ob.show_axis = active.show_axis
    ob.show_bounds = active.show_bounds
    ob.display_bounds_type = active.display_bounds_type
    ob.show_name = active.show_name
    ob.show_texture_space = active.show_texture_space
    ob.show_transparent = active.show_transparent
    ob.show_wire = active.show_wire
    ob.show_in_front = active.show_in_front
    ob.empty_display_type = active.empty_display_type
    ob.empty_display_size = active.empty_display_size


def obDup(ob, active, context):
    generic_copy(active, ob, "instance_type")
    return('INFO', "Duplication method copied")


def obCol(ob, active, context):
    ob.color = active.color


def obLok(ob, active, context):
    for index, state in enumerate(active.lock_location):
        ob.lock_location[index] = state
    for index, state in enumerate(active.lock_rotation):
        ob.lock_rotation[index] = state
    ob.lock_rotations_4d = active.lock_rotations_4d
    ob.lock_rotation_w = active.lock_rotation_w
    for index, state in enumerate(active.lock_scale):
        ob.lock_scale[index] = state
    return('INFO', "Transform locks copied")


def obCon(ob, active, context):
    # for consistency with 2.49, delete old constraints first
    for removeconst in ob.constraints:
        ob.constraints.remove(removeconst)
    for old_constraint in active.constraints.values():
        new_constraint = ob.constraints.new(old_constraint.type)
        generic_copy(old_constraint, new_constraint)
    return('INFO', "Constraints copied")


def obTex(ob, active, context):
    if 'texspace_location' in dir(ob.data) and 'texspace_location' in dir(
       active.data):
        ob.data.texspace_location[:] = active.data.texspace_location[:]
    if 'texspace_size' in dir(ob.data) and 'texspace_size' in dir(active.data):
        ob.data.texspace_size[:] = active.data.texspace_size[:]
    return('INFO', "Texture space copied")


def obIdx(ob, active, context):
    ob.pass_index = active.pass_index
    return('INFO', "Pass index copied")


def obMod(ob, active, context):
    for modifier in ob.modifiers:
        # remove existing before adding new:
        ob.modifiers.remove(modifier)
    for old_modifier in active.modifiers.values():
        new_modifier = ob.modifiers.new(name=old_modifier.name,
                                        type=old_modifier.type)
        generic_copy(old_modifier, new_modifier)
    return('INFO', "Modifiers copied")


def obCollections(ob, active, context):
    for collection in bpy.data.collections:
        if active.name in collection.objects and ob.name not in collection.objects:
            collection.objects.link(ob)
    return('INFO', "Collections copied")


def obWei(ob, active, context):
    # sanity check: are source and target both mesh objects?
    if ob.type != 'MESH' or active.type != 'MESH':
        return('ERROR', "objects have to be of mesh type, doing nothing")
    me_source = active.data
    me_target = ob.data
    # sanity check: do source and target have the same amount of verts?
    if len(me_source.vertices) != len(me_target.vertices):
        return('ERROR', "objects have different vertex counts, doing nothing")
    vgroups_IndexName = {}
    for i in range(0, len(active.vertex_groups)):
        groups = active.vertex_groups[i]
        vgroups_IndexName[groups.index] = groups.name
    data = {}  # vert_indices, [(vgroup_index, weights)]
    for v in me_source.vertices:
        vg = v.groups
        vi = v.index
        if len(vg) > 0:
            vgroup_collect = []
            for i in range(0, len(vg)):
                vgroup_collect.append((vg[i].group, vg[i].weight))
            data[vi] = vgroup_collect
    # write data to target
    if ob != active:
        # add missing vertex groups
        for vgroup_name in vgroups_IndexName.values():
            # check if group already exists...
            already_present = 0
            for i in range(0, len(ob.vertex_groups)):
                if ob.vertex_groups[i].name == vgroup_name:
                    already_present = 1
            # ... if not, then add
            if already_present == 0:
                ob.vertex_groups.new(name=vgroup_name)
        # write weights
        for v in me_target.vertices:
            for vi_source, vgroupIndex_weight in data.items():
                if v.index == vi_source:

                    for i in range(0, len(vgroupIndex_weight)):
                        groupName = vgroups_IndexName[vgroupIndex_weight[i][0]]
                        groups = ob.vertex_groups
                        for vgs in range(0, len(groups)):
                            if groups[vgs].name == groupName:
                                groups[vgs].add((v.index,),
                                                vgroupIndex_weight[i][1], "REPLACE")
    return('INFO', "Weights copied")


object_copies = (
    # ('obj_loc', "Location",
    # "Copy Location from Active to Selected", obLoc),
    # ('obj_rot', "Rotation",
    # "Copy Rotation from Active to Selected", obRot),
    # ('obj_sca', "Scale",
    # "Copy Scale from Active to Selected", obSca),
    ('obj_vis_loc', "Location",
     "Copy Location from Active to Selected", obVisLoc),
    ('obj_vis_rot', "Rotation",
     "Copy Rotation from Active to Selected", obVisRot),
    ('obj_vis_sca', "Scale",
     "Copy Scale from Active to Selected", obVisSca),
    ('obj_drw', "Draw Options",
     "Copy Draw Options from Active to Selected", obDrw),
    ('obj_dup', "Instancing",
     "Copy instancing properties from Active to Selected", obDup),
    ('obj_col', "Object Color",
     "Copy Object Color from Active to Selected", obCol),
    # ('obj_dmp', "Damping",
    # "Copy Damping from Active to Selected"),
    # ('obj_all', "All Physical Attributes",
    # "Copy Physical Attributes from Active to Selected"),
    # ('obj_prp', "Properties",
    # "Copy Properties from Active to Selected"),
    ('obj_lok', "Protected Transform",
     "Copy Protected Transforms from Active to Selected", obLok),
    ('obj_con', "Object Constraints",
     "Copy Object Constraints from Active to Selected", obCon),
    # ('obj_nla', "NLA Strips",
    # "Copy NLA Strips from Active to Selected"),
    # ('obj_tex', "Texture Space",
    # "Copy Texture Space from Active to Selected", obTex),
    # ('obj_sub', "Subdivision Surface Settings",
    # "Copy Subdivision Surface Settings from Active to Selected"),
    # ('obj_smo', "AutoSmooth",
    # "Copy AutoSmooth from Active to Selected"),
    ('obj_idx', "Pass Index",
     "Copy Pass Index from Active to Selected", obIdx),
    ('obj_mod', "Modifiers",
     "Copy Modifiers from Active to Selected", obMod),
    ('obj_wei', "Vertex Weights",
     "Copy vertex weights based on indices", obWei),
    ('obj_grp', "Collection Links",
     "Copy selected into active object's collection", obCollections)
)


@classmethod
def object_poll_func(cls, context):
    return (len(context.selected_objects) > 1)


def object_invoke_func(self, context, event):
    wm = context.window_manager
    wm.invoke_props_dialog(self)
    return {'RUNNING_MODAL'}


class CopySelectedObjectConstraints(Operator):
    """Copy Chosen constraints from active to selected"""
    bl_idname = "object.copy_selected_constraints"
    bl_label = "Copy Selected Constraints"

    selection: BoolVectorProperty(
        size=32,
        options={'SKIP_SAVE'}
    )

    poll = object_poll_func
    invoke = object_invoke_func

    def draw(self, context):
        layout = self.layout
        for idx, const in enumerate(context.active_object.constraints):
            layout.prop(self, "selection", index=idx, text=const.name,
                        toggle=True)

    def execute(self, context):
        active = context.active_object
        selected = context.selected_objects[:]
        selected.remove(active)
        for obj in selected:
            for index, flag in enumerate(self.selection):
                if flag:
                    obj.constraints.copy(active.constraints[index])
        return{'FINISHED'}


class CopySelectedObjectModifiers(Operator):
    """Copy Chosen modifiers from active to selected"""
    bl_idname = "object.copy_selected_modifiers"
    bl_label = "Copy Selected Modifiers"

    selection: BoolVectorProperty(
        size=32,
        options={'SKIP_SAVE'}
    )

    poll = object_poll_func
    invoke = object_invoke_func

    def draw(self, context):
        layout = self.layout
        for idx, const in enumerate(context.active_object.modifiers):
            layout.prop(self, 'selection', index=idx, text=const.name,
                        toggle=True)

    def execute(self, context):
        active = context.active_object
        selected = context.selected_objects[:]
        selected.remove(active)
        for obj in selected:
            for index, flag in enumerate(self.selection):
                if flag:
                    old_modifier = active.modifiers[index]
                    new_modifier = obj.modifiers.new(
                        type=active.modifiers[index].type,
                        name=active.modifiers[index].name
                    )
                    generic_copy(old_modifier, new_modifier)
        return{'FINISHED'}


class CopySelectedObjectCustomProperties(CopyCustomProperties, Operator):
    """Copy Chosen custom properties from active to selected objects"""
    bl_idname = "object.copy_selected_custom_props"
    bl_label = "Copy Selected Custom Properties"
    bl_options = {'REGISTER', 'UNDO'}

    poll = object_poll_func
    invoke = object_invoke_func

    def draw(self, context):
        self.draw_bools(context.object.keys())

    def execute(self, context):
        self.copy_selected_custom_props(context.object, context.selected_objects)
        return {'FINISHED'}

object_ops = []
genops(object_copies, object_ops, "object.copy_", object_poll_func, obLoopExec)


class VIEW3D_MT_copypopup(Menu):
    bl_label = "Copy Attributes"

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

        layout.operator_context = 'INVOKE_REGION_WIN'
        layout.operator("view3d.copybuffer", icon="COPY_ID")

        if (len(context.selected_objects) <= 1):
            layout.separator()
            layout.label(text="Please select at least two objects", icon="INFO")
            layout.separator()

        for entry, op in enumerate(object_copies):
            if entry and entry % 4 == 0:
                layout.separator()
            layout.operator("object.copy_" + op[0])
        layout.operator("object.copy_selected_constraints")
        layout.operator("object.copy_selected_modifiers")
        layout.operator("object.copy_selected_custom_props")


# Begin Mesh copy settings:

class MESH_MT_CopyFaceSettings(Menu):
    bl_label = "Copy Face Settings"

    @classmethod
    def poll(cls, context):
        return context.mode == 'EDIT_MESH'

    def draw(self, context):
        mesh = context.object.data
        uv = len(mesh.uv_layers) > 1
        vc = len(mesh.vertex_colors) > 1

        layout = self.layout

        op = layout.operator("mesh.copy_face_settings", text="Copy Material")
        op['layer'] = ''
        op['mode'] = 'MAT'

        if mesh.uv_layers.active:
            op = layout.operator("mesh.copy_face_settings", text="Copy Active UV Coords")
            op['layer'] = ''
            op['mode'] = 'UV'

        if mesh.vertex_colors.active:
            op = layout.operator("mesh.copy_face_settings", text="Copy Active Vertex Colors")
            op['layer'] = ''
            op['mode'] = 'VCOL'

        if uv or vc:
            layout.separator()
            if uv:
                layout.menu("MESH_MT_CopyUVCoordsFromLayer")
            if vc:
                layout.menu("MESH_MT_CopyVertexColorsFromLayer")


# Data (UV map, Image and Vertex color) menus calling MESH_OT_CopyFaceSettings
# Explicitly defined as using the generator code was broken in case of Menus
# causing issues with access and registration


class MESH_MT_CopyUVCoordsFromLayer(Menu):
    bl_label = "Copy UV Coordinates from Layer"

    @classmethod
    def poll(cls, context):
        obj = context.active_object
        return obj and obj.mode == "EDIT_MESH" and len(
            obj.data.uv_layers) > 1

    def draw(self, context):
        mesh = context.active_object.data
        _buildmenu(self, mesh, 'UV', "GROUP_UVS")


class MESH_MT_CopyVertexColorsFromLayer(Menu):
    bl_label = "Copy Vertex Colors from Layer"

    @classmethod
    def poll(cls, context):
        obj = context.active_object
        return obj and obj.mode == "EDIT_MESH" and len(
            obj.data.vertex_colors) > 1

    def draw(self, context):
        mesh = context.active_object.data
        _buildmenu(self, mesh, 'VCOL', "GROUP_VCOL")


def _buildmenu(self, mesh, mode, icon):
    layout = self.layout
    if mode == 'VCOL':
        layers = mesh.vertex_colors
    else:
        layers = mesh.uv_layers
    for layer in layers:
        if not layer.active:
            op = layout.operator("mesh.copy_face_settings",
                                 text=layer.name, icon=icon)
            op['layer'] = layer.name
            op['mode'] = mode


class MESH_OT_CopyFaceSettings(Operator):
    """Copy settings from active face to all selected faces"""
    bl_idname = 'mesh.copy_face_settings'
    bl_label = "Copy Face Settings"
    bl_options = {'REGISTER', 'UNDO'}

    mode: StringProperty(
        name="Mode",
        options={"HIDDEN"},
    )
    layer: StringProperty(
        name="Layer",
        options={"HIDDEN"},
    )

    @classmethod
    def poll(cls, context):
        return context.mode == 'EDIT_MESH'

    def execute(self, context):
        mode = getattr(self, 'mode', '')
        if mode not in {'MAT', 'VCOL', 'UV'}:
            self.report({'ERROR'}, "No mode specified or invalid mode")
            return self._end(context, {'CANCELLED'})
        layername = getattr(self, 'layer', '')
        mesh = context.object.data

        # Switching out of edit mode updates the selected state of faces and
        # makes the data from the uv texture and vertex color layers available.
        bpy.ops.object.editmode_toggle()

        polys = mesh.polygons
        if mode == 'MAT':
            to_data = from_data = polys
        else:
            if mode == 'VCOL':
                layers = mesh.vertex_colors
                act_layer = mesh.vertex_colors.active
            elif mode == 'UV':
                layers = mesh.uv_layers
                act_layer = mesh.uv_layers.active
            if not layers or (layername and layername not in layers):
                self.report({'ERROR'}, "Invalid UV or color layer. Operation Cancelled")
                return self._end(context, {'CANCELLED'})
            from_data = layers[layername or act_layer.name].data
            to_data = act_layer.data
        from_index = polys.active

        for f in polys:
            if f.select:
                if to_data != from_data:
                    # Copying from another layer.
                    # from_face is to_face's counterpart from other layer.
                    from_index = f.index
                elif f.index == from_index:
                    # Otherwise skip copying a face to itself.
                    continue
                if mode == 'MAT':
                    f.material_index = polys[from_index].material_index
                    continue
                if len(f.loop_indices) != len(polys[from_index].loop_indices):
                    self.report({'WARNING'}, "Different number of vertices.")
                for i in range(len(f.loop_indices)):
                    to_vertex = f.loop_indices[i]
                    from_vertex = polys[from_index].loop_indices[i]
                    if mode == 'VCOL':
                        to_data[to_vertex].color = from_data[from_vertex].color
                    elif mode == 'UV':
                        to_data[to_vertex].uv = from_data[from_vertex].uv

        return self._end(context, {'FINISHED'})

    def _end(self, context, retval):
        if context.mode != 'EDIT_MESH':
            # Clean up by returning to edit mode like it was before.
            bpy.ops.object.editmode_toggle()
        return(retval)


classes = (
    CopySelectedPoseConstraints,
    CopySelectedBoneCustomProperties,
    VIEW3D_MT_posecopypopup,
    CopySelectedObjectConstraints,
    CopySelectedObjectModifiers,
    CopySelectedObjectCustomProperties,
    VIEW3D_MT_copypopup,
    MESH_MT_CopyFaceSettings,
    MESH_MT_CopyUVCoordsFromLayer,
    MESH_MT_CopyVertexColorsFromLayer,
    MESH_OT_CopyFaceSettings,
    *pose_ops,
    *object_ops,
)

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

    # mostly to get the keymap working
    kc = bpy.context.window_manager.keyconfigs.addon
    if kc:
        km = kc.keymaps.new(name="Object Mode")
        kmi = km.keymap_items.new('wm.call_menu', 'C', 'PRESS', ctrl=True)
        kmi.properties.name = 'VIEW3D_MT_copypopup'

        km = kc.keymaps.new(name="Pose")
        kmi = km.keymap_items.get("pose.copy")
        if kmi is not None:
            kmi.idname = 'wm.call_menu'
        else:
            kmi = km.keymap_items.new('wm.call_menu', 'C', 'PRESS', ctrl=True)
        kmi.properties.name = 'VIEW3D_MT_posecopypopup'

        km = kc.keymaps.new(name="Mesh")
        kmi = km.keymap_items.new('wm.call_menu', 'C', 'PRESS')
        kmi.ctrl = True
        kmi.properties.name = 'MESH_MT_CopyFaceSettings'


def unregister():
    # mostly to remove the keymap
    kc = bpy.context.window_manager.keyconfigs.addon
    if kc:
        kms = kc.keymaps.get('Pose')
        if kms is not None:
            for item in kms.keymap_items:
                if item.name == 'Call Menu' and item.idname == 'wm.call_menu' and \
                   item.properties.name == 'VIEW3D_MT_posecopypopup':
                    item.idname = 'pose.copy'
                    break

        km = kc.keymaps.get('Mesh')
        if km is not None:
            for kmi in km.keymap_items:
                if kmi.idname == 'wm.call_menu':
                    if kmi.properties.name == 'MESH_MT_CopyFaceSettings':
                        km.keymap_items.remove(kmi)

        km = kc.keymaps.get('Object Mode')
        if km is not None:
            for kmi in km.keymap_items:
                if kmi.idname == 'wm.call_menu':
                    if kmi.properties.name == 'VIEW3D_MT_copypopup':
                        km.keymap_items.remove(kmi)

    from bpy.utils import unregister_class
    for cls in classes:
        unregister_class(cls)


if __name__ == "__main__":
    register()
