"""
properties.py
-------------
All Blender PropertyGroup classes for knot_animation.

P4 fix — KnotGeneratorSettings
--------------------------------
Instead of 70+ individual property declarations, three helper functions
(_add_rand_int, _add_rand_float, _add_rand_bool) append range-triplets
directly to the class's __annotations__ dict before registration.

  _add_rand_int  → r_{prop} (Bool) + min_{prop} / max_{prop} (Int)
  _add_rand_float→ r_{prop} (Bool) + min_{prop} / max_{prop} (Float)
  _add_rand_bool → r_{prop} (Bool) + prob_{prop} (Float factor)

The property *identifiers* are identical to the monolith, so existing
.blend files deserialise without changes.
"""
from __future__ import annotations

import random

import bpy

from .constants import KNOT_OBJ_NAME
from .materials import (
    SHADER_IDS,
    _ensure_item_preset_material,
    prebuild_playlist_blend_materials,
    _update_viewport_knot_material,
)


# ---------------------------------------------------------------------------
# Range-property registration helpers  (P4)
# ---------------------------------------------------------------------------

def _add_rand_int(cls, prop: str, label: str, min_d: int, max_d: int,
                  rand_default: bool = False, val_min: int | None = None) -> None:
    """Add r_{prop} (Bool) + min_{prop}/max_{prop} (Int) to a PropertyGroup."""
    cls.__annotations__[f"r_{prop}"] = bpy.props.BoolProperty(name=label, default=rand_default)
    int_kw: dict = {"name": "Min", "default": min_d}
    if val_min is not None:
        int_kw["min"] = val_min
    cls.__annotations__[f"min_{prop}"] = bpy.props.IntProperty(**int_kw)
    cls.__annotations__[f"max_{prop}"] = bpy.props.IntProperty(
        name="Max", default=max_d, **({} if val_min is None else {"min": val_min})
    )


def _add_rand_float(cls, prop: str, label: str, min_d: float, max_d: float,
                    rand_default: bool = False, val_min: float | None = None) -> None:
    """Add r_{prop} (Bool) + min_{prop}/max_{prop} (Float) to a PropertyGroup."""
    cls.__annotations__[f"r_{prop}"] = bpy.props.BoolProperty(name=label, default=rand_default)
    float_kw: dict = {"name": "Min", "default": min_d}
    if val_min is not None:
        float_kw["min"] = val_min
    cls.__annotations__[f"min_{prop}"] = bpy.props.FloatProperty(**float_kw)
    cls.__annotations__[f"max_{prop}"] = bpy.props.FloatProperty(
        name="Max", default=max_d, **({} if val_min is None else {"min": val_min})
    )


def _add_rand_bool(cls, prop: str, label: str, rand_default: bool = False,
                   prob_default: float = 0.5, prob_label: str = "% True") -> None:
    """Add r_{prop} (Bool) + prob_{prop} (Float factor) to a PropertyGroup."""
    cls.__annotations__[f"r_{prop}"]    = bpy.props.BoolProperty(name=label, default=rand_default)
    cls.__annotations__[f"prob_{prop}"] = bpy.props.FloatProperty(
        name=prob_label, default=prob_default, min=0.0, max=1.0, subtype='FACTOR'
    )


# ---------------------------------------------------------------------------
# Property update callback
# ---------------------------------------------------------------------------

def _update_knot_material_cb(self, context) -> None:
    """Called whenever a material-related KnotItem property changes."""
    if not self.uid:
        self.uid = f"knot_{random.randint(100000, 999999)}"

    try:
        scene = getattr(context, "scene", None)
        if scene is None:
            return

        if self.material_mode == 'PRESET':
            _ensure_item_preset_material(self)

        # Rebuild blend materials to keep transitions up to date
        prebuild_playlist_blend_materials(scene)

        # Instantly update the active viewport knot's material
        _update_viewport_knot_material(scene)

        # Force redraw of 3-D viewport if screen exists
        screen = getattr(context, "screen", None)
        if screen:
            for area in screen.areas:
                if area.type == 'VIEW_3D':
                    area.tag_redraw()
    except Exception:
        # Safe fallback: if this callback fires during depsgraph evaluation
        # (e.g., driven by an F-curve), context accesses will throw internal state bugs.
        pass


# ---------------------------------------------------------------------------
# PropertyGroups
# ---------------------------------------------------------------------------

