# ZELLEREND MY SHOE — unified add-on
# Combines:
#  - STEP_001_WORKS: cleanup, view, UV project from view, unwrap surfaces
#  - STEP_002_MATERIAL_WORKS: build & assign shoe diffuse shaders
#  - STEP_003_MATERIAL_SURFACE_WORKS: build & assign surface diffuse shaders
#
# Install in Blender via: Edit > Preferences > Add-ons > Install... (select this .py)
# Panel location: 3D Viewport > N panel > "ZELLEREND MY SHOE"

bl_info = {
    "name": "ZELLEREND MY SHOE",
    "author": "Darijan Kalauzovic",
    "version": (1, 0, 0),
    "blender": (3, 0, 0),
    "location": "View3D > Sidebar (N) > ZELLEREND MY SHOE",
    "description": "One-click prep, UV, and materials for shoe + surfaces",
    "category": "3D View",
}

import bpy
import bmesh
import math
import mathutils
import os
from math import radians
from bpy.types import Operator, Panel, AddonPreferences
from bpy.props import StringProperty, FloatProperty

# -----------------------------------------------------------------------------
# Preferences
# -----------------------------------------------------------------------------

class ZMS_AddonPreferences(AddonPreferences):
    bl_idname = __name__

    texture_dir: StringProperty(
        name="Texture Folder",
        description="Folder containing the noodle textures (color + alpha)",
        subtype='DIR_PATH',
        default=""
    )

    mod_value: FloatProperty(
        name="Stripe Mod Value",
        description="Stripe period (density) used in shoe bump stripe effect",
        default=19.32,
        min=0.0001
    )

    def draw(self, context):
        layout = self.layout
        layout.prop(self, "texture_dir")
        layout.prop(self, "mod_value")


def _prefs(context):
    # Helper to fetch this add-on's preferences
    return context.preferences.addons[__name__].preferences if __name__ in context.preferences.addons else None


# -----------------------------------------------------------------------------
# Utilities
# -----------------------------------------------------------------------------

def get_object(name):
    for obj in bpy.data.objects:
        if obj.name.lower() == name.lower():
            return obj
    return None

def get_material(name):
    for mat in bpy.data.materials:
        if mat.name.lower() == name.lower():
            return mat
    return None

def ensure_3d_view_region():
    """Find a 3D View + Region3D to read its perspective matrix. Returns (area, region, space)."""
    for area in bpy.context.screen.areas:
        if area.type == 'VIEW_3D':
            space = area.spaces.active
            for region in area.regions:
                if region.type == 'WINDOW':
                    return area, region, space
    return None, None, None


# -----------------------------------------------------------------------------
# STEP 1 — Cleanup, View, Project UVs, Unwrap Surfaces
# -----------------------------------------------------------------------------

def step1_clean_and_position(self):
    found_shoe = False
    found_surfaces = False

    for obj in bpy.data.objects:
        if obj.type == 'MESH':
            if obj.name.lower().startswith("shoe"):
                obj.name = "shoe"
                obj.data.name = "shoe"
                found_shoe = True
            elif obj.name.lower().startswith("surfaces"):
                obj.name = "surfaces"
                obj.data.name = "surfaces"
                found_surfaces = True

    if not found_shoe:
        msg = "No object starting with 'shoe' found."
        if self: self.report({'ERROR'}, msg)
        print("❌", msg)
        return False
    if not found_surfaces:
        msg = "No object starting with 'surfaces' found."
        if self: self.report({'ERROR'}, msg)
        print("❌", msg)
        return False

    shoe_obj = bpy.data.objects["shoe"]
    surfaces_obj = bpy.data.objects["surfaces"]

    surfaces_obj.parent = shoe_obj
    shoe_obj.rotation_euler.y = math.radians(-120)

    # Set orthographic front view
    for area in bpy.context.screen.areas:
        if area.type == 'VIEW_3D':
            region_3d = area.spaces.active.region_3d
            region_3d.view_perspective = 'ORTHO'
            region_3d.view_rotation = mathutils.Euler(
                (math.radians(90), 0, math.radians(180)), 'XYZ'
            ).to_quaternion()
            break

    # Select all faces on shoe
    bpy.context.view_layer.objects.active = shoe_obj
    bpy.ops.object.mode_set(mode='EDIT')
    bpy.ops.mesh.select_all(action='SELECT')
    bpy.ops.object.mode_set(mode='OBJECT')

    # Select all faces on surfaces
    bpy.context.view_layer.objects.active = surfaces_obj
    bpy.ops.object.mode_set(mode='EDIT')
    bpy.ops.mesh.select_all(action='SELECT')
    bpy.ops.object.mode_set(mode='OBJECT')

    # Frame shoe in view
    bbox = [shoe_obj.matrix_world @ mathutils.Vector(c) for c in shoe_obj.bound_box]
    center = sum(bbox, mathutils.Vector()) / 8.0
    radius = max((c - center).length for c in bbox)
    for area in bpy.context.screen.areas:
        if area.type == 'VIEW_3D':
            region_3d = area.spaces.active.region_3d
            region_3d.view_location = center
            region_3d.view_distance = radius * 2
            break

    print("✅ Cleanup done: renamed, parented, rotated, ortho view set, faces selected, view framed.")
    if self: self.report({'INFO'}, "Cleanup & positioning done")
    return True


