"""Utility operator for quickly editing UV Map names and syncronising with the active object."""

from uuid import uuid4

import bpy
from bpy.props import BoolProperty, CollectionProperty
from bpy.types import Context, Event, Object, PropertyGroup, UILayout

from rizomuvbridge_ultimate.functions.bpy_funcs.validation import (
    repair_uvmap_name,
    validate_uvmap_name,
)


class EditUVMaps(bpy.types.Operator):
    """Utility operator for quickly editing UV Map names and syncronising with the active object."""

    bl_description = "Quickly batch edit UV Maps"
    bl_idname = "ruv.edit_uvmaps"
    bl_label = "Edit/Sync UV Map Names"
    bl_options = {"REGISTER", "INTERNAL", "UNDO"}

    auto_fix_names: BoolProperty(
        name="Auto Fix UV Map Names",
        default=True,
        description="Automatically replace any characters that can cause problems with RizomUV",
    )
    remove_extra_uvmaps: BoolProperty(
        name="Remove Extra UV Maps",
        default=True,
        description="Remove UV maps that are not present on the active object",
    )
    add_missing_uvmaps: BoolProperty(
        name="Add Missing UV Maps",
        default=True,
        description="Add missing UV maps that are present on the active object",
    )

    uv_names: CollectionProperty(name="UV Maps", type=PropertyGroup)

    @classmethod
    def poll(cls, context: Context) -> bool:
        """Check if there is an active mesh object.

        Returns
        -------
            True if there is an active mesh object

        """
        return context.active_object is not None and context.active_object.type == "MESH"

    def invoke(self, context: Context, event: Event):
        """Invoke the operator."""
        obj = context.active_object
        self.uv_names.clear()

        if obj.type == "MESH":
            for uvmap in obj.data.uv_layers:
                item = self.uv_names.add()
                item.name = uvmap.name

        return self.execute(context)

    def draw(self, context: Context):
        """Draw custom props."""
        layout = self.layout
        box = layout.box()

        def create_row(parent: UILayout, enabled=True, scale: float = 1.3):
            row = parent.row(align=True)
            row.scale_y = scale
            row.enabled = enabled
            return row

        create_row(box).prop(self, "auto_fix_names")
        create_row(box).prop(self, "remove_extra_uvmaps")
        create_row(box).prop(self, "add_missing_uvmaps")

        box = layout.box()
        create_row(box).label(text="UV Map Names:")
        for i, uvmap in enumerate(self.uv_names):
            create_row(box).prop(uvmap, "name", text=f"{i + 1}")

    def add_uvmaps(self, add_count: int, obj: Object):
        """Add UV maps.

        Args:
        ----
            add_count: The number of UV maps to add.
            obj: The object to add UV maps to.

        """
        uvmaps = obj.data.uv_layers
        missing_names = [
            p.name for p in self.uv_names if p.name not in {uvmap.name for uvmap in uvmaps}
        ]
        add_count = min(len(missing_names), add_count)

        for i in range(add_count):
            uvmaps.new(name=missing_names[i])

    def edit_uvmap_names(self, obj: Object):
        """Rename UV maps.

        Args:
        ----
            obj: The object to edit UV Map names.

        """
        uvmaps = obj.data.uv_layers
        current_names = {uvmap.name for uvmap in uvmaps}

        for i, property in enumerate(self.uv_names):
            if i >= len(uvmaps):
                continue
            if uvmaps[i].name != property.name:
                # Check if the name is assigned in wrong place and replace to avoid conflict.
                # This isn't optimal but shouldn't matter in real use.
                if property.name in current_names:
                    for uvmap in uvmaps:
                        if uvmap.name == property.name:
                            uvmap.name = uuid4().hex
                            break
                uvmaps[i].name = property.name

    def remove_uvmaps(self, remove_count: int, obj: Object):
        """Remove UV maps.

        Args:
        ----
            remove_count: The number of UV maps to remove.
            obj: The object to remove UV maps from.

        """
        uvmaps = obj.data.uv_layers
        uvmaps_to_remove = [uvmap.name for uvmap in uvmaps if uvmap.name not in self.uv_names]
        remove_range = min(remove_count, len(uvmaps_to_remove))

        for i in range(remove_range):
            uvmaps.remove(uvmaps[uvmaps_to_remove[i]])

    def execute(self, context: Context) -> set[str]:
        """Execute the operator."""
        sel_objs = [
            obj
            for obj in context.selectable_objects
            if obj.type == "MESH" and obj != context.active_object
        ]
        act_obj = context.active_object

        if act_obj.type == "MESH":
            for i, uvmap in enumerate(act_obj.data.uv_layers):
                old_name = uvmap.name
                uvmap.name = self.uv_names[i].name

                valid, msg = validate_uvmap_name(act_obj, uvmap.name)
                if not valid:
                    if self.auto_fix_names:
                        uvmap.name = repair_uvmap_name(uvmap.name)
                        self.uv_names[i].name = uvmap.name
                        self.report({"INFO"}, message=f"Updated {old_name} to {uvmap.name}")
                    else:
                        self.report({"WARNING"}, msg)

        for obj in sel_objs:
            uvmaps = obj.data.uv_layers
            if len(uvmaps) > len(self.uv_names) and self.remove_extra_uvmaps:
                diff = len(uvmaps) - len(self.uv_names)
                self.remove_uvmaps(diff, obj)
            elif len(uvmaps) < len(self.uv_names) and self.add_missing_uvmaps:
                diff = len(self.uv_names) - len(uvmaps)
                self.add_uvmaps(diff, obj)

            self.edit_uvmap_names(obj)

        return {"FINISHED"}
