# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright 2009-2010 Michel J. Anders (varkenvarken)

import bpy
from bpy.types import Operator
from math import (
        atan, asin, cos,
        sin, tan, pi,
        radians,
        )
from bpy.props import (
        FloatProperty,
        IntProperty,
        BoolProperty,
        StringProperty,
        FloatVectorProperty
        )
from mathutils import (
        Vector,
        Matrix,
        )
from bpy_extras import object_utils

# A very simple "bridge" tool.
# Connects two equally long vertex rows with faces.
# Returns a list of the new faces (list of  lists)
#
# vertIdx1 ... First vertex list (list of vertex indices)
# vertIdx2 ... Second vertex list (list of vertex indices)
# closed ... Creates a loop (first & last are closed)
# flipped ... Invert the normal of the face(s)
#
# Note: You can set vertIdx1 to a single vertex index to create
#       a fan/star of faces
# Note: If both vertex idx list are the same length they have
#       to have at least 2 vertices

def createFaces(vertIdx1, vertIdx2, closed=False, flipped=False):
    faces = []

    if not vertIdx1 or not vertIdx2:
        return None

    if len(vertIdx1) < 2 and len(vertIdx2) < 2:
        return None

    fan = False
    if (len(vertIdx1) != len(vertIdx2)):
        if (len(vertIdx1) == 1 and len(vertIdx2) > 1):
            fan = True
        else:
            return None

    total = len(vertIdx2)

    if closed:
        # Bridge the start with the end.
        if flipped:
            face = [
                vertIdx1[0],
                vertIdx2[0],
                vertIdx2[total - 1]]
            if not fan:
                face.append(vertIdx1[total - 1])
            faces.append(face)

        else:
            face = [vertIdx2[0], vertIdx1[0]]
            if not fan:
                face.append(vertIdx1[total - 1])
            face.append(vertIdx2[total - 1])
            faces.append(face)

    # Bridge the rest of the faces.
    for num in range(total - 1):
        if flipped:
            if fan:
                face = [vertIdx2[num], vertIdx1[0], vertIdx2[num + 1]]
            else:
                face = [vertIdx2[num], vertIdx1[num],
                    vertIdx1[num + 1], vertIdx2[num + 1]]
            faces.append(face)
        else:
            if fan:
                face = [vertIdx1[0], vertIdx2[num], vertIdx2[num + 1]]
            else:
                face = [vertIdx1[num], vertIdx2[num],
                    vertIdx2[num + 1], vertIdx1[num + 1]]
            faces.append(face)

    return faces


# Calculate the vertex coordinates for a single
# section of a gear tooth.
# Returns 4 lists of vertex coords (list of tuples):
#  *-*---*---*  (1.) verts_inner_base
#  | |   |   |
#  *-*---*---*  (2.) verts_outer_base
#    |   |   |
#    *---*---*  (3.) verts_middle_tooth
#     \  |  /
#      *-*-*    (4.) verts_tip_tooth
#
# a
# t
# d
# radius
# Ad
# De
# base
# p_angle
# rack
# crown

def add_tooth(a, t, d, radius, Ad, De, base, p_angle, rack=0, crown=0.0):
    A = [a, a + t / 4, a + t / 2, a + 3 * t / 4]
    C = [cos(i) for i in A]
    S = [sin(i) for i in A]

    Ra = radius + Ad
    Rd = radius - De
    Rb = Rd - base

    # Pressure angle calc
    O = Ad * tan(p_angle)
    if Ra != 0:
        p_angle = atan(O / Ra)
    else:
        p_angle = atan(O)

    if radius < 0:
        p_angle = -p_angle

    if rack:
        S = [sin(t / 4) * I for I in range(-2, 3)]
        Sp = [0, sin(-t / 4 + p_angle), 0, sin(t / 4 - p_angle)]

        verts_inner_base = [(Rb, radius * S[I], d) for I in range(4)]
        verts_outer_base = [(Rd, radius * S[I], d) for I in range(4)]
        verts_middle_tooth = [(radius, radius * S[I], d) for I in range(1, 4)]
        verts_tip_tooth = [(Ra, radius * Sp[I], d) for I in range(1, 4)]

    else:
        Cp = [
            0,
            cos(a + t / 4 + p_angle),
            cos(a + t / 2),
            cos(a + 3 * t / 4 - p_angle)]
        Sp = [0,
            sin(a + t / 4 + p_angle),
            sin(a + t / 2),
            sin(a + 3 * t / 4 - p_angle)]

        verts_inner_base = [(Rb * C[I], Rb * S[I], d)
            for I in range(4)]
        verts_outer_base = [(Rd * C[I], Rd * S[I], d)
            for I in range(4)]
        verts_middle_tooth = [(radius * C[I], radius * S[I], d + crown / 3)
            for I in range(1, 4)]
        verts_tip_tooth = [(Ra * Cp[I], Ra * Sp[I], d + crown)
            for I in range(1, 4)]

    return (verts_inner_base, verts_outer_base,
        verts_middle_tooth, verts_tip_tooth)


