"""
operators.py
------------
All KNOT_OT_* operator classes for knot_animation.

P3 fix — module-level randomisation helpers
--------------------------------------------
The four helpers that were previously defined as nested closures inside
``KNOT_OT_GenerateRandom.execute()`` are now module-level functions.  They
take ``gen`` (the KnotGeneratorSettings) explicitly, making them independently
callable and testable.
"""
from __future__ import annotations

import random

import bpy

from .constants  import KNOT_CONFIGS, SHADER_BLEND_MAT_NAME
from .materials  import (
    SHADER_IDS,
    prebuild_playlist_blend_materials,
    prewarm_materials_and_blends,
    _ensure_item_preset_material,
)
from .geometry   import _make_torus_knot
from .handler    import compute_playlist_duration



# ---------------------------------------------------------------------------
# Module-level randomisation helpers  (P3)
# ---------------------------------------------------------------------------

def _rand_apply_int(gen, item, prop: str) -> None:
    """Set item.{prop} to a random int in [gen.min_{prop}, gen.max_{prop}]
    if gen.r_{prop} is True."""
    if getattr(gen, f"r_{prop}"):
        setattr(item, prop, random.randint(
            getattr(gen, f"min_{prop}"),
            getattr(gen, f"max_{prop}"),
        ))


def _rand_apply_float(gen, item, prop: str) -> None:
    """Set item.{prop} to a random float in [gen.min_{prop}, gen.max_{prop}]
    if gen.r_{prop} is True."""
    if getattr(gen, f"r_{prop}"):
        setattr(item, prop, random.uniform(
            getattr(gen, f"min_{prop}"),
            getattr(gen, f"max_{prop}"),
        ))


def _rand_apply_bool(gen, item, prop: str) -> None:
    """Set item.{prop} to True/False with probability gen.prob_{prop}
    if gen.r_{prop} is True."""
    if getattr(gen, f"r_{prop}"):
        prob = getattr(gen, f"prob_{prop}", 0.5)
        setattr(item, prop, random.random() < prob)


def _rand_apply_choice(gen, item, prop: str, choices: list) -> None:
    """Set item.{prop} to a random element of choices if gen.r_{prop} is True."""
    if getattr(gen, f"r_{prop}"):
        setattr(item, prop, random.choice(choices))


# ---------------------------------------------------------------------------
# Operators
# ---------------------------------------------------------------------------

class KNOT_OT_Add(bpy.types.Operator):
    bl_idname     = "knot.add_item"
    bl_label      = "Add Knot"
    bl_description = "Add a new knot entry to the end of the playlist"

    def execute(self, context):
        item = context.scene.knot_list.add()
        item.uid           = f"knot_{random.randint(100000, 999999)}"
        item.name          = f"Knot {len(context.scene.knot_list)}"
        item.material_mode = 'PRESET'
        context.scene.knot_list_index = len(context.scene.knot_list) - 1
        prebuild_playlist_blend_materials(context.scene)
        return {'FINISHED'}


class KNOT_OT_Remove(bpy.types.Operator):
    bl_idname     = "knot.remove_item"
    bl_label      = "Remove Knot"
    bl_description = "Remove the selected knot entry from the playlist"

    def execute(self, context):
        idx = context.scene.knot_list_index
        if 0 <= idx < len(context.scene.knot_list):
            context.scene.knot_list.remove(idx)
            if idx > 0:
                context.scene.knot_list_index = idx - 1
        prebuild_playlist_blend_materials(context.scene)
        return {'FINISHED'}


class KNOT_OT_Move(bpy.types.Operator):
    bl_idname     = "knot.move_item"
    bl_label      = "Move Knot"
    bl_description = "Move the selected knot up or down in the playlist order"
    direction: bpy.props.EnumProperty(items=[('UP', 'Up', 'Move knot earlier in the playlist'), ('DOWN', 'Down', 'Move knot later in the playlist')])

    def execute(self, context):
        idx     = context.scene.knot_list_index
        new_idx = idx - 1 if self.direction == 'UP' else idx + 1
        if 0 <= new_idx < len(context.scene.knot_list):
            context.scene.knot_list.move(idx, new_idx)
            context.scene.knot_list_index = new_idx
        prebuild_playlist_blend_materials(context.scene)
        return {'FINISHED'}