def step1_project_uv_from_view(self):
    obj = get_object("shoe")
    if obj is None:
        msg = "'shoe' object not found."
        if self: self.report({'ERROR'}, msg)
        print("❌", msg)
        return False

    bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
    area, region, space = ensure_3d_view_region()
    if not area:
        msg = "No 3D Viewport found."
        if self: self.report({'ERROR'}, msg)
        raise RuntimeError("❌ " + msg)

    region_data = space.region_3d
    perspective_matrix = region_data.perspective_matrix

    me = obj.data
    bm = bmesh.new()
    bm.from_mesh(me)
    uv_layer = bm.loops.layers.uv.verify()
    projected_uvs = []

    for face in bm.faces:
        for loop in face.loops:
            co_world = obj.matrix_world @ loop.vert.co
            co4d = mathutils.Vector((co_world.x, co_world.y, co_world.z, 1.0))
            co_proj = perspective_matrix @ co4d
            if co_proj.w != 0.0:
                co_proj /= co_proj.w
            uv = mathutils.Vector(((co_proj.x + 1) / 2, (co_proj.y + 1) / 2))
            projected_uvs.append(uv)

    if not projected_uvs:
        bm.free()
        msg = "No projected UVs."
        if self: self.report({'ERROR'}, msg)
        print("❌", msg)
        return False

    min_x = min(uv.x for uv in projected_uvs)
    max_x = max(uv.x for uv in projected_uvs)
    min_y = min(uv.y for uv in projected_uvs)
    max_y = max(uv.y for uv in projected_uvs)
    range_x = max_x - min_x or 1.0
    range_y = max_y - min_y or 1.0

    i = 0
    for face in bm.faces:
        for loop in face.loops:
            uv = projected_uvs[i]
            uv_remapped = mathutils.Vector(((uv.x - min_x) / range_x, (uv.y - min_y) / range_y))
            loop[uv_layer].uv = uv_remapped
            i += 1

    bm.to_mesh(me)
    bm.free()
    print(f"✅ UVs projected for {obj.name}")
    if self: self.report({'INFO'}, "Projected UVs from view (shoe)")
    return True


def step1_unwrap_surfaces(self):
    obj = get_object("surfaces")
    if obj is None:
        print("⚠️ 'surfaces' object not found.")
        if self: self.report({'WARNING'}, "'surfaces' object not found")
        return True  # Not fatal

    bpy.ops.object.select_all(action='DESELECT')
    obj.select_set(True)
    bpy.context.view_layer.objects.active = obj
    bpy.ops.object.mode_set(mode='EDIT')
    bpy.ops.mesh.select_all(action='SELECT')
    bpy.ops.uv.unwrap(method='CONFORMAL')
    bpy.ops.object.mode_set(mode='OBJECT')
    print("✅ 'surfaces' unwrapped using CONFORMAL")

    # Reset shoe rotation after unwrap
    shoe_obj = get_object("shoe")
    if shoe_obj:
        shoe_obj.rotation_euler = (0.0, 0.0, 0.0)
        print("🔄 'shoe' rotation reset to zero")

    if self: self.report({'INFO'}, "Unwrapped 'surfaces'")
    return True