# EXPERIMENTAL Calculate the vertex coordinates for a single
# section of a gearspoke.
# Returns them as a list of tuples
#
# a
# t
# d
# radius
# De
# base
# s
# w
# l
# gap
# width
#
# @todo Finish this.

def add_spoke(a, t, d, radius, De, base, s, w, l, gap=0, width=19):
    Rd = radius - De
    Rb = Rd - base

    verts = []
    edgefaces = []
    edgefaces2 = []
    sf = []

    if not gap:
        for N in range(width, 1, -2):
            edgefaces.append(len(verts))
            ts = t / 4
            tm = a + 2 * ts
            te = asin(w / Rb)
            td = te - ts
            t4 = ts + td * (width - N) / (width - 3.0)
            A = [tm + (i - int(N / 2)) * t4 for i in range(N)]
            C = [cos(i) for i in A]
            S = [sin(i) for i in A]

            verts.extend((Rb * I, Rb * J, d) for (I, J) in zip(C, S))
            edgefaces2.append(len(verts) - 1)

            Rb = Rb - s

        n = 0
        for N in range(width, 3, -2):
            sf.extend([(i + n, i + 1 + n, i + 2 + n, i + N + n)
                for i in range(0, N - 1, 2)])
            sf.extend([(i + 2 + n, i + N + n, i + N + 1 + n, i + N + 2 + n)
                for i in range(0, N - 3, 2)])

            n = n + N

    return verts, edgefaces, edgefaces2, sf


# Create gear geometry.
# Returns:
# * A list of vertices (list of tuples)
# * A list of faces (list of lists)
# * A list (group) of vertices of the tip (list of vertex indices)
# * A list (group) of vertices of the valley (list of vertex indices)
#
# teethNum ... Number of teeth on the gear
# radius ... Radius of the gear, negative for crown gear
# Ad ... Addendum, extent of tooth above radius
# De ... Dedendum, extent of tooth below radius
# base ... Base, extent of gear below radius
# p_angle ... Pressure angle. Skewness of tooth tip. (radiant)
# width ... Width, thickness of gear
# skew ... Skew of teeth. (radiant)
# conangle ... Conical angle of gear. (radiant)
# rack
# crown ... Inward pointing extend of crown teeth
#
# inner radius = radius - (De + base)

