#!/usr/bin/env python3
"""
Kit Texture Converter for CrowdInjectV2
Converts FM 3D kit textures to crowd mesh UV layout with adjustable regions.

Outputs a single texture atlas for use with IL2CPP-compatible shader approach.
"""

import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from pathlib import Path
import threading
import os
import json
import time

try:
    from PIL import Image, ImageTk, ImageDraw
except ImportError:
    import sys
    print("Pillow is required. Run: pip install Pillow")
    sys.exit(1)


# Default UV regions for FM kit textures (can be adjusted in GUI)
# These values match FM24/FM26 3D kit layout
DEFAULT_FM_REGIONS = {
    'shirt_front': {'x': 0.0, 'y': 0.023, 'w': 0.290, 'h': 0.387},
    'shirt_back': {'x': 0.294, 'y': 0.021, 'w': 0.298, 'h': 0.387},
    'sleeves': {'x': 0.739, 'y': 0.295, 'w': 0.261, 'h': 0.349},
}

# Crowd mesh UV layout (from Male_ClothingMask.png):
# Sleeves: W-shape upper-left
# Shirt: vertical strip on right edge
DEFAULT_CROWD_UV_MALE = {
    'shirt': {'x': 0.77, 'y': 0.22, 'w': 0.21, 'h': 0.71},   # Right strip
    'sleeves': {'x': 0.05, 'y': 0.10, 'w': 0.47, 'h': 0.42},  # W-shape matches mask
}

CONFIG_FILE = "kit_converter_settings.json"

def get_app_dir():
    """Get the directory where config/preset files should be stored.
    Uses %APPDATA% so presets persist across exe rebuilds.
    """
    import os
    appdata = os.environ.get('APPDATA')
    if appdata:
        config_dir = Path(appdata) / 'CrowdInjectV2_KitConverter'
        config_dir.mkdir(exist_ok=True)
        return config_dir
    else:
        # Fallback for non-Windows
        return Path(__file__).parent


def get_exe_dir():
    """Get the directory where the exe/script is located (for resources like icons)."""
    import sys
    if getattr(sys, 'frozen', False):
        return Path(sys.executable).parent
    else:
        return Path(__file__).parent

# Atlas settings
ATLAS_GRID_COLS = 4  # 4x4 grid = 16 kits max
ATLAS_GRID_ROWS = 4
ATLAS_KIT_SIZE = 512  # Each kit is 512x512 in the atlas
ATLAS_SIZE = ATLAS_GRID_COLS * ATLAS_KIT_SIZE  # 2048x2048 total

# Section types for crowd
SECTION_TYPES = ['Ultra', 'Hardcore', 'Heavy', 'Pop', 'Folk', 'Neutral']

# Default section config
DEFAULT_SECTION_CONFIG = {
    'Ultra': {'probability': 0.70, 'indices': [0]},
    'Hardcore': {'probability': 0.50, 'indices': [0]},
    'Heavy': {'probability': 0.40, 'indices': [0]},
    'Pop': {'probability': 0.30, 'indices': [0, 1]},
    'Folk': {'probability': 0.20, 'indices': [0, 1, 2]},
    'Neutral': {'probability': 0.00, 'indices': []},
}


class SectionConfigDialog(tk.Toplevel):
    """Dialog to configure kit probability and allowed indices per crowd section."""

    def __init__(self, parent, kit_count, existing_config=None):
        super().__init__(parent)
        self.title("Section Kit Configuration")
        self.kit_count = kit_count
        self.result = None  # Will hold the config when OK is clicked

        # Make modal
        self.transient(parent)
        self.grab_set()

        # Calculate size based on screen DPI (scale for high-DPI displays)
        base_width, base_height = 550, 620
        try:
            dpi_scale = self.winfo_fpixels('1i') / 96.0  # 96 DPI is standard
            width = int(base_width * max(1.0, dpi_scale))
            height = int(base_height * max(1.0, dpi_scale))
        except:
            width, height = base_width, base_height

        # Center on screen
        screen_w = self.winfo_screenwidth()
        screen_h = self.winfo_screenheight()
        x = (screen_w - width) // 2
        y = (screen_h - height) // 2
        self.geometry(f"{width}x{height}+{x}+{y}")

        # Allow resizing
        self.minsize(500, 550)

        # Load existing or default config
        self.config = existing_config.copy() if existing_config else {}
        for section in SECTION_TYPES:
            if section not in self.config:
                self.config[section] = DEFAULT_SECTION_CONFIG[section].copy()

        self._create_ui()

        # Wait for dialog to close
        self.protocol("WM_DELETE_WINDOW", self._on_cancel)

    def _create_ui(self):
        # Header
        header = ttk.Label(self, text="Configure which kits each fan section can wear",
                          font=('TkDefaultFont', 10, 'bold'))
        header.pack(pady=(10, 5))

        subtitle = ttk.Label(self, text="Probability = % of fans in section who wear kits\n"
                                        "Indices = which kit slots they can wear (0=home, 1=away, etc.)")
        subtitle.pack(pady=(0, 10))

        # Scrollable frame for sections
        container = ttk.Frame(self)
        container.pack(fill='both', expand=True, padx=10)

        # Store widgets for each section
        self.prob_vars = {}
        self.index_vars = {}

        for section in SECTION_TYPES:
            frame = ttk.LabelFrame(container, text=section)
            frame.pack(fill='x', pady=5)

            # Probability slider + entry
            prob_frame = ttk.Frame(frame)
            prob_frame.pack(fill='x', padx=5, pady=2)

            ttk.Label(prob_frame, text="Probability:", width=12).pack(side='left')

            prob_var = tk.IntVar(value=int(self.config[section]['probability'] * 100))
            self.prob_vars[section] = prob_var

            prob_scale = ttk.Scale(prob_frame, from_=0, to=100, variable=prob_var,
                                   orient='horizontal', length=150)
            prob_scale.pack(side='left', padx=5)

            # Entry for manual input
            prob_entry = ttk.Entry(prob_frame, textvariable=prob_var, width=4)
            prob_entry.pack(side='left', padx=2)
            ttk.Label(prob_frame, text="%").pack(side='left')

            # Validate entry to keep in 0-100 range
            def validate_prob(var=prob_var):
                try:
                    val = var.get()
                    if val < 0:
                        var.set(0)
                    elif val > 100:
                        var.set(100)
                except:
                    pass
            prob_var.trace_add('write', lambda *args, v=prob_var: validate_prob(v))

            # Kit index checkboxes
            idx_frame = ttk.Frame(frame)
            idx_frame.pack(fill='x', padx=5, pady=2)

            ttk.Label(idx_frame, text="Kit indices:", width=12).pack(side='left')

            self.index_vars[section] = []
            current_indices = self.config[section].get('indices', [])

            for i in range(self.kit_count):
                var = tk.BooleanVar(value=(i in current_indices))
                self.index_vars[section].append(var)
                cb = ttk.Checkbutton(idx_frame, text=str(i), variable=var)
                cb.pack(side='left', padx=3)

        # Buttons
        btn_frame = ttk.Frame(self)
        btn_frame.pack(fill='x', padx=10, pady=10)

        ttk.Button(btn_frame, text="Reset to Defaults", command=self._reset_defaults).pack(side='left')
        ttk.Button(btn_frame, text="Skip (Use Defaults)", command=self._on_cancel).pack(side='right', padx=5)
        ttk.Button(btn_frame, text="Save to KitAtlas.json", command=self._on_ok).pack(side='right')

    def _reset_defaults(self):
        """Reset all sections to default values."""
        for section in SECTION_TYPES:
            defaults = DEFAULT_SECTION_CONFIG[section]
            self.prob_vars[section].set(int(defaults['probability'] * 100))
            for i, var in enumerate(self.index_vars[section]):
                var.set(i in defaults['indices'])

    def _on_ok(self):
        """Save config and close."""
        self.result = {}
        for section in SECTION_TYPES:
            indices = [i for i, var in enumerate(self.index_vars[section]) if var.get()]
            self.result[section] = {
                'probability': self.prob_vars[section].get() / 100.0,
                'indices': indices
            }
        self.destroy()

    def _on_cancel(self):
        """Close without saving."""
        self.result = None
        self.destroy()