class KnotAllowedMaterial(bpy.types.PropertyGroup):
    """Entry in the generator's allowed-materials filter list."""
    material:  bpy.props.PointerProperty(type=bpy.types.Material, name="Material")
    preset_id: bpy.props.StringProperty(name="Preset ID", default="")
    is_preset: bpy.props.BoolProperty(name="Is Preset", default=False)
    enabled:   bpy.props.BoolProperty(name="Enabled",   default=True)
    name:      bpy.props.StringProperty(name="Name",     default="")


class KnotGlobalSettings(bpy.types.PropertyGroup):
    """Scene-level playback and rendering settings."""
    frames_per_knot:   bpy.props.IntProperty(
        name="Frames per Knot", default=12, min=1,
        description="Base display duration (in frames) for each playlist entry. Each knot's effective duration is frames_per_knot × cycle_rate")
    preview_resolution: bpy.props.IntProperty(
        name="Preview Curve Res", default=64, min=3, max=1024,
        description="NURBS subdivision used in the viewport. Higher values produce a smoother curve but update more slowly")
    render_resolution: bpy.props.IntProperty(
        name="Render Curve Res", default=128, min=3, max=2048,
        description="NURBS subdivision used during renders and baking. Higher values produce higher quality output")
    preview_bevel_resolution: bpy.props.IntProperty(
        name="Preview Bevel Res", default=4, min=0, max=64,
        description="Number of sides on the tube cross-section in the viewport")
    render_bevel_resolution: bpy.props.IntProperty(
        name="Render Bevel Res", default=8, min=0, max=64,
        description="Number of sides on the tube cross-section during renders and baking")
    knot_scale:        bpy.props.FloatProperty(
        name="Global Scale", default=1.0, min=0.01,
        description="Uniform scale multiplier applied to all knots")
    global_speed:      bpy.props.FloatProperty(
        name="Global Speed", default=1.0, min=0.01,
        description="Multiplies the effective frame counter for all knots. Keyframeable.")
    animation_phase:   bpy.props.FloatProperty(
        name="Animation Phase", default=0.0,
        description="Frame offset added before all calculations. Keyframe or drive this for reactive control.")
    reactivity_factor: bpy.props.FloatProperty(
        name="Reactivity", default=1.0, min=0.0, max=10.0,
        description="Scales all per-knot animation rates. Wire a driver here for audio-reactive animation.")

    # New 8 Global Variables
    global_turbulence: bpy.props.FloatProperty(
        name="Turbulence (Noise)", default=0.0, min=0.0,
        description="Injects 3D noise/displacement into the final curve vertices.")
    global_emission_multiplier: bpy.props.FloatProperty(
        name="Emission Multiplier", default=1.0, min=0.0,
        description="Master scalar for the shader's emission strength.")
    global_master_thickness: bpy.props.FloatProperty(
        name="Master Thickness", default=1.0, min=0.0,
        description="Multiplier for the knot's bevel depth.")
    global_hue_shift: bpy.props.FloatProperty(
        name="Hue Shift", default=0.0,
        description="Offset applied to the material's color hue.")
    camera_shake_amplitude: bpy.props.FloatProperty(
        name="Camera Shake Amp", default=0.0, min=0.0,
        description="Injects X/Y/Z jitter into the active camera's location.")
    auto_turntable_speed: bpy.props.FloatProperty(
        name="Turntable Speed", default=0.0,
        description="Constant Z-axis rotation speed applied to the master knot object.")
    smooth_shading: bpy.props.BoolProperty(
        name="Smooth Shading", default=True,
        description="Enforce Smooth shading on the generated mesh.")
    viewport_wireframe: bpy.props.BoolProperty(
        name="Viewport Wireframe", default=False,
        description="Force the object to display as wireframe in the 3D viewport.")

    # UI expand state
    ui_show_global:   bpy.props.BoolProperty(name="Global Settings", default=True)
    ui_show_playback: bpy.props.BoolProperty(name="Playback",        default=True)
    ui_show_playlist: bpy.props.BoolProperty(name="Playlist",        default=True)
    ui_show_knot_edit: bpy.props.BoolProperty(name="Selected Knot",  default=True)


