# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright Campbell Barton.


def ensure_active_color_attribute(me):
    if me.attributes.active_color:
        return me.attributes.active_color
    return me.color_attributes.new("Color", 'BYTE_COLOR', 'CORNER')


def applyVertexDirt(me, blur_iterations, blur_strength, clamp_dirt, clamp_clean, dirt_only, normalize):
    from mathutils import Vector
    from math import acos
    import array

    # We simulate the accumulation of dirt in the creases of geometric surfaces
    # by comparing the vertex normal to the average direction of all vertices
    # connected to that vertex. We can also simulate surfaces being buffed or
    # worn by testing protruding surfaces.
    #
    # So if the angle between the normal and geometric direction is:
    # < 90 - dirt has accumulated in the crease
    # > 90 - surface has been worn or buffed
    # ~ 90 - surface is flat and is generally unworn and clean
    #
    # This method is limited by the complexity or lack there of in the geometry.
    #
    # Original code and method by Keith "Wahooney" Boshoff.

    vert_tone = array.array("f", [0.0]) * len(me.vertices)

    # create lookup table for each vertex's connected vertices (via edges)
    con = [[] for i in range(len(me.vertices))]

    # add connected verts
    for e in me.edges:
        con[e.vertices[0]].append(e.vertices[1])
        con[e.vertices[1]].append(e.vertices[0])

    for i, v in enumerate(me.vertices):
        vec = Vector()
        no = v.normal
        co = v.co

        # get the direction of the vectors between the vertex and it's connected vertices
        for c in con[i]:
            vec += (me.vertices[c].co - co).normalized()

        # average the vector by dividing by the number of connected verts
        tot_con = len(con[i])

        if tot_con == 0:
            ang = pi / 2.0  # assume 90°, i. e. flat
        else:
            vec /= tot_con

            # angle is the acos() of the dot product between normal and connected verts.
            # > 90 degrees: convex
            # < 90 degrees: concave
            ang = acos(no.dot(vec))

        # enforce min/max
        ang = max(clamp_dirt, ang)

        if not dirt_only:
            ang = min(clamp_clean, ang)

        vert_tone[i] = ang

    # blur tones
    for i in range(blur_iterations):
        # backup the original tones
        orig_vert_tone = vert_tone[:]

        # use connected verts look up for blurring
        for j, c in enumerate(con):
            for v in c:
                vert_tone[j] += blur_strength * orig_vert_tone[v]

            vert_tone[j] /= len(c) * blur_strength + 1
        del orig_vert_tone

    if normalize:
        min_tone = min(vert_tone)
        max_tone = max(vert_tone)
    else:
        min_tone = clamp_dirt
        max_tone = clamp_clean

    tone_range = max_tone - min_tone

    if tone_range < 0.0001:
        # weak, don't cancel, see #43345
        tone_range = 0.0
    else:
        tone_range = 1.0 / tone_range

    active_color_attribute = ensure_active_color_attribute(me)
    if not active_color_attribute:
        return {'CANCELLED'}

    point_domain = active_color_attribute.domain == 'POINT'

    attribute_data = active_color_attribute.data

    use_paint_mask = me.use_paint_mask
    for i, p in enumerate(me.polygons):
        if not use_paint_mask or p.select:
            for loop_index in p.loop_indices:
                loop = me.loops[loop_index]
                v = loop.vertex_index
                col = attribute_data[v if point_domain else loop_index].color
                tone = vert_tone[v]
                tone = (tone - min_tone) * tone_range

                if dirt_only:
                    tone = min(tone, 0.5) * 2.0

                col[0] = tone * col[0]
                col[1] = tone * col[1]
                col[2] = tone * col[2]
    me.update()
    return {'FINISHED'}


from bpy.types import Operator
from bpy.props import FloatProperty, IntProperty, BoolProperty
from math import pi


class VertexPaintDirt(Operator):
    """Generate a dirt map gradient based on cavity"""
    bl_idname = "paint.vertex_color_dirt"
    bl_label = "Dirty Vertex Colors"
    bl_options = {'REGISTER', 'UNDO'}

    blur_strength: FloatProperty(
        name="Blur Strength",
        description="Blur strength per iteration",
        min=0.01, max=1.0,
        default=1.0,
    )
    blur_iterations: IntProperty(
        name="Blur Iterations",
        description="Number of times to blur the colors (higher blurs more)",
        min=0, max=40,
        default=1,
    )
    clean_angle: FloatProperty(
        name="Highlight Angle",
        description="Less than 90 limits the angle used in the tonal range",
        min=0.0, max=pi,
        default=pi,
        unit='ROTATION',
    )
    dirt_angle: FloatProperty(
        name="Dirt Angle",
        description="Less than 90 limits the angle used in the tonal range",
        min=0.0, max=pi,
        default=0.0,
        unit='ROTATION',
    )
    dirt_only: BoolProperty(
        name="Dirt Only",
        description="Don't calculate cleans for convex areas",
        default=False,
    )
    normalize: BoolProperty(
        name="Normalize",
        description="Normalize the colors, increasing the contrast",
        default=True,
    )

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

    def execute(self, context):
        obj = context.object
        mesh = obj.data

        ret = applyVertexDirt(
            mesh,
            self.blur_iterations,
            self.blur_strength,
            self.dirt_angle,
            self.clean_angle,
            self.dirt_only,
            self.normalize,
        )

        return ret


classes = (
    VertexPaintDirt,
)