class KNOT_OT_Populate(bpy.types.Operator):
    bl_idname     = "knot.populate"
    bl_label      = "Populate Default List"
    bl_description = "Clear the playlist and load the 10 built-in example knots (trefoil, cinquefoil, Möbius, Lissajous, and more)"

    def execute(self, context):
        context.scene.knot_list.clear()
        for i, config in enumerate(KNOT_CONFIGS):
            item = context.scene.knot_list.add()
            item.uid           = f"knot_{random.randint(100000, 999999)}"
            item.name          = config.get("name", f"Knot {i+1}")
            item.material_mode = 'PRESET'
            for k, v in config.items():
                if k == "name": continue
                if hasattr(item, k):
                    setattr(item, k, v)
        context.scene.knot_list_index = 0
        prewarm_materials_and_blends(context.scene)
        return {'FINISHED'}


class KNOT_OT_BakePreview(bpy.types.Operator):
    bl_idname     = "knot.bake_preview"
    bl_label      = "Update Preview"
    bl_description = "Immediately regenerate the viewport knot using the currently selected playlist entry's settings, without waiting for a frame change"

    def execute(self, context):
        idx = context.scene.knot_list_index
        if 0 <= idx < len(context.scene.knot_list):
            config = context.scene.knot_list[idx].to_dict()
            glob   = context.scene.knot_globals
            _make_torus_knot(
                config,
                resolution=glob.resolution,
                bevel_resolution=glob.bevel_resolution,
                knot_scale=glob.knot_scale,
                scene=context.scene,
            )
        return {'FINISHED'}


class KNOT_OT_SyncGeneratorMaterials(bpy.types.Operator):
    bl_idname   = "knot.sync_generator_materials"
    bl_label    = "Sync Materials & Presets"
    bl_description = "Reload all available presets and project materials into the generator allowed list"

    def execute(self, context):
        scene = context.scene
        gen   = scene.knot_generator

        # Preserve previously enabled states
        enabled_presets = {}
        enabled_mats    = {}
        for entry in gen.allowed_materials:
            if entry.is_preset:
                enabled_presets[entry.preset_id] = entry.enabled
            elif entry.material:
                enabled_mats[entry.material.name] = entry.enabled

        gen.allowed_materials.clear()

        # 1. All shader presets
        for sid in SHADER_IDS:
            entry           = gen.allowed_materials.add()
            entry.name      = sid.replace('_', ' ').title()
            entry.preset_id = sid
            entry.is_preset = True
            entry.enabled   = enabled_presets.get(sid, True)

        # 2. All project materials (excluding generated ones)
        for mat in bpy.data.materials:
            name = mat.name
            if (name.startswith("KnotItem_Preset_") or name.startswith("KnotBlend_")
                    or name == SHADER_BLEND_MAT_NAME):
                continue
            entry          = gen.allowed_materials.add()
            entry.name     = name
            entry.material = mat
            entry.is_preset = False
            entry.enabled   = enabled_mats.get(name, True)

        return {'FINISHED'}