class KnotItem(bpy.types.PropertyGroup):
    """One entry in the knot playlist."""
    name: bpy.props.StringProperty(name="Name", default="Knot")
    uid:  bpy.props.StringProperty(name="Unique ID", default="")

    # Shape Type
    shape_type: bpy.props.EnumProperty(
        name="Shape",
        description="Parametric curve type used to generate this knot",
        items=[
            ('TORUS_KNOT', "Torus Knot",      "Classic (p,q) torus knot wound around a torus surface"),
            ('MOBIUS',     "Mobius Strip",     "One-sided surface with a half-twist — non-orientable loop"),
            ('LISSAJOUS',  "Lissajous 3D",    "3-axis frequency-ratio figure trace"),
            ('SPIRAL',     "Spherical Spiral", "Spherical spiral (loxodrome) with configurable turns and radius"),
        ],
        default='TORUS_KNOT'
    )

    # Topology (Torus Knot)
    torus_p: bpy.props.IntProperty(
        name="Revolutions (p)", default=2, min=1,
        description="Number of revolutions around the torus axis. Must be coprime with q for a true knot")
    mod_torus_p: bpy.props.FloatProperty(
        name="Mod p", default=0.0,
        description="Per-frame attenuverter offset applied to p before rendering")
    torus_q: bpy.props.IntProperty(
        name="Spins (q)", default=3, min=1,
        description="Number of spins around the torus tube. Must be coprime with p for a true knot")
    mod_torus_q: bpy.props.FloatProperty(
        name="Mod q", default=0.0,
        description="Per-frame attenuverter offset applied to q before rendering")

    # Topology (Mobius)
    mobius_twists: bpy.props.IntProperty(name="Half Twists", default=1, min=1)
    mod_mobius_twists: bpy.props.FloatProperty(name="Mod Twists", default=0.0)
    mobius_width: bpy.props.FloatProperty(name="Width", default=1.0, min=0.1)
    mod_mobius_width: bpy.props.FloatProperty(name="Mod Width", default=0.0)

    # Topology (Lissajous 3D)
    liss_kx: bpy.props.IntProperty(name="kx (Freq X)", default=3, min=1)
    mod_liss_kx: bpy.props.FloatProperty(name="Mod kx", default=0.0)
    liss_ky: bpy.props.IntProperty(name="ky (Freq Y)", default=2, min=1)
    mod_liss_ky: bpy.props.FloatProperty(name="Mod ky", default=0.0)
    liss_kz: bpy.props.IntProperty(name="kz (Freq Z)", default=4, min=1)
    mod_liss_kz: bpy.props.FloatProperty(name="Mod kz", default=0.0)
    liss_amp: bpy.props.FloatProperty(name="Amplitude", default=2.0, min=0.1)
    mod_liss_amp: bpy.props.FloatProperty(name="Mod Amp", default=0.0)

    # Topology (Spherical Spiral)
    spiral_turns: bpy.props.IntProperty(name="Turns", default=10, min=1)
    mod_spiral_turns: bpy.props.FloatProperty(name="Mod Turns", default=0.0)
    spiral_R: bpy.props.FloatProperty(name="Radius", default=2.0, min=0.1)
    mod_spiral_R: bpy.props.FloatProperty(name="Mod Radius", default=0.0)

    # Radii (Torus Knot)
    torus_R: bpy.props.FloatProperty(
        name="Major", default=2.0, min=0.0,
        description="Distance from the centre of the torus to the centre of the tube")
    mod_torus_R: bpy.props.FloatProperty(
        name="Mod Major", default=0.0,
        description="Per-frame attenuverter offset applied to the major radius")
    torus_r: bpy.props.FloatProperty(
        name="Minor", default=1.0, min=0.0,
        description="Radius of the tube itself")
    mod_torus_r: bpy.props.FloatProperty(
        name="Mod Minor", default=0.0,
        description="Per-frame attenuverter offset applied to the minor radius")

    # Colors (legacy TKP path)
    multiple_links: bpy.props.BoolProperty(
        name="Multiple Links", default=False,
        description="Render all gcd(p,q) link components as separate curves")
    use_colors:     bpy.props.BoolProperty(
        name="Use Colors", default=False,
        description="Apply per-link vertex colors to the curve (legacy TKP color mode)")
    colorSet:       bpy.props.EnumProperty(
        name="Color Set",
        description="Which built-in vertex color palette to use",
        items=[('1', "RGBish", "Red/Green/Blue-based palette"), ('2', "Rainbow", "Full-spectrum rainbow palette")],
        default='1')
    random_colors:  bpy.props.BoolProperty(
        name="Randomize Colors", default=False,
        description="Shuffle the vertex color assignment randomly each frame")

    # Multipliers & phases
    torus_u:  bpy.props.IntProperty(
        name="Rev. Multiplier", default=1, min=1,
        description="Repeats the revolution pattern — creates multiple overlapping copies of the knot")
    torus_v:  bpy.props.IntProperty(
        name="Spin Multiplier", default=1, min=1,
        description="Repeats the spin pattern around the tube")
    torus_rP: bpy.props.FloatProperty(
        name="Rev. Phase", default=0.0,
        description="Revolution phase offset in radians (orbit rotation)")
    torus_sP: bpy.props.FloatProperty(
        name="Spin Phase", default=0.0,
        description="Spin phase offset in radians (tube rotation)")

    # Animation rates
    spin_phase_rate: bpy.props.FloatProperty(
        name="Spin Phase Rate", default=0.0,
        description="Animate spin phase (tube rotation) over time. Scaled by Reactivity.")
    rev_phase_rate: bpy.props.FloatProperty(
        name="Rev. Phase Rate", default=0.0,
        description="Animate revolution phase (orbit rotation) over time. Scaled by Reactivity.")
    height_rate: bpy.props.FloatProperty(
        name="Height Pulse Rate", default=0.0,
        description="Oscillates torus height over time. Scaled by Reactivity.")
    scale_rate: bpy.props.FloatProperty(
        name="Scale Oscillation Rate", default=0.0,
        description="Frequency of per-knot scale oscillation. Scaled by Reactivity.")
    scale_amplitude: bpy.props.FloatProperty(
        name="Scale Oscillation Amplitude", default=0.0, min=0.0,
        description="Amplitude of per-knot scale oscillation (0 = none).")
    cycle_rate: bpy.props.FloatProperty(
        name="Cycle Rate", default=1.0, min=0.01,
        description="Multiplies frames_per_knot for this knot only.")

    # Transition
    transition_frames: bpy.props.IntProperty(
        name="Transition Frames", default=0, min=0,
        description="Number of frames to morph into the next knot (0 = instant)",
        update=_update_knot_material_cb)
    transition_easing: bpy.props.EnumProperty(
        name="Easing",
        description="Interpolation curve applied during the morph window",
        items=[
            ('LINEAR',      "Linear",           "Constant-rate blend between the two knots"),
            ('QUAD_IN_OUT', "Quadratic In/Out", "Slow start and end, faster in the middle"),
            ('SMOOTHSTEP',  "Smoothstep",       "S-curve blend — smooth at both endpoints"),
        ],
        default='QUAD_IN_OUT')

    # Geometry
    geo_extrude: bpy.props.FloatProperty(
        name="Extrude", default=0.0, min=0.0,
        description="Extrude the curve profile outward — creates a ribbon effect when combined with Offset")
    mod_geo_extrude: bpy.props.FloatProperty(
        name="Mod Extrude", default=0.0,
        description="Per-frame attenuverter offset applied to Extrude")
    geo_offset:  bpy.props.FloatProperty(
        name="Offset", default=0.0,
        description="Offset the extruded profile from the curve centreline")
    mod_geo_offset: bpy.props.FloatProperty(
        name="Mod Offset", default=0.0,
        description="Per-frame attenuverter offset applied to Offset")
    geo_bDepth:  bpy.props.FloatProperty(
        name="Bevel Depth", default=0.04, min=0.0,
        description="Tube thickness — controls how wide the knot strand is")
    mod_geo_bDepth: bpy.props.FloatProperty(
        name="Mod BDepth", default=0.0,
        description="Per-frame attenuverter offset applied to Bevel Depth")

    # Dimensions mode
    mode: bpy.props.EnumProperty(
        name="Dimensions Mode",
        description="Choose how to specify the torus radii",
        items=[
            ('MAJOR_MINOR', "Major/Minor",         "Specify inner and outer radius as Major and Minor"),
            ('EXT_INT',     "Exterior/Interior",   "Specify the outer edge and the hole as Exterior and Interior radii"),
        ],
        default='MAJOR_MINOR')
    torus_eR: bpy.props.FloatProperty(
        name="Exterior", default=3.0, min=0.0,
        description="Outer edge radius of the torus (Exterior/Interior mode)")
    mod_torus_eR: bpy.props.FloatProperty(
        name="Mod Ext", default=0.0,
        description="Per-frame attenuverter offset applied to Exterior radius")
    torus_iR: bpy.props.FloatProperty(
        name="Interior", default=1.0, min=0.0,
        description="Inner hole radius of the torus (Exterior/Interior mode)")
    mod_torus_iR: bpy.props.FloatProperty(
        name="Mod Int", default=0.0,
        description="Per-frame attenuverter offset applied to Interior radius")
    torus_h:  bpy.props.FloatProperty(
        name="Height", default=1.0, min=0.0,
        description="Vertical stretch of the torus — values above 1.0 elongate the knot")
    mod_torus_h: bpy.props.FloatProperty(
        name="Mod Height", default=0.0,
        description="Per-frame attenuverter offset applied to Height")

    # Direction
    flip_p: bpy.props.BoolProperty(
        name="Flip p", default=False,
        description="Reverse the direction of the p (revolution) winding")
    flip_q: bpy.props.BoolProperty(
        name="Flip q", default=False,
        description="Reverse the direction of the q (spin) winding")

    # UI expand state (display-only, not serialised to animation)
    ui_show_shape:    bpy.props.BoolProperty(name="Shape",           default=True)
    ui_show_geometry: bpy.props.BoolProperty(name="Geometry",        default=False)
    ui_show_links:    bpy.props.BoolProperty(name="Links & Phases",  default=False)
    ui_show_anim:     bpy.props.BoolProperty(name="Animation Rates", default=True)
    ui_show_material: bpy.props.BoolProperty(name="Material",        default=True)
    ui_show_colors:   bpy.props.BoolProperty(name="Colors",          default=False)
    ui_show_trans:    bpy.props.BoolProperty(name="Transition",      default=False)

    # Material
    material_mode: bpy.props.EnumProperty(
        name="Material Mode",
        description="Choose between a built-in shader preset or an existing material from the project",
        items=[
            ('PRESET',  "Shader Preset",    "Use one of the 20 built-in procedural shader presets"),
            ('PROJECT', "Project Material", "Use any material already in this .blend file"),
        ],
        default='PRESET',
        update=_update_knot_material_cb)
    project_material: bpy.props.PointerProperty(
        name="Material", type=bpy.types.Material,
        description="Referenced material from the project",
        update=_update_knot_material_cb)
    shader_id: bpy.props.EnumProperty(
        name="Shader Preset",
        description="Material preset applied to this knot",
        items=[(s, s.replace('_', ' ').title(), '') for s in SHADER_IDS],
        default='GLOSS_BLUE',
        update=_update_knot_material_cb)
    preset_color: bpy.props.FloatVectorProperty(
        name="Preset Color", subtype='COLOR', size=3,
        default=(0.2, 0.6, 1.0), min=0.0, max=1.0,
        description="Base colour tint passed into the selected shader preset",
        update=_update_knot_material_cb)
    preset_roughness: bpy.props.FloatProperty(
        name="Preset Roughness", default=0.1, min=0.0, max=1.0,
        description="Roughness override for the active preset (0 = mirror-smooth, 1 = fully diffuse)",
        update=_update_knot_material_cb)
    preset_metallic: bpy.props.FloatProperty(
        name="Preset Metallic", default=0.0, min=0.0, max=1.0,
        description="Metallic factor override for the active preset (0 = dielectric, 1 = metal)",
        update=_update_knot_material_cb)
    preset_emission_strength: bpy.props.FloatProperty(
        name="Preset Emission Strength", default=1.0, min=0.0, max=100.0,
        description="Emission strength override for presets that emit light",
        update=_update_knot_material_cb)

    def to_dict(self) -> dict:
        return {
            "shape_type":            self.shape_type,
            "torus_p":               self.torus_p,
            "mod_torus_p":           self.mod_torus_p,
            "torus_q":               self.torus_q,
            "mod_torus_q":           self.mod_torus_q,
            "mobius_twists":         self.mobius_twists,
            "mod_mobius_twists":     self.mod_mobius_twists,
            "mobius_width":          self.mobius_width,
            "mod_mobius_width":      self.mod_mobius_width,
            "liss_kx":               self.liss_kx,
            "mod_liss_kx":           self.mod_liss_kx,
            "liss_ky":               self.liss_ky,
            "mod_liss_ky":           self.mod_liss_ky,
            "liss_kz":               self.liss_kz,
            "mod_liss_kz":           self.mod_liss_kz,
            "liss_amp":              self.liss_amp,
            "mod_liss_amp":          self.mod_liss_amp,
            "spiral_turns":          self.spiral_turns,
            "mod_spiral_turns":      self.mod_spiral_turns,
            "spiral_R":              self.spiral_R,
            "mod_spiral_R":          self.mod_spiral_R,
            "torus_R":               self.torus_R,
            "mod_torus_R":           self.mod_torus_R,
            "torus_r":               self.torus_r,
            "mod_torus_r":           self.mod_torus_r,
            "multiple_links":        self.multiple_links,
            "use_colors":            self.use_colors,
            "colorSet":              self.colorSet,
            "random_colors":         self.random_colors,
            "torus_u":               self.torus_u,
            "torus_v":               self.torus_v,
            "torus_rP":              self.torus_rP,
            "torus_sP":              self.torus_sP,
            "spin_phase_rate":       self.spin_phase_rate,
            "rev_phase_rate":        self.rev_phase_rate,
            "height_rate":           self.height_rate,
            "scale_rate":            self.scale_rate,
            "scale_amplitude":       self.scale_amplitude,
            "cycle_rate":            self.cycle_rate,
            "transition_frames":     self.transition_frames,
            "transition_easing":     self.transition_easing,
            "geo_extrude":           self.geo_extrude,
            "geo_offset":            self.geo_offset,
            "geo_bDepth":            self.geo_bDepth,
            "mode":                  self.mode,
            "torus_eR":              self.torus_eR,
            "torus_iR":              self.torus_iR,
            "torus_h":               self.torus_h,
            "flip_p":                self.flip_p,
            "flip_q":                self.flip_q,
            "material_mode":         self.material_mode,
            "project_material":      self.project_material,
            "shader_id":             self.shader_id,
            "preset_color":          self.preset_color,
            "preset_roughness":      self.preset_roughness,
            "preset_metallic":       self.preset_metallic,
            "preset_emission_strength": self.preset_emission_strength,
            "uid":                   self.uid,
        }