def add_gear(teethNum, radius, Ad, De, base, p_angle,
             width=1, skew=0, conangle=0, rack=0, crown=0.0):

    if teethNum < 2:
        return None, None, None, None

    t = 2 * pi / teethNum

    if rack:
        teethNum = 1

    #print(radius, width, conangle)
    if radius != 0:
        scale = (radius - 2 * width * tan(conangle)) / radius
    else:
        scale = radius - 2 * width * tan(conangle)

    verts = []
    faces = []
    vgroup_top = []  # Vertex group of top/tip? vertices.
    vgroup_valley = []  # Vertex group of valley vertices

    verts_bridge_prev = []
    for toothCnt in range(teethNum):
        a = toothCnt * t

        verts_bridge_start = []
        verts_bridge_end = []

        verts_outside_top = []
        verts_outside_bottom = []
        for (s, d, c, top) \
           in [(0, -width, 1, True), (skew, width, scale, False)]:

            verts1, verts2, verts3, verts4 = add_tooth(a + s, t, d,
                radius * c, Ad * c, De * c, base * c, p_angle,
                rack, crown)

            vertsIdx1 = list(range(len(verts), len(verts) + len(verts1)))
            verts.extend(verts1)
            vertsIdx2 = list(range(len(verts), len(verts) + len(verts2)))
            verts.extend(verts2)
            vertsIdx3 = list(range(len(verts), len(verts) + len(verts3)))
            verts.extend(verts3)
            vertsIdx4 = list(range(len(verts), len(verts) + len(verts4)))
            verts.extend(verts4)

            verts_outside = []
            verts_outside.extend(vertsIdx2[:2])
            verts_outside.append(vertsIdx3[0])
            verts_outside.extend(vertsIdx4)
            verts_outside.append(vertsIdx3[-1])
            verts_outside.append(vertsIdx2[-1])

            if top:
                # verts_inside_top = vertsIdx1
                verts_outside_top = verts_outside

                verts_bridge_start.append(vertsIdx1[0])
                verts_bridge_start.append(vertsIdx2[0])
                verts_bridge_end.append(vertsIdx1[-1])
                verts_bridge_end.append(vertsIdx2[-1])

            else:
                # verts_inside_bottom = vertsIdx1
                verts_outside_bottom = verts_outside

                verts_bridge_start.append(vertsIdx2[0])
                verts_bridge_start.append(vertsIdx1[0])
                verts_bridge_end.append(vertsIdx2[-1])
                verts_bridge_end.append(vertsIdx1[-1])

            # Valley = first 2 vertices of outer base:
            vgroup_valley.extend(vertsIdx2[:1])
            # Top/tip vertices:
            vgroup_top.extend(vertsIdx4)

            faces_tooth_middle_top = createFaces(vertsIdx2[1:], vertsIdx3,
                flipped=top)
            faces_tooth_outer_top = createFaces(vertsIdx3, vertsIdx4,
                flipped=top)

            faces_base_top = createFaces(vertsIdx1, vertsIdx2, flipped=top)
            faces.extend(faces_base_top)

            faces.extend(faces_tooth_middle_top)
            faces.extend(faces_tooth_outer_top)

        # faces_inside = createFaces(verts_inside_top, verts_inside_bottom)
        # faces.extend(faces_inside)

        faces_outside = createFaces(verts_outside_top, verts_outside_bottom,
            flipped=True)
        faces.extend(faces_outside)

        if toothCnt == 0:
            verts_bridge_first = verts_bridge_start

        # Bridge one tooth to the next
        if verts_bridge_prev:
            faces_bridge = createFaces(verts_bridge_prev, verts_bridge_start)
            faces.extend(faces_bridge)

        # Remember "end" vertices for next tooth.
        verts_bridge_prev = verts_bridge_end

    # Bridge the first to the last tooth.
    faces_bridge_f_l = createFaces(verts_bridge_prev, verts_bridge_first)
    faces.extend(faces_bridge_f_l)

    return verts, faces, vgroup_top, vgroup_valley


# Create spokes geometry
# Returns:
# * A list of vertices (list of tuples)
# * A list of faces (list of lists)
#
# teethNum ... Number of teeth on the gear.
# radius ... Radius of the gear, negative for crown gear
# De ... Dedendum, extent of tooth below radius
# base ... Base, extent of gear below radius
# width ... Width, thickness of gear
# conangle ... Conical angle of gear. (radiant)
# rack
# spoke
# spbevel
# spwidth
# splength
# spresol
#
# @todo Finish this
# @todo Create a function that takes a "Gear" and creates a
#       matching "Gear Spokes" object

def add_spokes(teethNum, radius, De, base, width=1, conangle=0, rack=0,
               spoke=3, spbevel=0.1, spwidth=0.2, splength=1.0, spresol=9):

    if teethNum < 2:
        return None, None, None, None

    if spoke < 2:
        return None, None, None, None

    t = 2 * pi / teethNum

    if rack:
        teethNum = 1

    scale = (radius - 2 * width * tan(conangle)) / radius

    verts = []
    faces = []

    c = scale   # debug

    fl = len(verts)
    for toothCnt in range(teethNum):
        a = toothCnt * t
        s = 0       # For test

        if toothCnt % spoke == 0:
            for d in (-width, width):
                sv, edgefaces, edgefaces2, sf = add_spoke(a + s, t, d,
                    radius * c, De * c, base * c,
                    spbevel, spwidth, splength, 0, spresol)
                verts.extend(sv)
                faces.extend([j + fl for j in i] for i in sf)
                fl += len(sv)

            d1 = fl - len(sv)
            d2 = fl - 2 * len(sv)

            faces.extend([(i + d2, j + d2, j + d1, i + d1)
                for (i, j) in zip(edgefaces[:-1], edgefaces[1:])])
            faces.extend([(i + d2, j + d2, j + d1, i + d1)
                for (i, j) in zip(edgefaces2[:-1], edgefaces2[1:])])

        else:
            for d in (-width, width):
                sv, edgefaces, edgefaces2, sf = add_spoke(a + s, t, d,
                    radius * c, De * c, base * c,
                    spbevel, spwidth, splength, 1, spresol)

                verts.extend(sv)
                fl += len(sv)

            d1 = fl - len(sv)
            d2 = fl - 2 * len(sv)

            faces.extend([[i + d2, i + 1 + d2, i + 1 + d1, i + d1]
                for (i) in range(0, 3)])
            faces.extend([[i + d2, i + 1 + d2, i + 1 + d1, i + d1]
                for (i) in range(5, 8)])

    return verts, faces