class KNOT_OT_GenerateRandom(bpy.types.Operator):
    bl_idname     = "knot.generate_random"
    bl_label      = "Generate Playlist"
    bl_description = "Clear the playlist and fill it with randomly configured knots using the ranges and toggles set in the Randomise Toggles section below"

    def execute(self, context):
        scene = context.scene
        gen   = scene.knot_generator
        base  = gen.base_knot

        scene.knot_list.clear()

        for i in range(gen.num_knots):
            item      = scene.knot_list.add()
            item.name = f"Random Knot {i+1}"

            # Copy base defaults
            for k, v in base.to_dict().items():
                if hasattr(item, k):
                    setattr(item, k, v)

            # Apply randomisations using module-level helpers (P3)
            if gen.r_shape_type:
                _rand_apply_choice(gen, item, "shape_type", ['TORUS_KNOT', 'MOBIUS', 'LISSAJOUS', 'SPIRAL'])
                
            _rand_apply_int(gen, item, "torus_p")
            _rand_apply_int(gen, item, "torus_q")
            _rand_apply_int(gen, item, "mobius_twists")
            _rand_apply_int(gen, item, "liss_kx")
            _rand_apply_int(gen, item, "liss_ky")
            _rand_apply_int(gen, item, "liss_kz")
            _rand_apply_int(gen, item, "spiral_turns")
            _rand_apply_int(gen, item, "torus_u")
            _rand_apply_int(gen, item, "torus_v")
            _rand_apply_int(gen, item, "transition_frames")

            _rand_apply_float(gen, item, "torus_R")
            _rand_apply_float(gen, item, "torus_r")
            _rand_apply_float(gen, item, "mobius_width")
            _rand_apply_float(gen, item, "liss_amp")
            _rand_apply_float(gen, item, "spiral_R")
            _rand_apply_float(gen, item, "torus_eR")
            _rand_apply_float(gen, item, "torus_iR")
            _rand_apply_float(gen, item, "geo_extrude")
            _rand_apply_float(gen, item, "geo_offset")
            _rand_apply_float(gen, item, "geo_bDepth")
            _rand_apply_float(gen, item, "torus_h")
            _rand_apply_float(gen, item, "torus_rP")
            _rand_apply_float(gen, item, "torus_sP")
            _rand_apply_float(gen, item, "spin_phase_rate")
            _rand_apply_float(gen, item, "rev_phase_rate")
            _rand_apply_float(gen, item, "height_rate")
            _rand_apply_float(gen, item, "scale_rate")
            _rand_apply_float(gen, item, "scale_amplitude")
            _rand_apply_float(gen, item, "cycle_rate")

            if getattr(gen, "r_modulations", False):
                import random
                for k in [
                    "mod_torus_R", "mod_torus_r", "mod_torus_eR", "mod_torus_iR", "mod_torus_h",
                    "mod_mobius_width", "mod_liss_amp", "mod_spiral_R",
                    "mod_geo_extrude", "mod_geo_offset", "mod_geo_bDepth"
                ]:
                    setattr(item, k, random.uniform(-0.5, 0.5))
                for k in [
                    "mod_torus_p", "mod_torus_q", "mod_mobius_twists",
                    "mod_liss_kx", "mod_liss_ky", "mod_liss_kz", "mod_spiral_turns"
                ]:
                    setattr(item, k, random.uniform(-2.0, 2.0))

            _rand_apply_bool(gen, item, "multiple_links")
            _rand_apply_bool(gen, item, "use_colors")
            _rand_apply_bool(gen, item, "random_colors")
            _rand_apply_bool(gen, item, "flip_p")
            _rand_apply_bool(gen, item, "flip_q")

            if gen.r_colorSet:
                item.colorSet = '2' if random.random() < gen.prob_colorSet else '1'
            if gen.r_mode:
                item.mode = 'EXT_INT' if random.random() < gen.prob_mode else 'MAJOR_MINOR'

            _rand_apply_choice(gen, item, "transition_easing",
                               ['LINEAR', 'QUAD_IN_OUT', 'SMOOTHSTEP'])

            item.uid = f"knot_{random.randint(100000, 999999)}"

            if gen.r_material:
                enabled_items = [am for am in gen.allowed_materials if am.enabled]
                if enabled_items:
                    chosen = random.choice(enabled_items)
                    if chosen.is_preset:
                        item.material_mode = 'PRESET'
                        item.shader_id     = chosen.preset_id
                        if getattr(gen, "r_preset_params", True):
                            item.preset_color            = (random.random(), random.random(), random.random())
                            item.preset_roughness        = random.uniform(0.0, 1.0)
                            item.preset_metallic         = random.uniform(0.0, 1.0)
                            item.preset_emission_strength = random.uniform(0.5, 5.0)
                    else:
                        item.material_mode    = 'PROJECT'
                        item.project_material = chosen.material
                else:
                    item.material_mode = 'PRESET'
                    item.shader_id     = 'GLOSS_BLUE'

        scene.knot_list_index = 0
        prewarm_materials_and_blends(scene)
        return {'FINISHED'}


class KNOT_OT_FitTimeline(bpy.types.Operator):
    """Set frame_end so the full playlist fits exactly once in the timeline."""
    bl_idname    = "knot.fit_timeline"
    bl_label     = "Fit Timeline to Playlist"
    bl_description = "Extend Total Frames so every knot plays through once"

    def execute(self, context):
        scene = context.scene
        glob  = scene.knot_globals
        total = compute_playlist_duration(scene) or glob.frames_per_knot
        scene.frame_end = max(scene.frame_start, total)
        return {'FINISHED'}


class KNOT_OT_FitPlaylist(bpy.types.Operator):
    """Adjust frames_per_knot so the playlist fills the current frame_end exactly."""
    bl_idname    = "knot.fit_playlist"
    bl_label     = "Fit Playlist to Timeline"
    bl_description = "Shrink/grow Frames per Knot so all knots fill the current Total Frames"

    def execute(self, context):
        scene = context.scene
        glob  = scene.knot_globals
        n     = len(scene.knot_list)
        if n < 1:
            self.report({'WARNING'}, "Playlist is empty")
            return {'CANCELLED'}
        avg_rate = sum(scene.knot_list[k].cycle_rate for k in range(n)) / n
        avg_rate = max(0.01, avg_rate)
        duration = scene.frame_end - scene.frame_start + 1
        glob.frames_per_knot = max(1, int(duration / (n * avg_rate)))
        return {'FINISHED'}