class RegionEditor(ttk.LabelFrame):
    """Widget to edit a UV region with sliders and rotation."""

    def __init__(self, parent, name, region, on_change=None):
        super().__init__(parent, text=name)
        self.region = region.copy()
        # Ensure rotation key exists
        if 'rot' not in self.region:
            self.region['rot'] = 0
        self.on_change = on_change
        self._suppress_callback = False  # Flag to prevent callback during bulk updates

        self.vars = {}
        row = 0
        for key in ['x', 'y', 'w', 'h']:
            ttk.Label(self, text=f"{key.upper()}:", width=3).grid(row=row, column=0, sticky='e')
            var = tk.DoubleVar(value=region[key])
            var.trace_add('write', self._on_var_change)
            self.vars[key] = var

            scale = ttk.Scale(self, from_=0, to=1, variable=var, length=100)
            scale.grid(row=row, column=1, sticky='ew', padx=2)

            entry = ttk.Entry(self, textvariable=var, width=6)
            entry.grid(row=row, column=2, padx=2)
            row += 1

        # Rotation dropdown
        ttk.Label(self, text="Rot:", width=3).grid(row=row, column=0, sticky='e')
        self.rot_var = tk.StringVar(value=f"{self.region.get('rot', 0)}°")
        self.rot_combo = ttk.Combobox(self, textvariable=self.rot_var,
                                       values=["0°", "90°", "180°", "270°"],
                                       state='readonly', width=5)
        self.rot_combo.grid(row=row, column=1, sticky='w', padx=2)
        self.rot_combo.bind('<<ComboboxSelected>>', self._on_rot_change)

        self.columnconfigure(1, weight=1)

    def _on_var_change(self, *args):
        if self._suppress_callback:
            return
        for key, var in self.vars.items():
            try:
                self.region[key] = var.get()
            except:
                pass
        if self.on_change:
            self.on_change()

    def _on_rot_change(self, event=None):
        if self._suppress_callback:
            return
        # Parse "90°" -> 90
        rot_str = self.rot_var.get().replace('°', '')
        try:
            self.region['rot'] = int(rot_str)
        except:
            self.region['rot'] = 0
        if self.on_change:
            self.on_change()

    def get_region(self):
        return self.region.copy()

    def set_region(self, region, suppress_callback=False):
        """Set region values. suppress_callback=True prevents triggering on_change."""
        old_suppress = self._suppress_callback
        if suppress_callback:
            self._suppress_callback = True

        self.region = region.copy()
        # Ensure rotation key exists
        if 'rot' not in self.region:
            self.region['rot'] = 0

        for key, val in region.items():
            if key in self.vars:
                self.vars[key].set(val)

        # Update rotation combobox
        self.rot_var.set(f"{self.region.get('rot', 0)}°")

        self._suppress_callback = old_suppress