class KnotGeneratorSettings(bpy.types.PropertyGroup):
    """Settings for the random knot generator.

    Range triplets (r_*/min_*/max_*) and bool/prob pairs are added below the
    class definition via _add_rand_* helpers — see P4 in the refactor notes.
    """
    num_knots:  bpy.props.IntProperty(name="Number of Knots", default=10, min=1, max=100)
    base_knot:  bpy.props.PointerProperty(type=KnotItem)

    # Standalone booleans without an associated range or prob
    r_shape_type: bpy.props.BoolProperty(name="Randomize Shape Type", default=True)
    r_modulations: bpy.props.BoolProperty(name="Randomize Mods", default=True, description="Add random attenuverter modulations to the generated knots")
    r_transition_easing: bpy.props.BoolProperty(name="Easing", default=False)
    r_material:     bpy.props.BoolProperty(
        name="Randomize Material", default=True,
        description="Randomize the material/preset for each generated knot")
    r_preset_params: bpy.props.BoolProperty(
        name="Randomize Preset Parameters", default=True,
        description="Randomize color, roughness, metallic, etc. for preset materials")

    # Generator allowed-materials filter
    allowed_materials:       bpy.props.CollectionProperty(type=KnotAllowedMaterial)
    allowed_materials_index: bpy.props.IntProperty(name="Index", default=0)

    # UI expand state for generator rand sub-boxes
    ui_show_rand_shape:   bpy.props.BoolProperty(name="Shape",              default=True)
    ui_show_rand_geo:     bpy.props.BoolProperty(name="Geometry",           default=False)
    ui_show_rand_anim:    bpy.props.BoolProperty(name="Animation",          default=True)
    ui_show_rand_mat:     bpy.props.BoolProperty(name="Material",           default=True)
    ui_show_base:         bpy.props.BoolProperty(name="Base Knot Defaults", default=False)
    ui_show_generator:    bpy.props.BoolProperty(name="Random Generator",   default=True)
    ui_show_rand_toggles: bpy.props.BoolProperty(name="Randomise Toggles", default=True)