# -----------------------------------------------------------------------------
# STEP 2 — Shoe materials (Diffuse + bump stripes), assign to 'shoe*' objects
# -----------------------------------------------------------------------------

TEXTURE_MAP = {
    "seamless_noodle_pattern_beige.png":  "Shoe_diffuse_shader_beige",
    "seamless_noodle_pattern_black.png":  "Shoe_diffuse_shader_black",
    "seamless_noodle_pattern_blue.png":   "Shoe_diffuse_shader_blue",
    "seamless_noodle_pattern_orange.png": "Shoe_diffuse_shader_orange",
    "seamless_noodle_pattern_red.png":    "Shoe_diffuse_shader_red",
    "seamless_texture_-ALPHA.png":        "Shoe_diffuse_shader_alpha",
}

ASSIGN_PRIORITY_SHOE = [
    "Shoe_diffuse_shader_blue",
    "Shoe_diffuse_shader_black",
    "Shoe_diffuse_shader_red",
    "Shoe_diffuse_shader_beige",
    "Shoe_diffuse_shader_orange",
    "Shoe_diffuse_shader_alpha",
]

SHOE_NAME_TOKEN = "shoe"


def build_shoe_diffuse_shader(mat: bpy.types.Material, image: bpy.types.Image, mod_value: float):
    """
    Node layout:
      TexCoord(UV) → Mapping(Point, Rot Z=90°, Scale 40/40/30) → Image(Color) → Diffuse(Color)
      Mapping(Vector) → SeparateXYZ(X) → Math(Modulo, Mod=mod_value) → Math(Floor)
         → Invert → Bump → Diffuse(Normal)
      Diffuse → Output(Surface)
    """
    mat.use_nodes = True
    nt = mat.node_tree
    nodes = nt.nodes
    links = nt.links

    # Clear existing nodes
    for n in list(nodes):
        nodes.remove(n)

    # Nodes
    texcoord = nodes.new('ShaderNodeTexCoord'); texcoord.location = (-1300, 0)

    mapping = nodes.new('ShaderNodeMapping'); mapping.location = (-1080, 0)
    mapping.vector_type = 'POINT'
    mapping.inputs["Rotation"].default_value[2] = radians(90)
    mapping.inputs["Scale"].default_value = (40.0, 40.0, 30.0)

    img = nodes.new('ShaderNodeTexImage'); img.location = (-800, 60)
    img.image = image
    img.interpolation = 'Linear'
    img.extension = 'REPEAT'

    sep = nodes.new('ShaderNodeSeparateXYZ'); sep.location = (-800, -220)

    m_mod = nodes.new('ShaderNodeMath'); m_mod.location = (-560, -220)
    m_mod.operation = 'MODULO'
    if "Value_001" in m_mod.inputs:
        m_mod.inputs["Value_001"].default_value = mod_value  # second input is the modulus

    m_floor = nodes.new('ShaderNodeMath'); m_floor.location = (-360, -220)
    m_floor.operation = 'FLOOR'

    invert = nodes.new('ShaderNodeInvert'); invert.location = (-160, -220)
    bump = nodes.new('ShaderNodeBump'); bump.location = (60, -220)

    diffuse = nodes.new('ShaderNodeBsdfDiffuse'); diffuse.location = (300, 20)
    output = nodes.new('ShaderNodeOutputMaterial'); output.location = (580, 20)

    # Links (by name, no indices)
    links.new(texcoord.outputs['UV'], mapping.inputs['Vector'])
    links.new(mapping.outputs['Vector'], img.inputs['Vector'])
    links.new(img.outputs['Color'], diffuse.inputs['Color'])

    links.new(mapping.outputs['Vector'], sep.inputs['Vector'])
    links.new(sep.outputs['X'], m_mod.inputs['Value'])
    links.new(m_mod.outputs['Value'], m_floor.inputs['Value'])
    links.new(m_floor.outputs['Value'], invert.inputs['Color'])
    links.new(invert.outputs['Color'], bump.inputs['Height'])
    links.new(bump.outputs['Normal'], diffuse.inputs['Normal'])

    links.new(diffuse.outputs['BSDF'], output.inputs['Surface'])

    # Ensure bump-only displacement in Cycles (optional)
    try:
        mat.cycles.displacement_method = 'BUMP'
    except Exception:
        pass