class KitConverterApp:
    def __init__(self, root):
        self.root = root
        self.root.title("CrowdInjectV2 Kit Converter")

        # DPI-aware window sizing
        try:
            dpi_scale = root.winfo_fpixels('1i') / 96.0  # 96 DPI is standard
            base_w, base_h = 1100, 750
            min_w, min_h = 900, 600
            width = int(base_w * max(1.0, dpi_scale))
            height = int(base_h * max(1.0, dpi_scale))
            min_width = int(min_w * max(1.0, dpi_scale))
            min_height = int(min_h * max(1.0, dpi_scale))
        except:
            width, height = 1100, 750
            min_width, min_height = 900, 600

        # Center on screen
        screen_w = root.winfo_screenwidth()
        screen_h = root.winfo_screenheight()
        x = (screen_w - width) // 2
        y = (screen_h - height) // 2
        self.root.geometry(f"{width}x{height}+{x}+{y}")
        self.root.minsize(min_width, min_height)

        # State
        self.input_files = []
        self.current_image = None
        self.current_index = 0

        # Settings
        self.output_folder = tk.StringVar(value="")
        self.start_index = tk.IntVar(value=0)
        self.exclude_shorts = tk.BooleanVar(value=True)
        self.output_atlas = tk.BooleanVar(value=True)  # New: output as atlas

        # Team/Kit organization settings
        self.kits_root_path = tk.StringVar(value="")
        self.team_id = tk.StringVar(value="")
        self.country = tk.StringVar(value="")
        self.club_name = tk.StringVar(value="")

        # Mask settings
        self.masks_folder = tk.StringVar(value="")
        self.mask_image = None  # Cached mask for preview background
        self.mask_photo = None  # PhotoImage reference

        # UV Regions (editable)
        self.fm_regions = {k: v.copy() for k, v in DEFAULT_FM_REGIONS.items()}
        self.crowd_regions = {k: v.copy() for k, v in DEFAULT_CROWD_UV_MALE.items()}

        # Performance: throttle preview updates during drag
        self._last_preview_time = 0
        self._preview_throttle_ms = 50  # Max ~20 FPS during drag
        self._pending_preview = None
        self._is_dragging = False

        # Undo history
        self._undo_stack = []
        self._max_undo = 50

        # Load saved settings
        self._load_settings()

        self._create_ui()

        # Bind Ctrl+Z for undo
        self.root.bind('<Control-z>', self._undo)

        # Load mask after UI created
        self._load_mask_image()

    def _load_settings(self):
        """Load settings from config file."""
        config_path = get_app_dir() / CONFIG_FILE
        if config_path.exists():
            try:
                with open(config_path, 'r') as f:
                    data = json.load(f)
                if 'fm_regions' in data:
                    self.fm_regions = data['fm_regions']
                if 'crowd_regions' in data:
                    self.crowd_regions = data['crowd_regions']
                if 'output_folder' in data:
                    self.output_folder.set(data['output_folder'])
                if 'exclude_shorts' in data:
                    self.exclude_shorts.set(data['exclude_shorts'])
                if 'output_atlas' in data:
                    self.output_atlas.set(data['output_atlas'])
                # Team/Kit organization settings
                if 'kits_root_path' in data:
                    self.kits_root_path.set(data['kits_root_path'])
                if 'team_id' in data:
                    self.team_id.set(data['team_id'])
                if 'country' in data:
                    self.country.set(data['country'])
                if 'club_name' in data:
                    self.club_name.set(data['club_name'])
                if 'masks_folder' in data:
                    self.masks_folder.set(data['masks_folder'])
            except:
                pass

        # Auto-detect kits folder if not set
        if not self.kits_root_path.get():
            detected = self._auto_detect_kits_folder()
            if detected:
                self.kits_root_path.set(detected)

        # Auto-detect masks folder if not set
        if not self.masks_folder.get():
            detected = self._auto_detect_masks_folder()
            if detected:
                self.masks_folder.set(detected)

    def _auto_detect_kits_folder(self):
        """Auto-detect the CrowdInjectV2 Kits folder from common Steam paths."""
        # Common Steam install locations
        steam_paths = [
            Path("C:/Program Files (x86)/Steam"),
            Path("C:/Program Files/Steam"),
            Path("D:/Steam"),
            Path("D:/SteamLibrary"),
            Path("E:/Steam"),
            Path("E:/SteamLibrary"),
            Path("F:/Steam"),
            Path("F:/SteamLibrary"),
        ]

        # Also check Steam's libraryfolders.vdf for additional library paths
        for steam_base in steam_paths[:2]:  # Check default Steam locations
            vdf_path = steam_base / "steamapps" / "libraryfolders.vdf"
            if vdf_path.exists():
                try:
                    with open(vdf_path, 'r', encoding='utf-8') as f:
                        content = f.read()
                    # Simple parse for "path" entries
                    import re
                    paths = re.findall(r'"path"\s+"([^"]+)"', content)
                    for p in paths:
                        steam_paths.append(Path(p.replace("\\\\", "/")))
                except:
                    pass

        # Look for FM26 in each Steam library
        for steam_path in steam_paths:
            kits_folder = steam_path / "steamapps/common/Football Manager 26/BepInEx/plugins/CrowdInjectV2/Kits"
            if kits_folder.exists():
                return str(kits_folder)

        return None

    def _auto_detect_masks_folder(self):
        """Auto-detect the CrowdInjectV2 Masks folder from common Steam paths."""
        # Same Steam path detection as kits
        steam_paths = [
            Path("C:/Program Files (x86)/Steam"),
            Path("C:/Program Files/Steam"),
            Path("D:/Steam"),
            Path("D:/SteamLibrary"),
            Path("E:/Steam"),
            Path("E:/SteamLibrary"),
            Path("F:/Steam"),
            Path("F:/SteamLibrary"),
        ]

        # Check Steam library folders
        for steam_base in steam_paths[:2]:
            vdf_path = steam_base / "steamapps" / "libraryfolders.vdf"
            if vdf_path.exists():
                try:
                    with open(vdf_path, 'r', encoding='utf-8') as f:
                        content = f.read()
                    import re
                    paths = re.findall(r'"path"\s+"([^"]+)"', content)
                    for p in paths:
                        steam_paths.append(Path(p.replace("\\\\", "/")))
                except:
                    pass

        # Look for FM26 Masks folder
        for steam_path in steam_paths:
            masks_folder = steam_path / "steamapps/common/Football Manager 26/BepInEx/plugins/CrowdInjectV2/Masks"
            if masks_folder.exists():
                return str(masks_folder)

        return None

    def _load_mask_image(self):
        """Load the clothing mask image for preview background."""
        masks_path = self.masks_folder.get()
        if not masks_path:
            return

        # Try male mask first
        mask_file = Path(masks_path) / "Male_ClothingMask.png"
        if mask_file.exists():
            try:
                self.mask_image = Image.open(mask_file).convert('RGBA')
                print(f"Loaded mask: {mask_file}")
            except Exception as e:
                print(f"Failed to load mask: {e}")
                self.mask_image = None

    def _save_settings(self):
        """Save settings to config file."""
        config_path = get_app_dir() / CONFIG_FILE
        data = {
            'fm_regions': self.fm_regions,
            'crowd_regions': self.crowd_regions,
            'output_folder': self.output_folder.get(),
            'exclude_shorts': self.exclude_shorts.get(),
            'output_atlas': self.output_atlas.get(),
            # Team/Kit organization settings
            'kits_root_path': self.kits_root_path.get(),
            'team_id': self.team_id.get(),
            'country': self.country.get(),
            'club_name': self.club_name.get(),
            'masks_folder': self.masks_folder.get(),
        }
        try:
            with open(config_path, 'w') as f:
                json.dump(data, f, indent=2)
        except:
            pass

    def _create_ui(self):
        # Configure grid weights
        self.root.columnconfigure(0, weight=1)
        self.root.rowconfigure(0, weight=1)

        # Main paned window
        paned = ttk.PanedWindow(self.root, orient='horizontal')
        paned.grid(row=0, column=0, sticky='nsew', padx=5, pady=5)

        # === Left Panel: File List ===
        left_frame = ttk.Frame(paned)
        paned.add(left_frame, weight=1)

        # File list
        list_label = ttk.Label(left_frame, text="Kit Textures:", font=('', 10, 'bold'))
        list_label.pack(anchor='w')

        list_frame = ttk.Frame(left_frame)
        list_frame.pack(fill='both', expand=True, pady=(5, 0))

        self.file_listbox = tk.Listbox(list_frame, selectmode=tk.EXTENDED, font=('Consolas', 9))
        self.file_listbox.pack(side='left', fill='both', expand=True)
        self.file_listbox.bind('<<ListboxSelect>>', self._on_file_select)

        scrollbar = ttk.Scrollbar(list_frame, orient='vertical', command=self.file_listbox.yview)
        scrollbar.pack(side='right', fill='y')
        self.file_listbox.config(yscrollcommand=scrollbar.set)

        # File buttons
        btn_frame = ttk.Frame(left_frame)
        btn_frame.pack(fill='x', pady=(5, 0))

        ttk.Button(btn_frame, text="Add Files", command=self._add_files).pack(side='left', padx=(0, 5))
        ttk.Button(btn_frame, text="Add Folder", command=self._add_folder).pack(side='left', padx=(0, 5))
        ttk.Button(btn_frame, text="Remove", command=self._remove_selected).pack(side='left', padx=(0, 5))
        ttk.Button(btn_frame, text="Clear All", command=self._clear_files).pack(side='left')

        # Output folder (legacy/fallback)
        out_frame = ttk.LabelFrame(left_frame, text="Output Folder (Legacy)", padding=5)
        out_frame.pack(fill='x', pady=(10, 0))

        ttk.Entry(out_frame, textvariable=self.output_folder).pack(side='left', fill='x', expand=True)
        ttk.Button(out_frame, text="...", width=3, command=self._browse_output).pack(side='right', padx=(5, 0))

        # ===== Masks Folder =====
        masks_frame = ttk.LabelFrame(left_frame, text="Clothing Masks", padding=5)
        masks_frame.pack(fill='x', pady=(10, 0))

        masks_row = ttk.Frame(masks_frame)
        masks_row.pack(fill='x')
        ttk.Label(masks_row, text="Masks:", width=10).pack(side='left')
        ttk.Entry(masks_row, textvariable=self.masks_folder).pack(side='left', fill='x', expand=True, padx=(5, 0))
        ttk.Button(masks_row, text="...", width=3, command=self._browse_masks).pack(side='right', padx=(5, 0))
        ttk.Button(masks_row, text="Reload", width=6, command=self._reload_mask).pack(side='right', padx=(5, 0))

        # ===== Team/Kit Organization =====
        team_frame = ttk.LabelFrame(left_frame, text="Team Kit Organization", padding=5)
        team_frame.pack(fill='x', pady=(10, 0))

        # Kits Root Path
        kits_root_row = ttk.Frame(team_frame)
        kits_root_row.pack(fill='x', pady=(0, 5))
        ttk.Label(kits_root_row, text="Kits Root:", width=10).pack(side='left')
        ttk.Entry(kits_root_row, textvariable=self.kits_root_path).pack(side='left', fill='x', expand=True, padx=(5, 0))
        ttk.Button(kits_root_row, text="...", width=3, command=self._browse_kits_root).pack(side='right', padx=(5, 0))

        # Country
        country_row = ttk.Frame(team_frame)
        country_row.pack(fill='x', pady=(0, 5))
        ttk.Label(country_row, text="Country:", width=10).pack(side='left')
        ttk.Entry(country_row, textvariable=self.country).pack(side='left', fill='x', expand=True, padx=(5, 0))

        # Club Name
        club_row = ttk.Frame(team_frame)
        club_row.pack(fill='x', pady=(0, 5))
        ttk.Label(club_row, text="Club Name:", width=10).pack(side='left')
        ttk.Entry(club_row, textvariable=self.club_name).pack(side='left', fill='x', expand=True, padx=(5, 0))

        # Team ID
        team_id_row = ttk.Frame(team_frame)
        team_id_row.pack(fill='x', pady=(0, 0))
        ttk.Label(team_id_row, text="Team ID:", width=10).pack(side='left')
        ttk.Entry(team_id_row, textvariable=self.team_id, width=15).pack(side='left', padx=(5, 0))
        ttk.Label(team_id_row, text="(from FM database)", font=('', 8)).pack(side='left', padx=(10, 0))

        # Options
        opt_frame = ttk.LabelFrame(left_frame, text="Options", padding=5)
        opt_frame.pack(fill='x', pady=(10, 0))

        ttk.Checkbutton(opt_frame, text="Exclude shorts (crop lower part)",
                       variable=self.exclude_shorts, command=self._update_preview).pack(anchor='w')

        ttk.Checkbutton(opt_frame, text="Output as atlas (single PNG, IL2CPP compatible)",
                       variable=self.output_atlas).pack(anchor='w', pady=(5, 0))

        idx_frame = ttk.Frame(opt_frame)
        idx_frame.pack(fill='x', pady=(5, 0))
        ttk.Label(idx_frame, text="Start index:").pack(side='left')
        ttk.Spinbox(idx_frame, from_=0, to=15, width=5, textvariable=self.start_index).pack(side='left', padx=5)
        ttk.Label(idx_frame, text="(0-15 for atlas mode)").pack(side='left', padx=(10, 0))

        # Convert button
        self.convert_btn = ttk.Button(left_frame, text="Convert All", command=self._convert_all)
        self.convert_btn.pack(fill='x', pady=(10, 0))

        # Progress
        self.progress = ttk.Progressbar(left_frame, mode='determinate')
        self.progress.pack(fill='x', pady=(5, 0))

        self.status_label = ttk.Label(left_frame, text="Select kit textures to begin")
        self.status_label.pack(anchor='w', pady=(5, 0))

        # === Center Panel: Preview ===
        center_frame = ttk.Frame(paned)
        paned.add(center_frame, weight=2)

        preview_label = ttk.Label(center_frame, text="Preview:", font=('', 10, 'bold'))
        preview_label.pack(anchor='w')

        # Preview canvases side by side
        preview_container = ttk.Frame(center_frame)
        preview_container.pack(fill='both', expand=True, pady=(5, 0))
        preview_container.columnconfigure(0, weight=1)
        preview_container.columnconfigure(1, weight=1)
        preview_container.rowconfigure(1, weight=1)

        ttk.Label(preview_container, text="Source (FM Kit)").grid(row=0, column=0)
        ttk.Label(preview_container, text="Output (Crowd UV)").grid(row=0, column=1)

        self.source_canvas = tk.Canvas(preview_container, bg='#2a2a2a', highlightthickness=1, highlightbackground='#555')
        self.source_canvas.grid(row=1, column=0, sticky='nsew', padx=(0, 5))

        self.output_canvas = tk.Canvas(preview_container, bg='#2a2a2a', highlightthickness=1, highlightbackground='#555')
        self.output_canvas.grid(row=1, column=1, sticky='nsew', padx=(5, 0))

        # Bind resize
        self.source_canvas.bind('<Configure>', lambda e: self._update_preview())

        # Drag interaction for source regions
        self._drag_region = None  # Which region is being dragged
        self._drag_mode = None    # 'move' or 'resize'
        self._drag_start = None   # (x, y) start position
        self._drag_corner = None  # Which corner for resize
        self._drag_canvas = None  # 'source' or 'output'
        self.source_canvas.bind('<Button-1>', lambda e: self._on_canvas_click(e, 'source'))
        self.source_canvas.bind('<B1-Motion>', lambda e: self._on_canvas_drag(e, 'source'))
        self.source_canvas.bind('<ButtonRelease-1>', self._on_canvas_release)

        # Drag interaction for output/destination regions
        self.output_canvas.bind('<Button-1>', lambda e: self._on_canvas_click(e, 'output'))
        self.output_canvas.bind('<B1-Motion>', lambda e: self._on_canvas_drag(e, 'output'))
        self.output_canvas.bind('<ButtonRelease-1>', self._on_canvas_release)

        # === Right Panel: UV Region Editor (scrollable) ===
        right_frame = ttk.Frame(paned)
        paned.add(right_frame, weight=1)

        # Create scrollable canvas for right panel
        right_canvas = tk.Canvas(right_frame, highlightthickness=0)
        right_scrollbar = ttk.Scrollbar(right_frame, orient="vertical", command=right_canvas.yview)
        right_scrollable = ttk.Frame(right_canvas)

        right_scrollable.bind("<Configure>", lambda e: right_canvas.configure(scrollregion=right_canvas.bbox("all")))
        window_id = right_canvas.create_window((0, 0), window=right_scrollable, anchor="nw")
        right_canvas.configure(yscrollcommand=right_scrollbar.set)

        # Make scrollable frame fill the canvas width (fixes empty grey space)
        def _on_canvas_configure(event):
            right_canvas.itemconfig(window_id, width=event.width)
        right_canvas.bind('<Configure>', _on_canvas_configure)

        right_canvas.pack(side="left", fill="both", expand=True)
        right_scrollbar.pack(side="right", fill="y")

        # Bind mousewheel to scroll
        def _on_mousewheel(event):
            right_canvas.yview_scroll(int(-1*(event.delta/120)), "units")
        right_canvas.bind_all("<MouseWheel>", _on_mousewheel)

        # === Presets Section ===
        preset_frame = ttk.LabelFrame(right_scrollable, text="Presets", padding=5)
        preset_frame.pack(fill='x', pady=(0, 10))

        preset_btn_frame = ttk.Frame(preset_frame)
        preset_btn_frame.pack(fill='x')

        ttk.Label(preset_btn_frame, text="Save:").pack(side='left')
        for i in range(1, 6):
            ttk.Button(preset_btn_frame, text=str(i), width=3,
                      command=lambda idx=i: self._save_preset(idx)).pack(side='left', padx=2)

        preset_load_frame = ttk.Frame(preset_frame)
        preset_load_frame.pack(fill='x', pady=(5, 0))

        ttk.Label(preset_load_frame, text="Load:").pack(side='left')
        for i in range(1, 6):
            ttk.Button(preset_load_frame, text=str(i), width=3,
                      command=lambda idx=i: self._load_preset(idx)).pack(side='left', padx=2)

        # === Source UV Regions ===
        src_label = ttk.Label(right_scrollable, text="Source UV Regions:", font=('', 10, 'bold'))
        src_label.pack(anchor='w', pady=(10, 0))
        ttk.Label(right_scrollable, text="Where to extract FROM the FM kit", font=('', 8)).pack(anchor='w')

        # Source region editors
        self.region_editors = {}
        for name in ['shirt_front', 'shirt_back', 'sleeves']:
            editor = RegionEditor(right_scrollable, name, self.fm_regions[name], self._update_preview)
            editor.pack(fill='x', pady=(5, 0))
            self.region_editors[name] = editor

        # === Destination UV Regions ===
        dest_label = ttk.Label(right_scrollable, text="Destination UV Regions:", font=('', 10, 'bold'))
        dest_label.pack(anchor='w', pady=(15, 0))
        ttk.Label(right_scrollable, text="Where to place ON the crowd mesh", font=('', 8)).pack(anchor='w')

        # Destination region editors
        self.dest_region_editors = {}
        for name in ['shirt', 'sleeves']:
            editor = RegionEditor(right_scrollable, f"dest_{name}", self.crowd_regions[name], self._update_preview)
            editor.pack(fill='x', pady=(5, 0))
            self.dest_region_editors[name] = editor

        # Reset button
        ttk.Button(right_scrollable, text="Reset All to Defaults", command=self._reset_regions).pack(pady=(15, 0))

        # Save settings on close
        self.root.protocol("WM_DELETE_WINDOW", self._on_close)

        # Image references
        self._source_photo = None
        self._output_photo = None

    def _on_close(self):
        self._save_settings()
        self.root.destroy()

    def _add_files(self):
        files = filedialog.askopenfilenames(
            title="Select FM Kit Textures",
            filetypes=[("PNG Images", "*.png"), ("All Files", "*.*")]
        )
        self._add_file_list(files)

    def _add_folder(self):
        folder = filedialog.askdirectory(title="Select Folder with Kit Textures")
        if folder:
            # Find all *3D.png files
            files = list(Path(folder).glob("*3D.png"))
            if not files:
                # Try all PNGs
                files = list(Path(folder).glob("*.png"))
            self._add_file_list([str(f) for f in files])

    def _add_file_list(self, files):
        for f in files:
            if f not in self.input_files:
                self.input_files.append(f)
                self.file_listbox.insert(tk.END, Path(f).name)
        self._update_status()

    def _clear_files(self):
        self.input_files.clear()
        self.file_listbox.delete(0, tk.END)
        self.current_image = None
        self._clear_canvases()
        self._update_status()

    def _remove_selected(self):
        """Remove only the selected files from the list."""
        selection = list(self.file_listbox.curselection())
        if not selection:
            return

        # Remove in reverse order to preserve indices
        for idx in reversed(selection):
            del self.input_files[idx]
            self.file_listbox.delete(idx)

        # Clear preview if current image was removed
        self.current_image = None
        self._clear_canvases()
        self._update_status()

    def _clear_canvases(self):
        self.source_canvas.delete('all')
        self.output_canvas.delete('all')
        self._source_photo = None
        self._output_photo = None

    def _canvas_to_normalized(self, canvas_x, canvas_y):
        """Convert canvas coordinates to normalized (0-1) image coordinates."""
        if self.current_image is None:
            return None, None

        cw = self.source_canvas.winfo_width()
        ch = self.source_canvas.winfo_height()

        # Get the displayed image size (after thumbnail)
        img_w, img_h = self.current_image.size
        # Calculate thumbnail size
        scale = min((cw - 10) / img_w, (ch - 10) / img_h)
        disp_w = int(img_w * scale)
        disp_h = int(img_h * scale)

        # Image is centered in canvas
        offset_x = (cw - disp_w) / 2
        offset_y = (ch - disp_h) / 2

        # Convert to image-relative coords
        img_x = (canvas_x - offset_x) / disp_w
        img_y = (canvas_y - offset_y) / disp_h

        return img_x, img_y

    def _on_canvas_click(self, event, canvas_type='source'):
        """Handle click on canvas - select region to drag."""
        if self.current_image is None:
            return

        # Get normalized coordinates based on canvas type
        if canvas_type == 'source':
            nx, ny = self._canvas_to_normalized(event.x, event.y)
            regions = self.fm_regions
            region_names = ['shirt_front', 'shirt_back', 'sleeves']
        else:  # output
            nx, ny = self._output_canvas_to_normalized(event.x, event.y)
            regions = self.crowd_regions
            region_names = ['shirt', 'sleeves']

        if nx is None:
            return

        threshold = 0.03  # Edge threshold for resize vs move

        for name in region_names:
            if name not in regions:
                continue
            region = regions[name]

            x1, y1 = region['x'], region['y']
            x2, y2 = x1 + region['w'], y1 + region['h']

            # Check if click is inside region
            if x1 <= nx <= x2 and y1 <= ny <= y2:
                # Save state for undo before starting drag
                self._save_undo_state()

                self._drag_region = name
                self._drag_start = (nx, ny)
                self._drag_canvas = canvas_type
                self._is_dragging = True  # Enable throttling

                # Check if near edge (resize) or center (move)
                near_left = abs(nx - x1) < threshold
                near_right = abs(nx - x2) < threshold
                near_top = abs(ny - y1) < threshold
                near_bottom = abs(ny - y2) < threshold

                if near_right and near_bottom:
                    self._drag_mode = 'resize'
                    self._drag_corner = 'br'
                elif near_left and near_top:
                    self._drag_mode = 'resize'
                    self._drag_corner = 'tl'
                elif near_right and near_top:
                    self._drag_mode = 'resize'
                    self._drag_corner = 'tr'
                elif near_left and near_bottom:
                    self._drag_mode = 'resize'
                    self._drag_corner = 'bl'
                elif near_right:
                    self._drag_mode = 'resize'
                    self._drag_corner = 'r'
                elif near_bottom:
                    self._drag_mode = 'resize'
                    self._drag_corner = 'b'
                elif near_left:
                    self._drag_mode = 'resize'
                    self._drag_corner = 'l'
                elif near_top:
                    self._drag_mode = 'resize'
                    self._drag_corner = 't'
                else:
                    self._drag_mode = 'move'
                    self._drag_corner = None

                return

    def _on_canvas_drag(self, event, canvas_type='source'):
        """Handle drag on canvas - move/resize region."""
        if self._drag_region is None or self._drag_start is None:
            return

        # Get normalized coordinates based on canvas type
        if self._drag_canvas == 'source':
            nx, ny = self._canvas_to_normalized(event.x, event.y)
            regions = self.fm_regions
            editors = self.region_editors
        else:  # output
            nx, ny = self._output_canvas_to_normalized(event.x, event.y)
            regions = self.crowd_regions
            editors = self.dest_region_editors if hasattr(self, 'dest_region_editors') else {}

        if nx is None:
            return

        dx = nx - self._drag_start[0]
        dy = ny - self._drag_start[1]

        region = regions[self._drag_region]

        if self._drag_mode == 'move':
            # Move entire region
            new_x = max(0, min(1 - region['w'], region['x'] + dx))
            new_y = max(0, min(1 - region['h'], region['y'] + dy))
            region['x'] = new_x
            region['y'] = new_y
        elif self._drag_mode == 'resize':
            # Resize based on which edge/corner
            corner = self._drag_corner
            if 'r' in corner:
                region['w'] = max(0.05, min(1 - region['x'], region['w'] + dx))
            if 'b' in corner:
                region['h'] = max(0.05, min(1 - region['y'], region['h'] + dy))
            if 'l' in corner:
                new_x = max(0, region['x'] + dx)
                region['w'] = max(0.05, region['w'] - dx)
                region['x'] = new_x
            if 't' in corner:
                new_y = max(0, region['y'] + dy)
                region['h'] = max(0.05, region['h'] - dy)
                region['y'] = new_y

        self._drag_start = (nx, ny)

        # Update editor sliders
        if self._drag_region in editors:
            editors[self._drag_region].set_region(region)

        self._update_preview()

    def _output_canvas_to_normalized(self, canvas_x, canvas_y):
        """Convert output canvas coordinates to normalized (0-1) coordinates."""
        cw = self.output_canvas.winfo_width()
        ch = self.output_canvas.winfo_height()

        # Output preview is square, centered in canvas
        preview_size = min(cw, ch) - 10
        offset_x = (cw - preview_size) / 2
        offset_y = (ch - preview_size) / 2

        # Convert to normalized coords
        norm_x = (canvas_x - offset_x) / preview_size
        norm_y = (canvas_y - offset_y) / preview_size

        return norm_x, norm_y

    def _on_canvas_release(self, event):
        """Handle mouse release - stop dragging."""
        was_dragging = self._is_dragging
        self._drag_region = None
        self._drag_mode = None
        self._drag_start = None
        self._drag_corner = None
        self._drag_canvas = None
        self._is_dragging = False

        # Do a final high-quality preview update after drag ends
        if was_dragging:
            # Cancel any pending low-quality update
            if self._pending_preview is not None:
                self.root.after_cancel(self._pending_preview)
                self._pending_preview = None
            self._update_preview(force=True)

    def _browse_output(self):
        folder = filedialog.askdirectory(title="Select Output Folder")
        if folder:
            self.output_folder.set(folder)

    def _browse_kits_root(self):
        folder = filedialog.askdirectory(title="Select Kits Root Folder")
        if folder:
            self.kits_root_path.set(folder)

    def _browse_masks(self):
        folder = filedialog.askdirectory(title="Select Masks Folder")
        if folder:
            self.masks_folder.set(folder)
            self._load_mask_image()
            self._update_preview()

    def _reload_mask(self):
        """Reload the mask image from the current path."""
        self._load_mask_image()
        self._update_preview()

    def _on_file_select(self, event):
        selection = self.file_listbox.curselection()
        if not selection:
            return

        idx = selection[0]
        filepath = self.input_files[idx]
        self.current_index = idx

        try:
            self.current_image = Image.open(filepath).convert('RGBA')
            self._update_preview()
        except Exception as e:
            messagebox.showerror("Error", f"Failed to load image:\n{e}")

    def _update_preview(self, *args, force=False):
        if self.current_image is None:
            return

        # Throttle updates during drag for performance
        if self._is_dragging and not force:
            current_time = time.time() * 1000  # ms
            if current_time - self._last_preview_time < self._preview_throttle_ms:
                # Schedule a deferred update
                if self._pending_preview is None:
                    self._pending_preview = self.root.after(
                        self._preview_throttle_ms,
                        lambda: self._do_preview_update(fast=True)
                    )
                return
            self._last_preview_time = current_time

        self._do_preview_update(fast=self._is_dragging)

    def _do_preview_update(self, fast=False):
        """Actually perform the preview update. fast=True uses cheaper resampling."""
        self._pending_preview = None

        if self.current_image is None:
            return

        # Update fm_regions from source editors
        for name, editor in self.region_editors.items():
            self.fm_regions[name] = editor.get_region()

        # Update crowd_regions from destination editors
        if hasattr(self, 'dest_region_editors'):
            for name, editor in self.dest_region_editors.items():
                self.crowd_regions[name] = editor.get_region()

        # Get canvas sizes
        src_w = self.source_canvas.winfo_width()
        src_h = self.source_canvas.winfo_height()
        out_w = self.output_canvas.winfo_width()
        out_h = self.output_canvas.winfo_height()

        if src_w < 10 or src_h < 10:
            return  # Not yet rendered

        # Choose resampling method based on mode
        resample = Image.Resampling.BILINEAR if fast else Image.Resampling.LANCZOS

        # === Source Preview with region overlay ===
        src_img = self.current_image.copy()

        # Draw region rectangles on source
        draw = ImageDraw.Draw(src_img, 'RGBA')
        colors = {'shirt_front': (255, 0, 0, 100), 'shirt_back': (0, 255, 0, 100), 'sleeves': (0, 0, 255, 100)}

        img_w, img_h = src_img.size
        for name, region in self.fm_regions.items():
            if name in colors:
                x1 = int(region['x'] * img_w)
                y1 = int(region['y'] * img_h)
                x2 = int((region['x'] + region['w']) * img_w)
                y2 = int((region['y'] + region['h']) * img_h)
                draw.rectangle([x1, y1, x2, y2], fill=colors[name], outline=colors[name][:3] + (255,), width=2)

        # Fit to canvas
        src_img.thumbnail((src_w - 10, src_h - 10), resample)
        self._source_photo = ImageTk.PhotoImage(src_img)
        self.source_canvas.delete('all')
        self.source_canvas.create_image(src_w//2, src_h//2, image=self._source_photo)

        # === Output Preview with mask background ===
        try:
            out_img = self._convert_image(self.current_image, output_size=512 if fast else 1024)

            # Composite with mask as background if available
            if self.mask_image is not None:
                # Create background from mask (tint it slightly so it's visible)
                mask_resized = self.mask_image.resize(out_img.size, resample)
                # Darken mask and tint it cyan for visibility
                mask_bg = Image.new('RGBA', out_img.size, (20, 40, 50, 255))
                # Use mask as alpha-weighted overlay
                mask_gray = mask_resized.convert('L')
                mask_tinted = Image.new('RGBA', out_img.size, (60, 100, 120, 255))
                mask_bg = Image.composite(mask_tinted, mask_bg, mask_gray)
                # Paste kit output on top of mask background
                out_img = Image.alpha_composite(mask_bg, out_img)

            # Draw target region outlines on output for visualization
            draw_out = ImageDraw.Draw(out_img, 'RGBA')
            out_size = out_img.size[0]
            target_colors = {
                'shirt': (255, 0, 0, 150),            # Red - shirt strip on right
                'sleeves': (255, 128, 0, 150),        # Orange - sleeves region
            }
            for name, region in self.crowd_regions.items():
                if name in target_colors:
                    x1 = int(region['x'] * out_size)
                    y1 = int(region['y'] * out_size)
                    x2 = int((region['x'] + region['w']) * out_size)
                    y2 = int((region['y'] + region['h']) * out_size)
                    draw_out.rectangle([x1, y1, x2, y2], outline=target_colors[name][:3] + (255,), width=2)

            out_img.thumbnail((out_w - 10, out_h - 10), resample)
            self._output_photo = ImageTk.PhotoImage(out_img)
            self.output_canvas.delete('all')
            self.output_canvas.create_image(out_w//2, out_h//2, image=self._output_photo)
        except Exception as e:
            print(f"Preview error: {e}")

    def _convert_image(self, src_img, output_size=1024):
        """Convert FM kit to crowd UV layout."""
        output = Image.new('RGBA', (output_size, output_size), (0, 0, 0, 0))  # Transparent so mask shows through

        img_w, img_h = src_img.size

        # Helper to apply rotation from region settings
        def apply_rotation(crop, region):
            rot = region.get('rot', 0)
            if rot == 90:
                return crop.rotate(-90, expand=True)  # PIL rotates CCW, so -90 = CW 90
            elif rot == 180:
                return crop.rotate(180)
            elif rot == 270:
                return crop.rotate(90, expand=True)   # PIL rotates CCW, so 90 = CW 270
            return crop

        # Extract shirt_front from FM kit
        src_front = self.fm_regions['shirt_front']
        fx1 = int(src_front['x'] * img_w)
        fy1 = int(src_front['y'] * img_h)
        fx2 = int((src_front['x'] + src_front['w']) * img_w)
        if self.exclude_shorts.get():
            fy2 = int((src_front['y'] + src_front['h'] * 0.85) * img_h)
        else:
            fy2 = int((src_front['y'] + src_front['h']) * img_h)
        front_crop = src_img.crop((fx1, fy1, fx2, fy2))
        front_crop = apply_rotation(front_crop, src_front)

        # Extract shirt_back from FM kit
        src_back = self.fm_regions['shirt_back']
        bx1 = int(src_back['x'] * img_w)
        by1 = int(src_back['y'] * img_h)
        bx2 = int((src_back['x'] + src_back['w']) * img_w)
        if self.exclude_shorts.get():
            by2 = int((src_back['y'] + src_back['h'] * 0.85) * img_h)
        else:
            by2 = int((src_back['y'] + src_back['h']) * img_h)
        back_crop = src_img.crop((bx1, by1, bx2, by2))
        back_crop = apply_rotation(back_crop, src_back)

        # Place shirt in destination
        if 'shirt' in self.crowd_regions:
            dest_region = self.crowd_regions['shirt']
            dx = int(dest_region['x'] * output_size)
            dy = int(dest_region['y'] * output_size)
            dw = int(dest_region['w'] * output_size)
            dh = int(dest_region['h'] * output_size)

            # Stack vertically: back on top (rotated 180° - both X and Y), front on bottom
            half_h = dh // 2
            back_rotated = back_crop.rotate(180)
            back_scaled = back_rotated.resize((dw, half_h), Image.Resampling.LANCZOS)
            front_scaled = front_crop.resize((dw, half_h), Image.Resampling.LANCZOS)
            output.paste(back_scaled, (dx, dy), back_scaled)              # Back on top (rotated 180)
            output.paste(front_scaled, (dx, dy + half_h), front_scaled)    # Front on bottom
        else:
            # Old format: separate front/back regions
            for src_name, dest_name in [('shirt_front', 'shirt_front'), ('shirt_back', 'shirt_back')]:
                if dest_name not in self.crowd_regions:
                    continue
                crop = front_crop if src_name == 'shirt_front' else back_crop
                dest_region = self.crowd_regions[dest_name]
                dx = int(dest_region['x'] * output_size)
                dy = int(dest_region['y'] * output_size)
                dw = int(dest_region['w'] * output_size)
                dh = int(dest_region['h'] * output_size)
                scaled = crop.resize((dw, dh), Image.Resampling.LANCZOS)
                output.paste(scaled, (dx, dy), scaled)

        # Sleeves - W-shape region, stacked horizontally (left sleeve | right sleeve)
        if 'sleeves' in self.crowd_regions:
            src_region = self.fm_regions['sleeves']
            x1 = int(src_region['x'] * img_w)
            y1 = int(src_region['y'] * img_h)
            x2 = int((src_region['x'] + src_region['w']) * img_w)
            y2 = int((src_region['y'] + src_region['h']) * img_h)
            sleeve_crop = src_img.crop((x1, y1, x2, y2))
            sleeve_crop = apply_rotation(sleeve_crop, src_region)

            dest = self.crowd_regions['sleeves']
            dx, dy = int(dest['x'] * output_size), int(dest['y'] * output_size)
            dw, dh = int(dest['w'] * output_size), int(dest['h'] * output_size)

            # Stack horizontally: left half and right half
            half_w = dw // 2
            sleeve_scaled = sleeve_crop.resize((half_w, dh), Image.Resampling.LANCZOS)
            output.paste(sleeve_scaled, (dx, dy), sleeve_scaled)              # Left half
            output.paste(sleeve_scaled, (dx + half_w, dy), sleeve_scaled)     # Right half (same image)

        return output

    def _reset_regions(self):
        # Save current state to undo stack before reset
        self._save_undo_state()

        # Reset FM source regions to defaults
        self.fm_regions = {k: v.copy() for k, v in DEFAULT_FM_REGIONS.items()}
        # Reset crowd target regions to defaults
        self.crowd_regions = {k: v.copy() for k, v in DEFAULT_CROWD_UV_MALE.items()}

        # Update all source editors with default values (suppress callbacks during bulk update)
        for name, editor in self.region_editors.items():
            if name in self.fm_regions:
                editor.set_region(self.fm_regions[name], suppress_callback=True)

        # Update all destination editors
        for name, editor in self.dest_region_editors.items():
            if name in self.crowd_regions:
                editor.set_region(self.crowd_regions[name], suppress_callback=True)

        # Force single preview update after all editors updated
        self.root.after(10, self._update_preview)

    def _save_preset(self, slot):
        """Save current regions to a preset slot (1-5)."""
        preset = {
            'fm_regions': {k: v.copy() for k, v in self.fm_regions.items()},
            'crowd_regions': {k: v.copy() for k, v in self.crowd_regions.items()},
        }
        config_path = get_app_dir() / f"kit_preset_{slot}.json"
        try:
            with open(config_path, 'w') as f:
                json.dump(preset, f, indent=2)
            self.status_label.config(text=f"Saved preset {slot}")
        except Exception as e:
            self.status_label.config(text=f"Failed to save preset: {e}")

    def _load_preset(self, slot):
        """Load regions from a preset slot (1-5)."""
        config_path = get_app_dir() / f"kit_preset_{slot}.json"
        if not config_path.exists():
            self.status_label.config(text=f"Preset {slot} not found")
            return

        try:
            with open(config_path, 'r') as f:
                preset = json.load(f)

            # Save current state for undo
            self._save_undo_state()

            # Load values
            if 'fm_regions' in preset:
                self.fm_regions = preset['fm_regions']
            if 'crowd_regions' in preset:
                self.crowd_regions = preset['crowd_regions']

            # Update source editors
            for name, editor in self.region_editors.items():
                if name in self.fm_regions:
                    editor.set_region(self.fm_regions[name], suppress_callback=True)

            # Update destination editors
            for name, editor in self.dest_region_editors.items():
                if name in self.crowd_regions:
                    editor.set_region(self.crowd_regions[name], suppress_callback=True)

            self.root.after(10, self._update_preview)
            self.status_label.config(text=f"Loaded preset {slot}")
        except Exception as e:
            self.status_label.config(text=f"Failed to load preset: {e}")

    def _save_undo_state(self):
        """Save current region state to undo stack."""
        state = {
            'fm_regions': {k: v.copy() for k, v in self.fm_regions.items()},
            'crowd_regions': {k: v.copy() for k, v in self.crowd_regions.items()},
        }
        self._undo_stack.append(state)
        # Limit stack size
        if len(self._undo_stack) > self._max_undo:
            self._undo_stack.pop(0)

    def _undo(self, event=None):
        """Restore previous region state (Ctrl+Z)."""
        if not self._undo_stack:
            return

        state = self._undo_stack.pop()
        self.fm_regions = state['fm_regions']
        self.crowd_regions = state['crowd_regions']

        # Update source editors (suppress callbacks during bulk update)
        for name, editor in self.region_editors.items():
            if name in self.fm_regions:
                editor.set_region(self.fm_regions[name], suppress_callback=True)

        # Update destination editors
        if hasattr(self, 'dest_region_editors'):
            for name, editor in self.dest_region_editors.items():
                if name in self.crowd_regions:
                    editor.set_region(self.crowd_regions[name], suppress_callback=True)

        # Force preview update
        self.root.after(10, self._update_preview)

    def _update_status(self):
        count = len(self.input_files)
        self.status_label.config(text=f"{count} file(s) loaded")

    def _get_next_kit_number(self, club_folder):
        """Find the next available kit number in a club folder."""
        if not club_folder.exists():
            return 1

        # Find existing CrowdKit_XX.png files
        existing = list(club_folder.glob("CrowdKit_*.png"))
        if not existing:
            return 1

        # Extract numbers and find max
        max_num = 0
        for f in existing:
            try:
                # Extract number from CrowdKit_XX.png
                num_str = f.stem.replace("CrowdKit_", "")
                num = int(num_str)
                max_num = max(max_num, num)
            except ValueError:
                pass
        return max_num + 1

    def _update_kit_index(self, kits_root, team_id, country, club_name):
        """Update kit_mappings.txt with team's kit folder path.

        Format matches StadiumInjection's team_mappings.txt:
        # Comment lines start with #
        TeamID:Country/ClubName
        """
        index_path = Path(kits_root) / "kit_mappings.txt"
        relative_folder = f"{country}/{club_name}"

        # Load existing mappings
        mappings = {}  # team_id -> folder_path
        comments_before = []  # Store header comments

        if index_path.exists():
            try:
                with open(index_path, 'r', encoding='utf-8') as f:
                    for line in f:
                        line = line.strip()
                        if not line:
                            continue
                        if line.startswith('#'):
                            # Only keep header comments (before first mapping)
                            if not mappings:
                                comments_before.append(line)
                            continue
                        if ':' in line:
                            tid, folder = line.split(':', 1)
                            mappings[tid.strip()] = folder.strip()
            except:
                pass

        # Add default header if new file
        if not comments_before:
            comments_before = [
                "# CrowdInjectV2 - Kit Mappings",
                "# Format: TeamID:Country/ClubName",
                "# One mapping per line",
                ""
            ]

        # Update or add this team's mapping
        mappings[team_id] = relative_folder

        # Write back
        try:
            with open(index_path, 'w', encoding='utf-8') as f:
                for comment in comments_before:
                    f.write(comment + '\n')
                if comments_before and not comments_before[-1] == '':
                    f.write('\n')
                for tid, folder in sorted(mappings.items()):
                    f.write(f"{tid}:{folder}\n")
        except Exception as e:
            print(f"Failed to update kit mappings: {e}")

    def _convert_all(self):
        if not self.input_files:
            messagebox.showwarning("No Files", "Add some kit textures first.")
            return

        # Check kit count for atlas mode
        use_atlas = self.output_atlas.get()
        max_kits = ATLAS_GRID_COLS * ATLAS_GRID_ROWS if use_atlas else 16

        if len(self.input_files) > max_kits:
            messagebox.showwarning("Too Many Kits",
                f"Maximum {max_kits} kits supported in {'atlas' if use_atlas else 'individual'} mode.\n"
                f"You have {len(self.input_files)} files selected.")
            return

        # Check if using team-based organization or legacy output
        kits_root = self.kits_root_path.get().strip()
        team_id = self.team_id.get().strip()
        country = self.country.get().strip()
        club_name = self.club_name.get().strip()

        use_team_mode = bool(kits_root and country and club_name)

        if use_team_mode:
            # Team-based organization
            if not team_id:
                messagebox.showwarning("Missing Team ID", "Team ID is required for kit organization.\nWithout it, the plugin won't know which team these kits belong to.")
                return
        else:
            # Legacy mode - require output folder
            output_dir = self.output_folder.get()
            if not output_dir:
                messagebox.showwarning("No Output", "Either set Kits Root + Country + Club Name for team-based organization,\nor set Output Folder for legacy mode.")
                return

        self.convert_btn.config(state='disabled')
        thread = threading.Thread(target=self._do_convert, daemon=True)
        thread.start()

    def _do_convert(self):
        total = len(self.input_files)
        use_atlas = self.output_atlas.get()

        # Determine output mode
        kits_root = self.kits_root_path.get().strip()
        team_id = self.team_id.get().strip()
        country = self.country.get().strip()
        club_name = self.club_name.get().strip()

        use_team_mode = bool(kits_root and country and club_name)

        if use_team_mode:
            # Team-based: output to Country/ClubName/ subfolder
            output_dir = Path(kits_root) / country / club_name
            output_dir.mkdir(parents=True, exist_ok=True)
            start_idx = self._get_next_kit_number(output_dir) if not use_atlas else 0
        else:
            # Legacy mode
            output_dir = Path(self.output_folder.get())
            output_dir.mkdir(parents=True, exist_ok=True)
            start_idx = self.start_index.get()

        converted = 0
        errors = []

        if use_atlas:
            # Atlas mode: create single texture with all kits in a grid
            atlas = Image.new('RGBA', (ATLAS_SIZE, ATLAS_SIZE), (0, 0, 0, 0))
            max_kits = ATLAS_GRID_COLS * ATLAS_GRID_ROWS

            for i, filepath in enumerate(self.input_files):
                self.root.after(0, lambda p=(i/total)*100: self.progress.config(value=p))
                self.root.after(0, lambda f=Path(filepath).name: self.status_label.config(text=f"Converting: {f}"))

                try:
                    kit_idx = start_idx + i

                    if kit_idx >= max_kits:
                        errors.append(f"{Path(filepath).name}: Index {kit_idx} exceeds max ({max_kits-1})")
                        continue

                    src_img = Image.open(filepath).convert('RGBA')
                    out_img = self._convert_image(src_img, output_size=ATLAS_KIT_SIZE)

                    # Calculate position in grid
                    grid_x = kit_idx % ATLAS_GRID_COLS
                    grid_y = kit_idx // ATLAS_GRID_COLS
                    paste_x = grid_x * ATLAS_KIT_SIZE
                    paste_y = grid_y * ATLAS_KIT_SIZE

                    atlas.paste(out_img, (paste_x, paste_y), out_img)
                    converted += 1

                except Exception as e:
                    errors.append(f"{Path(filepath).name}: {e}")

            # Save atlas
            if converted > 0:
                atlas_path = output_dir / "KitAtlas.png"
                atlas.save(atlas_path, 'PNG')

                # Show section config dialog on main thread
                indices = list(range(start_idx, start_idx + converted))
                self._pending_atlas_info = {
                    'output_dir': output_dir,
                    'indices': indices,
                    'converted': converted,
                }
                self.root.after(0, self._show_section_config_dialog)

        else:
            # Individual files mode (legacy)
            for i, filepath in enumerate(self.input_files):
                self.root.after(0, lambda p=(i/total)*100: self.progress.config(value=p))
                self.root.after(0, lambda f=Path(filepath).name: self.status_label.config(text=f"Converting: {f}"))

                try:
                    kit_idx = start_idx + i

                    if not use_team_mode and kit_idx > 15:
                        errors.append(f"{Path(filepath).name}: Index {kit_idx} exceeds max (15)")
                        continue

                    src_img = Image.open(filepath).convert('RGBA')
                    out_img = self._convert_image(src_img)

                    # Use 2-digit zero-padded numbering for team mode
                    if use_team_mode:
                        out_path = output_dir / f"CrowdKit_{kit_idx:02d}.png"
                    else:
                        out_path = output_dir / f"CrowdKit_{kit_idx}.png"

                    out_img.save(out_path, 'PNG')
                    converted += 1

                except Exception as e:
                    errors.append(f"{Path(filepath).name}: {e}")

        # Update kit index if using team mode
        if use_team_mode and converted > 0 and team_id:
            self._update_kit_index(kits_root, team_id, country, club_name)

        self.root.after(0, lambda: self.progress.config(value=100))
        self.root.after(0, lambda: self.convert_btn.config(state='normal'))

        if use_atlas:
            msg = f"Created atlas with {converted}/{total} kit(s)\nOutput: {output_dir / 'KitAtlas.png'}"
            msg += f"\nAtlas: {ATLAS_SIZE}x{ATLAS_SIZE}, {ATLAS_GRID_COLS}x{ATLAS_GRID_ROWS} grid"
        else:
            msg = f"Converted {converted}/{total} kit(s)\nOutput: {output_dir}"
        if use_team_mode:
            msg += f"\nTeam ID {team_id} added to kit_mappings.txt"
        if errors:
            msg += "\n\nErrors:\n" + "\n".join(errors[:5])

        self.root.after(0, lambda: messagebox.showinfo("Complete", msg))
        self.root.after(0, lambda: self.status_label.config(text=f"Done: {converted}/{total}"))

        self._save_settings()

    def _show_section_config_dialog(self):
        """Show dialog to configure section kit settings, then save atlas JSON."""
        if not hasattr(self, '_pending_atlas_info'):
            return

        info = self._pending_atlas_info
        output_dir = info['output_dir']
        indices = info['indices']
        converted = info['converted']

        # Check for existing config in the output folder
        existing_json_path = output_dir / "KitAtlas.json"
        existing_config = None
        if existing_json_path.exists():
            try:
                with open(existing_json_path, 'r') as f:
                    existing = json.load(f)
                    existing_config = existing.get('sections')
            except:
                pass

        # Show the dialog
        dialog = SectionConfigDialog(self.root, converted, existing_config)
        self.root.wait_window(dialog)

        # Get result (None if cancelled)
        section_config = dialog.result
        if section_config is None:
            # User cancelled - use defaults
            section_config = {}
            for section in SECTION_TYPES:
                defaults = DEFAULT_SECTION_CONFIG[section]
                # Limit indices to available kits
                valid_indices = [i for i in defaults['indices'] if i < converted]
                section_config[section] = {
                    'probability': defaults['probability'],
                    'indices': valid_indices if valid_indices else ([0] if converted > 0 else [])
                }

        # Build and save atlas info JSON
        atlas_info = {
            'gridCols': ATLAS_GRID_COLS,
            'gridRows': ATLAS_GRID_ROWS,
            'kitSize': ATLAS_KIT_SIZE,
            'atlasSize': ATLAS_SIZE,
            'kitCount': converted,
            'indices': indices,
            'sections': section_config,
        }

        info_path = output_dir / "KitAtlas.json"
        with open(info_path, 'w') as f:
            json.dump(atlas_info, f, indent=2)

        # Clean up
        del self._pending_atlas_info


def main():
    import sys

    # Enable DPI awareness for sharp rendering on high-DPI displays
    try:
        import ctypes
        # Windows 8.1+ per-monitor DPI awareness
        ctypes.windll.shcore.SetProcessDpiAwareness(2)
    except:
        try:
            # Fallback for older Windows
            ctypes.windll.user32.SetProcessDPIAware()
        except:
            pass

    # Set Windows AppUserModelID BEFORE creating any windows
    # This is critical for taskbar icon to work
    try:
        import ctypes
        ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID('CrowdInjectV2.KitConverter.1.0')
    except:
        pass

    root = tk.Tk()

    # Enable tkinter DPI scaling
    try:
        root.tk.call('tk', 'scaling', root.winfo_fpixels('1i') / 72.0)
    except:
        pass

    # Set app icon - use get_exe_dir() for resources (icon is next to exe, not in AppData)
    icon_path = get_exe_dir() / "kit_icon.ico"
    try:
        if icon_path.exists():
            # Set window icon
            root.iconbitmap(str(icon_path))
            # Also set as default for all toplevels
            root.iconbitmap(default=str(icon_path))
    except Exception as e:
        print(f"Failed to set icon: {e}")

    # For frozen exe, try to use the embedded icon
    if getattr(sys, 'frozen', False):
        try:
            # PyInstaller stores icon in the exe itself
            import ctypes
            # Force Windows to use the exe's embedded icon for taskbar
            exe_path = sys.executable
            root.iconbitmap(default=exe_path)
        except:
            pass

    # Set theme
    style = ttk.Style()
    if 'clam' in style.theme_names():
        style.theme_use('clam')

    app = KitConverterApp(root)
    root.mainloop()


if __name__ == '__main__':
    main()