# ── Integer range triplets ────────────────────────────────────────────────────
_add_rand_int(KnotGeneratorSettings, "torus_p",          "Revolutions (p)", 1,  8,  rand_default=True, val_min=1)
_add_rand_int(KnotGeneratorSettings, "torus_q",          "Spins (q)",       1,  8,  rand_default=True, val_min=1)
_add_rand_int(KnotGeneratorSettings, "mobius_twists",    "Mobius Twists",   1,  5,  val_min=1)
_add_rand_int(KnotGeneratorSettings, "liss_kx",          "Lissajous kx",    1,  5,  val_min=1)
_add_rand_int(KnotGeneratorSettings, "liss_ky",          "Lissajous ky",    1,  5,  val_min=1)
_add_rand_int(KnotGeneratorSettings, "liss_kz",          "Lissajous kz",    1,  5,  val_min=1)
_add_rand_int(KnotGeneratorSettings, "spiral_turns",     "Spiral Turns",    5,  20, val_min=1)
_add_rand_int(KnotGeneratorSettings, "torus_u",          "Rev. Multiplier", 1,  4,  val_min=1)
_add_rand_int(KnotGeneratorSettings, "torus_v",          "Spin Multiplier", 1,  4,  val_min=1)
_add_rand_int(KnotGeneratorSettings, "transition_frames","Transition Frames",0, 36, val_min=0)