def create_all_shoe_diffuse_shaders(texture_dir: str, mod_value: float):
    created_materials = []
    missing = []

    for tex_file, mat_name in TEXTURE_MAP.items():
        tex_path = os.path.join(texture_dir, tex_file)
        if not os.path.exists(tex_path):
            print(f"Missing texture: {tex_file}")
            missing.append(tex_file)
            continue

        image = bpy.data.images.load(tex_path, check_existing=True)
        mat = bpy.data.materials.get(mat_name) or bpy.data.materials.new(mat_name)
        build_shoe_diffuse_shader(mat, image, mod_value)
        created_materials.append(mat)
        print(f"Built: {mat_name}  ←  {tex_file}")

    if missing:
        print("\nSome textures were not found:")
        for m in missing:
            print(" -", m)

    return created_materials


def pick_material_to_assign(created_materials, priority_list):
    by_name = {m.name: m for m in created_materials}
    for wanted in priority_list:
        if wanted in by_name:
            return by_name[wanted]
    return created_materials[0] if created_materials else None


def assign_material_to_shoes(target_mat: bpy.types.Material):
    if not target_mat:
        print("No material available to assign.")
        return 0
    count = 0
    for obj in bpy.data.objects:
        if SHOE_NAME_TOKEN in obj.name.lower() and getattr(obj.data, "materials", None) is not None:
            if len(obj.data.materials) == 0:
                obj.data.materials.append(target_mat)
            else:
                obj.data.materials[0] = target_mat
            count += 1
            print(f"Assigned {target_mat.name} to {obj.name}")
    if count == 0:
        print(f"No objects found with '{SHOE_NAME_TOKEN}' in the name to assign {target_mat.name}.")
    return count


# -----------------------------------------------------------------------------
# STEP 3 — Surface materials (Diffuse driven by color/alpha mix), assign to 'surfaces' collection
# -----------------------------------------------------------------------------

COLOR_TEXTURES = {
    "seamless_noodle_pattern_beige.png":  "Surface_diffuse_shader_beige",
    "seamless_noodle_pattern_black.png":  "Surface_diffuse_shader_black",
    "seamless_noodle_pattern_blue.png":   "Surface_diffuse_shader_blue",
    "seamless_noodle_pattern_orange.png": "Surface_diffuse_shader_orange",
    "seamless_noodle_pattern_red.png":    "Surface_diffuse_shader_red",
}
SURFACES_COLLECTION_NAME = "surfaces"

APPLY_PRIORITY_SURFACE = [
    "Surface_diffuse_shader_blue",
    "Surface_diffuse_shader_black",
    "Surface_diffuse_shader_red",
    "Surface_diffuse_shader_beige",
    "Surface_diffuse_shader_orange",
]


def find_alpha_texture(folder: str):
    if not os.path.isdir(folder):
        return None
    for fn in os.listdir(folder):
        low = fn.lower()
        if "alpha" in low and low.endswith((".png", ".jpg", ".jpeg", ".tif", ".tiff", ".exr")):
            return os.path.join(folder, fn)
    return None


def new_subtract_color_mix(nodes):
    """Return a Mix node set to Color/Subtract (4.x ShaderNodeMix, 3.x MixRGB)."""
    try:
        n = nodes.new("ShaderNodeMix")          # 4.x
        n.data_type = "RGBA"                    # COLOR mode
        n.blend_type = "SUBTRACT"
        n.clamp_result = True
        n.clamp_factor = True
        return n, "Factor", "A", "B", "Result"
    except Exception:
        n = nodes.new("ShaderNodeMixRGB")       # 3.x fallback (always color)
        n.blend_type = "SUBTRACT"
        n.use_clamp = True
        return n, "Fac", "Color1", "Color2", "Color"