# Create worm geometry.
# Returns:
# * A list of vertices
# * A list of faces
# * A list (group) of vertices of the tip
# * A list (group) of vertices of the valley
#
# teethNum ... Number of teeth on the worm
# radius ... Radius of the gear, negative for crown gear
# Ad ... Addendum, extent of tooth above radius
# De ... Dedendum, extent of tooth below radius
# p_angle ... Pressure angle. Skewness of tooth tip. (radiant)
# width ... Width, thickness of gear
# crown ... Inward pointing extend of crown teeth
#
# @todo: Fix teethNum. Some numbers are not possible yet
# @todo: Create start & end geometry (closing faces)

def add_worm(teethNum, rowNum, radius, Ad, De, p_angle,
             width=1, skew=radians(11.25), crown=0.0):

    worm = teethNum
    teethNum = 24

    t = 2 * pi / teethNum

    verts = []
    faces = []
    vgroup_top = []  # Vertex group of top/tip? vertices.
    vgroup_valley = []  # Vertex group of valley vertices

    # width = width / 2.0

    edgeloop_prev = []
    for Row in range(rowNum):
        edgeloop = []

        for toothCnt in range(teethNum):
            a = toothCnt * t

            s = Row * skew
            d = Row * width
            c = 1

            isTooth = False
            if toothCnt % (teethNum / worm) != 0:
                # Flat
                verts1, verts2, verts3, verts4 = add_tooth(a + s, t, d,
                    radius - De, 0.0, 0.0, 0, p_angle)

                # Ignore other verts than the "other base".
                verts1 = verts3 = verts4 = []

            else:
                # Tooth
                isTooth = True
                verts1, verts2, verts3, verts4 = add_tooth(a + s, t, d,
                    radius * c, Ad * c, De * c, 0 * c, p_angle, 0, crown)

                # Remove various unneeded verts (if we are "inside" the tooth)
                del(verts2[2])  # Central vertex in the base of the tooth.
                del(verts3[1])  # Central vertex in the middle of the tooth.

            vertsIdx2 = list(range(len(verts), len(verts) + len(verts2)))
            verts.extend(verts2)
            vertsIdx3 = list(range(len(verts), len(verts) + len(verts3)))
            verts.extend(verts3)
            vertsIdx4 = list(range(len(verts), len(verts) + len(verts4)))
            verts.extend(verts4)

            if isTooth:
                verts_current = []
                verts_current.extend(vertsIdx2[:2])
                verts_current.append(vertsIdx3[0])
                verts_current.extend(vertsIdx4)
                verts_current.append(vertsIdx3[-1])
                verts_current.append(vertsIdx2[-1])

                # Valley = first 2 vertices of outer base:
                vgroup_valley.extend(vertsIdx2[:1])
                # Top/tip vertices:
                vgroup_top.extend(vertsIdx4)

            else:
                # Flat
                verts_current = vertsIdx2

                # Valley - all of them.
                vgroup_valley.extend(vertsIdx2)

            edgeloop.extend(verts_current)

        # Create faces between rings/rows.
        if edgeloop_prev:
            faces_row = createFaces(edgeloop, edgeloop_prev, closed=True)
            faces.extend(faces_row)

        # Remember last ring/row of vertices for next ring/row iteration.
        edgeloop_prev = edgeloop

    return verts, faces, vgroup_top, vgroup_valley