# ── Float range triplets ──────────────────────────────────────────────────────
_add_rand_float(KnotGeneratorSettings, "torus_R",         "Major Radius",         1.0,  4.0,  val_min=0.0)
_add_rand_float(KnotGeneratorSettings, "torus_r",         "Minor Radius",         0.1,  1.5,  val_min=0.0)
_add_rand_float(KnotGeneratorSettings, "mobius_width",    "Mobius Width",         0.5,  2.0,  val_min=0.1)
_add_rand_float(KnotGeneratorSettings, "liss_amp",        "Lissajous Amp",        1.0,  4.0,  val_min=0.1)
_add_rand_float(KnotGeneratorSettings, "spiral_R",        "Spiral Radius",        1.0,  4.0,  val_min=0.1)
_add_rand_float(KnotGeneratorSettings, "torus_eR",        "Exterior Radius",      2.0,  5.0,  val_min=0.0)
_add_rand_float(KnotGeneratorSettings, "torus_iR",        "Interior Radius",      0.5,  2.0,  val_min=0.0)
_add_rand_float(KnotGeneratorSettings, "geo_extrude",     "Extrude",              0.0,  0.5,  val_min=0.0)
_add_rand_float(KnotGeneratorSettings, "geo_offset",      "Offset",               0.0,  0.5)
_add_rand_float(KnotGeneratorSettings, "geo_bDepth",      "Bevel Depth",          0.01, 0.2,  rand_default=True, val_min=0.0)
_add_rand_float(KnotGeneratorSettings, "torus_h",         "Height",               0.5,  3.0,  val_min=0.0)
_add_rand_float(KnotGeneratorSettings, "torus_rP",        "Rev. Phase",           0.0,  2.0)
_add_rand_float(KnotGeneratorSettings, "torus_sP",        "Spin Phase",           0.0,  2.0)
_add_rand_float(KnotGeneratorSettings, "spin_phase_rate", "Spin Phase Rate",     -0.5,  0.5)
_add_rand_float(KnotGeneratorSettings, "rev_phase_rate",  "Rev. Phase Rate",     -0.5,  0.5)
_add_rand_float(KnotGeneratorSettings, "height_rate",     "Height Pulse Rate",    0.0,  0.2,  val_min=0.0)
_add_rand_float(KnotGeneratorSettings, "scale_rate",      "Scale Osc. Rate",      0.0,  0.3,  val_min=0.0)
_add_rand_float(KnotGeneratorSettings, "scale_amplitude", "Scale Osc. Amplitude", 0.0,  0.4,  val_min=0.0)
_add_rand_float(KnotGeneratorSettings, "cycle_rate",      "Cycle Rate",           0.5,  2.0,  val_min=0.01)

# ── Bool/prob pairs ───────────────────────────────────────────────────────────
_add_rand_bool(KnotGeneratorSettings, "multiple_links", "Multiple Links")
_add_rand_bool(KnotGeneratorSettings, "use_colors",     "Use Colors",          rand_default=True)
_add_rand_bool(KnotGeneratorSettings, "colorSet",       "Color Set",           rand_default=True, prob_label="% Set 2")
_add_rand_bool(KnotGeneratorSettings, "random_colors",  "Randomize Colors",    rand_default=True)
_add_rand_bool(KnotGeneratorSettings, "flip_p",         "Flip p")
_add_rand_bool(KnotGeneratorSettings, "flip_q",         "Flip q")
_add_rand_bool(KnotGeneratorSettings, "mode",           "Mode",                prob_label="% Ext/Int")