def build_surface_from_spec(mat: bpy.types.Material, color_img: bpy.types.Image, alpha_img: bpy.types.Image):
    """
    EXACT wiring:
      UV -> Mapping(Point, Rot Z=90°, Scale 40/40/30)
      Mapping -> ColorTex(Vector) & AlphaTex(Vector)
      ColorTex Color -> Mix A
      AlphaTex Alpha -> Mix Factor
      AlphaTex Color -> Invert Color -> Mix B
      Mix Result -> Diffuse Color -> Output Surface
    """
    mat.use_nodes = True
    nt = mat.node_tree
    nodes, links = nt.nodes, nt.links
    for n in list(nodes): nodes.remove(n)

    tex = nodes.new("ShaderNodeTexCoord"); tex.location = (-1227, 138)
    mapn = nodes.new("ShaderNodeMapping"); mapn.location = (-1007, 138)
    mapn.vector_type = "POINT"
    mapn.inputs["Rotation"].default_value[2] = radians(90.0)
    mapn.inputs["Scale"].default_value = (40.0, 40.0, 30.0)

    img_col = nodes.new("ShaderNodeTexImage"); img_col.location = (-747, 258)
    img_col.image = color_img
    img_col.interpolation = "Linear"; img_col.extension = "REPEAT"  # sRGB (default)

    img_a = nodes.new("ShaderNodeTexImage"); img_a.location = (-747, -42)
    img_a.image = alpha_img
    img_a.interpolation = "Linear"; img_a.extension = "REPEAT"      # keep default CS

    inv = nodes.new("ShaderNodeInvert"); inv.location = (-467, -42)

    mix, FAC, A, B, OUT = new_subtract_color_mix(nodes)
    mix.location = (-167, 178)

    diff = nodes.new("ShaderNodeBsdfDiffuse"); diff.location = (133, 178)
    outp = nodes.new("ShaderNodeOutputMaterial"); outp.location = (353, 178)

    # Links — exact
    links.new(tex.outputs["UV"], mapn.inputs["Vector"])
    links.new(mapn.outputs["Vector"], img_col.inputs["Vector"])
    links.new(mapn.outputs["Vector"], img_a.inputs["Vector"])

    links.new(img_a.outputs["Color"], inv.inputs["Color"])    # alpha COLOR → Invert(Color)
    links.new(img_col.outputs["Color"], mix.inputs[A])        # color → A
    if "Alpha" in img_a.outputs:
        links.new(img_a.outputs["Alpha"], mix.inputs[FAC])    # alpha ALPHA → Factor
    else:
        links.new(img_a.outputs["Color"], mix.inputs[FAC])    # fallback if no alpha socket
    links.new(inv.outputs["Color"], mix.inputs[B])            # inverted grayscale → B

    links.new(mix.outputs[OUT], diff.inputs["Color"])
    links.new(diff.outputs["BSDF"], outp.inputs["Surface"])

    # keep bump-only mode (harmless)
    try:
        mat.cycles.displacement_method = "BUMP"
    except Exception:
        pass


def create_all_surface_shaders_exact(texture_dir: str):
    alpha_path = find_alpha_texture(texture_dir)
    if not alpha_path or not os.path.exists(alpha_path):
        print("[Surface] Alpha texture not found in:", texture_dir)
        return []

    alpha_img = bpy.data.images.load(alpha_path, check_existing=True)
    print(f"[Surface] Using alpha texture: {os.path.basename(alpha_path)}")

    created = []
    for color_file, mat_name in COLOR_TEXTURES.items():
        color_path = os.path.join(texture_dir, color_file)
        if not os.path.exists(color_path):
            print(f"[Surface] Missing color texture: {color_file}")
            continue
        color_img = bpy.data.images.load(color_path, check_existing=True)
        mat = bpy.data.materials.get(mat_name) or bpy.data.materials.new(mat_name)
        build_surface_from_spec(mat, color_img, alpha_img)
        created.append(mat)
        print(f"[Surface] Built {mat_name}  ←  {color_file}")
    return created


def find_collection_ci(name_ci: str):
    name_ci = name_ci.lower()
    for coll in bpy.data.collections:
        if coll.name.lower() == name_ci:
            return coll
    return None


def iter_objects_in_collection(coll: bpy.types.Collection):
    """Yield objects in collection, including children recursively."""
    seen = set()
    def _walk(c):
        for obj in c.objects:
            if obj.name not in seen:
                seen.add(obj.name)
                yield obj
        for child in c.children:
            yield from _walk(child)
    if coll:
        yield from _walk(coll)