def AddGearMesh(self, context):

    verts, faces, verts_tip, verts_valley = add_gear(
            self.number_of_teeth,
            self.radius,
            self.addendum,
            self.dedendum,
            self.base,
            self.angle,
            width=self.width,
            skew=self.skew,
            conangle=self.conangle,
            crown=self.crown
            )

    mesh = bpy.data.meshes.new("Gear")
    mesh.from_pydata(verts, [], faces)

    return mesh, verts_tip, verts_valley


class AddGear(Operator, object_utils.AddObjectHelper):
    bl_idname = "mesh.primitive_gear"
    bl_label = "Add Gear"
    bl_description = "Construct a gear mesh"
    bl_options = {'REGISTER', 'UNDO', 'PRESET'}

    Gear : BoolProperty(name = "Gear",
                default = True,
                description = "Gear")

    #### change properties
    name : StringProperty(name = "Name",
                    description = "Name")

    change : BoolProperty(name = "Change",
                default = False,
                description = "change Gear")

    number_of_teeth: IntProperty(name="Number of Teeth",
            description="Number of teeth on the gear",
            min=2,
            soft_max=1000,
            default=12
            )
    radius: FloatProperty(name="Radius",
            description="Radius of the gear, negative for crown gear",
            soft_min=-1000.0,
            soft_max=1000.0,
            unit='LENGTH',
            default=1.0
            )
    addendum: FloatProperty(name="Addendum",
            description="Addendum, extent of tooth above radius",
            soft_min=-1000.0,
            soft_max=1000.0,
            unit='LENGTH',
            default=0.1
            )
    dedendum: FloatProperty(name="Dedendum",
            description="Dedendum, extent of tooth below radius",
            soft_min=-1000.0,
            soft_max=1000.0,
            unit='LENGTH',
            default=0.1
            )
    angle: FloatProperty(name="Pressure Angle",
            description="Pressure angle, skewness of tooth tip",
            soft_min=radians(-45.0),
            soft_max=radians(45.0),
            unit='ROTATION',
            default=radians(20.0)
            )
    base: FloatProperty(name="Base",
            description="Base, extent of gear below radius",
            soft_min=-1000.0,
            soft_max=1000.0,
            unit='LENGTH',
            default=0.2
            )
    width: FloatProperty(name="Width",
            description="Width, thickness of gear",
            soft_min=-1000.0,
            soft_max=1000.0,
            unit='LENGTH',
            default=0.2
            )
    skew: FloatProperty(name="Skewness",
            description="Skew of teeth",
            soft_min=radians(-360.0),
            soft_max=radians(360.0),
            unit='ROTATION',
            default=radians(0.0)
            )
    conangle: FloatProperty(name="Conical angle",
            description="Conical angle of gear",
            soft_min=radians(-360.0),
            soft_max=radians(360.0),
            unit='ROTATION',
            default=radians(0.0)
            )
    crown: FloatProperty(name="Crown",
            description="Inward pointing extend of crown teeth",
            soft_min=-1000.0,
            soft_max=1000.0,
            unit='LENGTH',
            default=0.0
            )

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

        box = layout.box()
        box.prop(self, 'number_of_teeth')

        box = layout.box()
        box.prop(self, 'radius')
        box.prop(self, 'width')
        box.prop(self, 'base')

        box = layout.box()
        box.prop(self, 'dedendum')
        box.prop(self, 'addendum')

        box = layout.box()
        box.prop(self, 'angle')
        box.prop(self, 'skew')
        box.prop(self, 'conangle')
        box.prop(self, 'crown')

        if self.change == False:
            # generic transform props
            box = layout.box()
            box.prop(self, 'align', expand=True)
            box.prop(self, 'location', expand=True)
            box.prop(self, 'rotation', expand=True)

    @classmethod
    def poll(cls, context):
        return context.scene is not None

    def execute(self, context):
        # turn off 'Enter Edit Mode'
        use_enter_edit_mode = bpy.context.preferences.edit.use_enter_edit_mode
        bpy.context.preferences.edit.use_enter_edit_mode = False

        if bpy.context.mode == "OBJECT":
            if context.selected_objects != [] and context.active_object and \
                (context.active_object.data is not None) and ('Gear' in context.active_object.data.keys()) and \
                (self.change == True):
                obj = context.active_object
                oldmesh = obj.data
                oldmeshname = obj.data.name
                mesh, verts_tip, verts_valley = AddGearMesh(self, context)
                obj.data = mesh
                try:
                    bpy.ops.object.vertex_group_remove(all=True)
                except:
                    pass

                for material in oldmesh.materials:
                    obj.data.materials.append(material)

                bpy.data.meshes.remove(oldmesh)
                obj.data.name = oldmeshname
            else:
                mesh, verts_tip, verts_valley = AddGearMesh(self, context)
                obj = object_utils.object_data_add(context, mesh, operator=self)

            # Create vertex groups from stored vertices.
            tipGroup = obj.vertex_groups.new(name='Tips')
            tipGroup.add(verts_tip, 1.0, 'ADD')

            valleyGroup = obj.vertex_groups.new(name='Valleys')
            valleyGroup.add(verts_valley, 1.0, 'ADD')

            obj.data["Gear"] = True
            obj.data["change"] = False
            for prm in GearParameters():
                obj.data[prm] = getattr(self, prm)

        if bpy.context.mode == "EDIT_MESH":
            active_object = context.active_object
            name_active_object = active_object.name
            bpy.ops.object.mode_set(mode='OBJECT')
            mesh, verts_tip, verts_valley = AddGearMesh(self, context)
            obj = object_utils.object_data_add(context, mesh, operator=self)

            # Create vertex groups from stored vertices.
            tipGroup = obj.vertex_groups.new(name='Tips')
            tipGroup.add(verts_tip, 1.0, 'ADD')

            valleyGroup = obj.vertex_groups.new(name='Valleys')
            valleyGroup.add(verts_valley, 1.0, 'ADD')

            obj.select_set(True)
            active_object.select_set(True)
            bpy.context.view_layer.objects.active = active_object
            bpy.ops.object.join()
            context.active_object.name = name_active_object
            bpy.ops.object.mode_set(mode='EDIT')

        if use_enter_edit_mode:
            bpy.ops.object.mode_set(mode = 'EDIT')

        # restore pre operator state
        bpy.context.preferences.edit.use_enter_edit_mode = use_enter_edit_mode

        return {'FINISHED'}

    def invoke(self, context, event):
        self.execute(context)

        return {'FINISHED'}

def GearParameters():
    GearParameters = [
        "number_of_teeth",
        "radius",
        "addendum",
        "dedendum",
        "base",
        "angle",
        "width",
        "skew",
        "conangle",
        "crown",
        ]
    return GearParameters

def AddWormGearMesh(self, context):

    verts, faces, verts_tip, verts_valley = add_worm(
            self.number_of_teeth,
            self.number_of_rows,
            self.radius,
            self.addendum,
            self.dedendum,
            self.angle,
            width=self.row_height,
            skew=self.skew,
            crown=self.crown
            )

    mesh = bpy.data.meshes.new("Worm Gear")
    mesh.from_pydata(verts, [], faces)

    return mesh, verts_tip, verts_valley


class AddWormGear(Operator, object_utils.AddObjectHelper):
    bl_idname = "mesh.primitive_worm_gear"
    bl_label = "Add Worm Gear"
    bl_description = "Construct a worm gear mesh"
    bl_options = {'REGISTER', 'UNDO', 'PRESET'}

    WormGear : BoolProperty(name = "WormGear",
                default = True,
                description = "WormGear")

    #### change properties
    name : StringProperty(name = "Name",
                    description = "Name")

    change : BoolProperty(name = "Change",
                default = False,
                description = "change WormGear")

    number_of_teeth: IntProperty(
            name="Number of Teeth",
            description="Number of teeth on the gear",
            min=1,
            soft_max=1000,
            default=12
            )
    number_of_rows: IntProperty(
            name="Number of Rows",
            description="Number of rows on the worm gear",
            min=0,
            soft_max=1000,
            default=32
            )
    radius: FloatProperty(
            name="Radius",
            description="Radius of the gear, negative for crown gear",
            soft_min=-1000.0,
            soft_max=1000.0,
            unit='LENGTH',
            default=1.0
            )
    addendum: FloatProperty(
            name="Addendum",
            description="Addendum, extent of tooth above radius",
            soft_min=-1000.0,
            soft_max=1000.0,
            unit='LENGTH',
            default=0.1
            )
    dedendum: FloatProperty(
            name="Dedendum",
            description="Dedendum, extent of tooth below radius",
            soft_min=-1000.0,
            soft_max=1000.0,
            unit='LENGTH',
            default=0.1
            )
    angle: FloatProperty(
            name="Pressure Angle",
            description="Pressure angle, skewness of tooth tip",
            soft_min=radians(-45.0),
            soft_max=radians(45.0),
            default=radians(20.0),
            unit='ROTATION'
            )
    row_height: FloatProperty(
            name="Row Height",
            description="Height of each Row",
            soft_min=-1000.0,
            soft_max=1000.0,
            unit='LENGTH',
            default=0.2
            )
    skew: FloatProperty(
            name="Skewness per Row",
            description="Skew of each row",
            soft_min=radians(-360.0),
            soft_max=radians(360.0),
            default=radians(11.25),
            unit='ROTATION'
            )
    crown: FloatProperty(
            name="Crown",
            description="Inward pointing extend of crown teeth",
            soft_min=-1000.0,
            soft_max=1000.0,
            unit='LENGTH',
            default=0.0
            )

    def draw(self, context):
        layout = self.layout
        box = layout.box()
        box.prop(self, "number_of_teeth")
        box.prop(self, "number_of_rows")
        box.prop(self, "radius")
        box.prop(self, "row_height")

        box = layout.box()
        box.prop(self, "addendum")
        box.prop(self, "dedendum")

        box = layout.box()
        box.prop(self, "angle")
        box.prop(self, "skew")
        box.prop(self, "crown")

        if self.change == False:
            # generic transform props
            box = layout.box()
            box.prop(self, 'align', expand=True)
            box.prop(self, 'location', expand=True)
            box.prop(self, 'rotation', expand=True)

    def execute(self, context):
        # turn off 'Enter Edit Mode'
        use_enter_edit_mode = bpy.context.preferences.edit.use_enter_edit_mode
        bpy.context.preferences.edit.use_enter_edit_mode = False

        if bpy.context.mode == "OBJECT":
            if context.selected_objects != [] and context.active_object and \
                (context.active_object.data is not None) and ('WormGear' in context.active_object.data.keys()) and \
                (self.change == True):
                obj = context.active_object
                oldmesh = obj.data
                oldmeshname = obj.data.name

                mesh, verts_tip, verts_valley = AddWormGearMesh(self, context)
                obj.data = mesh
                try:
                    bpy.ops.object.vertex_group_remove(all=True)
                except:
                    pass

                for material in oldmesh.materials:
                    obj.data.materials.append(material)

                bpy.data.meshes.remove(oldmesh)
                obj.data.name = oldmeshname
            else:
                mesh, verts_tip, verts_valley = AddWormGearMesh(self, context)
                obj = object_utils.object_data_add(context, mesh, operator=self)

            # Create vertex groups from stored vertices.
            tipGroup = obj.vertex_groups.new(name = 'Tips')
            tipGroup.add(verts_tip, 1.0, 'ADD')

            valleyGroup = obj.vertex_groups.new(name = 'Valleys')
            valleyGroup.add(verts_valley, 1.0, 'ADD')

            obj.data["WormGear"] = True
            obj.data["change"] = False
            for prm in WormGearParameters():
                obj.data[prm] = getattr(self, prm)

        if bpy.context.mode == "EDIT_MESH":
            active_object = context.active_object
            name_active_object = active_object.name
            bpy.ops.object.mode_set(mode='OBJECT')
            mesh, verts_tip, verts_valley = AddWormGearMesh(self, context)
            obj = object_utils.object_data_add(context, mesh, operator=self)

            # Create vertex groups from stored vertices.
            tipGroup = obj.vertex_groups.new(name = 'Tips')
            tipGroup.add(verts_tip, 1.0, 'ADD')

            valleyGroup = obj.vertex_groups.new(name = 'Valleys')
            valleyGroup.add(verts_valley, 1.0, 'ADD')

            obj.select_set(True)
            active_object.select_set(True)
            bpy.context.view_layer.objects.active = active_object
            bpy.ops.object.join()
            context.active_object.name = name_active_object
            bpy.ops.object.mode_set(mode='EDIT')

        if use_enter_edit_mode:
            bpy.ops.object.mode_set(mode = 'EDIT')

        # restore pre operator state
        bpy.context.preferences.edit.use_enter_edit_mode = use_enter_edit_mode

        return {'FINISHED'}

def WormGearParameters():
    WormGearParameters = [
        "number_of_teeth",
        "number_of_rows",
        "radius",
        "addendum",
        "dedendum",
        "angle",
        "row_height",
        "skew",
        "crown",
        ]
    return WormGearParameters