def assign_material_to_surfaces(target_mat: bpy.types.Material):
    if not target_mat:
        print("[Surface] No material available to assign.")
        return 0
    total = 0

    # 1) Prefer a collection named "surfaces"
    coll = find_collection_ci(SURFACES_COLLECTION_NAME)
    if coll:
        for obj in iter_objects_in_collection(coll):
            if obj.type == "MESH" and getattr(obj.data, "materials", None) is not None:
                if len(obj.data.materials) == 0:
                    obj.data.materials.append(target_mat)
                else:
                    obj.data.materials[0] = target_mat
                total += 1
                print(f"[Surface] Assigned {target_mat.name} to {obj.name}")
    else:
        # 2) Fallback: any object whose name contains "surface"
        for obj in bpy.data.objects:
            if "surface" in obj.name.lower() and obj.type == "MESH" and getattr(obj.data, "materials", None) is not None:
                if len(obj.data.materials) == 0:
                    obj.data.materials.append(target_mat)
                else:
                    obj.data.materials[0] = target_mat
                total += 1
                print(f"[Surface] Assigned {target_mat.name} to {obj.name}")

    if total == 0:
        print(f"[Surface] No mesh objects found to assign in '{SURFACES_COLLECTION_NAME}' or by name match.")
    return total


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

class ZMS_OT_Step1_RunAll(Operator):
    bl_idname = "zms.step1_run_all"
    bl_label = "Step 1: Prep + UV + Unwrap"
    bl_description = "Cleanup/position, project UVs from current view (shoe), unwrap surfaces"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        ok = step1_clean_and_position(self)
        if not ok: return {'CANCELLED'}
        ok = step1_project_uv_from_view(self)
        if not ok: return {'CANCELLED'}
        step1_unwrap_surfaces(self)
        return {'FINISHED'}


class ZMS_OT_Step1_CleanAndPosition(Operator):
    bl_idname = "zms.step1_clean_position"
    bl_label = "Clean & Position"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        if not step1_clean_and_position(self):
            return {'CANCELLED'}
        return {'FINISHED'}


class ZMS_OT_Step1_ProjectUV(Operator):
    bl_idname = "zms.step1_project_uv"
    bl_label = "Project UVs From View (shoe)"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        if not step1_project_uv_from_view(self):
            return {'CANCELLED'}
        return {'FINISHED'}


class ZMS_OT_Step1_UnwrapSurfaces(Operator):
    bl_idname = "zms.step1_unwrap_surfaces"
    bl_label = "Unwrap Surfaces (Conformal)"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        if not step1_unwrap_surfaces(self):
            return {'CANCELLED'}
        return {'FINISHED'}


class ZMS_OT_Step2_BuildShoeMaterials(Operator):
    bl_idname = "zms.step2_build_shoe_materials"
    bl_label = "Step 2: Build + Assign Shoe Shaders"
    bl_description = "Builds shoe diffuse shaders from textures and assigns one to shoes"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        prefs = _prefs(context)
        if not prefs or not prefs.texture_dir:
            self.report({'ERROR'}, "Set the Texture Folder in Add-on Preferences first.")
            return {'CANCELLED'}
        created = create_all_shoe_diffuse_shaders(prefs.texture_dir, prefs.mod_value)
        if not created:
            self.report({'ERROR'}, "No shoe shaders were created. Check your texture folder.")
            return {'CANCELLED'}
        target = pick_material_to_assign(created, ASSIGN_PRIORITY_SHOE)
        count = assign_material_to_shoes(target)
        self.report({'INFO'}, f"Built {len(created)} shoe mats, assigned to {count} object(s).")
        return {'FINISHED'}


class ZMS_OT_Step3_BuildSurfaceMaterials(Operator):
    bl_idname = "zms.step3_build_surface_materials"
    bl_label = "Step 3: Build + Assign Surface Shaders"
    bl_description = "Builds surface diffuse shaders from textures and assigns one to the 'surfaces' collection"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        prefs = _prefs(context)
        if not prefs or not prefs.texture_dir:
            self.report({'ERROR'}, "Set the Texture Folder in Add-on Preferences first.")
            return {'CANCELLED'}
        created = create_all_surface_shaders_exact(prefs.texture_dir)
        if not created:
            self.report({'ERROR'}, "No surface shaders were created. Check your texture folder & alpha file.")
            return {'CANCELLED'}
        target = pick_material_to_assign(created, APPLY_PRIORITY_SURFACE)
        count = assign_material_to_surfaces(target)
        self.report({'INFO'}, f"Built {len(created)} surface mats, assigned to {count} mesh(es).")
        return {'FINISHED'}


class ZMS_OT_RunEverything(Operator):
    bl_idname = "zms.run_everything"
    bl_label = "RUN ALL (1 → 3)"
    bl_description = "Runs Step 1, then Step 2 and Step 3"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        # Step 1
        ok = step1_clean_and_position(self)
        if not ok: return {'CANCELLED'}
        ok = step1_project_uv_from_view(self)
        if not ok: return {'CANCELLED'}
        step1_unwrap_surfaces(self)

        # Step 2
        prefs = _prefs(context)
        if not prefs or not prefs.texture_dir:
            self.report({'ERROR'}, "Set the Texture Folder in Add-on Preferences first (for Steps 2–3).")
            return {'CANCELLED'}
        created_shoe = create_all_shoe_diffuse_shaders(prefs.texture_dir, prefs.mod_value)
        if created_shoe:
            target_shoe = pick_material_to_assign(created_shoe, ASSIGN_PRIORITY_SHOE)
            assign_material_to_shoes(target_shoe)

        # Step 3
        created_surf = create_all_surface_shaders_exact(prefs.texture_dir)
        if created_surf:
            target_surf = pick_material_to_assign(created_surf, APPLY_PRIORITY_SURFACE)
            assign_material_to_surfaces(target_surf)

        self.report({'INFO'}, "Full pipeline completed.")
        return {'FINISHED'}


# -----------------------------------------------------------------------------
# UI
# -----------------------------------------------------------------------------

class VIEW3D_PT_ZellerendMyShoe(Panel):
    bl_label = "ZELLEREND MY SHOE"
    bl_idname = "VIEW3D_PT_zellerend_my_shoe"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = "ZELLEREND"

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

        box = layout.box()
        box.label(text="Setup")
        if prefs:
            row = box.row()
            row.prop(prefs, "texture_dir")
            row = box.row()
            row.prop(prefs, "mod_value")
        else:
            box.label(text="Open Preferences and enable add-on", icon='PREFERENCES')

        box = layout.box()
        box.label(text="Step 1 — Model Prep & UV", icon='MESH_CUBE')
        col = box.column(align=True)
        col.operator("zms.step1_clean_position", icon='ORIENTATION_VIEW')
        col.operator("zms.step1_project_uv", icon='GROUP_UVS')
        col.operator("zms.step1_unwrap_surfaces", icon='UV')
        col.separator()
        col.operator("zms.step1_run_all", icon='CHECKMARK')

        box = layout.box()
        box.label(text="Step 2 — Shoe Materials", icon='MATERIAL')
        box.operator("zms.step2_build_shoe_materials", icon='NODE_MATERIAL')

        box = layout.box()
        box.label(text="Step 3 — Surface Materials", icon='MATERIAL')
        box.operator("zms.step3_build_surface_materials", icon='NODE_MATERIAL')

        layout.separator()
        layout.operator("zms.run_everything", icon='SEQUENCE')


# -----------------------------------------------------------------------------
# Registration
# -----------------------------------------------------------------------------

classes = (
    ZMS_AddonPreferences,
    ZMS_OT_Step1_RunAll,
    ZMS_OT_Step1_CleanAndPosition,
    ZMS_OT_Step1_ProjectUV,
    ZMS_OT_Step1_UnwrapSurfaces,
    ZMS_OT_Step2_BuildShoeMaterials,
    ZMS_OT_Step3_BuildSurfaceMaterials,
    ZMS_OT_RunEverything,
    VIEW3D_PT_ZellerendMyShoe,
)

def register():
    for cls in classes:
        bpy.utils.register_class(cls)

def unregister():
    for cls in reversed(classes):
        bpy.utils.unregister_class(cls)

if __name__ == "__main__":
    register()
