import bpy
import bmesh
from bpy.types import Operator, Panel, PropertyGroup, UIList
from bpy.props import StringProperty, IntProperty, FloatVectorProperty, FloatProperty, BoolProperty, EnumProperty, CollectionProperty, PointerProperty
import os
import tempfile
import subprocess
import sys
import json
import platform
import time

# Addon info
bl_info = {
    "name": "Text Texture Generator",
    "author": "Marc Mintel <marc@mintel.me>",
    "version": (2, 3, 2),
    "blender": (4, 0, 0),
    "location": "3D View > Sidebar (N) > Tool Tab | Shader Editor > Sidebar > Tool Tab",
    "description": "Generate image textures from text with accurate dimensions and instant live preview",
    "category": "Material",
}

# ============================================================================
# UTILITY FUNCTIONS
# ============================================================================

def install_pillow():
    """Install Pillow if not available"""
    try:
        import PIL
    except ImportError:
        print("Installing Pillow...")
        subprocess.check_call([sys.executable, "-m", "pip", "install", "pillow>=10.0.0"])

def get_system_fonts():
    """Get detailed list of available system fonts with paths"""
    try:
        from PIL import ImageFont
        import glob
        
        fonts = {}
        system = platform.system()
        
        if system == "Windows":
            font_dirs = [
                "C:/Windows/Fonts/",
                os.path.expanduser("~/AppData/Local/Microsoft/Windows/Fonts/")
            ]
            extensions = ["*.ttf", "*.ttc", "*.otf"]
        elif system == "Darwin":  # macOS
            font_dirs = [
                "/System/Library/Fonts/",
                "/Library/Fonts/",
                os.path.expanduser("~/Library/Fonts/")
            ]
            extensions = ["*.ttf", "*.ttc", "*.otf", "*.dfont"]
        else:  # Linux
            font_dirs = [
                "/usr/share/fonts/",
                "/usr/local/share/fonts/",
                os.path.expanduser("~/.local/share/fonts/"),
                os.path.expanduser("~/.fonts/")
            ]
            extensions = ["*.ttf", "*.ttc", "*.otf"]
        
        for font_dir in font_dirs:
            if os.path.exists(font_dir):
                for ext in extensions:
                    for font_path in glob.glob(os.path.join(font_dir, "**", ext), recursive=True):
                        try:
                            font_name = os.path.splitext(os.path.basename(font_path))[0]
                            font_name = font_name.replace("-", " ").replace("_", " ")
                            fonts[font_name] = font_path
                        except Exception:
                            continue
        
        return fonts
    except ImportError:
        return {}

def get_font_enum_items(self, context):
    """Dynamic enum items for font selection"""
    items = [("default", "Default Font", "Use system default font", 0)]
    
    if not hasattr(get_font_enum_items, 'cached_fonts'):
        get_font_enum_items.cached_fonts = get_system_fonts()
    
    fonts = get_font_enum_items.cached_fonts
    
    for i, (font_name, font_path) in enumerate(sorted(fonts.items())[:50]):
        items.append((font_path, font_name, f"Font: {font_name}", i + 1))
    
    return items

# ============================================================================
# PRESET STORAGE SYSTEM - V2.0 (PERSISTENT STORAGE)
# ============================================================================
#
# The Text Texture Generator now uses a THREE-TIER preset storage system
# that ensures preset persistence across addon updates:
#
# 1. BLEND FILE STORAGE (Highest Priority)
#    - Presets travel with the .blend file
#    - Perfect for project-specific presets
#    - Stored in scene custom properties
#
# 2. PERSISTENT FILE STORAGE (Medium Priority)
#    - Survives addon updates and Blender upgrades
#    - Stored in Blender's user config directory
#    - Shared across all blend files
#
# 3. LEGACY FILE STORAGE (Lowest Priority)
#    - Old system for backward compatibility
#    - Vulnerable to addon updates
#    - Will be automatically migrated to persistent storage
#
# MIGRATION SYSTEM:
# - Automatically detects addon version changes
# - Migrates legacy presets to persistent storage
# - Creates backups before migration
# - Provides detailed migration reports
#
# IMPORT/EXPORT SYSTEM:
# - Export all presets for manual backup
# - Import presets with conflict resolution
# - Supports cross-machine preset sharing
#
# ============================================================================

def get_preset_path():
    """Get path for storing presets (legacy local storage)
    
    LEGACY SYSTEM: This storage location is vulnerable to addon updates.
    Presets stored here will be lost when the addon is updated through
    Blender's Add-ons preferences. This function is maintained for
    backward compatibility only.
    
    For new installations, use get_persistent_preset_path() instead.
    """
    addon_dir = os.path.dirname(os.path.realpath(__file__))
    presets_dir = os.path.join(addon_dir, "presets")
    if not os.path.exists(presets_dir):
        try:
            os.makedirs(presets_dir)
        except OSError as e:
            print(f"Error creating presets directory: {e}")
    return presets_dir

def get_persistent_preset_path():
    """Get persistent path for storing presets (survives addon updates)"""
    import bpy
    
    # Get Blender's user config directory
    config_dir = bpy.utils.user_resource('CONFIG')
    if not config_dir:
        # Fallback to addon directory if config path unavailable
        print("[TTG] Warning: Could not get user config directory, falling back to addon directory")
        return get_preset_path()
    
    # Create addon-specific subdirectory
    persistent_dir = os.path.join(config_dir, "text_texture_generator", "presets")
    
    try:
        if not os.path.exists(persistent_dir):
            os.makedirs(persistent_dir, exist_ok=True)
        return persistent_dir
    except OSError as e:
        print(f"[TTG] Error creating persistent presets directory: {e}")
        # Fallback to addon directory
        return get_preset_path()

def get_addon_version():
    """Get current addon version for migration tracking"""
    return bl_info["version"]

def get_version_file_path():
    """Get path to version tracking file"""
    persistent_dir = os.path.dirname(get_persistent_preset_path())
    return os.path.join(persistent_dir, "addon_version.json")

def get_stored_version():
    """Get previously stored addon version"""
    version_file = get_version_file_path()
    try:
        if os.path.exists(version_file):
            with open(version_file, 'r') as f:
                data = json.load(f)
                return tuple(data.get('version', (0, 0, 0)))
    except (json.JSONDecodeError, OSError) as e:
        print(f"[TTG] Error reading version file: {e}")
    return (0, 0, 0)

def save_current_version():
    """Save current addon version to persistent storage"""
    version_file = get_version_file_path()
    try:
        version_data = {
            'version': get_addon_version(),
            'timestamp': time.time()
        }
        with open(version_file, 'w') as f:
            json.dump(version_data, f, indent=2)
        print(f"[TTG] Saved addon version {get_addon_version()} to persistent storage")
    except (OSError, json.JSONEncodeError) as e:
        print(f"[TTG] Error saving version file: {e}")

def migrate_presets_if_needed():
    """Migrate presets from old location to persistent storage if needed"""
    current_version = get_addon_version()
    stored_version = get_stored_version()
    
    print(f"[TTG] Version check - Current: {current_version}, Stored: {stored_version}")
    
    # Check if this is a fresh installation or version change
    if stored_version == (0, 0, 0) or current_version != stored_version:
        print(f"[TTG] Version change detected, checking for preset migration...")
        
        # Get paths
        old_preset_dir = get_preset_path()  # Legacy addon directory
        new_preset_dir = get_persistent_preset_path()  # Persistent location
        
        migrated_count = 0
        migration_errors = []
        
        # Migrate from old addon directory if it exists and has presets
        if os.path.exists(old_preset_dir) and old_preset_dir != new_preset_dir:
            print(f"[TTG] Migrating presets from {old_preset_dir} to {new_preset_dir}")
            
            try:
                # Create backup before migration
                backup_path = backup_presets()
                if backup_path:
                    print(f"[TTG] Created migration backup at {backup_path}")
                
                for filename in os.listdir(old_preset_dir):
                    if filename.endswith('.json'):
                        preset_name = os.path.splitext(filename)[0]
                        old_file = os.path.join(old_preset_dir, filename)
                        new_file = os.path.join(new_preset_dir, filename)
                        
                        try:
                            # Only copy if destination doesn't exist or is older
                            should_copy = True
                            if os.path.exists(new_file):
                                old_mtime = os.path.getmtime(old_file)
                                new_mtime = os.path.getmtime(new_file)
                                should_copy = old_mtime > new_mtime
                            
                            if should_copy:
                                import shutil
                                shutil.copy2(old_file, new_file)
                                migrated_count += 1
                                print(f"[TTG] ✅ Migrated preset: {preset_name}")
                            else:
                                print(f"[TTG] ⏭️ Skipped preset (newer exists): {preset_name}")
                        except Exception as preset_error:
                            error_msg = f"Failed to migrate '{preset_name}': {preset_error}"
                            migration_errors.append(error_msg)
                            print(f"[TTG] ❌ {error_msg}")
                
                # Store migration results for user notification
                migration_data = {
                    'migrated_count': migrated_count,
                    'total_found': len([f for f in os.listdir(old_preset_dir) if f.endswith('.json')]),
                    'errors': migration_errors,
                    'backup_path': backup_path,
                    'timestamp': time.time()
                }
                
                # Save migration report
                try:
                    migration_report_file = os.path.join(os.path.dirname(new_preset_dir), "migration_report.json")
                    with open(migration_report_file, 'w') as f:
                        json.dump(migration_data, f, indent=2)
                    print(f"[TTG] Migration report saved to {migration_report_file}")
                except Exception as e:
                    print(f"[TTG] Warning: Could not save migration report: {e}")
                
                if migrated_count > 0:
                    print(f"[TTG] ✅ Successfully migrated {migrated_count} presets to persistent storage")
                    print(f"[TTG] 🛡️ Your presets are now safe from addon updates!")
                else:
                    print(f"[TTG] ℹ️ No presets needed migration")
                
                if migration_errors:
                    print(f"[TTG] ⚠️ {len(migration_errors)} presets had migration errors")
                    
            except OSError as e:
                print(f"[TTG] ❌ Error during preset migration: {e}")
                migration_errors.append(f"General migration error: {e}")
        
        # Save current version
        save_current_version()
        
        # Return migration summary
        return {
            'migrated_count': migrated_count,
            'errors': migration_errors,
            'version_updated': True
        }
    
    return {
        'migrated_count': 0,
        'errors': [],
        'version_updated': False
    }

def backup_presets():
    """Create backup of current presets"""
    persistent_dir = get_persistent_preset_path()
    backup_dir = os.path.join(os.path.dirname(persistent_dir), "preset_backups")
    
    try:
        if not os.path.exists(backup_dir):
            os.makedirs(backup_dir, exist_ok=True)
        
        # Create timestamped backup
        import datetime
        timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
        backup_subdir = os.path.join(backup_dir, f"backup_{timestamp}")
        
        if os.path.exists(persistent_dir):
            import shutil
            shutil.copytree(persistent_dir, backup_subdir)
            print(f"[TTG] Created preset backup at {backup_subdir}")
            return backup_subdir
        
    except OSError as e:
        print(f"[TTG] Error creating preset backup: {e}")
    
    return None

def get_blend_file_presets():
    """Get presets stored in the current blend file"""
    try:
        # Get custom properties from the scene
        import bpy
        scene = bpy.context.scene
        
        # DETAILED LOGGING: Function entry
        print(f"[GET_BLEND_FILE_PRESETS DEBUG] ==================== LOADING FROM BLEND FILE ==================")
        print(f"[GET_BLEND_FILE_PRESETS DEBUG] Function called to get presets from blend file")
        
        # Store presets in scene custom properties
        if "text_texture_presets" not in scene:
            print(f"[GET_BLEND_FILE_PRESETS DEBUG] No 'text_texture_presets' found in scene, creating empty")
            scene["text_texture_presets"] = "{}"
        else:
            print(f"[GET_BLEND_FILE_PRESETS DEBUG] Found 'text_texture_presets' in scene")
        
        preset_data = scene.get("text_texture_presets", "{}")
        print(f"[GET_BLEND_FILE_PRESETS DEBUG] Raw preset data type: {type(preset_data)}")
        print(f"[GET_BLEND_FILE_PRESETS DEBUG] Raw preset data length: {len(str(preset_data))} characters")
        
        if isinstance(preset_data, str):
            print(f"[GET_BLEND_FILE_PRESETS DEBUG] Preset data is string, attempting JSON parse...")
            try:
                import json
                parsed_presets = json.loads(preset_data)
                print(f"[GET_BLEND_FILE_PRESETS DEBUG] Successfully parsed JSON")
                print(f"[GET_BLEND_FILE_PRESETS DEBUG] Parsed presets type: {type(parsed_presets)}")
                print(f"[GET_BLEND_FILE_PRESETS DEBUG] Parsed presets count: {len(parsed_presets) if isinstance(parsed_presets, dict) else 'N/A'}")
                
                if isinstance(parsed_presets, dict):
                    print(f"[GET_BLEND_FILE_PRESETS DEBUG] Preset names: {list(parsed_presets.keys())}")
                    
                    # DETAILED VERIFICATION: Check shader properties in each preset
                    for preset_name, preset_content in parsed_presets.items():
                        print(f"[GET_BLEND_FILE_PRESETS DEBUG] Preset '{preset_name}' analysis:")
                        if isinstance(preset_content, dict):
                            print(f"[GET_BLEND_FILE_PRESETS DEBUG]   Keys: {list(preset_content.keys())}")
                            print(f"[GET_BLEND_FILE_PRESETS DEBUG]   disable_shadows: {preset_content.get('disable_shadows', 'NOT FOUND')}")
                            print(f"[GET_BLEND_FILE_PRESETS DEBUG]   disable_reflections: {preset_content.get('disable_reflections', 'NOT FOUND')}")
                            print(f"[GET_BLEND_FILE_PRESETS DEBUG]   disable_backfacing: {preset_content.get('disable_backfacing', 'NOT FOUND')}")
                        else:
                            print(f"[GET_BLEND_FILE_PRESETS DEBUG]   ERROR: Preset content is not a dict: {type(preset_content)}")
                
                print(f"[GET_BLEND_FILE_PRESETS DEBUG] Returning parsed presets")
                print(f"[GET_BLEND_FILE_PRESETS DEBUG] ================================================================")
                return parsed_presets
                
            except json.JSONDecodeError as json_err:
                print(f"[GET_BLEND_FILE_PRESETS DEBUG] JSON decode error: {json_err}")
                print(f"[GET_BLEND_FILE_PRESETS DEBUG] Raw data causing error: {preset_data[:200]}...")  # First 200 chars
                print("Error parsing blend file presets, resetting to empty")
                scene["text_texture_presets"] = "{}"
                print(f"[GET_BLEND_FILE_PRESETS DEBUG] Reset to empty, returning empty dict")
                return {}
        else:
            print(f"[GET_BLEND_FILE_PRESETS DEBUG] Preset data is not string, returning empty dict")
            print(f"[GET_BLEND_FILE_PRESETS DEBUG] ================================================================")
            return {}
    except Exception as e:
        print(f"[GET_BLEND_FILE_PRESETS DEBUG] EXCEPTION occurred: {e}")
        import traceback
        print(f"[GET_BLEND_FILE_PRESETS DEBUG] Full traceback: {traceback.format_exc()}")
        print(f"Error getting blend file presets: {e}")
        return {}

def save_blend_file_preset(preset_name, preset_data):
    """Save a preset to the current blend file"""
    try:
        import bpy
        import json
        scene = bpy.context.scene
        
        # DETAILED LOGGING: Function entry
        print(f"[SAVE_BLEND_FILE_PRESET DEBUG] ==================== SAVING TO BLEND FILE ===================")
        print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Function called with preset_name: '{preset_name}'")
        print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Preset data type: {type(preset_data)}")
        if isinstance(preset_data, dict):
            print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Preset data keys: {list(preset_data.keys())}")
            print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Shader properties in preset_data:")
            print(f"[SAVE_BLEND_FILE_PRESET DEBUG]   disable_shadows: {preset_data.get('disable_shadows', 'NOT FOUND')}")
            print(f"[SAVE_BLEND_FILE_PRESET DEBUG]   disable_reflections: {preset_data.get('disable_reflections', 'NOT FOUND')}")
            print(f"[SAVE_BLEND_FILE_PRESET DEBUG]   disable_backfacing: {preset_data.get('disable_backfacing', 'NOT FOUND')}")
        else:
            print(f"[SAVE_BLEND_FILE_PRESET DEBUG] ERROR: preset_data is not a dictionary!")
        
        # Get existing presets
        print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Getting existing presets from blend file...")
        presets = get_blend_file_presets()
        print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Existing presets count: {len(presets)}")
        print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Existing preset names: {list(presets.keys())}")
        
        # Add or update the preset
        print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Adding/updating preset '{preset_name}'...")
        presets[preset_name] = preset_data
        print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Presets now contains {len(presets)} items")
        
        # VERIFY: Check that shader properties are in the preset we're about to save
        if preset_name in presets and isinstance(presets[preset_name], dict):
            saved_preset = presets[preset_name]
            print(f"[SAVE_BLEND_FILE_PRESET DEBUG] VERIFICATION - Shader properties in final preset:")
            print(f"[SAVE_BLEND_FILE_PRESET DEBUG]   disable_shadows: {saved_preset.get('disable_shadows', 'NOT FOUND')}")
            print(f"[SAVE_BLEND_FILE_PRESET DEBUG]   disable_reflections: {saved_preset.get('disable_reflections', 'NOT FOUND')}")
            print(f"[SAVE_BLEND_FILE_PRESET DEBUG]   disable_backfacing: {saved_preset.get('disable_backfacing', 'NOT FOUND')}")
        
        # Save back to scene custom properties
        print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Converting presets to JSON...")
        json_data = json.dumps(presets, indent=2)
        print(f"[SAVE_BLEND_FILE_PRESET DEBUG] JSON data length: {len(json_data)} characters")
        
        print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Saving to scene custom properties...")
        scene["text_texture_presets"] = json_data
        print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Successfully saved to scene['text_texture_presets']")
        
        # FINAL VERIFICATION: Read back from scene to confirm save
        print(f"[SAVE_BLEND_FILE_PRESET DEBUG] FINAL VERIFICATION - Reading back from scene...")
        readback_data = scene.get("text_texture_presets", "{}")
        if readback_data:
            try:
                readback_presets = json.loads(readback_data)
                if preset_name in readback_presets:
                    readback_preset = readback_presets[preset_name]
                    print(f"[SAVE_BLEND_FILE_PRESET DEBUG] READBACK VERIFICATION - Shader properties:")
                    print(f"[SAVE_BLEND_FILE_PRESET DEBUG]   disable_shadows: {readback_preset.get('disable_shadows', 'NOT FOUND')}")
                    print(f"[SAVE_BLEND_FILE_PRESET DEBUG]   disable_reflections: {readback_preset.get('disable_reflections', 'NOT FOUND')}")
                    print(f"[SAVE_BLEND_FILE_PRESET DEBUG]   disable_backfacing: {readback_preset.get('disable_backfacing', 'NOT FOUND')}")
                else:
                    print(f"[SAVE_BLEND_FILE_PRESET DEBUG] ERROR: Preset '{preset_name}' not found in readback data!")
            except json.JSONDecodeError as json_err:
                print(f"[SAVE_BLEND_FILE_PRESET DEBUG] ERROR: Could not parse readback JSON: {json_err}")
        else:
            print(f"[SAVE_BLEND_FILE_PRESET DEBUG] ERROR: No data in scene['text_texture_presets'] after save!")
        
        print(f"Saved preset '{preset_name}' to blend file")
        print(f"[SAVE_BLEND_FILE_PRESET DEBUG] ================================================================")
        return True
    except Exception as e:
        print(f"[SAVE_BLEND_FILE_PRESET DEBUG] EXCEPTION occurred: {e}")
        import traceback
        print(f"[SAVE_BLEND_FILE_PRESET DEBUG] Full traceback: {traceback.format_exc()}")
        print(f"Error saving preset to blend file: {e}")
        return False

def delete_blend_file_preset(preset_name):
    """Delete a preset from the current blend file"""
    try:
        import bpy
        import json
        scene = bpy.context.scene
        
        # Get existing presets
        presets = get_blend_file_presets()
        
        # Remove the preset if it exists
        if preset_name in presets:
            del presets[preset_name]
            scene["text_texture_presets"] = json.dumps(presets, indent=2)
            print(f"Deleted preset '{preset_name}' from blend file")
            return True
        else:
            print(f"Preset '{preset_name}' not found in blend file")
            return False
    except Exception as e:
        print(f"Error deleting preset from blend file: {e}")
        return False

def initialize_presets():
    """Initialize presets during addon registration - prevents race conditions"""
    try:
        import bpy
        if not hasattr(bpy.context, 'scene') or not bpy.context.scene:
            return
        
        props = bpy.context.scene.text_texture_props
        if not props:
            return
        
        # Check if migration is needed first
        migration_result = migrate_presets_if_needed()
        
        # Show migration notification if there were significant changes
        if migration_result.get('migrated_count', 0) > 0 or migration_result.get('errors'):
            def show_migration_notification():
                # Use a timer to show notification after UI is ready
                try:
                    import bpy
                    if hasattr(bpy.context, 'window_manager'):
                        migrated_count = migration_result.get('migrated_count', 0)
                        errors = migration_result.get('errors', [])
                        
                        if migrated_count > 0:
                            print(f"[TTG] 🎉 MIGRATION SUCCESS: {migrated_count} presets migrated to persistent storage!")
                            print(f"[TTG] 🛡️ Your presets are now protected from addon updates.")
                        
                        if errors:
                            print(f"[TTG] ⚠️ WARNING: {len(errors)} presets had migration errors.")
                            print(f"[TTG] Use 'Migration Report' button in Presets section for details.")
                except Exception as e:
                    print(f"[TTG] Could not show migration notification: {e}")
                return None  # Stop timer
            
            # Schedule notification
            bpy.app.timers.register(show_migration_notification, first_interval=2.0)
            
        # Get existing preset names for optimization
        existing_presets = {p.name: p for p in props.presets}
        
        # Three-tier loading priority: Blend File > Persistent File > Legacy File
        
        # 1. Load presets from blend file first (highest priority)
        blend_presets = get_blend_file_presets()
        updated_presets = set()
        
        for preset_name in blend_presets.keys():
            updated_presets.add(preset_name)
            if preset_name in existing_presets:
                # Update existing preset source if needed
                if existing_presets[preset_name].source != 'BLEND_FILE':
                    existing_presets[preset_name].source = 'BLEND_FILE'
            else:
                # Add new preset
                preset = props.presets.add()
                preset.name = preset_name
                preset.source = 'BLEND_FILE'
        
        # 2. Load from persistent storage (survives addon updates)
        persistent_dir = get_persistent_preset_path()
        if os.path.exists(persistent_dir):
            try:
                for filename in os.listdir(persistent_dir):
                    if filename.endswith('.json'):
                        preset_name = os.path.splitext(filename)[0]
                        # Only add if not already present from blend file
                        if preset_name not in blend_presets:
                            updated_presets.add(preset_name)
                            if preset_name in existing_presets:
                                # Update existing preset source if needed
                                if existing_presets[preset_name].source != 'PERSISTENT_FILE':
                                    existing_presets[preset_name].source = 'PERSISTENT_FILE'
                            else:
                                # Add new preset
                                preset = props.presets.add()
                                preset.name = preset_name
                                preset.source = 'PERSISTENT_FILE'
                print(f"[TTG] Loaded persistent presets from {persistent_dir}")
            except OSError as e:
                print(f"[TTG] Error loading persistent presets during initialization: {e}")
        
        # 3. Load legacy local presets as fallback (lowest priority)
        legacy_dir = get_preset_path()
        if os.path.exists(legacy_dir) and legacy_dir != persistent_dir:
            try:
                for filename in os.listdir(legacy_dir):
                    if filename.endswith('.json'):
                        preset_name = os.path.splitext(filename)[0]
                        # Only add if not already present from higher priority sources
                        if preset_name not in blend_presets and preset_name not in updated_presets:
                            updated_presets.add(preset_name)
                            if preset_name in existing_presets:
                                # Update existing preset source if needed
                                if existing_presets[preset_name].source != 'LOCAL_FILE':
                                    existing_presets[preset_name].source = 'LOCAL_FILE'
                            else:
                                # Add new preset
                                preset = props.presets.add()
                                preset.name = preset_name
                                preset.source = 'LOCAL_FILE'
                print(f"[TTG] Loaded legacy presets from {legacy_dir}")
            except OSError as e:
                print(f"[TTG] Error loading legacy presets during initialization: {e}")
        
        # Remove presets that no longer exist
        for i in range(len(props.presets) - 1, -1, -1):
            if props.presets[i].name not in updated_presets:
                props.presets.remove(i)
                
        # Count presets by source for reporting
        blend_count = len([p for p in props.presets if p.source == 'BLEND_FILE'])
        persistent_count = len([p for p in props.presets if p.source == 'PERSISTENT_FILE'])
        legacy_count = len([p for p in props.presets if p.source == 'LOCAL_FILE'])
        
        print(f"[TTG] Initialized {len(props.presets)} presets ({blend_count} blend file, {persistent_count} persistent, {legacy_count} legacy)")
    except Exception as e:
        print(f"[TTG] Error in initialize_presets: {e}")

# ============================================================================
# PREVIEW GENERATION FUNCTIONS
# ============================================================================

def test_font_mixed_case_support(font, test_size=20):
    """Test if a font supports mixed-case rendering properly"""
    try:
        from PIL import Image, ImageDraw
        
        # Test with mixed-case sample text
        test_text = "AaBbCc123"
        test_img = Image.new('RGB', (100, 50), (255, 255, 255))
        test_draw = ImageDraw.Draw(test_img)
        
        # DIAGNOSTIC: Add detailed logging for custom font debugging
        font_info = "UNKNOWN"
        if hasattr(font, 'path'):
            font_info = font.path
        elif hasattr(font, 'getname'):
            try:
                font_info = font.getname()
            except:
                pass
        print(f"CUSTOM FONT DEBUG: Testing mixed-case support for font: {font_info}")
        
        # Try to get bounding box - this will fail if font doesn't support the characters
        try:
            bbox = test_draw.textbbox((0, 0), test_text, font=font)
            print(f"CUSTOM FONT DEBUG: Mixed-case text bbox successful: {bbox}")
        except Exception as bbox_error:
            print(f"CUSTOM FONT DEBUG: Mixed-case text bbox FAILED: {bbox_error}")
            import traceback
            print(f"CUSTOM FONT DEBUG: Full exception trace: {traceback.format_exc()}")
            return False
        
        # Test actual rendering - draw the text and check if it renders properly
        try:
            test_draw.text((5, 5), test_text, fill=(0, 0, 0), font=font)
            print(f"CUSTOM FONT DEBUG: Mixed-case text rendering successful")
        except Exception as render_error:
            print(f"CUSTOM FONT DEBUG: Mixed-case text rendering FAILED: {render_error}")
            import traceback
            print(f"CUSTOM FONT DEBUG: Full exception trace: {traceback.format_exc()}")
            return False
        
        # Additional validation: check if lowercase letters render differently from uppercase
        try:
            upper_bbox = test_draw.textbbox((0, 0), "ABC", font=font)
            lower_bbox = test_draw.textbbox((0, 0), "abc", font=font)
            print(f"CUSTOM FONT DEBUG: Upper bbox: {upper_bbox}, Lower bbox: {lower_bbox}")
            
            # RELAXED VALIDATION: Allow fonts with identical bounding boxes
            # Many valid custom fonts may have identical bounding boxes but still render mixed case properly
            if upper_bbox == lower_bbox:
                print(f"CUSTOM FONT DEBUG: RELAXED VALIDATION - Identical bounding boxes detected")
                print(f"CUSTOM FONT DEBUG: Instead of rejecting, testing actual mixed-case rendering...")
                
                # Additional test: try rendering actual mixed case text to verify it works
                try:
                    mixed_test = "AaBbCcDd"
                    mixed_bbox = test_draw.textbbox((0, 0), mixed_test, font=font)
                    test_draw.text((10, 10), mixed_test, fill=(0, 0, 0), font=font)
                    print(f"CUSTOM FONT DEBUG: Mixed-case render test '{mixed_test}' PASSED: {mixed_bbox}")
                    print(f"CUSTOM FONT DEBUG: Font ACCEPTED despite identical bounding boxes (relaxed validation)")
                    return True
                except Exception as mixed_render_error:
                    print(f"CUSTOM FONT DEBUG: Mixed-case render test FAILED: {mixed_render_error}")
                    print(f"CUSTOM FONT DEBUG: Font REJECTED due to actual rendering failure")
                    import traceback
                    print(f"CUSTOM FONT DEBUG: Full exception trace: {traceback.format_exc()}")
                    return False
            
            print(f"CUSTOM FONT DEBUG: Font mixed-case test PASSED - supports both cases properly")
            return True
        except Exception as bbox_compare_error:
            print(f"CUSTOM FONT DEBUG: Bounding box comparison FAILED: {bbox_compare_error}")
            return False
        
    except Exception as e:
        print(f"CUSTOM FONT DEBUG: Font mixed-case test FAILED with exception: {e}")
        import traceback
        traceback.print_exc()
        return False

def get_validated_font(font_path, font_size):
    """Load and validate a font for mixed-case support"""
    try:
        from PIL import ImageFont
        
        # DIAGNOSTIC: Enhanced logging for custom font debugging
        print(f"CUSTOM FONT DEBUG: get_validated_font() called with:")
        print(f"CUSTOM FONT DEBUG:   font_path: '{font_path}'")
        print(f"CUSTOM FONT DEBUG:   font_size: {font_size}")
        print(f"CUSTOM FONT DEBUG:   path exists: {os.path.exists(font_path)}")
        
        if os.path.exists(font_path):
            # Get detailed file information for diagnostics
            try:
                import stat
                file_stats = os.stat(font_path)
                print(f"CUSTOM FONT DEBUG:   file size: {file_stats.st_size} bytes")
                print(f"CUSTOM FONT DEBUG:   file permissions: {stat.filemode(file_stats.st_mode)}")
                print(f"CUSTOM FONT DEBUG:   file extension: {os.path.splitext(font_path)[1]}")
            except Exception as stats_error:
                print(f"CUSTOM FONT DEBUG:   could not get detailed file stats: {stats_error}")
        
        if not os.path.exists(font_path):
            error_msg = f"Font file not found: {font_path}"
            print(f"CUSTOM FONT DEBUG: {error_msg}")
            return None, error_msg
        
        # Get file info for debugging
        try:
            file_stats = os.stat(font_path)
            print(f"CUSTOM FONT DEBUG:   file size: {file_stats.st_size} bytes")
            print(f"CUSTOM FONT DEBUG:   file extension: {os.path.splitext(font_path)[1]}")
        except Exception as stat_error:
            print(f"CUSTOM FONT DEBUG:   could not get file stats: {stat_error}")
        
        # Load the font
        print(f"CUSTOM FONT DEBUG: Attempting to load font with ImageFont.truetype()...")
        try:
            font = ImageFont.truetype(font_path, font_size)
            print(f"CUSTOM FONT DEBUG: Font loaded successfully: {font}")
        except Exception as load_error:
            error_msg = f"Font loading error - ImageFont.truetype() failed: {load_error}"
            print(f"CUSTOM FONT DEBUG: {error_msg}")
            import traceback
            print(f"CUSTOM FONT DEBUG: Full exception trace: {traceback.format_exc()}")
            return None, error_msg
        
        # Test mixed-case support
        print(f"CUSTOM FONT DEBUG: Testing mixed-case support...")
        if test_font_mixed_case_support(font):
            print(f"CUSTOM FONT DEBUG: Mixed-case validation PASSED")
            return font, "OK"
        else:
            error_msg = f"Font does not support mixed-case properly: {font_path}"
            print(f"CUSTOM FONT DEBUG: Mixed-case validation FAILED: {error_msg}")
            return None, error_msg
            
    except Exception as e:
        error_msg = f"Font loading error (outer exception): {e}"
        print(f"CUSTOM FONT DEBUG: {error_msg}")
        import traceback
        traceback.print_exc()
        return None, error_msg

def get_system_fallback_fonts():
    """Get platform-specific fallback fonts known to support mixed-case"""
    system = platform.system()
    
    if system == "Windows":
        # Windows fonts with guaranteed mixed-case support
        return [
            "C:/Windows/Fonts/arial.ttf",           # Arial - excellent mixed-case
            "C:/Windows/Fonts/calibri.ttf",        # Calibri - modern, mixed-case
            "C:/Windows/Fonts/segoeui.ttf",        # Segoe UI - system font, mixed-case
            "C:/Windows/Fonts/verdana.ttf",        # Verdana - web-safe, mixed-case
            "C:/Windows/Fonts/tahoma.ttf",         # Tahoma - fallback mixed-case
            "C:/Windows/Fonts/times.ttf",          # Times - serif mixed-case
            "C:/Windows/Fonts/trebuc.ttf"          # Trebuchet MS - mixed-case
        ]
    elif system == "Darwin":  # macOS
        # macOS fonts with guaranteed mixed-case support
        return [
            "/System/Library/Fonts/Helvetica.ttc",      # Helvetica - classic mixed-case
            "/Library/Fonts/Arial.ttf",                 # Arial - if available
            "/System/Library/Fonts/Times.ttc",          # Times - serif mixed-case
            "/System/Library/Fonts/Avenir.ttc",         # Avenir - modern mixed-case
            "/System/Library/Fonts/Geneva.ttf",         # Geneva - system mixed-case
            "/System/Library/Fonts/Palatino.ttc"        # Palatino - elegant mixed-case
        ]
    else:  # Linux
        # Linux fonts with guaranteed mixed-case support
        return [
            "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",  # LibreOffice mixed-case
            "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",                  # DejaVu - comprehensive
            "/usr/share/fonts/truetype/ubuntu/Ubuntu-R.ttf",                    # Ubuntu - modern mixed-case
            "/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf",              # Google Noto - universal
            "/usr/share/fonts/truetype/lato/Lato-Regular.ttf",                  # Lato - web font mixed-case
            "/usr/share/fonts/truetype/opensans/OpenSans-Regular.ttf"           # Open Sans - mixed-case
        ]

# ============================================================================
# NORMAL MAP GENERATION FUNCTIONS
# ============================================================================

def generate_normal_map_from_alpha(img, strength=1.0, blur_radius=1.0, invert=False):
    """Generate a normal map from an image's alpha channel using height-based algorithm"""
    try:
        from PIL import Image, ImageFilter
        import numpy as np
        
        print(f"[Normal Map] Starting generation with strength={strength}, blur={blur_radius}, invert={invert}")
        
        # Extract alpha channel as height map
        if img.mode != 'RGBA':
            img = img.convert('RGBA')
        
        # Get alpha channel
        alpha_channel = img.split()[3]  # Alpha is the 4th channel
        width, height = alpha_channel.size
        
        print(f"[Normal Map] Processing {width}x{height} alpha channel")
        
        # Apply blur if specified
        if blur_radius > 0:
            alpha_channel = alpha_channel.filter(ImageFilter.GaussianBlur(radius=blur_radius))
            print(f"[Normal Map] Applied blur with radius {blur_radius}")
        
        # Convert to numpy array for gradient calculations
        height_map = np.array(alpha_channel, dtype=np.float32) / 255.0
        
        # Apply strength multiplier
        height_map *= strength
        
        # Calculate gradients using Sobel operators
        # Sobel X kernel for horizontal gradients
        sobel_x = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], dtype=np.float32)
        # Sobel Y kernel for vertical gradients
        sobel_y = np.array([[-1, -2, -1], [0, 0, 0], [1, 2, 1]], dtype=np.float32)
        
        # Pad the height map to handle edges
        padded_height = np.pad(height_map, ((1, 1), (1, 1)), mode='edge')
        
        # Calculate gradients
        grad_x = np.zeros_like(height_map)
        grad_y = np.zeros_like(height_map)
        
        for i in range(height):
            for j in range(width):
                # Extract 3x3 neighborhood
                neighborhood = padded_height[i:i+3, j:j+3]
                # Apply Sobel operators
                grad_x[i, j] = np.sum(neighborhood * sobel_x)
                grad_y[i, j] = np.sum(neighborhood * sobel_y)
        
        print(f"[Normal Map] Calculated gradients")
        
        # Convert gradients to normal vectors
        # Normal map RGB values are calculated as:
        # R = (grad_x + 1) * 0.5  -> maps -1,1 to 0,1
        # G = (-grad_y + 1) * 0.5 -> maps -1,1 to 0,1 (Y is flipped for standard normal maps)
        # B = sqrt(1 - grad_x^2 - grad_y^2) -> Z component, always pointing up
        
        # Clamp gradients to reasonable range
        grad_x = np.clip(grad_x, -1, 1)
        grad_y = np.clip(grad_y, -1, 1)
        
        # Apply invert if specified
        if invert:
            grad_x = -grad_x
            grad_y = -grad_y
            print(f"[Normal Map] Applied inversion")
        
        # Calculate normal map channels
        # Red channel: X gradient mapped to 0-1
        normal_r = ((grad_x + 1.0) * 0.5 * 255).astype(np.uint8)
        
        # Green channel: Y gradient mapped to 0-1 (flipped)
        normal_g = ((-grad_y + 1.0) * 0.5 * 255).astype(np.uint8)
        
        # Blue channel: Z component (pointing up)
        # Calculate Z from X and Y to maintain unit length
        grad_magnitude_sq = grad_x**2 + grad_y**2
        grad_z = np.sqrt(np.maximum(0, 1.0 - grad_magnitude_sq))
        normal_b = (grad_z * 255).astype(np.uint8)
        
        print(f"[Normal Map] Calculated normal vectors")
        
        # Create the normal map image
        normal_map = Image.new('RGB', (width, height))
        
        # Combine channels
        for y in range(height):
            for x in range(width):
                r = int(normal_r[y, x])
                g = int(normal_g[y, x])
                b = int(normal_b[y, x])
                normal_map.putpixel((x, y), (r, g, b))
        
        print(f"[Normal Map] Normal map generation completed successfully")
        return normal_map
        
    except Exception as e:
        print(f"[Normal Map] Error generating normal map: {e}")
        import traceback
        traceback.print_exc()
        return None

# ============================================================================
# ENHANCED POSITIONING FUNCTIONS
# ============================================================================

def calculate_text_position(props, canvas_width, canvas_height, text_width, text_height):
    """Calculate text position based on enhanced positioning properties"""
    
    if props.position_mode == 'MANUAL':
        # Manual positioning mode
        if props.manual_position_unit == 'PERCENTAGE':
            x = int((props.manual_position_x / 100.0) * canvas_width)
            y = int((props.manual_position_y / 100.0) * canvas_height)
        else:  # PIXELS
            x = int(props.manual_position_x)
            y = int(props.manual_position_y)
    else:
        # Preset anchor-based positioning
        margin_x_px = int((props.margin_x / 100.0) * canvas_width)
        margin_y_px = int((props.margin_y / 100.0) * canvas_height)
        
        # Calculate base position based on anchor point
        if 'LEFT' in props.anchor_point:
            base_x = margin_x_px
        elif 'RIGHT' in props.anchor_point:
            base_x = canvas_width - text_width - margin_x_px
        else:  # CENTER
            base_x = (canvas_width - text_width) // 2
            
        if 'TOP' in props.anchor_point:
            base_y = margin_y_px
        elif 'BOTTOM' in props.anchor_point:
            base_y = canvas_height - text_height - margin_y_px
        else:  # MIDDLE
            base_y = (canvas_height - text_height) // 2
            
        # Apply fine-tuning offsets
        x = base_x + props.offset_x
        y = base_y + props.offset_y
    
    # Apply canvas constraints if enabled
    if props.constrain_to_canvas:
        x = max(0, min(x, canvas_width - text_width))
        y = max(0, min(y, canvas_height - text_height))
    
    return x, y

def sync_margin_values(props, changed_margin):
    """Synchronize margin values when linked"""
    if props.margins_linked:
        if changed_margin == 'x':
            props.margin_y = props.margin_x
        elif changed_margin == 'y':
            props.margin_x = props.margin_y

def get_anchor_point_matrix():
    """Return 3x3 matrix of anchor point identifiers for UI layout"""
    return [
        ['TOP_LEFT', 'TOP_CENTER', 'TOP_RIGHT'],
        ['MIDDLE_LEFT', 'MIDDLE_CENTER', 'MIDDLE_RIGHT'],
        ['BOTTOM_LEFT', 'BOTTOM_CENTER', 'BOTTOM_RIGHT']
    ]

def process_multiline_text(text_content, font, props):
    """Process text into lines for multiline rendering"""
    # Split text by newlines first
    lines = text_content.split('\n')
    processed_lines = []
    
    for line in lines:
        if props.auto_wrap and line.strip():
            # Calculate wrap width
            wrap_width = props.wrap_width if props.wrap_width > 0 else props.texture_width - 40  # 20px margin on each side
            wrapped_lines = wrap_text_to_width(line, font, wrap_width)
            processed_lines.extend(wrapped_lines)
        else:
            processed_lines.append(line)
    
    # Apply max lines limit
    if props.max_lines > 0 and len(processed_lines) > props.max_lines:
        processed_lines = processed_lines[:props.max_lines]
    
    return processed_lines

def wrap_text_to_width(text, font, max_width):
    """Wrap text to fit within specified width"""
    if not text.strip():
        return [text]
    
    # Ensure minimum width to prevent infinite loops
    if max_width < 10:
        return [text]
    
    words = text.split()
    if not words:
        return [text]
    
    lines = []
    current_line = ""
    
    for word in words:
        test_line = f"{current_line} {word}".strip()
        try:
            bbox = font.getbbox(test_line)
            text_width = bbox[2] - bbox[0]
        except:
            # Fallback for font issues
            text_width = len(test_line) * (font.size // 2)  # Rough estimation
        
        if text_width <= max_width:
            current_line = test_line
        else:
            if current_line:
                lines.append(current_line)
                current_line = word
            else:
                # Word is too long, add it anyway
                lines.append(word)
                current_line = ""
    
    if current_line:
        lines.append(current_line)
    
    return lines if lines else [text]

def draw_multiline_text(draw, lines, font, text_color, props, canvas_width, canvas_height):
    """Draw multiline text with proper alignment and spacing"""
    if not lines:
        return
    
    try:
        # Calculate line metrics
        bbox = font.getbbox("Ag")  # Use standard characters for height measurement
        base_line_height = bbox[3] - bbox[1]
        line_height = int(base_line_height * props.line_height) + props.line_spacing_pixels
        
        # Ensure minimum line height
        if line_height < 1:
            line_height = base_line_height
        
        # Calculate total text block dimensions
        max_line_width = 0
        for line in lines:
            if line.strip():  # Only measure non-empty lines
                try:
                    bbox = font.getbbox(line)
                    line_width = bbox[2] - bbox[0]
                    max_line_width = max(max_line_width, line_width)
                except:
                    # Fallback width calculation
                    line_width = len(line) * (font.size // 2)
                    max_line_width = max(max_line_width, line_width)
        
        total_height = len(lines) * line_height - props.line_spacing_pixels  # Remove extra spacing from last line
        
        # Calculate starting position based on text block - use legacy positioning for now
        # This maintains compatibility with existing positioning system
        x = (canvas_width - max_line_width) // 2
        y = (canvas_height - total_height) // 2
        
        # Draw each line
        current_y = y
        for line in lines:
            # Skip drawing if we're outside the canvas bounds
            if current_y >= canvas_height:
                break
                
            if line.strip():  # Only process non-empty lines for alignment
                # Calculate x position based on alignment
                try:
                    bbox = font.getbbox(line)
                    line_width = bbox[2] - bbox[0]
                except:
                    line_width = len(line) * (font.size // 2)
                
                if props.text_alignment == 'LEFT':
                    line_x = x
                elif props.text_alignment == 'CENTER':
                    line_x = x + max(0, (max_line_width - line_width) // 2)
                else:  # RIGHT
                    line_x = x + max(0, (max_line_width - line_width))
                
                # Ensure we don't draw outside canvas bounds
                line_x = max(0, min(line_x, canvas_width))
                
                # Draw the line
                draw.text((line_x, current_y), line, fill=text_color, font=font)
            
            current_y += line_height
            
    except Exception as e:
        print(f"Error drawing multiline text: {e}")
        # Fallback to drawing first line only
        if lines and lines[0]:
            draw.text((10, 10), lines[0], fill=text_color, font=font)

def draw_multiline_text_with_stroke(draw, lines, font, text_color, props, canvas_width, canvas_height):
    """Draw multiline text with stroke support"""
    if not lines:
        return
    
    try:
        # Calculate line metrics
        bbox = font.getbbox("Ag")  # Use standard characters for height measurement
        base_line_height = bbox[3] - bbox[1]
        line_height = int(base_line_height * props.line_height) + props.line_spacing_pixels
        
        # Ensure minimum line height
        if line_height < 1:
            line_height = base_line_height
        
        # Calculate total text block dimensions
        max_line_width = 0
        for line in lines:
            if line.strip():  # Only measure non-empty lines
                try:
                    bbox = font.getbbox(line)
                    line_width = bbox[2] - bbox[0]
                    max_line_width = max(max_line_width, line_width)
                except:
                    # Fallback width calculation
                    line_width = len(line) * (font.size // 2)
                    max_line_width = max(max_line_width, line_width)
        
        total_height = len(lines) * line_height - props.line_spacing_pixels  # Remove extra spacing from last line
        
        # Calculate starting position based on text block
        x = (canvas_width - max_line_width) // 2
        y = (canvas_height - total_height) // 2
        
        # Stroke helper function with validation and error handling (same as main rendering)
        def draw_text_with_stroke(draw_obj, text_str, pos_x, pos_y, font_obj, text_col, stroke_enabled, stroke_width, stroke_color, stroke_pos):
            """Draw text with optional stroke"""
            try:
                # Validation and bounds checking
                if not stroke_enabled or stroke_width <= 0 or not text_str.strip():
                    # No stroke or empty text, draw normally
                    draw_obj.text((pos_x, pos_y), text_str, fill=text_col, font=font_obj)
                    return
                
                # Validate stroke width bounds (0-20 pixels as per property definition)
                stroke_width = max(0, min(20, int(stroke_width)))
                if stroke_width == 0:
                    draw_obj.text((pos_x, pos_y), text_str, fill=text_col, font=font_obj)
                    return
                
                # Validate stroke color
                if not stroke_color or len(stroke_color) < 3:
                    print(f"[Stroke] Warning: Invalid stroke color, using black")
                    stroke_color = (0.0, 0.0, 0.0, 1.0)
                
                # Convert stroke color with validation
                stroke_col = []
                for i, c in enumerate(stroke_color[:4]):
                    val = max(0.0, min(1.0, float(c)))  # Clamp to 0-1 range
                    stroke_col.append(int(val * 255))
                stroke_col = tuple(stroke_col[:3] + ([stroke_col[3]] if len(stroke_col) > 3 else [255]))
                
                # Validate stroke position
                if stroke_pos not in ['OUTER', 'INNER', 'CENTER']:
                    print(f"[Stroke] Warning: Invalid stroke position '{stroke_pos}', using OUTER")
                    stroke_pos = 'OUTER'
                
                # Draw stroke based on position
                if stroke_pos == 'OUTER':
                    # Outer stroke: draw stroke offsets around text
                    for dx in range(-stroke_width, stroke_width + 1):
                        for dy in range(-stroke_width, stroke_width + 1):
                            if dx*dx + dy*dy <= stroke_width*stroke_width:  # Circular stroke
                                draw_obj.text((pos_x + dx, pos_y + dy), text_str, fill=stroke_col, font=font_obj)
                    # Draw text on top
                    draw_obj.text((pos_x, pos_y), text_str, fill=text_col, font=font_obj)
                
                elif stroke_pos == 'CENTER':
                    # Center stroke: similar to outer but smaller offset
                    half_stroke = max(1, stroke_width // 2)
                    for dx in range(-half_stroke, half_stroke + 1):
                        for dy in range(-half_stroke, half_stroke + 1):
                            if dx*dx + dy*dy <= half_stroke*half_stroke:
                                draw_obj.text((pos_x + dx, pos_y + dy), text_str, fill=stroke_col, font=font_obj)
                    # Draw text on top
                    draw_obj.text((pos_x, pos_y), text_str, fill=text_col, font=font_obj)
                
                elif stroke_pos == 'INNER':
                    # Inner stroke: draw text first, then smaller inner stroke
                    draw_obj.text((pos_x, pos_y), text_str, fill=text_col, font=font_obj)
                    # Create inner stroke effect by drawing smaller stroke inside
                    inner_offset = max(1, stroke_width // 3)
                    for dx in range(-inner_offset, inner_offset + 1):
                        for dy in range(-inner_offset, inner_offset + 1):
                            if dx*dx + dy*dy <= inner_offset*inner_offset and (dx != 0 or dy != 0):
                                # Use stroke color with some transparency for inner effect
                                inner_col = stroke_col[:3]
                                if len(stroke_col) > 3:
                                    inner_col += (int(stroke_col[3] * 0.7),)
                                else:
                                    inner_col += (int(255 * 0.7),)
                                draw_obj.text((pos_x + dx, pos_y + dy), text_str, fill=inner_col, font=font_obj)
                
            except Exception as e:
                print(f"[Stroke] Error drawing multiline stroke: {e}")
                # Fallback to normal text drawing
                try:
                    draw_obj.text((pos_x, pos_y), text_str, fill=text_col, font=font_obj)
                except Exception as fallback_error:
                    print(f"[Stroke] Error in fallback multiline text drawing: {fallback_error}")
        
        # Draw each line with stroke
        current_y = y
        for line in lines:
            # Skip drawing if we're outside the canvas bounds
            if current_y >= canvas_height:
                break
                
            if line.strip():  # Only process non-empty lines for alignment
                # Calculate x position based on alignment
                try:
                    bbox = font.getbbox(line)
                    line_width = bbox[2] - bbox[0]
                except:
                    line_width = len(line) * (font.size // 2)
                
                if props.text_alignment == 'LEFT':
                    line_x = x
                elif props.text_alignment == 'CENTER':
                    line_x = x + max(0, (max_line_width - line_width) // 2)
                else:  # RIGHT
                    line_x = x + max(0, (max_line_width - line_width))
                
                # Ensure we don't draw outside canvas bounds
                line_x = max(0, min(line_x, canvas_width))
                
                # Draw the line with stroke
                draw_text_with_stroke(draw, line, line_x, current_y, font, text_color,
                                    props.enable_stroke, props.stroke_width, props.stroke_color, props.stroke_position)
            
            current_y += line_height
            
    except Exception as e:
        print(f"Error drawing multiline text with stroke: {e}")
        # Fallback to drawing first line only
        if lines and lines[0]:
            draw.text((10, 10), lines[0], fill=text_color, font=font)

def apply_tiling(image, props):
    """Apply tiling effects to the image."""
    try:
        if not props.enable_tiling:
            return image
        
        from PIL import Image
        
        # Get original image dimensions
        orig_width, orig_height = image.size
        
        # Determine tile counts based on mode
        if props.tiling_mode == 'TILE' or props.tiling_mode == 'MIRROR':
            tile_x = props.tile_count_x
            tile_y = props.tile_count_y
        elif props.tiling_mode == 'REPEAT_X':
            tile_x = props.tile_count_x
            tile_y = 1
        elif props.tiling_mode == 'REPEAT_Y':
            tile_x = 1
            tile_y = props.tile_count_y
        else:
            return image
        
        # Calculate spacing
        spacing = props.tile_spacing
        
        # Calculate tile dimensions
        if props.expand_tiling_canvas:
            # Expand canvas to fit all tiles
            tile_width = orig_width
            tile_height = orig_height
            canvas_width = tile_x * tile_width + (tile_x - 1) * spacing
            canvas_height = tile_y * tile_height + (tile_y - 1) * spacing
        else:
            # Scale down tiles to fit in original canvas
            available_width = orig_width - (tile_x - 1) * spacing
            available_height = orig_height - (tile_y - 1) * spacing
            tile_width = max(1, available_width // tile_x)
            tile_height = max(1, available_height // tile_y)
            canvas_width = orig_width
            canvas_height = orig_height
            
            # Resize the original image if needed
            if tile_width != orig_width or tile_height != orig_height:
                image = image.resize((tile_width, tile_height), Image.LANCZOS)
        
        # Create the tiled canvas
        tiled_image = Image.new('RGBA', (canvas_width, canvas_height), (0, 0, 0, 0))
        
        # Generate tiles
        for row in range(tile_y):
            for col in range(tile_x):
                # Calculate position
                x = col * (tile_width + spacing)
                y = row * (tile_height + spacing)
                
                # Get the tile image based on tiling mode
                if props.tiling_mode == 'MIRROR':
                    # Mirror alternating tiles for checkerboard pattern
                    tile_image = image.copy()
                    if (row + col) % 2 == 1:
                        # Alternate between horizontal and vertical mirroring
                        if row % 2 == 1:
                            tile_image = tile_image.transpose(Image.FLIP_TOP_BOTTOM)
                        if col % 2 == 1:
                            tile_image = tile_image.transpose(Image.FLIP_LEFT_RIGHT)
                else:
                    # Regular tile (TILE, REPEAT_X, REPEAT_Y)
                    tile_image = image
                
                # Paste the tile
                if tile_image.mode == 'RGBA':
                    tiled_image.paste(tile_image, (x, y), tile_image)
                else:
                    tiled_image.paste(tile_image, (x, y))
        
        return tiled_image
        
    except Exception as e:
        print(f"Error applying tiling: {e}")
        return image


def generate_texture_image(props, width, height):
    """Generate the actual texture image with overlays - width and height are REQUIRED
    Returns: (diffuse_image, normal_map_image) or (diffuse_image, None) if normal map disabled"""
    try:
        install_pillow()
        from PIL import Image, ImageDraw, ImageFont, ImageFilter
        import math
        
        # Use the exact dimensions provided
        print(f"Generating PIL image at {width}x{height}")
        
        # Ensure minimum dimensions
        width = max(16, width)
        height = max(16, height)
        
        print(f"DEBUG: Generating texture - requested: {width}x{height}, texture settings: {props.texture_width}x{props.texture_height}")
        print(f"DEBUG: Normal map generation enabled: {props.generate_normal_map}")
        
        # Scale font size proportionally for preview
        scale_factor = width / props.texture_width
        base_font_size = max(6, int(props.font_size * scale_factor))
        padding = max(0, int(props.padding * scale_factor))
        
        print(f"DEBUG: Scale factor: {scale_factor}, scaled font size: {base_font_size}, scaled padding: {padding}")
        
        # Create base image with background
        if props.background_transparent:
            base_img = Image.new('RGBA', (width, height), (0, 0, 0, 0))
        else:
            bg_color = tuple(int(c * 255) for c in props.background_color[:3])
            if len(props.background_color) > 3:
                bg_color += (int(props.background_color[3] * 255),)
            base_img = Image.new('RGBA', (width, height), bg_color)
        
        print(f"Base image created: {base_img.size}")
        
        # Collect all elements with z-index (text is z-index 0)
        elements = []
        
        # ENHANCEMENT: Filter disabled overlays early to completely exclude them from all calculations
        # This ensures disabled overlays don't consume space in APPEND/PREPEND modes and don't render
        # When overlays are disabled (enabled=False), they are completely excluded from:
        # - Space calculations, text bounds calculations, image positioning, spacing calculations, and rendering
        enabled_overlays = [overlay for overlay in props.image_overlays if overlay.enabled]
        
        # Separate enabled overlays by positioning mode and sort them
        absolute_overlays = []
        prepend_overlays = []
        append_overlays = []
        
        for overlay in enabled_overlays:
            if overlay.image_path and os.path.exists(overlay.image_path):
                if overlay.positioning_mode == 'ABSOLUTE':
                    absolute_overlays.append(overlay)
                elif overlay.positioning_mode == 'PREPEND':
                    prepend_overlays.append(overlay)
                elif overlay.positioning_mode == 'APPEND':
                    append_overlays.append(overlay)
        
        # Sort prepend and append overlays by sequence_index
        prepend_overlays.sort(key=lambda x: x.sequence_index)
        append_overlays.sort(key=lambda x: x.sequence_index)
        
        # PLACEHOLDER: text_bbox will be calculated after text setup
        text_bbox = None
        
        # PERFORMANCE FIX: Cache loaded overlay images to prevent multiple loading
        overlay_image_cache = {}
        
        def load_overlay_image_cached(overlay_path, scale, scale_factor):
            """Load and cache overlay images to improve performance"""
            cache_key = (overlay_path, scale, scale_factor)
            if cache_key in overlay_image_cache:
                return overlay_image_cache[cache_key], None  # Return cached image, no error
            
            try:
                # ERROR HANDLING: Check file existence and validate path
                if not overlay_path or not os.path.exists(overlay_path):
                    return None, f"Image file not found: {overlay_path}"
                
                # EDGE CASE: Check file size to prevent memory issues
                try:
                    file_size = os.path.getsize(overlay_path)
                    if file_size > 50 * 1024 * 1024:  # 50MB limit
                        return None, f"Image file too large: {file_size} bytes"
                except OSError:
                    return None, f"Cannot access image file: {overlay_path}"
                
                # Load and convert image
                overlay_img = Image.open(overlay_path).convert("RGBA")
                
                # EDGE CASE: Check for zero dimensions
                if overlay_img.width == 0 or overlay_img.height == 0:
                    return None, f"Image has zero dimensions: {overlay_img.width}x{overlay_img.height}"
                
                # Apply scaling
                if scale != 1.0:
                    # EDGE CASE: Prevent extreme scaling that could cause memory issues
                    final_scale = max(0.01, min(10.0, scale * scale_factor))  # Limit scale between 1% and 1000%
                    new_width = max(1, int(overlay_img.width * final_scale))
                    new_height = max(1, int(overlay_img.height * final_scale))
                    
                    # EDGE CASE: Prevent extremely large scaled images
                    if new_width > 4096 or new_height > 4096:
                        return None, f"Scaled image too large: {new_width}x{new_height}"
                    
                    overlay_img = overlay_img.resize((new_width, new_height), Image.LANCZOS)
                
                # Cache the processed image
                overlay_image_cache[cache_key] = overlay_img
                return overlay_img, None
                
            except Exception as e:
                error_msg = f"Error loading overlay image '{overlay_path}': {str(e)}"
                return None, error_msg
        
        # Apply overlays in z-index order, grouped by positioning mode
        all_overlays_with_mode = []
        
        # Add absolute overlays (maintain backward compatibility)
        for overlay in absolute_overlays:
            all_overlays_with_mode.append((overlay, 'ABSOLUTE'))
        
        # Add prepend overlays (positioned to the left of text)
        prepend_x_offset = 0
        for i, overlay in enumerate(prepend_overlays):
            all_overlays_with_mode.append((overlay, 'PREPEND', prepend_x_offset))
            if text_bbox:
                # Calculate spacing for next overlay using cached loading
                temp_overlay_img, error = load_overlay_image_cached(overlay.image_path, overlay.scale, scale_factor)
                if temp_overlay_img:
                    # EDGE CASE: Validate spacing values to prevent extreme spacing
                    text_spacing = max(0, min(1000, (overlay.text_spacing / 100.0) * base_font_size))
                    image_spacing = max(0, min(1000, (overlay.image_spacing / 100.0) * base_font_size))
                    
                    # Add image width plus spacing for next position
                    prepend_x_offset += temp_overlay_img.width + (text_spacing if i == 0 else image_spacing)
                else:
                    print(f"Warning: {error}")
                    # Fallback spacing if image can't be loaded
                    prepend_x_offset += base_font_size
        
        # Add append overlays (positioned to the right of text)
        append_x_offset = 0
        for i, overlay in enumerate(append_overlays):
            all_overlays_with_mode.append((overlay, 'APPEND', append_x_offset))
            if text_bbox:
                # Calculate spacing for next overlay using cached loading
                temp_overlay_img, error = load_overlay_image_cached(overlay.image_path, overlay.scale, scale_factor)
                if temp_overlay_img:
                    # EDGE CASE: Validate spacing values to prevent extreme spacing
                    text_spacing = max(0, min(1000, (overlay.text_spacing / 100.0) * base_font_size))
                    image_spacing = max(0, min(1000, (overlay.image_spacing / 100.0) * base_font_size))
                    
                    # Calculate position for this overlay, then update offset for next
                    if i > 0:
                        append_x_offset += image_spacing
                    else:
                        append_x_offset += text_spacing
                    
                    # Update offset for next overlay
                    if i < len(append_overlays) - 1:  # Don't add for last overlay
                        append_x_offset += temp_overlay_img.width
                else:
                    print(f"Warning: {error}")
                    # Fallback spacing if image can't be loaded
                    append_x_offset += base_font_size if i == 0 else base_font_size * 0.5
        
        # Sort all overlays by z-index
        all_overlays_with_mode.sort(key=lambda x: x[0].z_index)
        
        # Apply overlays using cached loading with comprehensive error handling
        for overlay_data in all_overlays_with_mode:
            overlay = overlay_data[0]
            mode = overlay_data[1]
            
            # Use cached loading for better performance and error handling
            overlay_img, error = load_overlay_image_cached(overlay.image_path, overlay.scale, scale_factor)
            
            if overlay_img is None:
                print(f"Skipping overlay {overlay.name}: {error}")
                continue
            
            try:
                # Apply rotation with error handling
                if overlay.rotation != 0:
                    # EDGE CASE: Validate rotation range
                    rotation_angle = overlay.rotation % 360  # Normalize to 0-360
                    if abs(rotation_angle) > 0.1:  # Only rotate if meaningful angle
                        overlay_img = overlay_img.rotate(rotation_angle, expand=True)
                
                # Calculate position based on positioning mode
                if mode == 'ABSOLUTE':
                    # Use existing absolute positioning (backward compatibility)
                    # EDGE CASE: Ensure position values are valid
                    x_position = max(0.0, min(1.0, overlay.x_position))
                    y_position = max(0.0, min(1.0, overlay.y_position))
                    x_pos = int(x_position * width - overlay_img.width / 2)
                    y_pos = int((1.0 - y_position) * height - overlay_img.height / 2)
                elif mode == 'PREPEND' and text_bbox:
                    # Position to the left of text bounds
                    x_offset = overlay_data[2] if len(overlay_data) > 2 else 0
                    x_pos = int(text_bbox[0] - x_offset - overlay_img.width)
                    y_pos = int(text_bbox[1] + (text_bbox[3] - text_bbox[1] - overlay_img.height) / 2)
                elif mode == 'APPEND' and text_bbox:
                    # Position to the right of text bounds
                    x_offset = overlay_data[2] if len(overlay_data) > 2 else 0
                    x_pos = int(text_bbox[2] + x_offset)
                    y_pos = int(text_bbox[1] + (text_bbox[3] - text_bbox[1] - overlay_img.height) / 2)
                else:
                    # Fallback to absolute positioning
                    x_position = max(0.0, min(1.0, overlay.x_position))
                    y_position = max(0.0, min(1.0, overlay.y_position))
                    x_pos = int(x_position * width - overlay_img.width / 2)
                    y_pos = int((1.0 - y_position) * height - overlay_img.height / 2)
                
                # EDGE CASE: Ensure positions are within bounds and handle negative dimensions gracefully
                if overlay_img.width <= 0 or overlay_img.height <= 0:
                    print(f"Warning: Overlay {overlay.name} has invalid dimensions: {overlay_img.width}x{overlay_img.height}")
                    continue
                
                x_pos = max(0, min(x_pos, width - overlay_img.width))
                y_pos = max(0, min(y_pos, height - overlay_img.height))
                
                # EDGE CASE: Skip overlay if it's completely outside the canvas
                if x_pos >= width or y_pos >= height or x_pos + overlay_img.width <= 0 or y_pos + overlay_img.height <= 0:
                    print(f"Warning: Overlay {overlay.name} is positioned outside canvas bounds, skipping")
                    continue
                
                elements.append((overlay.z_index, 'image', overlay_img, x_pos, y_pos))
                print(f"Successfully added overlay {overlay.name} ({mode}) at z-index {overlay.z_index}, pos ({x_pos}, {y_pos})")
                
            except Exception as e:
                print(f"Error processing overlay {overlay.name}: {e}")
                import traceback
                traceback.print_exc()
        
        # Create text element (z-index 0)
        text_img = Image.new('RGBA', (width, height), (0, 0, 0, 0))
        draw = ImageDraw.Draw(text_img)
        
        # Load font with enhanced mixed-case validation
        def load_font_at_size(size):
            """Helper function to load font at specific size"""
            font = None
            font_loaded = False
            
            # Try custom font first with validation
            if props.use_custom_font and props.custom_font_path:
                font, error_msg = get_validated_font(props.custom_font_path, size)
                if font:
                    font_loaded = True
                else:
                    print(f"FAILED: Custom font validation failed: {error_msg}")
            
            # Try selected system font with validation
            if not font_loaded and props.font_path != "default":
                font, error_msg = get_validated_font(props.font_path, size)
                if font:
                    font_loaded = True
                else:
                    print(f"FAILED: System font validation failed: {error_msg}")
            
            # Enhanced fallback with mixed-case validation
            if not font_loaded:
                fallback_fonts = get_system_fallback_fonts()
                
                for fallback_path in fallback_fonts:
                    if os.path.exists(fallback_path):
                        font, error_msg = get_validated_font(fallback_path, size)
                        if font:
                            font_loaded = True
                            break
                        else:
                            print(f"FAILED: Fallback font validation failed: {error_msg}")
            
            # Last resort - use default font but warn about mixed-case limitations
            if not font_loaded:
                font = ImageFont.load_default()
                font_loaded = True
                print("WARNING: Using default font - this may not support mixed-case properly!")
                print("WARNING: Consider installing additional fonts for proper mixed-case support")
            
            return font, font_loaded
        
        # Initial font loading
        print("DEBUG UPPERCASE FIX: Starting enhanced font loading with mixed-case validation")
        font_size_pixels = base_font_size
        font, font_loaded = load_font_at_size(font_size_pixels)
        
        # ============================================================================
        # PREPEND/APPEND TEXT PROCESSING
        # ============================================================================
        
        # Get the main text
        main_text = props.text if props.text is not None else ""
        
        # Get prepend and append text
        prepend_text = props.prepend_text.strip() if props.prepend_text else ""
        append_text = props.append_text.strip() if props.append_text else ""
        
        # Calculate margins as pixels (percentage of font size)
        prepend_margin_px = int((props.prepend_margin / 100.0) * font_size_pixels) if prepend_text else 0
        append_margin_px = int((props.append_margin / 100.0) * font_size_pixels) if append_text else 0
        
        # Combine text based on layout
        if props.prepend_append_layout == 'HORIZONTAL':
            # Horizontal layout: [prepend] [margin] [main] [margin] [append]
            text_parts = []
            if prepend_text:
                text_parts.append(prepend_text)
                # Add proper spacing based on margin percentage
                if prepend_margin_px > 0:
                    # Convert margin pixels to approximate space characters
                    space_count = max(1, int(prepend_margin_px / (font_size_pixels * 0.3)))
                    text_parts.append(" " * space_count)
                else:
                    text_parts.append(" ")  # Default single space
            
            text_parts.append(main_text)
            
            if append_text:
                # Add proper spacing based on margin percentage
                if append_margin_px > 0:
                    space_count = max(1, int(append_margin_px / (font_size_pixels * 0.3)))
                    text_parts.append(" " * space_count)
                else:
                    text_parts.append(" ")  # Default single space
                text_parts.append(append_text)
            
            text = "".join(text_parts)
        else:
            # Vertical layout will be handled separately - for now use main text
            text = main_text
        
        # ENHANCED DIAGNOSTIC LOGGING for uppercase text issue
        print(f"DEBUG PREPEND/APPEND - Original main text: '{props.text}'")
        print(f"DEBUG PREPEND/APPEND - Prepend text: '{prepend_text}'")
        print(f"DEBUG PREPEND/APPEND - Append text: '{append_text}'")
        print(f"DEBUG PREPEND/APPEND - Layout: '{props.prepend_append_layout}'")
        print(f"DEBUG PREPEND/APPEND - Final combined text: '{text}'")
        print(f"DEBUG UPPERCASE ISSUE - Text type: {type(text)}")
        print(f"DEBUG UPPERCASE ISSUE - Has upper chars: {any(c.isupper() for c in text)}")
        print(f"DEBUG UPPERCASE ISSUE - Has lower chars: {any(c.islower() for c in text)}")
        print(f"DEBUG UPPERCASE ISSUE - Font being used: {font}")
        if hasattr(font, 'path'):
            print(f"DEBUG UPPERCASE ISSUE - Font path: {font.path}")
        
        # Test the current font's mixed-case rendering capability
        print("DEBUG UPPERCASE ISSUE - Testing current font mixed-case capability...")
        if test_font_mixed_case_support(font):
            print("SUCCESS: Current font supports mixed-case rendering properly!")
        else:
            print("WARNING: Current font may not support mixed-case rendering properly!")
            print("This could be the cause of uppercase-only text rendering")
        
        # ============================================================================
        # TEXT FITTING SYSTEM
        # ============================================================================
        
        # ============================================================================
        # PREPEND/APPEND TEXT PROCESSING
        # ============================================================================
        
        # REMOVED: Duplicate text processing code block - already handled above
        
        # Apply text fitting if enabled
        if props.enable_text_fitting:
            print(f"DEBUG TEXT FITTING: Starting text fitting process")
            print(f"DEBUG TEXT FITTING: Canvas size: {width}x{height}")
            print(f"DEBUG TEXT FITTING: Initial font size: {font_size_pixels}")
            print(f"DEBUG TEXT FITTING: Fitting mode: {props.text_fitting_mode}")
            
            # Calculate available space with margin
            margin_px_x = int((props.text_fitting_margin / 100.0) * width)
            margin_px_y = int((props.text_fitting_margin / 100.0) * height)
            available_width = width - (2 * margin_px_x)
            available_height = height - (2 * margin_px_y)
            
            print(f"DEBUG TEXT FITTING: Available space: {available_width}x{available_height} (margin: {margin_px_x}x{margin_px_y})")
            
            # Apply scale factor to min/max font sizes
            min_font_size_scaled = max(4, int(props.min_font_size * scale_factor))
            max_font_size_scaled = max(min_font_size_scaled, int(props.max_font_size * scale_factor))
            
            # Binary search for optimal font size
            def calculate_text_dimensions(test_font_size):
                """Calculate text dimensions for a given font size"""
                try:
                    test_font, _ = load_font_at_size(test_font_size)
                    temp_draw = ImageDraw.Draw(Image.new('RGBA', (100, 100)))
                    
                    if props.prepend_append_layout == 'VERTICAL' and (prepend_text or append_text):
                        # Calculate vertical layout dimensions
                        total_width = 0
                        total_height = 0
                        
                        if prepend_text:
                            prepend_bbox = temp_draw.textbbox((0, 0), prepend_text, font=test_font)
                            prepend_width = prepend_bbox[2] - prepend_bbox[0]
                            prepend_height = prepend_bbox[3] - prepend_bbox[1]
                            total_width = max(total_width, prepend_width)
                            total_height += prepend_height + int((props.prepend_margin / 100.0) * test_font_size)
                        
                        main_bbox = temp_draw.textbbox((0, 0), main_text, font=test_font)
                        main_width = main_bbox[2] - main_bbox[0]
                        main_height = main_bbox[3] - main_bbox[1]
                        total_width = max(total_width, main_width)
                        total_height += main_height
                        
                        if append_text:
                            append_bbox = temp_draw.textbbox((0, 0), append_text, font=test_font)
                            append_width = append_bbox[2] - append_bbox[0]
                            append_height = append_bbox[3] - append_bbox[1]
                            total_width = max(total_width, append_width)
                            total_height += int((props.append_margin / 100.0) * test_font_size) + append_height
                        
                        return total_width, total_height
                    else:
                        # Calculate horizontal layout dimensions
                        bbox = temp_draw.textbbox((0, 0), text, font=test_font)
                        return bbox[2] - bbox[0], bbox[3] - bbox[1]
                except Exception as e:
                    print(f"DEBUG TEXT FITTING: Error calculating dimensions: {e}")
                    # Return fallback dimensions
                    return int(len(text) * test_font_size * 0.6), test_font_size
            
            # Binary search for optimal font size
            low_size = min_font_size_scaled
            high_size = max_font_size_scaled
            optimal_size = font_size_pixels
            
            print(f"DEBUG TEXT FITTING: Binary search range: {low_size} to {high_size}")
            
            try:
                for iteration in range(20):  # Limit iterations to prevent infinite loops
                    mid_size = (low_size + high_size) // 2
                    if mid_size == low_size or mid_size == high_size:
                        break
                    
                    test_width, test_height = calculate_text_dimensions(mid_size)
                    
                    fits_criteria = False
                    if props.text_fitting_mode == 'FIT_WIDTH':
                        fits_criteria = test_width <= available_width
                    elif props.text_fitting_mode == 'FIT_HEIGHT':
                        fits_criteria = test_height <= available_height
                    elif props.text_fitting_mode == 'FIT_BOTH':
                        fits_criteria = test_width <= available_width and test_height <= available_height
                    elif props.text_fitting_mode == 'FIT_CONTAIN':
                        fits_criteria = test_width <= available_width and test_height <= available_height
                    
                    print(f"DEBUG TEXT FITTING: Iteration {iteration}: size={mid_size}, dims={test_width}x{test_height}, fits={fits_criteria}")
                    
                    if fits_criteria:
                        optimal_size = mid_size
                        low_size = mid_size
                    else:
                        high_size = mid_size
                
                # Apply the optimal font size
                if optimal_size != font_size_pixels:
                    font_size_pixels = optimal_size
                    font, font_loaded = load_font_at_size(font_size_pixels)
                    print(f"DEBUG TEXT FITTING: Applied optimal font size: {font_size_pixels}")
                else:
                    print(f"DEBUG TEXT FITTING: No adjustment needed, keeping original size: {font_size_pixels}")
            except Exception as e:
                print(f"DEBUG TEXT FITTING: Error in binary search: {e}")
                print("DEBUG TEXT FITTING: Continuing with original font size")
        
        # Handle vertical layout separately if needed
        if props.prepend_append_layout == 'VERTICAL' and (prepend_text or append_text):
            # For vertical layout, we need to render each text part separately
            # and position them vertically with proper margins
            
            # Calculate dimensions for each text part
            main_bbox = draw.textbbox((0, 0), main_text, font=font)
            main_width = max(1, main_bbox[2] - main_bbox[0])
            main_height = max(1, main_bbox[3] - main_bbox[1])
            
            total_width = main_width
            total_height = main_height
            text_elements = [(main_text, main_width, main_height)]
            
            if prepend_text:
                prepend_bbox = draw.textbbox((0, 0), prepend_text, font=font)
                prepend_width = max(1, prepend_bbox[2] - prepend_bbox[0])
                prepend_height = max(1, prepend_bbox[3] - prepend_bbox[1])
                total_width = max(total_width, prepend_width)
                total_height += prepend_height + prepend_margin_px
                text_elements.insert(0, (prepend_text, prepend_width, prepend_height))
            
            if append_text:
                append_bbox = draw.textbbox((0, 0), append_text, font=font)
                append_width = max(1, append_bbox[2] - append_bbox[0])
                append_height = max(1, append_bbox[3] - append_bbox[1])
                total_width = max(total_width, append_width)
                total_height += append_height + append_margin_px
                text_elements.append((append_text, append_width, append_height))
            
            text_width = total_width
            text_height = total_height
            
            print(f"DEBUG VERTICAL LAYOUT - Total dimensions: {text_width}x{text_height}")
            print(f"DEBUG VERTICAL LAYOUT - Text elements: {len(text_elements)}")
            
        else:
            # Standard bbox calculation for horizontal layout or single text
            try:
                bbox = draw.textbbox((0, 0), text, font=font)
                text_width = max(1, bbox[2] - bbox[0])
                text_height = max(1, bbox[3] - bbox[1])
            except (OSError, ZeroDivisionError, ValueError) as e:
                print(f"Error getting text bbox: {e}")
                text_width = int(len(text) * font_size_pixels * 0.6)
                text_height = font_size_pixels
        
        # ============================================================================
        # ENHANCED POSITIONING SYSTEM - Replaces hardcoded vertical centering
        # ============================================================================
        
        # Use enhanced positioning system
        x, y = calculate_text_position(props, width, height, text_width, text_height)
        
        # Apply legacy text_align for backward compatibility if using MIDDLE_CENTER anchor
        if props.anchor_point == 'MIDDLE_CENTER' and props.position_mode == 'PRESET':
            if props.text_align == 'center':
                x = (width - text_width) // 2
            elif props.text_align == 'right':
                x = width - text_width - padding
            elif props.text_align == 'left':
                x = padding
            
            # Apply offsets even in legacy mode
            x += props.offset_x
            y += props.offset_y
        
        # Calculate text bounds for positioning relative overlays (FIXED BUG #1)
        # Now that font, x, y, and text variables are properly defined, we can safely calculate text_bbox
        if prepend_overlays or append_overlays:
            if props.prepend_append_layout == 'VERTICAL' and (prepend_text or append_text):
                # For vertical layout, use the total calculated dimensions
                text_bbox = (x, y, x + text_width, y + text_height)
            else:
                # For horizontal layout, calculate bbox using the combined text
                try:
                    # Try new textbbox method (PIL 8.0+)
                    temp_draw = ImageDraw.Draw(Image.new('RGBA', (100, 100)))
                    bbox = temp_draw.textbbox((0, 0), text, font=font)
                    actual_text_width = bbox[2] - bbox[0]
                    actual_text_height = bbox[3] - bbox[1]
                    text_bbox = (x, y, x + actual_text_width, y + actual_text_height)
                except AttributeError:
                    # Fallback to deprecated textsize method
                    temp_draw = ImageDraw.Draw(Image.new('RGBA', (100, 100)))
                    actual_text_width, actual_text_height = temp_draw.textsize(text, font=font)
                    text_bbox = (x, y, x + actual_text_width, y + actual_text_height)
        else:
            # No relative overlays, set bbox to None
            text_bbox = None
        
        # Draw text with stroke support
        text_color = tuple(int(c * 255) for c in props.text_color[:3])
        if len(props.text_color) > 3:
            text_color += (int(props.text_color[3] * 255),)
        
        # Stroke rendering helper function with validation and error handling
        def draw_text_with_stroke(draw_obj, text_str, pos_x, pos_y, font_obj, text_col, stroke_enabled, stroke_width, stroke_color, stroke_pos):
            """Draw text with optional stroke"""
            try:
                # Validation and bounds checking
                if not stroke_enabled or stroke_width <= 0 or not text_str.strip():
                    # No stroke or empty text, draw normally
                    draw_obj.text((pos_x, pos_y), text_str, fill=text_col, font=font_obj)
                    return
                
                # Validate stroke width bounds (0-20 pixels as per property definition)
                stroke_width = max(0, min(20, int(stroke_width)))
                if stroke_width == 0:
                    draw_obj.text((pos_x, pos_y), text_str, fill=text_col, font=font_obj)
                    return
                
                # Validate stroke color
                if not stroke_color or len(stroke_color) < 3:
                    print(f"[Stroke] Warning: Invalid stroke color, using black")
                    stroke_color = (0.0, 0.0, 0.0, 1.0)
                
                # Convert stroke color with validation
                stroke_col = []
                for i, c in enumerate(stroke_color[:4]):
                    val = max(0.0, min(1.0, float(c)))  # Clamp to 0-1 range
                    stroke_col.append(int(val * 255))
                stroke_col = tuple(stroke_col[:3] + ([stroke_col[3]] if len(stroke_col) > 3 else [255]))
                
                # Validate stroke position
                if stroke_pos not in ['OUTER', 'INNER', 'CENTER']:
                    print(f"[Stroke] Warning: Invalid stroke position '{stroke_pos}', using OUTER")
                    stroke_pos = 'OUTER'
                
                # Draw stroke based on position
                if stroke_pos == 'OUTER':
                    # Outer stroke: draw stroke offsets around text
                    for dx in range(-stroke_width, stroke_width + 1):
                        for dy in range(-stroke_width, stroke_width + 1):
                            if dx*dx + dy*dy <= stroke_width*stroke_width:  # Circular stroke
                                draw_obj.text((pos_x + dx, pos_y + dy), text_str, fill=stroke_col, font=font_obj)
                    # Draw text on top
                    draw_obj.text((pos_x, pos_y), text_str, fill=text_col, font=font_obj)
                
                elif stroke_pos == 'CENTER':
                    # Center stroke: similar to outer but smaller offset
                    half_stroke = max(1, stroke_width // 2)
                    for dx in range(-half_stroke, half_stroke + 1):
                        for dy in range(-half_stroke, half_stroke + 1):
                            if dx*dx + dy*dy <= half_stroke*half_stroke:
                                draw_obj.text((pos_x + dx, pos_y + dy), text_str, fill=stroke_col, font=font_obj)
                    # Draw text on top
                    draw_obj.text((pos_x, pos_y), text_str, fill=text_col, font=font_obj)
                
                elif stroke_pos == 'INNER':
                    # Inner stroke: draw text first, then smaller inner stroke
                    draw_obj.text((pos_x, pos_y), text_str, fill=text_col, font=font_obj)
                    # Create inner stroke effect by drawing smaller stroke inside
                    inner_offset = max(1, stroke_width // 3)
                    for dx in range(-inner_offset, inner_offset + 1):
                        for dy in range(-inner_offset, inner_offset + 1):
                            if dx*dx + dy*dy <= inner_offset*inner_offset and (dx != 0 or dy != 0):
                                # Use stroke color with some transparency for inner effect
                                inner_col = stroke_col[:3]
                                if len(stroke_col) > 3:
                                    inner_col += (int(stroke_col[3] * 0.7),)
                                else:
                                    inner_col += (int(255 * 0.7),)
                                draw_obj.text((pos_x + dx, pos_y + dy), text_str, fill=inner_col, font=font_obj)
                
            except Exception as e:
                print(f"[Stroke] Error drawing stroke: {e}")
                # Fallback to normal text drawing
                try:
                    draw_obj.text((pos_x, pos_y), text_str, fill=text_col, font=font_obj)
                except Exception as fallback_error:
                    print(f"[Stroke] Error in fallback text drawing: {fallback_error}")
        
        # Handle vertical layout rendering with stroke
        if props.prepend_append_layout == 'VERTICAL' and (prepend_text or append_text):
            print(f"DEBUG VERTICAL LAYOUT - Rendering vertical text layout with stroke: {props.enable_stroke}")
            
            # Calculate starting Y position for vertical stack
            current_y = y
            
            # Render prepend text if exists
            if prepend_text:
                print(f"DEBUG VERTICAL LAYOUT - Drawing prepend text: '{prepend_text}' at y={current_y}")
                draw_text_with_stroke(draw, prepend_text, x, current_y, font, text_color,
                                    props.enable_stroke, props.stroke_width, props.stroke_color, props.stroke_position)
                prepend_bbox = draw.textbbox((0, 0), prepend_text, font=font)
                prepend_height = prepend_bbox[3] - prepend_bbox[1]
                current_y += prepend_height + prepend_margin_px
            
            # Render main text
            print(f"DEBUG VERTICAL LAYOUT - Drawing main text: '{main_text}' at y={current_y}")
            draw_text_with_stroke(draw, main_text, x, current_y, font, text_color,
                                props.enable_stroke, props.stroke_width, props.stroke_color, props.stroke_position)
            main_bbox = draw.textbbox((0, 0), main_text, font=font)
            main_height = main_bbox[3] - main_bbox[1]
            current_y += main_height + append_margin_px
            
            # Render append text if exists
            if append_text:
                print(f"DEBUG VERTICAL LAYOUT - Drawing append text: '{append_text}' at y={current_y}")
                draw_text_with_stroke(draw, append_text, x, current_y, font, text_color,
                                    props.enable_stroke, props.stroke_width, props.stroke_color, props.stroke_position)
            
            print(f"DEBUG VERTICAL LAYOUT - Vertical text rendering with stroke completed")
            
        # MULTILINE TEXT PROCESSING with stroke support
        elif props.enable_multiline:
            print("DEBUG MULTILINE: Processing multiline text with stroke support")
            # Process multiline text
            lines = process_multiline_text(text, font, props)
            print(f"DEBUG MULTILINE: Processed into {len(lines)} lines: {lines}")
            
            # Draw multiline text with stroke support
            draw_multiline_text_with_stroke(draw, lines, font, text_color, props, width, height)
            print("DEBUG MULTILINE: Multiline text rendering with stroke completed")
        
        else:
            # Standard horizontal rendering with stroke support
            print(f"DEBUG UPPERCASE ISSUE - About to draw text with stroke: '{text}' with font: {font}")
            draw_text_with_stroke(draw, text, x, y, font, text_color,
                                props.enable_stroke, props.stroke_width, props.stroke_color, props.stroke_position)
            print(f"Text drawn at ({x}, {y}) with stroke: {props.enable_stroke}")
            print(f"DEBUG UPPERCASE ISSUE - Text drawing with stroke completed")
        
        # Add shadow/glow effects if enabled
        shadow_glow_img = None
        if props.enable_shadow or props.enable_glow:
            print(f"DEBUG SHADOW/GLOW: Generating shadow/glow effects - shadow: {props.enable_shadow}, glow: {props.enable_glow}")
            shadow_glow_img = Image.new('RGBA', (width, height), (0, 0, 0, 0))
            
            # Apply effects in rendering order: shadow first, then glow
            if props.enable_shadow:
                print(f"DEBUG SHADOW: Creating drop shadow - offset: ({props.shadow_offset_x}, {props.shadow_offset_y}), blur: {props.shadow_blur}")
                
                # Create shadow by offsetting and blurring the text
                shadow_img = text_img.copy()
                
                # Apply shadow color to the text (keep alpha channel from original)
                shadow_pixels = list(shadow_img.getdata())
                shadow_color_rgb = tuple(int(c * 255) for c in props.shadow_color[:3])
                shadow_alpha_multiplier = props.shadow_color[3] if len(props.shadow_color) > 3 else 1.0
                
                new_shadow_pixels = []
                for pixel in shadow_pixels:
                    if len(pixel) >= 4:  # RGBA
                        r, g, b, a = pixel
                        if a > 0:  # Only modify non-transparent pixels
                            # Apply shadow color and alpha
                            new_a = int(a * shadow_alpha_multiplier)
                            new_shadow_pixels.append((*shadow_color_rgb, new_a))
                        else:
                            new_shadow_pixels.append(pixel)
                    else:  # RGB
                        new_shadow_pixels.append((*shadow_color_rgb, 255))
                
                shadow_img.putdata(new_shadow_pixels)
                
                # Apply blur if specified
                if props.shadow_blur > 0:
                    shadow_img = shadow_img.filter(ImageFilter.GaussianBlur(radius=props.shadow_blur))
                
                # Create offset shadow image
                offset_shadow_img = Image.new('RGBA', (width, height), (0, 0, 0, 0))
                
                # Calculate offset positions
                offset_x = int(props.shadow_offset_x)
                offset_y = int(props.shadow_offset_y)
                
                # Paste shadow at offset position
                try:
                    offset_shadow_img.paste(shadow_img, (offset_x, offset_y), shadow_img)
                except Exception as e:
                    print(f"DEBUG SHADOW: Error pasting shadow: {e}")
                    # Fallback to center if offset fails
                    offset_shadow_img.paste(shadow_img, (0, 0), shadow_img)
                
                # Composite shadow with shadow_glow_img
                shadow_glow_img = Image.alpha_composite(shadow_glow_img, offset_shadow_img)
                print(f"DEBUG SHADOW: Drop shadow applied successfully")
            
            if props.enable_glow:
                print(f"DEBUG GLOW: Creating glow effect - blur: {props.glow_blur}")
                
                # Create glow by blurring the text without offset
                glow_img = text_img.copy()
                
                # Apply glow color to the text
                glow_pixels = list(glow_img.getdata())
                glow_color_rgb = tuple(int(c * 255) for c in props.glow_color[:3])
                glow_alpha_multiplier = props.glow_color[3] if len(props.glow_color) > 3 else 1.0
                
                new_glow_pixels = []
                for pixel in glow_pixels:
                    if len(pixel) >= 4:  # RGBA
                        r, g, b, a = pixel
                        if a > 0:  # Only modify non-transparent pixels
                            # Apply glow color and alpha
                            new_a = int(a * glow_alpha_multiplier)
                            new_glow_pixels.append((*glow_color_rgb, new_a))
                        else:
                            new_glow_pixels.append(pixel)
                    else:  # RGB
                        new_glow_pixels.append((*glow_color_rgb, 255))
                
                glow_img.putdata(new_glow_pixels)
                
                # Apply blur for glow effect
                if props.glow_blur > 0:
                    glow_img = glow_img.filter(ImageFilter.GaussianBlur(radius=props.glow_blur))
                
                # Composite glow with shadow_glow_img
                shadow_glow_img = Image.alpha_composite(shadow_glow_img, glow_img)
                print(f"DEBUG GLOW: Glow effect applied successfully")
        
        # Add shadow/glow to elements list first (behind text, z-index -1)
        if shadow_glow_img:
            elements.append((-1, 'shadow_glow', shadow_glow_img, 0, 0))
            print(f"DEBUG SHADOW/GLOW: Added shadow/glow to elements list at z-index -1")
        
        # Add text to elements list
        elements.append((0, 'text', text_img, 0, 0))
        
        # Sort elements by z-index
        elements.sort(key=lambda x: x[0])
        
        # Composite all elements
        final_img = base_img.copy()
        for z_index, element_type, element_img, pos_x, pos_y in elements:
            if element_type == 'text':
                final_img = Image.alpha_composite(final_img, element_img)
            elif element_type == 'image':
                # Create a temporary image to paste the overlay
                temp_img = Image.new('RGBA', (width, height), (0, 0, 0, 0))
                temp_img.paste(element_img, (pos_x, pos_y), element_img)
                final_img = Image.alpha_composite(final_img, temp_img)
        
        print(f"Final composite image created with {len(elements)} elements")
        
        # Generate normal map if enabled
        normal_map_img = None
        if props.generate_normal_map:
            print(f"[Normal Map] Generating normal map with settings: strength={props.normal_map_strength}, blur={props.normal_map_blur_radius}, invert={props.normal_map_invert}")
            normal_map_img = generate_normal_map_from_alpha(
                final_img,
                strength=props.normal_map_strength,
                blur_radius=props.normal_map_blur_radius,
                invert=props.normal_map_invert
            )
            if normal_map_img:
                print(f"[Normal Map] Successfully generated normal map")
            else:
                print(f"[Normal Map] Failed to generate normal map")
        
        # Apply tiling if enabled
        if props.enable_tiling:
            final_img = apply_tiling(final_img, props)
        
        return final_img, normal_map_img
        
    except Exception as e:
        print(f"Error in generate_texture_image: {e}")
        import traceback
        traceback.print_exc()
        return None, None

def generate_preview(context):
    """Generate preview texture"""
    if not context or not hasattr(context, 'scene'):
        print("No context or scene")
        return None
    
    props = context.scene.text_texture_props
    
    # Always generate preview - removed live_preview_enabled check
    
    try:
        # Generate at full texture size for 100% quality preview
        preview_w = props.texture_width
        preview_h = props.texture_height
        
        print(f"[TTG] Generating 100% quality preview at full resolution: {preview_w}x{preview_h}")
        
        print(f"Starting preview generation at {preview_w}x{preview_h} (full resolution)")
        print(f"DEBUG: Texture size is {props.texture_width}x{props.texture_height}, preview at {preview_w}x{preview_h}")
        
        # Generate the images (diffuse and normal map)
        result = generate_texture_image(props, preview_w, preview_h)
        if not result:
            print("Failed to generate PIL image")
            return None
        
        # Extract diffuse image (and optionally normal map)
        if isinstance(result, tuple):
            img, normal_map_img = result
        else:
            # Backward compatibility in case function returns single image
            img = result
            normal_map_img = None
        
        if not img:
            print("Failed to generate diffuse image")
            return None
        
        # Apply preview background if needed
        if props.preview_bg_type != 'transparent':
            from PIL import Image, ImageDraw
            
            # Create background image
            bg_img = Image.new('RGBA', (preview_w, preview_h))
            draw = ImageDraw.Draw(bg_img)
            
            if props.preview_bg_type == 'color':
                # Solid color background
                bg_color = tuple(int(c * 255) for c in props.preview_bg_color[:3])
                bg_color += (int(props.preview_bg_color[3] * 255),)
                bg_img.paste(bg_color, (0, 0, preview_w, preview_h))
            elif props.preview_bg_type == 'gradient':
                # Gradient background
                color1 = tuple(int(c * 255) for c in props.preview_bg_color[:3])
                color2 = tuple(int(c * 255) for c in props.preview_bg_color2[:3])
                
                for y in range(preview_h):
                    ratio = y / preview_h
                    r = int(color1[0] * (1 - ratio) + color2[0] * ratio)
                    g = int(color1[1] * (1 - ratio) + color2[1] * ratio)
                    b = int(color1[2] * (1 - ratio) + color2[2] * ratio)
                    draw.rectangle([(0, y), (preview_w, y+1)], fill=(r, g, b, 255))
            else:  # transparent - create checkerboard
                # Checkerboard pattern for transparent
                checker_size = 16
                color1 = (128, 128, 128, 255)
                color2 = (192, 192, 192, 255)
                
                for y in range(0, preview_h, checker_size):
                    for x in range(0, preview_w, checker_size):
                        color = color1 if ((x // checker_size) + (y // checker_size)) % 2 == 0 else color2
                        draw.rectangle([(x, y), (min(x + checker_size, preview_w), min(y + checker_size, preview_h))],
                                     fill=color)
            
            # Composite the text image over the background
            bg_img.alpha_composite(img)
            img = bg_img
        
        # Name for preview image
        preview_img_name = "TextTexturePreview"
        
        # Remove old preview if it exists
        if preview_img_name in bpy.data.images:
            print("Removing old preview")
            old_img = bpy.data.images[preview_img_name]
            bpy.data.images.remove(old_img)
        
        # Create new Blender image for preview
        print("Creating new Blender image")
        blender_img = bpy.data.images.new(preview_img_name, preview_w, preview_h, alpha=True)
        
        # Convert PIL to Blender
        print("Converting pixels")
        pixels = []
        for y in range(preview_h):
            for x in range(preview_w):
                pixel = img.getpixel((x, preview_h - 1 - y))
                if len(pixel) == 4:
                    r, g, b, a = pixel
                else:
                    r, g, b = pixel
                    a = 255
                pixels.extend([r/255.0, g/255.0, b/255.0, a/255.0])
        
        print("Setting pixels")
        blender_img.pixels[:] = pixels
        
        # Store reference to the preview image only if not in draw context
        try:
            props.preview_image = blender_img
            print(f"Preview stored: {props.preview_image}")
        except AttributeError as attr_err:
            if "Writing to ID classes in this context is not allowed" in str(attr_err):
                print("Cannot set preview_image during draw - will be set later")
            else:
                raise
        
        # Force UI redraw
        for area in context.screen.areas:
            area.tag_redraw()
        
        print(f"Preview generated successfully: {blender_img.name}")
        return blender_img
        
    except Exception as e:
        print(f"Preview generation error: {e}")
        import traceback
        traceback.print_exc()
        try:
            props.preview_image = None
        except AttributeError as attr_err:
            if "Writing to ID classes in this context is not allowed" not in str(attr_err):
                raise
        return None

# Removed update_preview_enabled function - preview is always enabled

def update_live_preview(self, context):
    """Update preview when properties change"""
    if not hasattr(context, 'scene'):
        return
    
    props = context.scene.text_texture_props
    
    print(f"DEBUG: update_live_preview called, is_updating: {props.is_updating}")
    
    # Always update preview unless already updating
    if not props.is_updating:
        print("DEBUG: Triggering preview generation from property update")
        # Use a timer to debounce rapid property changes for better performance
        if hasattr(update_live_preview, '_timer'):
            bpy.app.timers.unregister(update_live_preview._timer)
        
        def delayed_preview():
            generate_preview(context)
            return None  # Stop timer
        
        update_live_preview._timer = delayed_preview
        bpy.app.timers.register(delayed_preview, first_interval=0.1)

# ============================================================================
# PROPERTY GROUPS
# ============================================================================

class TEXT_TEXTURE_ImageOverlay(PropertyGroup):
    """Single image overlay item"""
    name: StringProperty(
        name="Image Name",
        description="Name identifier for this image overlay",
        default="Overlay",
        update=update_live_preview
    )
    
    image_path: StringProperty(
        name="Image File",
        description="Path to image file",
        subtype='FILE_PATH',
        default="",
        update=update_live_preview
    )
    
    x_position: FloatProperty(
        name="X Position",
        description="X position (0.0 = left, 1.0 = right)",
        default=0.5,
        min=0.0,
        max=1.0,
        update=update_live_preview
    )
    
    y_position: FloatProperty(
        name="Y Position",
        description="Y position (0.0 = bottom, 1.0 = top)",
        default=0.5,
        min=0.0,
        max=1.0,
        update=update_live_preview
    )
    
    scale: FloatProperty(
        name="Scale",
        description="Scale of the image",
        default=1.0,
        min=0.1,
        max=5.0,
        update=update_live_preview
    )
    
    rotation: FloatProperty(
        name="Rotation",
        description="Rotation in degrees",
        default=0.0,
        min=-360.0,
        max=360.0,
        update=update_live_preview
    )
    
    z_index: IntProperty(
        name="Z-Index",
        description="Layer order (0=text level, -1=below text, 1+=above text)",
        default=1,
        min=-10,
        max=10,
        update=update_live_preview
    )
    
    positioning_mode: EnumProperty(
        name="Positioning Mode",
        description="How the image overlay is positioned relative to the text",
        items=[
            ('ABSOLUTE', "Absolute", "Position overlay at absolute coordinates (legacy mode)"),
            ('APPEND', "Append", "Position overlay after the text content"),
            ('PREPEND', "Prepend", "Position overlay before the text content")
        ],
        default='ABSOLUTE',
        update=update_live_preview
    )
    
    text_spacing: FloatProperty(
        name="Text Spacing",
        description="Spacing between text and image as percentage of font size (0-200)",
        default=50.0,
        min=0.0,
        max=200.0,
        update=update_live_preview
    )
    
    image_spacing: FloatProperty(
        name="Image Spacing",
        description="Spacing between consecutive images as percentage of font size (0-200)",
        default=25.0,
        min=0.0,
        max=200.0,
        update=update_live_preview
    )
    
    sequence_index: IntProperty(
        name="Sequence Index",
        description="Order index for positioning within append/prepend modes",
        default=0,
        min=0,
        max=99,
        update=update_live_preview
    )
    
    enabled: BoolProperty(
        name="Enabled",
        description="Enable this image overlay",
        default=True,
        update=update_live_preview
    )

class TEXT_TEXTURE_Preset(PropertyGroup):
    """Single preset item"""
    name: StringProperty(
        name="Preset Name",
        description="Name of the preset",
        default="New Preset"
    )
    
    source: EnumProperty(
        name="Source",
        description="Where this preset is stored",
        items=[
            ('BLEND_FILE', 'Blend File', 'Preset stored in the blend file (travels with the file)'),
            ('PERSISTENT_FILE', 'Persistent File', 'Preset stored in persistent location (survives addon updates)'),
            ('LOCAL_FILE', 'Local File', 'Preset stored locally (legacy system)')
        ],
        default='BLEND_FILE'
    )

class TEXT_TEXTURE_Properties(PropertyGroup):
    """Properties for text texture generation"""
    
    is_updating: BoolProperty(default=False)
    
    preview_image: PointerProperty(
        type=bpy.types.Image,
        name="Preview Image"
    )
    
    # Collapsible section expand/collapse states
    expand_multiline: BoolProperty(
        name="Multiline Text",
        description="Show/hide Multiline Text section",
        default=False
    )
    
    expand_image_overlays: BoolProperty(
        name="Image Overlays",
        description="Show/hide Image Overlays section",
        default=False
    )
    
    expand_positioning: BoolProperty(
        name="Positioning & Layout",
        description="Show/hide Positioning & Layout section",
        default=False
    )
    
    expand_font_settings: BoolProperty(
        name="Font Settings",
        description="Show/hide Font Settings section",
        default=False
    )
    
    expand_preview_options: BoolProperty(
        name="Preview Options",
        description="Show/hide Preview Options section",
        default=False
    )
    
    expand_presets: BoolProperty(
        name="Presets",
        description="Show/hide Presets section",
        default=False
    )
    
    expand_colors: BoolProperty(
        name="Colors",
        description="Show/hide Colors section",
        default=False
    )
    
    text: StringProperty(
        name="Text",
        description="Text to render as texture",
        default="",
        maxlen=1024,
        update=update_live_preview
    )
    
    # Prepend/Append Text Properties
    prepend_text: StringProperty(
        name="Prepend Text",
        description="Text to prepend before main text",
        default="",
        maxlen=512,
        update=update_live_preview
    )
    
    append_text: StringProperty(
        name="Append Text",
        description="Text to append after main text",
        default="",
        maxlen=512,
        update=update_live_preview
    )
    
    prepend_margin: FloatProperty(
        name="Prepend Margin",
        description="Margin between prepend text and main text as percentage of font size",
        default=50.0,
        min=0.0,
        max=200.0,
        precision=1,
        update=update_live_preview
    )
    
    append_margin: FloatProperty(
        name="Append Margin",
        description="Margin between main text and append text as percentage of font size",
        default=50.0,
        min=0.0,
        max=200.0,
        precision=1,
        update=update_live_preview
    )
    
    prepend_append_layout: EnumProperty(
        name="Layout",
        description="Layout arrangement for prepend/main/append text",
        items=[
            ('HORIZONTAL', 'Horizontal', 'Arrange text horizontally: [prepend] [main] [append]'),
            ('VERTICAL', 'Vertical', 'Arrange text vertically: prepend above, main center, append below'),
        ],
        default='HORIZONTAL',
        update=update_live_preview
    )
    
    texture_width: IntProperty(
        name="Width",
        description="Texture width in pixels",
        default=1024,
        min=64,
        max=4096,
        update=update_live_preview
    )
    
    texture_height: IntProperty(
        name="Height",
        description="Texture height in pixels",
        default=1024,
        min=64,
        max=4096,
        update=update_live_preview
    )
    
    font_size: IntProperty(
        name="Font Size",
        description="Font size in pixels",
        default=96,
        min=8,
        max=500,
        update=update_live_preview
    )
    
    padding: IntProperty(
        name="Padding",
        description="Padding around text in pixels",
        default=20,
        min=0,
        max=200,
        update=update_live_preview
    )
    
    text_color: FloatVectorProperty(
        name="Text Color",
        description="Color of the text",
        subtype='COLOR',
        default=(1.0, 1.0, 1.0, 1.0),
        size=4,
        min=0.0,
        max=1.0,
        update=update_live_preview
    )
    
    background_transparent: BoolProperty(
        name="Transparent Background",
        description="Use transparent background",
        default=True,
        update=update_live_preview
    )
    
    background_color: FloatVectorProperty(
        name="Background Color",
        description="Background color (when not transparent)",
        subtype='COLOR',
        default=(0.0, 0.0, 0.0, 1.0),
        size=4,
        min=0.0,
        max=1.0,
        update=update_live_preview
    )
    
    font_path: EnumProperty(
        name="Font",
        description="Font to use for text rendering",
        items=get_font_enum_items,
        update=update_live_preview
    )
    
    custom_font_path: StringProperty(
        name="Custom Font File",
        description="Path to custom font file (.ttf, .otf)",
        subtype='FILE_PATH',
        default="",
        update=update_live_preview
    )
    
    use_custom_font: BoolProperty(
        name="Use Custom Font",
        description="Use custom font file instead of system fonts",
        default=False,
        update=update_live_preview
    )
    
    text_align: EnumProperty(
        name="Alignment",
        description="Text alignment",
        items=[
            ('left', 'Left', 'Align text to the left'),
            ('center', 'Center', 'Center text'),
            ('right', 'Right', 'Align text to the right'),
        ],
        default='center',
        update=update_live_preview
    )
    
    # ============================================================================
    # ENHANCED POSITIONING & ALIGNMENT PROPERTIES
    # ============================================================================
    
    # 9-point anchor system
    anchor_point: EnumProperty(
        name="Anchor Point",
        description="Text anchor position on canvas",
        items=[
            ('TOP_LEFT', 'Top Left', 'Position text from top-left corner'),
            ('TOP_CENTER', 'Top Center', 'Position text from top center'),
            ('TOP_RIGHT', 'Top Right', 'Position text from top-right corner'),
            ('MIDDLE_LEFT', 'Middle Left', 'Position text from middle-left'),
            ('MIDDLE_CENTER', 'Middle Center', 'Position text from center (default)'),
            ('MIDDLE_RIGHT', 'Middle Right', 'Position text from middle-right'),
            ('BOTTOM_LEFT', 'Bottom Left', 'Position text from bottom-left corner'),
            ('BOTTOM_CENTER', 'Bottom Center', 'Position text from bottom center'),
            ('BOTTOM_RIGHT', 'Bottom Right', 'Position text from bottom-right corner'),
        ],
        default='MIDDLE_CENTER',
        update=update_live_preview
    )
    
    # Position mode toggle
    position_mode: EnumProperty(
        name="Position Mode",
        description="Choose between preset anchor positions or manual positioning",
        items=[
            ('PRESET', 'Preset', 'Use anchor points with margins and offsets'),
            ('MANUAL', 'Manual', 'Manually specify X,Y coordinates'),
        ],
        default='PRESET',
        update=update_live_preview
    )
    
    # Percentage-based margins (0-50%)
    margin_x: FloatProperty(
        name="X Margin",
        description="Horizontal margin from edge as percentage (0-50%)",
        subtype='PERCENTAGE',
        default=0.0,
        min=0.0,
        max=50.0,
        precision=1,
        update=update_live_preview
    )
    
    margin_y: FloatProperty(
        name="Y Margin",
        description="Vertical margin from edge as percentage (0-50%)",
        subtype='PERCENTAGE',
        default=0.0,
        min=0.0,
        max=50.0,
        precision=1,
        update=update_live_preview
    )
    
    # Link margins option
    margins_linked: BoolProperty(
        name="Link Margins",
        description="Keep X and Y margins synchronized",
        default=False,
        update=update_live_preview
    )
    
    # Pixel offset fine-tuning (-200 to +200px)
    offset_x: IntProperty(
        name="X Offset",
        description="Fine-tune X position in pixels (-200 to +200)",
        default=0,
        min=-200,
        max=200,
        update=update_live_preview
    )
    
    offset_y: IntProperty(
        name="Y Offset",
        description="Fine-tune Y position in pixels (-200 to +200)",
        default=0,
        min=-200,
        max=200,
        update=update_live_preview
    )
    
    # Manual positioning properties
    manual_position_x: FloatProperty(
        name="Manual X",
        description="Manual X position as percentage (0-100%)",
        subtype='PERCENTAGE',
        default=50.0,
        min=0.0,
        max=100.0,
        precision=1,
        update=update_live_preview
    )
    
    manual_position_y: FloatProperty(
        name="Manual Y",
        description="Manual Y position as percentage (0-100%)",
        subtype='PERCENTAGE',
        default=50.0,
        min=0.0,
        max=100.0,
        precision=1,
        update=update_live_preview
    )
    
    manual_position_unit: EnumProperty(
        name="Position Unit",
        description="Units for manual positioning",
        items=[
            ('PERCENTAGE', 'Percentage', 'Position as percentage of canvas'),
            ('PIXELS', 'Pixels', 'Position in absolute pixels'),
        ],
        default='PERCENTAGE',
        update=update_live_preview
    )
    
    # Canvas constraint option
    constrain_to_canvas: BoolProperty(
        name="Constrain to Canvas",
        description="Keep text within canvas bounds",
        default=True,
        update=update_live_preview
    )
    
    # Visual feedback options
    show_position_guides: BoolProperty(
        name="Show Position Guides",
        description="Display positioning guide lines in preview",
        default=False,
        update=update_live_preview
    )
    
    show_text_bounds: BoolProperty(
        name="Show Text Bounds",
        description="Display text bounding box in preview",
        default=False,
        update=update_live_preview
    )
    
    show_canvas_grid: BoolProperty(
        name="Show Canvas Grid",
        description="Display alignment grid overlay in preview",
        default=False,
        update=update_live_preview
    )
    
    grid_density: IntProperty(
        name="Grid Density",
        description="Number of grid divisions (4-16)",
        default=8,
        min=4,
        max=16,
        update=update_live_preview
    )
    
    # Removed live_preview_enabled - preview is always on
    
    preview_size: IntProperty(
        name="Preview Size",
        description="Size of the preview image (does not affect final output)",
        default=512,
        min=128,
        max=2048,
        update=update_live_preview
    )
    
    preview_zoom_level: FloatProperty(
        name="Zoom",
        description="Zoom level for preview",
        default=1.0,
        min=0.5,
        max=4.0,
        update=update_live_preview
    )
    
    # Preview background options
    preview_bg_type: EnumProperty(
        name="Preview Background",
        description="Background type for preview display",
        items=[
            ('transparent', 'Transparent', 'Transparent checkerboard pattern'),
            ('color', 'Solid Color', 'Use a solid color background'),
            ('gradient', 'Gradient', 'Use a gradient background'),
        ],
        default='transparent',
        update=update_live_preview
    )
    
    preview_bg_color: FloatVectorProperty(
        name="Preview BG Color",
        description="Background color for preview",
        subtype='COLOR',
        default=(0.2, 0.2, 0.2, 1.0),
        size=4,
        min=0.0,
        max=1.0,
        update=update_live_preview
    )
    
    preview_bg_color2: FloatVectorProperty(
        name="Preview BG Color 2",
        description="Second color for gradient background",
        subtype='COLOR',
        default=(0.4, 0.4, 0.4, 1.0),
        size=4,
        min=0.0,
        max=1.0,
        update=update_live_preview
    )
    
    # Image overlays
    image_overlays: CollectionProperty(type=TEXT_TEXTURE_ImageOverlay)
    active_overlay_index: IntProperty(default=0)
    
    # Presets
    presets: CollectionProperty(type=TEXT_TEXTURE_Preset)
    active_preset_index: IntProperty(default=0)
    
    new_preset_name: StringProperty(
        name="Preset Name",
        description="Name for new preset (Press Enter to save)",
        default="New Preset"
    )
    
    # Property to track when user wants to save with Enter
    preset_save_requested: BoolProperty(
        name="Save Requested",
        description="Internal flag for Enter key save requests",
        default=False
    )
    
    # ============================================================================
    # SHADER GENERATION PROPERTIES
    # ============================================================================
    
    expand_shader: BoolProperty(
        name="Shader Settings",
        description="Show/hide Shader Settings section",
        default=False
    )
    
    shader_type: EnumProperty(
        name="Shader Type",
        description="Type of shader to generate",
        items=[
            ('PRINCIPLED', 'Principled BSDF', 'Standard PBR shader with text texture'),
            ('EMISSION', 'Emission', 'Emissive shader for glowing text effects'),
            ('TRANSPARENT', 'Transparent', 'Transparent shader with alpha blending'),
        ],
        default='PRINCIPLED'
    )
    
    shader_connection: EnumProperty(
        name="Texture Connection",
        description="How to connect the texture to the shader",
        items=[
            ('BASE_COLOR', 'Base Color', 'Connect texture to base color input'),
            ('EMISSION', 'Emission', 'Connect texture to emission input'),
            ('ALPHA', 'Alpha', 'Connect texture alpha to transparency'),
        ],
        default='BASE_COLOR'
    )
    
    use_alpha: BoolProperty(
        name="Use Alpha Channel",
        description="Use texture alpha channel for transparency",
        default=True
    )
    
    emission_strength: FloatProperty(
        name="Emission Strength",
        description="Strength of emission shader",
        default=1.0,
        min=0.0,
        max=10.0
    )
    
    auto_assign_material: BoolProperty(
        name="Auto-Assign Material",
        description="Automatically assign generated material to active object",
        default=True
    )
    
    disable_shadows: BoolProperty(
        name="Disable Shadows",
        description="Make material not cast shadows using light path",
        default=False
    )
    
    disable_reflections: BoolProperty(
        name="Disable Reflections",
        description="Make material not appear in reflections using light path",
        default=False
    )
    
    disable_backfacing: BoolProperty(
        name="Disable Backfacing",
        description="Make material not render on backfacing geometry using light path",
        default=False
    )
    
    # ============================================================================
    # NORMAL MAP GENERATION PROPERTIES
    # ============================================================================
    
    expand_normal_maps: BoolProperty(
        name="Normal Maps",
        description="Show/hide Normal Maps section",
        default=False
    )
    
    generate_normal_map: BoolProperty(
        name="Generate Normal Map",
        description="Automatically generate normal map from text alpha channel",
        default=False,
        update=update_live_preview
    )
    
    normal_map_strength: FloatProperty(
        name="Normal Strength",
        description="Intensity of the normal map effect (higher = more pronounced)",
        default=1.0,
        min=0.1,
        max=5.0,
        precision=2,
        update=update_live_preview
    )
    
    normal_map_blur_radius: FloatProperty(
        name="Blur Radius",
        description="Blur radius for smoothing normal map (0 = no blur)",
        default=1.0,
        min=0.0,
        max=10.0,
        precision=1,
        update=update_live_preview
    )
    
    normal_map_invert: BoolProperty(
        name="Invert Normal Map",
        description="Invert the normal map for different height effects (emboss vs deboss)",
        default=False,
        update=update_live_preview
    )
    
    # ============================================================================
    # TEXT FITTING PROPERTIES
    # ============================================================================
    
    expand_text_fitting: BoolProperty(
        name="Text Fitting",
        description="Show/hide Text Fitting section",
        default=False
    )
    
    enable_text_fitting: BoolProperty(
        name="Enable Text Fitting",
        description="Automatically adjust text size to fit within the canvas dimensions",
        default=False,
        update=update_live_preview
    )
    
    text_fitting_mode: EnumProperty(
        name="Fitting Mode",
        description="How to fit text within the canvas",
        items=[
            ('FIT_WIDTH', 'Fit Width', 'Scale text to fit canvas width'),
            ('FIT_HEIGHT', 'Fit Height', 'Scale text to fit canvas height'),
            ('FIT_BOTH', 'Fit Both', 'Scale text to fit both width and height (may cause distortion)'),
            ('FIT_CONTAIN', 'Fit Contain', 'Scale text to fit entirely within canvas (preserves aspect ratio)'),
        ],
        default='FIT_CONTAIN',
        update=update_live_preview
    )
    
    text_fitting_margin: FloatProperty(
        name="Fitting Margin",
        description="Margin to leave around text when fitting (as percentage of canvas)",
        default=10.0,
        min=0.0,
        max=50.0,
        precision=1,
        subtype='PERCENTAGE',
        update=update_live_preview
    )
    
    min_font_size: IntProperty(
        name="Minimum Font Size",
        description="Minimum font size when fitting text (prevents text from becoming too small)",
        default=8,
        min=4,
        max=200,
        update=update_live_preview
    )
    
    max_font_size: IntProperty(
        name="Maximum Font Size",
        description="Maximum font size when fitting text (prevents text from becoming too large)",
        default=500,
        min=20,
        max=1000,
        update=update_live_preview
    )
    
    # ============================================================================
    # MULTILINE TEXT PROPERTIES
    # ============================================================================
    
    enable_multiline: BoolProperty(
        name="Enable Multiline",
        description="Enable multiline text rendering",
        default=False,
        update=update_live_preview
    )
    
    line_height: FloatProperty(
        name="Line Height",
        description="Line height multiplier (1.0 = normal, 1.5 = 150% spacing)",
        default=1.2,
        min=0.1,
        max=5.0,
        step=0.1,
        precision=2,
        update=update_live_preview
    )
    
    text_alignment: EnumProperty(
        name="Text Alignment",
        description="Text alignment for multiline text",
        items=[
            ('LEFT', "Left", "Left align text"),
            ('CENTER', "Center", "Center align text"),
            ('RIGHT', "Right", "Right align text")
        ],
        default='CENTER',
        update=update_live_preview
    )
    
    auto_wrap: BoolProperty(
        name="Auto Wrap",
        description="Automatically wrap text to fit texture width",
        default=False,
        update=update_live_preview
    )
    
    wrap_width: IntProperty(
        name="Wrap Width",
        description="Maximum width in pixels before text wraps (0 = use texture width)",
        default=0,
        min=0,
        max=10000,
        update=update_live_preview
    )
    
    max_lines: IntProperty(
        name="Max Lines",
        description="Maximum number of lines (0 = unlimited)",
        default=0,
        min=0,
        max=100,
        update=update_live_preview
    )
    
    line_spacing_pixels: IntProperty(
        name="Line Spacing (px)",
        description="Additional spacing between lines in pixels",
        default=0,
        min=-50,
        max=200,
        update=update_live_preview
    )
    
    # ============================================================================
    # STROKE SYSTEM PROPERTIES
    # ============================================================================
    
    expand_stroke: BoolProperty(
        name="Stroke & Outlines",
        description="Show/hide Stroke & Outlines section",
        default=False
    )
    
    enable_stroke: BoolProperty(
        name="Enable Stroke",
        description="Enable text stroke/outline rendering",
        default=False,
        update=update_live_preview
    )
    
    stroke_width: IntProperty(
        name="Stroke Width",
        description="Width of the text stroke in pixels",
        default=2,
        min=0,
        max=20,
        update=update_live_preview
    )
    
    stroke_position: EnumProperty(
        name="Stroke Position",
        description="How the stroke is positioned relative to the text",
        items=[
            ('OUTER', 'Outer', 'Stroke outside text (expands text bounds)'),
            ('INNER', 'Inner', 'Stroke inside text (shrinks text)'),
            ('CENTER', 'Center', 'Stroke centered on text edge'),
        ],
        default='OUTER',
        update=update_live_preview
    )
    
    stroke_color: FloatVectorProperty(
        name="Stroke Color",
        description="Color of the text stroke",
        subtype='COLOR',
        default=(0.0, 0.0, 0.0, 1.0),
        size=4,
        min=0.0,
        max=1.0,
        update=update_live_preview
    )
    
    # ============================================================================
    # SHADOW/GLOW SYSTEM PROPERTIES
    # ============================================================================
    
    expand_shadow_glow: BoolProperty(
        name="Shadow & Glow Effects",
        description="Show/hide Shadow & Glow Effects section",
        default=False
    )
    
    enable_shadow: BoolProperty(
        name="Enable Drop Shadow",
        description="Enable drop shadow effect",
        default=False,
        update=update_live_preview
    )
    
    shadow_offset_x: FloatProperty(
        name="Shadow Offset X",
        description="Horizontal offset for drop shadow",
        default=5.0,
        min=-100.0,
        max=100.0,
        step=0.1,
        precision=1,
        update=update_live_preview
    )
    
    shadow_offset_y: FloatProperty(
        name="Shadow Offset Y",
        description="Vertical offset for drop shadow",
        default=5.0,
        min=-100.0,
        max=100.0,
        step=0.1,
        precision=1,
        update=update_live_preview
    )
    
    shadow_blur: FloatProperty(
        name="Shadow Blur",
        description="Blur radius for drop shadow",
        default=5.0,
        min=0.0,
        max=50.0,
        step=0.1,
        precision=1,
        update=update_live_preview
    )
    
    shadow_color: FloatVectorProperty(
        name="Shadow Color",
        description="Color of drop shadow",
        subtype='COLOR',
        default=(0.0, 0.0, 0.0, 0.7),
        size=4,
        min=0.0,
        max=1.0,
        update=update_live_preview
    )
    
    enable_glow: BoolProperty(
        name="Enable Glow",
        description="Enable glow effect",
        default=False,
        update=update_live_preview
    )
    
    glow_blur: FloatProperty(
        name="Glow Blur",
        description="Blur radius for glow effect",
        default=10.0,
        min=0.0,
        max=50.0,
        step=0.1,
        precision=1,
        update=update_live_preview
    )
    
    glow_color: FloatVectorProperty(
        name="Glow Color",
        description="Color of glow effect",
        subtype='COLOR',
        default=(1.0, 1.0, 1.0, 0.8),
        size=4,
        min=0.0,
        max=1.0,
        update=update_live_preview
    )
    
    # ============================================================================
    # TILING SYSTEM PROPERTIES
    # ============================================================================
    
    expand_tiling: BoolProperty(
        name="Tiling System",
        description="Show/hide Tiling System section",
        default=False
    )
    
    enable_tiling: BoolProperty(
        name="Enable Tiling",
        description="Enable tiling to repeat text patterns across texture space",
        default=False,
        update=update_live_preview
    )
    
    tiling_mode: EnumProperty(
        name="Tiling Mode",
        description="How the texture should be tiled/repeated",
        items=[
            ('TILE', "Tile", "Simple repetition of the texture"),
            ('MIRROR', "Mirror", "Alternating normal/mirrored tiles"),
            ('REPEAT_X', "Repeat X", "Tile horizontally only"),
            ('REPEAT_Y', "Repeat Y", "Tile vertically only")
        ],
        default='TILE',
        update=update_live_preview
    )
    
    tile_count_x: IntProperty(
        name="Tile Count X",
        description="Number of horizontal tiles",
        default=2,
        min=1,
        max=10,
        update=update_live_preview
    )
    
    tile_count_y: IntProperty(
        name="Tile Count Y",
        description="Number of vertical tiles",
        default=2,
        min=1,
        max=10,
        update=update_live_preview
    )
    
    tile_spacing: IntProperty(
        name="Tile Spacing",
        description="Spacing between tiles in pixels",
        default=0,
        min=0,
        max=100,
        update=update_live_preview
    )
    
    expand_tiling_canvas: BoolProperty(
        name="Expand Canvas",
        description="Expand canvas to fit all tiles instead of scaling down",
        default=True,
        update=update_live_preview
    )
    
    # ============================================================================
    # BATCH PROCESSING PROPERTIES
    # ============================================================================
    
    expand_batch_processing: BoolProperty(
        name="Batch Processing",
        description="Show/hide Batch Processing section",
        default=False
    )
    
    batch_word_list: StringProperty(
        name="Word List",
        description="Comma-separated list of words/phrases for batch processing",
        default="",
        maxlen=8192
    )
    
    batch_output_folder: StringProperty(
        name="Output Folder",
        description="Folder path for batch output files",
        default="//batch_output/",
        subtype='DIR_PATH'
    )
    
    batch_filename_prefix: StringProperty(
        name="Filename Prefix",
        description="Prefix for batch output filenames",
        default="text_texture_"
    )
    
    batch_auto_save: BoolProperty(
        name="Auto Save",
        description="Automatically save each generated texture",
        default=True
    )
    
    batch_progress: IntProperty(
        name="Progress",
        description="Current batch processing progress",
        default=0,
        min=0,
        max=100,
        subtype='PERCENTAGE'
    )
    
    batch_template_name: StringProperty(
        name="Template Name",
        description="Name for saving/loading batch templates",
        default="batch_template"
    )

# ============================================================================
# OPERATORS
# ============================================================================

class TEXT_TEXTURE_OT_generate(Operator):
    """Generate final texture and pack it"""
    bl_idname = "text_texture.generate"
    bl_label = "Generate Text Texture"
    bl_description = "Generate final high-quality texture and pack it into the blend file"
    bl_options = {'REGISTER', 'UNDO'}
    
    def execute(self, context):
        try:
            props = context.scene.text_texture_props
            
            # IMPORTANT: Generate at EXACT user-specified dimensions
            final_width = props.texture_width
            final_height = props.texture_height
            
            print(f"Generating texture at {final_width}x{final_height}px")
            
            # Generate at FULL resolution specified by user
            result = generate_texture_image(props, final_width, final_height)
            if not result:
                self.report({'ERROR'}, "Failed to generate texture image")
                return {'CANCELLED'}
            
            # Extract diffuse and normal map images
            if isinstance(result, tuple):
                img, normal_map_img = result
            else:
                # Backward compatibility
                img = result
                normal_map_img = None
            
            if not img:
                self.report({'ERROR'}, "Failed to generate diffuse image")
                return {'CANCELLED'}
            
            # Create diffuse texture
            img_name = f"TextTexture_{props.text[:20].replace(' ', '_')}"
            
            # Remove existing diffuse texture
            if img_name in bpy.data.images:
                bpy.data.images.remove(bpy.data.images[img_name])
            
            # Create new diffuse image with USER SPECIFIED dimensions
            blender_img = bpy.data.images.new(img_name, final_width, final_height, alpha=True)
            
            # Convert PIL to Blender for diffuse texture
            pixels = []
            for y in range(final_height):
                for x in range(final_width):
                    r, g, b, a = img.getpixel((x, final_height - 1 - y))
                    pixels.extend([r/255.0, g/255.0, b/255.0, a/255.0])
            
            blender_img.pixels = pixels
            blender_img.pack()  # PACK FINAL IMAGE
            
            # Create normal map texture if enabled and generated
            normal_map_blender_img = None
            if normal_map_img and props.generate_normal_map:
                normal_map_name = f"TextTexture_Normal_{props.text[:20].replace(' ', '_')}"
                
                # Remove existing normal map texture
                if normal_map_name in bpy.data.images:
                    bpy.data.images.remove(bpy.data.images[normal_map_name])
                
                # Create new normal map image
                normal_map_blender_img = bpy.data.images.new(normal_map_name, final_width, final_height, alpha=False)
                normal_map_blender_img.colorspace_settings.name = 'Non-Color'  # Important for normal maps
                
                # Convert PIL to Blender for normal map
                normal_pixels = []
                for y in range(final_height):
                    for x in range(final_width):
                        r, g, b = normal_map_img.getpixel((x, final_height - 1 - y))
                        normal_pixels.extend([r/255.0, g/255.0, b/255.0, 1.0])  # Alpha always 1.0 for normal maps
                
                normal_map_blender_img.pixels = normal_pixels
                normal_map_blender_img.pack()  # PACK NORMAL MAP
                print(f"[Normal Map] Created and packed normal map texture: {normal_map_name}")
            
            # Add to shader
            if context.space_data and context.space_data.type == 'NODE_EDITOR':
                if context.space_data.tree_type == 'ShaderNodeTree':
                    material = context.space_data.id
                    if material and material.use_nodes:
                        nodes = material.node_tree.nodes
                        
                        # Add diffuse texture node
                        img_node = nodes.new(type='ShaderNodeTexImage')
                        img_node.image = blender_img
                        img_node.location = (0, 0)
                        img_node.select = True
                        nodes.active = img_node
                        
                        # Add normal map node if normal map was created
                        if normal_map_blender_img:
                            normal_node = nodes.new(type='ShaderNodeTexImage')
                            normal_node.image = normal_map_blender_img
                            normal_node.location = (0, -300)
                            normal_node.label = "Normal Map"
                            print(f"[Normal Map] Added normal map node to shader editor")
            
            # Provide comprehensive feedback
            if normal_map_blender_img:
                self.report({'INFO'}, f"Generated and packed: {img_name} + {normal_map_blender_img.name} ({final_width}x{final_height}px)")
            else:
                self.report({'INFO'}, f"Generated and packed: {img_name} ({final_width}x{final_height}px)")
            return {'FINISHED'}
            
        except Exception as e:
            self.report({'ERROR'}, f"Failed: {str(e)}")
            return {'CANCELLED'}

class TEXT_TEXTURE_OT_generate_shader(Operator):
    """Generate complete shader with text texture"""
    bl_idname = "text_texture.generate_shader"
    bl_label = "Generate Shader"
    bl_description = "Generate a complete material shader with the text texture"
    bl_options = {'REGISTER', 'UNDO'}
    
    def execute(self, context):
        try:
            props = context.scene.text_texture_props
            
            # Generate texture first
            final_width = props.texture_width
            final_height = props.texture_height
            
            print(f"Generating shader with texture at {final_width}x{final_height}px")
            
            result = generate_texture_image(props, final_width, final_height)
            if not result:
                self.report({'ERROR'}, "Failed to generate texture image")
                return {'CANCELLED'}
            
            # Extract diffuse and normal map images
            if isinstance(result, tuple):
                img, normal_map_img = result
            else:
                # Backward compatibility
                img = result
                normal_map_img = None
            
            if not img:
                self.report({'ERROR'}, "Failed to generate diffuse image")
                return {'CANCELLED'}
            
            # Create diffuse texture name
            img_name = f"TextTexture_{props.text[:20].replace(' ', '_')}"
            
            # Remove existing diffuse texture if it exists
            if img_name in bpy.data.images:
                bpy.data.images.remove(bpy.data.images[img_name])
            
            # Create new diffuse texture
            blender_img = bpy.data.images.new(img_name, final_width, final_height, alpha=True)
            
            # Convert PIL to Blender for diffuse texture
            pixels = []
            for y in range(final_height):
                for x in range(final_width):
                    r, g, b, a = img.getpixel((x, final_height - 1 - y))
                    pixels.extend([r/255.0, g/255.0, b/255.0, a/255.0])
            
            blender_img.pixels = pixels
            blender_img.pack()
            
            # Create normal map texture if enabled and generated
            normal_map_blender_img = None
            if normal_map_img and props.generate_normal_map:
                normal_map_name = f"TextTexture_Normal_{props.text[:20].replace(' ', '_')}"
                
                # Remove existing normal map texture
                if normal_map_name in bpy.data.images:
                    bpy.data.images.remove(bpy.data.images[normal_map_name])
                
                # Create new normal map image
                normal_map_blender_img = bpy.data.images.new(normal_map_name, final_width, final_height, alpha=False)
                normal_map_blender_img.colorspace_settings.name = 'Non-Color'  # Important for normal maps
                
                # Convert PIL to Blender for normal map
                normal_pixels = []
                for y in range(final_height):
                    for x in range(final_width):
                        r, g, b = normal_map_img.getpixel((x, final_height - 1 - y))
                        normal_pixels.extend([r/255.0, g/255.0, b/255.0, 1.0])  # Alpha always 1.0 for normal maps
                
                normal_map_blender_img.pixels = normal_pixels
                normal_map_blender_img.pack()
                print(f"[Normal Map] Created normal map texture for shader: {normal_map_name}")
            
            # Create or get material
            material_name = f"TextTexture_Mat_{props.text[:15].replace(' ', '_')}"
            
            if material_name in bpy.data.materials:
                mat = bpy.data.materials[material_name]
                # Clear existing nodes
                mat.node_tree.nodes.clear()
            else:
                mat = bpy.data.materials.new(name=material_name)
                mat.use_nodes = True
                mat.node_tree.nodes.clear()
            
            # Set up shader nodes
            nodes = mat.node_tree.nodes
            links = mat.node_tree.links
            
            # Create nodes
            output_node = nodes.new(type='ShaderNodeOutputMaterial')
            output_node.location = (400, 0)
            
            # Shader type based on settings
            if props.shader_type == 'PRINCIPLED':
                shader_node = nodes.new(type='ShaderNodeBsdfPrincipled')
                shader_node.location = (200, 0)
                
                # Create diffuse texture node
                tex_node = nodes.new(type='ShaderNodeTexImage')
                tex_node.image = blender_img
                tex_node.location = (-200, 0)
                
                # Connect texture to shader
                if props.shader_connection == 'BASE_COLOR':
                    links.new(tex_node.outputs['Color'], shader_node.inputs['Base Color'])
                elif props.shader_connection == 'EMISSION':
                    links.new(tex_node.outputs['Color'], shader_node.inputs['Emission Color'])
                    shader_node.inputs['Emission Strength'].default_value = props.emission_strength
                elif props.shader_connection == 'ALPHA':
                    links.new(tex_node.outputs['Alpha'], shader_node.inputs['Alpha'])
                    # Also connect color for visibility
                    links.new(tex_node.outputs['Color'], shader_node.inputs['Base Color'])
                    mat.blend_method = 'BLEND'
                
                # Connect alpha if enabled
                if props.use_alpha:
                    links.new(tex_node.outputs['Alpha'], shader_node.inputs['Alpha'])
                    mat.blend_method = 'BLEND'
                
                # Add normal map nodes if normal map was generated
                if normal_map_blender_img:
                    # Create normal map texture node
                    normal_tex_node = nodes.new(type='ShaderNodeTexImage')
                    normal_tex_node.image = normal_map_blender_img
                    normal_tex_node.location = (-200, -300)
                    normal_tex_node.label = "Normal Map"
                    
                    # Create normal map node
                    normal_map_node = nodes.new(type='ShaderNodeNormalMap')
                    normal_map_node.location = (0, -300)
                    
                    # Connect normal map texture to normal map node
                    links.new(normal_tex_node.outputs['Color'], normal_map_node.inputs['Color'])
                    
                    # Connect normal map node to shader
                    links.new(normal_map_node.outputs['Normal'], shader_node.inputs['Normal'])
                    
                    print(f"[Normal Map] Added normal map nodes to Principled BSDF shader")
                
                # Handle shadow/reflection disabling
                current_shader = shader_node
                final_x_location = 400
                
                # Disable shadows if enabled
                if props.disable_shadows:
                    # Create light path node
                    light_path_node = nodes.new(type='ShaderNodeLightPath')
                    light_path_node.location = (50, 150)
                    
                    # Create transparent BSDF
                    transparent_node = nodes.new(type='ShaderNodeBsdfTransparent')
                    transparent_node.location = (200, 150)
                    
                    # Create mix shader for shadows
                    shadow_mix_node = nodes.new(type='ShaderNodeMixShader')
                    shadow_mix_node.location = (350, 50)
                    
                    # Connect light path "Is Shadow Ray" to mix factor
                    links.new(light_path_node.outputs['Is Shadow Ray'], shadow_mix_node.inputs['Fac'])
                    
                    # Mix principled BSDF with transparent BSDF
                    links.new(current_shader.outputs['BSDF'], shadow_mix_node.inputs[1])  # Shader 1 (not shadow)
                    links.new(transparent_node.outputs['BSDF'], shadow_mix_node.inputs[2])  # Shader 2 (shadow)
                    
                    current_shader = shadow_mix_node
                    final_x_location = 500
                
                # Disable reflections if enabled
                if props.disable_reflections:
                    # Create or reuse light path node
                    if not props.disable_shadows:
                        light_path_node = nodes.new(type='ShaderNodeLightPath')
                        light_path_node.location = (50, 200)
                    
                    # Create transparent BSDF for reflections
                    reflection_transparent_node = nodes.new(type='ShaderNodeBsdfTransparent')
                    reflection_transparent_node.location = (200, 200)
                    
                    # Create mix shader for reflections
                    reflection_mix_node = nodes.new(type='ShaderNodeMixShader')
                    reflection_mix_node.location = (final_x_location - 50, 100)
                    
                    # Connect light path "Is Reflection Ray" to mix factor
                    links.new(light_path_node.outputs['Is Reflection Ray'], reflection_mix_node.inputs['Fac'])
                    
                    # Mix current shader with transparent BSDF
                    links.new(current_shader.outputs['Shader' if props.disable_shadows else 'BSDF'], reflection_mix_node.inputs[1])  # Shader 1 (not reflection)
                    links.new(reflection_transparent_node.outputs['BSDF'], reflection_mix_node.inputs[2])  # Shader 2 (reflection)
                    
                    current_shader = reflection_mix_node
                    final_x_location = final_x_location + 50
                
                # Disable backfacing if enabled
                if props.disable_backfacing:
                    # Create or reuse light path node
                    if not props.disable_shadows and not props.disable_reflections:
                        light_path_node = nodes.new(type='ShaderNodeLightPath')
                        light_path_node.location = (50, 250)
                    
                    # Create geometry node for backfacing detection
                    geometry_node = nodes.new(type='ShaderNodeNewGeometry')
                    geometry_node.location = (50, 300)
                    
                    # Create transparent BSDF for backfaces
                    backface_transparent_node = nodes.new(type='ShaderNodeBsdfTransparent')
                    backface_transparent_node.location = (200, 250)
                    
                    # Create mix shader for backfaces
                    backface_mix_node = nodes.new(type='ShaderNodeMixShader')
                    backface_mix_node.location = (final_x_location - 50, 150)
                    
                    # Connect geometry "Backfacing" to mix factor
                    links.new(geometry_node.outputs['Backfacing'], backface_mix_node.inputs['Fac'])
                    
                    # Mix current shader with transparent BSDF
                    current_output = 'Shader' if (props.disable_shadows or props.disable_reflections) else 'BSDF'
                    links.new(current_shader.outputs[current_output], backface_mix_node.inputs[1])  # Shader 1 (front face)
                    links.new(backface_transparent_node.outputs['BSDF'], backface_mix_node.inputs[2])  # Shader 2 (backface)
                    
                    current_shader = backface_mix_node
                    final_x_location = final_x_location + 50
                
                # Update output node position
                output_node.location = (final_x_location, 0)
                
                # Connect final shader to output
                output_socket = 'Shader' if (props.disable_shadows or props.disable_reflections) else 'BSDF'
                links.new(current_shader.outputs[output_socket], output_node.inputs['Surface'])
                
            elif props.shader_type == 'EMISSION':
                shader_node = nodes.new(type='ShaderNodeEmission')
                shader_node.location = (200, 0)
                
                # Create texture node
                tex_node = nodes.new(type='ShaderNodeTexImage')
                tex_node.image = blender_img
                tex_node.location = (-200, 0)
                
                # Connect texture to emission
                links.new(tex_node.outputs['Color'], shader_node.inputs['Color'])
                shader_node.inputs['Strength'].default_value = props.emission_strength
                
                # Handle shadow/reflection disabling
                current_shader = shader_node
                final_x_location = 400
                
                # Disable shadows if enabled
                if props.disable_shadows:
                    # Create light path node
                    light_path_node = nodes.new(type='ShaderNodeLightPath')
                    light_path_node.location = (50, 150)
                    
                    # Create transparent BSDF
                    transparent_node = nodes.new(type='ShaderNodeBsdfTransparent')
                    transparent_node.location = (200, 150)
                    
                    # Create mix shader for shadows
                    shadow_mix_node = nodes.new(type='ShaderNodeMixShader')
                    shadow_mix_node.location = (350, 50)
                    
                    # Connect light path "Is Shadow Ray" to mix factor
                    links.new(light_path_node.outputs['Is Shadow Ray'], shadow_mix_node.inputs['Fac'])
                    
                    # Mix emission with transparent BSDF
                    links.new(current_shader.outputs['Emission'], shadow_mix_node.inputs[1])  # Shader 1 (not shadow)
                    links.new(transparent_node.outputs['BSDF'], shadow_mix_node.inputs[2])  # Shader 2 (shadow)
                    
                    current_shader = shadow_mix_node
                    final_x_location = 500
                
                # Disable reflections if enabled
                if props.disable_reflections:
                    # Create or reuse light path node
                    if not props.disable_shadows:
                        light_path_node = nodes.new(type='ShaderNodeLightPath')
                        light_path_node.location = (50, 200)
                    
                    # Create transparent BSDF for reflections
                    reflection_transparent_node = nodes.new(type='ShaderNodeBsdfTransparent')
                    reflection_transparent_node.location = (200, 200)
                    
                    # Create mix shader for reflections
                    reflection_mix_node = nodes.new(type='ShaderNodeMixShader')
                    reflection_mix_node.location = (final_x_location - 50, 100)
                    
                    # Connect light path "Is Reflection Ray" to mix factor
                    links.new(light_path_node.outputs['Is Reflection Ray'], reflection_mix_node.inputs['Fac'])
                    
                    # Mix current shader with transparent BSDF
                    links.new(current_shader.outputs['Shader' if props.disable_shadows else 'Emission'], reflection_mix_node.inputs[1])  # Shader 1 (not reflection)
                    links.new(reflection_transparent_node.outputs['BSDF'], reflection_mix_node.inputs[2])  # Shader 2 (reflection)
                    
                    current_shader = reflection_mix_node
                    final_x_location = final_x_location + 50
                
                # Update output node position
                output_node.location = (final_x_location, 0)
                
                # Connect final shader to output
                output_socket = 'Shader' if (props.disable_shadows or props.disable_reflections) else 'Emission'
                links.new(current_shader.outputs[output_socket], output_node.inputs['Surface'])
                
            elif props.shader_type == 'TRANSPARENT':
                shader_node = nodes.new(type='ShaderNodeBsdfTransparent')
                mix_node = nodes.new(type='ShaderNodeMixShader')
                principled_node = nodes.new(type='ShaderNodeBsdfPrincipled')
                
                shader_node.location = (200, 100)
                mix_node.location = (350, 0)
                principled_node.location = (200, -100)
                
                # Create texture node
                tex_node = nodes.new(type='ShaderNodeTexImage')
                tex_node.image = blender_img
                tex_node.location = (-200, 0)
                
                # Connect texture
                links.new(tex_node.outputs['Color'], principled_node.inputs['Base Color'])
                links.new(tex_node.outputs['Alpha'], mix_node.inputs['Fac'])
                
                # Connect shaders to mix
                links.new(shader_node.outputs['BSDF'], mix_node.inputs[1])
                links.new(principled_node.outputs['BSDF'], mix_node.inputs[2])
                
                # Handle shadow/reflection disabling
                current_shader = mix_node
                final_x_location = 500
                
                # Disable shadows if enabled
                if props.disable_shadows:
                    # Create light path node
                    light_path_node = nodes.new(type='ShaderNodeLightPath')
                    light_path_node.location = (50, 200)
                    
                    # Create transparent BSDF for shadows (reuse existing transparent node)
                    shadow_transparent_node = nodes.new(type='ShaderNodeBsdfTransparent')
                    shadow_transparent_node.location = (350, 150)
                    
                    # Create mix shader for shadows
                    shadow_mix_node = nodes.new(type='ShaderNodeMixShader')
                    shadow_mix_node.location = (500, 50)
                    
                    # Connect light path "Is Shadow Ray" to mix factor
                    links.new(light_path_node.outputs['Is Shadow Ray'], shadow_mix_node.inputs['Fac'])
                    
                    # Mix current shader with transparent BSDF
                    links.new(current_shader.outputs['Shader'], shadow_mix_node.inputs[1])  # Shader 1 (not shadow)
                    links.new(shadow_transparent_node.outputs['BSDF'], shadow_mix_node.inputs[2])  # Shader 2 (shadow)
                    
                    current_shader = shadow_mix_node
                    final_x_location = 650
                
                # Disable reflections if enabled
                if props.disable_reflections:
                    # Create or reuse light path node
                    if not props.disable_shadows:
                        light_path_node = nodes.new(type='ShaderNodeLightPath')
                        light_path_node.location = (50, 250)
                    
                    # Create transparent BSDF for reflections
                    reflection_transparent_node = nodes.new(type='ShaderNodeBsdfTransparent')
                    reflection_transparent_node.location = (350, 200)
                    
                    # Create mix shader for reflections
                    reflection_mix_node = nodes.new(type='ShaderNodeMixShader')
                    reflection_mix_node.location = (final_x_location - 50, 100)
                    
                    # Connect light path "Is Reflection Ray" to mix factor
                    links.new(light_path_node.outputs['Is Reflection Ray'], reflection_mix_node.inputs['Fac'])
                    
                    # Mix current shader with transparent BSDF
                    links.new(current_shader.outputs['Shader'], reflection_mix_node.inputs[1])  # Shader 1 (not reflection)
                    links.new(reflection_transparent_node.outputs['BSDF'], reflection_mix_node.inputs[2])  # Shader 2 (reflection)
                    
                    current_shader = reflection_mix_node
                    final_x_location = final_x_location + 50
                
                # Disable backfacing if enabled
                if props.disable_backfacing:
                    # Create or reuse light path node
                    if not props.disable_shadows and not props.disable_reflections:
                        light_path_node = nodes.new(type='ShaderNodeLightPath')
                        light_path_node.location = (50, 300)
                    
                    # Create geometry node for backfacing detection
                    geometry_node = nodes.new(type='ShaderNodeNewGeometry')
                    geometry_node.location = (50, 350)
                    
                    # Create transparent BSDF for backfaces
                    backface_transparent_node = nodes.new(type='ShaderNodeBsdfTransparent')
                    backface_transparent_node.location = (350, 300)
                    
                    # Create mix shader for backfaces
                    backface_mix_node = nodes.new(type='ShaderNodeMixShader')
                    backface_mix_node.location = (final_x_location + 50, 150)
                    
                    # Connect geometry "Backfacing" to mix factor
                    links.new(geometry_node.outputs['Backfacing'], backface_mix_node.inputs['Fac'])
                    
                    # Mix current shader with transparent BSDF
                    links.new(current_shader.outputs['Shader'], backface_mix_node.inputs[1])  # Shader 1 (front face)
                    links.new(backface_transparent_node.outputs['BSDF'], backface_mix_node.inputs[2])  # Shader 2 (backface)
                    
                    current_shader = backface_mix_node
                    final_x_location = final_x_location + 150
                
                # Update output node position
                output_node.location = (final_x_location, 0)
                
                # Connect final shader to output
                links.new(current_shader.outputs['Shader'], output_node.inputs['Surface'])
                
                mat.blend_method = 'BLEND'
            
            # Assign material to active object if enabled and possible
            if props.auto_assign_material and context.active_object and context.active_object.type in ['MESH', 'CURVE']:
                obj = context.active_object
                # Always create a new material slot
                obj.data.materials.append(mat)
                
                self.report({'INFO'}, f"Generated shader '{material_name}' and assigned to {obj.name}")
            else:
                self.report({'INFO'}, f"Generated shader '{material_name}' (assign manually to object)")
            
            # Switch to Shader Editor if available and set material
            for area in context.screen.areas:
                if area.type == 'NODE_EDITOR':
                    for space in area.spaces:
                        if space.type == 'NODE_EDITOR':
                            space.shader_type = 'OBJECT'
                            space.id = mat
                            area.tag_redraw()
                            break
            
            return {'FINISHED'}
            
        except Exception as e:
            self.report({'ERROR'}, f"Shader generation failed: {str(e)}")
            import traceback
            traceback.print_exc()
            return {'CANCELLED'}

class TEXT_TEXTURE_OT_refresh_preview(Operator):
    """Force refresh the preview"""
    bl_idname = "text_texture.refresh_preview"
    bl_label = "Refresh Preview"
    bl_description = "Force refresh the preview image"
    bl_options = {'REGISTER', 'UNDO'}
    
    def execute(self, context):
        props = context.scene.text_texture_props
        
        # Clear existing preview first
        props.preview_image = None
        
        # Generate new preview
        result = generate_preview(context)
        
        if result:
            self.report({'INFO'}, f"Preview refreshed at {props.preview_size}px")
        else:
            self.report({'ERROR'}, "Failed to generate preview - check console for details")
        
        # Force UI update
        for area in context.screen.areas:
            area.tag_redraw()
        
        return {'FINISHED'}

class TEXT_TEXTURE_OT_view_preview_zoom(Operator):
    """Open preview in new Image Editor window with zoom support"""
    bl_idname = "text_texture.view_preview_zoom"
    bl_label = "Enable Zoom"
    bl_description = "Open preview in new Image Editor window with zoom and pan controls"
    bl_options = {'REGISTER', 'UNDO'}
    
    def execute(self, context):
        props = context.scene.text_texture_props
        
        # Generate preview image when button is clicked
        print("[TTG] Generating preview on button click...")
        result = generate_preview(context)
        
        if not result:
            self.report({'ERROR'}, "Failed to generate preview")
            return {'CANCELLED'}
        
        if not props.preview_image:
            self.report({'ERROR'}, "No preview image to display")
            return {'CANCELLED'}
        
        # Open a new window with Image Editor
        print(f"[ZOOM] Opening new window for preview image: {props.preview_image.name}")
        
        try:
            # Create a new window
            bpy.ops.screen.userpref_show('INVOKE_DEFAULT')
            
            # Wait a moment and then change the newly opened window to Image Editor
            def setup_image_window():
                # Find the newest window (should be the one we just opened)
                newest_window = None
                for window in bpy.context.window_manager.windows:
                    if window.screen.name.startswith("temp"):
                        newest_window = window
                        break
                
                if not newest_window:
                    # Fallback: use any non-main window
                    for window in bpy.context.window_manager.windows:
                        if window != bpy.context.window:
                            newest_window = window
                            break
                
                if newest_window:
                    # Set the area type to Image Editor
                    for area in newest_window.screen.areas:
                        if area.type == 'PREFERENCES':
                            area.type = 'IMAGE_EDITOR'
                            print("[ZOOM] Changed area to Image Editor")
                            
                            # Set the preview image
                            for space in area.spaces:
                                if space.type == 'IMAGE_EDITOR':
                                    space.image = props.preview_image
                                    print(f"[ZOOM] Set preview image: {props.preview_image.name}")
                                    
                                    # Set up proper viewing
                                    override = {
                                        'window': newest_window,
                                        'screen': newest_window.screen,
                                        'area': area,
                                        'region': area.regions[0] if area.regions else None
                                    }
                                    
                                    with bpy.context.temp_override(**override):
                                        try:
                                            bpy.ops.image.view_all(fit_view=True)
                                            print("[ZOOM] Applied fit to view")
                                        except Exception as e:
                                            print(f"[ZOOM] Error applying fit to view: {e}")
                                    
                                    break
                            break
                    
                    self.report({'INFO'}, "✅ NEW WINDOW OPENED! Use mouse wheel to zoom, middle-mouse to pan")
                    print("[ZOOM] New window zoom feature activated successfully")
                else:
                    print("[ZOOM] Could not find new window")
                    self.report({'ERROR'}, "Failed to setup new window")
                
                return None  # Stop timer
            
            # Use a timer to set up the window after it opens
            bpy.app.timers.register(setup_image_window, first_interval=0.1)
            
        except Exception as e:
            print(f"[ZOOM] Error creating new window: {e}")
            # Fallback to old behavior if new window creation fails
            self.report({'WARNING'}, "New window failed, trying area split instead")
            
            # Find any area to split
            for area in context.screen.areas:
                if area.type in ['NODE_EDITOR', 'VIEW_3D', 'PROPERTIES']:
                    print(f"[ZOOM] Fallback: Splitting {area.type} to create Image Editor")
                    
                    # Split horizontally to create a new area
                    with context.temp_override(area=area):
                        bpy.ops.screen.area_split(direction='HORIZONTAL', factor=0.6)
                    
                    # Find the newly created area
                    for new_area in context.screen.areas:
                        if new_area != area and new_area.y != area.y:
                            new_area.type = 'IMAGE_EDITOR'
                            
                            # Set the image in the Image Editor
                            for space in new_area.spaces:
                                if space.type == 'IMAGE_EDITOR':
                                    space.image = props.preview_image
                                    print("[ZOOM] Fallback: Image set in split Image Editor")
                                    break
                            
                            # Tag for redraw and fit to view
                            new_area.tag_redraw()
                            override = context.copy()
                            override['area'] = new_area
                            override['region'] = new_area.regions[0]
                            
                            with context.temp_override(**override):
                                try:
                                    bpy.ops.image.view_all(fit_view=True)
                                    print("[ZOOM] Fallback: Fit to view applied")
                                except Exception as e:
                                    print(f"[ZOOM] Fallback: Error applying fit to view: {e}")
                            
                            self.report({'INFO'}, "✅ ZOOM ENABLED! Use mouse wheel to zoom, middle-mouse to pan")
                            return {'FINISHED'}
                    break
            
            self.report({'ERROR'}, "Failed to create Image Editor")
            return {'CANCELLED'}
        
        return {'FINISHED'}


class TEXT_TEXTURE_OT_save_preset(Operator):
    """Save current settings as preset"""
    bl_idname = "text_texture.save_preset"
    bl_label = "Save Preset"
    bl_description = "Save current settings as preset"
    bl_options = {'REGISTER', 'UNDO'}
    
    overwrite: bpy.props.BoolProperty(
        name="Overwrite",
        description="Overwrite existing preset",
        default=False
    )
    
    preset_name: bpy.props.StringProperty(
        name="Preset Name",
        description="Name of preset to save/overwrite",
        default=""
    )
    
    def execute(self, context):
        props = context.scene.text_texture_props
        
        # Use provided preset name or fallback to input field
        preset_name = self.preset_name.strip() if self.preset_name else props.new_preset_name.strip()
        if not preset_name:
            self.report({'ERROR'}, "Please enter a preset name")
            return {'CANCELLED'}
        
        # When overwriting from button, update the input field for consistency
        if self.preset_name and self.preset_name.strip():
            props.new_preset_name = preset_name
        
        # Validate preset name characters (avoid filesystem issues)
        import re
        # Allow Unicode letters, numbers, spaces, hyphens, underscores, and common accented characters
        # This pattern supports international characters like ö, ü, ñ, etc.
        if not re.match(r'^[\w\s\-]+$', preset_name, re.UNICODE):
            self.report({'ERROR'}, "Preset name contains invalid characters for filesystem compatibility")
            return {'CANCELLED'}
        
        # Additional check for filesystem-unsafe characters
        invalid_chars = ['<', '>', ':', '"', '|', '?', '*', '/', '\\']
        if any(char in preset_name for char in invalid_chars):
            self.report({'ERROR'}, f"Preset name cannot contain: {' '.join(invalid_chars)}")
            return {'CANCELLED'}
        
        # Check for duplicate names
        preset_file = os.path.join(get_preset_path(), f"{preset_name}.json")
        if os.path.exists(preset_file) and not self.overwrite:
            # Suggest alternative names
            base_name = preset_name
            counter = 1
            suggested_name = f"{base_name}_{counter}"
            while os.path.exists(os.path.join(get_preset_path(), f"{suggested_name}.json")):
                counter += 1
                suggested_name = f"{base_name}_{counter}"
            
            self.report({'ERROR'}, f"Preset '{preset_name}' already exists. Suggestion: try '{suggested_name}' or delete the existing preset first.")
            return {'CANCELLED'}
        
        # DETAILED LOGGING: Current shader properties before saving
        print(f"[PRESET SAVE DEBUG] ==================== SAVING PRESET: '{preset_name}' ====================")
        print(f"[PRESET SAVE DEBUG] Current shader properties from props:")
        print(f"[PRESET SAVE DEBUG]   disable_shadows: {props.disable_shadows}")
        print(f"[PRESET SAVE DEBUG]   disable_reflections: {props.disable_reflections}")
        print(f"[PRESET SAVE DEBUG]   disable_backfacing: {props.disable_backfacing}")
        print(f"[PRESET SAVE DEBUG]   shader_type: {props.shader_type}")
        print(f"[PRESET SAVE DEBUG]   shader_connection: {props.shader_connection}")
        print(f"[PRESET SAVE DEBUG]   use_alpha: {props.use_alpha}")
        print(f"[PRESET SAVE DEBUG]   emission_strength: {props.emission_strength}")
        print(f"[PRESET SAVE DEBUG]   auto_assign_material: {props.auto_assign_material}")
        
        preset_data = {
            'text': props.text,
            'texture_width': props.texture_width,
            'texture_height': props.texture_height,
            'font_size': props.font_size,
            'padding': props.padding,
            'text_color': list(props.text_color),
            'background_transparent': props.background_transparent,
            'background_color': list(props.background_color),
            'font_path': props.font_path,
            'custom_font_path': props.custom_font_path,
            'use_custom_font': props.use_custom_font,
            'text_align': props.text_align,
            'prepend_text': props.prepend_text,
            'append_text': props.append_text,
            'prepend_margin': props.prepend_margin,
            'append_margin': props.append_margin,
            'prepend_append_layout': props.prepend_append_layout,
            'shader_type': props.shader_type,
            'shader_connection': props.shader_connection,
            'use_alpha': props.use_alpha,
            'emission_strength': props.emission_strength,
            'auto_assign_material': props.auto_assign_material,
            'disable_shadows': props.disable_shadows,
            'disable_reflections': props.disable_reflections,
            'disable_backfacing': props.disable_backfacing,
            'generate_normal_map': props.generate_normal_map,
            'normal_map_strength': props.normal_map_strength,
            'normal_map_blur_radius': props.normal_map_blur_radius,
            'normal_map_invert': props.normal_map_invert,
            'enable_multiline': props.enable_multiline,
            'line_height': props.line_height,
            'text_alignment': props.text_alignment,
            'auto_wrap': props.auto_wrap,
            'wrap_width': props.wrap_width,
            'max_lines': props.max_lines,
            'line_spacing_pixels': props.line_spacing_pixels,
            'enable_stroke': props.enable_stroke,
            'stroke_width': props.stroke_width,
            'stroke_position': props.stroke_position,
            'stroke_color': list(props.stroke_color),
            'enable_shadow': props.enable_shadow,
            'shadow_offset_x': props.shadow_offset_x,
            'shadow_offset_y': props.shadow_offset_y,
            'shadow_blur': props.shadow_blur,
            'shadow_color': list(props.shadow_color),
            'enable_glow': props.enable_glow,
            'glow_blur': props.glow_blur,
            'glow_color': list(props.glow_color),
            'enable_tiling': props.enable_tiling,
            'tiling_mode': props.tiling_mode,
            'tile_count_x': props.tile_count_x,
            'tile_count_y': props.tile_count_y,
            'tile_spacing': props.tile_spacing,
            'expand_tiling_canvas': props.expand_tiling_canvas,
            'image_overlays': []
        }
        
        # DETAILED LOGGING: Verify shader properties in preset_data
        print(f"[PRESET SAVE DEBUG] Shader properties in preset_data dictionary:")
        print(f"[PRESET SAVE DEBUG]   disable_shadows: {preset_data.get('disable_shadows', 'NOT FOUND')}")
        print(f"[PRESET SAVE DEBUG]   disable_reflections: {preset_data.get('disable_reflections', 'NOT FOUND')}")
        print(f"[PRESET SAVE DEBUG]   disable_backfacing: {preset_data.get('disable_backfacing', 'NOT FOUND')}")
        
        # Save overlay data
        for overlay in props.image_overlays:
            overlay_data = {
                'name': overlay.name,
                'image_path': overlay.image_path,
                'x_position': overlay.x_position,
                'y_position': overlay.y_position,
                'scale': overlay.scale,
                'rotation': overlay.rotation,
                'z_index': overlay.z_index,
                'enabled': overlay.enabled
            }
            preset_data['image_overlays'].append(overlay_data)
        
        try:
            # Primary: Save to blend file (new system)
            print(f"[PRESET SAVE DEBUG] Attempting to save to blend file...")
            blend_success = save_blend_file_preset(preset_name, preset_data)
            print(f"[PRESET SAVE DEBUG] Blend file save result: {blend_success}")
            
            # ALWAYS save to persistent storage for cross-blend-file sharing and addon update survival
            persistent_file = os.path.join(get_persistent_preset_path(), f"{preset_name}.json")
            print(f"[PRESET SAVE DEBUG] Attempting to save to persistent file: {persistent_file}")
            persistent_success = True
            try:
                with open(persistent_file, 'w') as f:
                    json.dump(preset_data, f, indent=2)
                print(f"[PRESET SAVE DEBUG] Successfully saved preset '{preset_name}' to persistent storage")
                
                # VERIFY: Read back the persistent file to confirm shader properties were saved
                print(f"[PRESET SAVE DEBUG] Verifying persistent file contents...")
                with open(persistent_file, 'r') as f:
                    saved_data = json.load(f)
                print(f"[PRESET SAVE DEBUG] Persistent file shader properties verification:")
                print(f"[PRESET SAVE DEBUG]   disable_shadows: {saved_data.get('disable_shadows', 'NOT FOUND')}")
                print(f"[PRESET SAVE DEBUG]   disable_reflections: {saved_data.get('disable_reflections', 'NOT FOUND')}")
                print(f"[PRESET SAVE DEBUG]   disable_backfacing: {saved_data.get('disable_backfacing', 'NOT FOUND')}")
                
            except Exception as persistent_error:
                print(f"[PRESET SAVE DEBUG] Warning: Failed to save persistent preset file: {persistent_error}")
                persistent_success = False
            
            # Also save to legacy location for backward compatibility (optional)
            legacy_success = True
            try:
                legacy_file = os.path.join(get_preset_path(), f"{preset_name}.json")
                with open(legacy_file, 'w') as f:
                    json.dump(preset_data, f, indent=2)
                print(f"[PRESET SAVE DEBUG] Also saved to legacy location for backward compatibility")
            except Exception as legacy_error:
                print(f"[PRESET SAVE DEBUG] Note: Could not save to legacy location: {legacy_error}")
                legacy_success = False
            
            if not blend_success and not persistent_success:
                self.report({'ERROR'}, f"Failed to save preset to both blend file and persistent storage")
                return {'CANCELLED'}
            
            # Check if preset already exists in the collection and update it
            existing_preset = None
            for preset in props.presets:
                if preset.name == preset_name:
                    existing_preset = preset
                    break
            
            if not existing_preset:
                preset = props.presets.add()
                preset.name = preset_name
                # Priority: Blend file > Persistent file
                preset.source = 'BLEND_FILE' if blend_success else 'PERSISTENT_FILE'
            else:
                # Update source based on where it was successfully saved
                existing_preset.source = 'BLEND_FILE' if blend_success else 'PERSISTENT_FILE'
            
            # Don't clear the input field - keep the name for easy re-saving
            # props.new_preset_name = "New Preset"
            
            # Provide clear feedback
            storage_info = ""
            if blend_success and persistent_success:
                storage_info = " (saved to blend file + persistent storage)"
            elif blend_success:
                storage_info = " (saved to blend file)"
            elif persistent_success:
                storage_info = " (saved to persistent storage)"
            
            if self.overwrite:
                self.report({'INFO'}, f"✅ Preset '{preset_name}' updated successfully{storage_info}")
            else:
                self.report({'INFO'}, f"✅ Preset '{preset_name}' saved successfully{storage_info}")
            
        except (OSError, IOError, json.JSONEncodeError) as e:
            self.report({'ERROR'}, f"Failed to save preset: {str(e)}")
            return {'CANCELLED'}
        except Exception as e:
            self.report({'ERROR'}, f"Unexpected error saving preset: {str(e)}")
            return {'CANCELLED'}
        
        return {'FINISHED'}
    
    def invoke(self, context, event):
        # Check if preset exists and prompt for overwrite
        props = context.scene.text_texture_props
        preset_name = props.new_preset_name.strip()
        
        if preset_name and preset_name != "New Preset":
            preset_file = os.path.join(get_preset_path(), f"{preset_name}.json")
            if os.path.exists(preset_file):
                return context.window_manager.invoke_confirm(self, event)
        
        return self.execute(context)
    
    def draw(self, context):
        layout = self.layout
        props = context.scene.text_texture_props
        preset_name = props.new_preset_name.strip()
        layout.label(text=f"Overwrite existing preset '{preset_name}'?")

class TEXT_TEXTURE_OT_save_preset_enter(Operator):
    """Save preset when Enter key is pressed in name field"""
    bl_idname = "text_texture.save_preset_enter"
    bl_label = "Save Preset (Enter)"
    bl_description = "Save current settings as preset when Enter is pressed"
    bl_options = {'REGISTER', 'UNDO'}
    
    def execute(self, context):
        # Delegate to the main save preset operator
        return bpy.ops.text_texture.save_preset('INVOKE_DEFAULT')

class TEXT_TEXTURE_OT_load_preset(Operator):
    """Load preset"""
    bl_idname = "text_texture.load_preset"
    bl_label = "Load Preset"
    bl_description = "Load selected preset"
    bl_options = {'REGISTER', 'UNDO'}
    
    preset_name: StringProperty()
    
    def execute(self, context):
        props = context.scene.text_texture_props
        
        # DETAILED LOGGING: Current shader properties before loading
        print(f"[PRESET LOAD DEBUG] ==================== LOADING PRESET: '{self.preset_name}' ====================")
        print(f"[PRESET LOAD DEBUG] Current shader properties BEFORE loading:")
        print(f"[PRESET LOAD DEBUG]   disable_shadows: {props.disable_shadows}")
        print(f"[PRESET LOAD DEBUG]   disable_reflections: {props.disable_reflections}")
        print(f"[PRESET LOAD DEBUG]   disable_backfacing: {props.disable_backfacing}")
        print(f"[PRESET LOAD DEBUG]   shader_type: {props.shader_type}")
        print(f"[PRESET LOAD DEBUG]   shader_connection: {props.shader_connection}")
        
        try:
            # Three-tier loading priority: Blend File > Persistent File > Legacy File
            blend_presets = get_blend_file_presets()
            preset_data = None
            source = 'BLEND_FILE'
            
            print(f"[PRESET LOAD DEBUG] Available blend file presets: {list(blend_presets.keys())}")
            
            if self.preset_name in blend_presets:
                preset_data = blend_presets[self.preset_name]
                source = 'BLEND_FILE'
                print(f"[PRESET LOAD DEBUG] Loading preset '{self.preset_name}' from blend file")
                print(f"[PRESET LOAD DEBUG] Blend file preset shader properties:")
                print(f"[PRESET LOAD DEBUG]   disable_shadows: {preset_data.get('disable_shadows', 'NOT FOUND')}")
                print(f"[PRESET LOAD DEBUG]   disable_reflections: {preset_data.get('disable_reflections', 'NOT FOUND')}")
                print(f"[PRESET LOAD DEBUG]   disable_backfacing: {preset_data.get('disable_backfacing', 'NOT FOUND')}")
            else:
                # Try persistent storage next
                persistent_file = os.path.join(get_persistent_preset_path(), f"{self.preset_name}.json")
                print(f"[PRESET LOAD DEBUG] Preset not in blend file, trying persistent file: {persistent_file}")
                if os.path.exists(persistent_file):
                    with open(persistent_file, 'r') as f:
                        preset_data = json.load(f)
                    source = 'PERSISTENT_FILE'
                    print(f"[PRESET LOAD DEBUG] Loading preset '{self.preset_name}' from persistent file")
                    print(f"[PRESET LOAD DEBUG] Persistent file preset shader properties:")
                    print(f"[PRESET LOAD DEBUG]   disable_shadows: {preset_data.get('disable_shadows', 'NOT FOUND')}")
                    print(f"[PRESET LOAD DEBUG]   disable_reflections: {preset_data.get('disable_reflections', 'NOT FOUND')}")
                    print(f"[PRESET LOAD DEBUG]   disable_backfacing: {preset_data.get('disable_backfacing', 'NOT FOUND')}")
                else:
                    # Fallback to legacy local file
                    legacy_file = os.path.join(get_preset_path(), f"{self.preset_name}.json")
                    print(f"[PRESET LOAD DEBUG] Preset not in persistent storage, trying legacy file: {legacy_file}")
                    if os.path.exists(legacy_file):
                        with open(legacy_file, 'r') as f:
                            preset_data = json.load(f)
                        source = 'LOCAL_FILE'
                        print(f"[PRESET LOAD DEBUG] Loading preset '{self.preset_name}' from legacy file")
                        print(f"[PRESET LOAD DEBUG] Legacy file preset shader properties:")
                        print(f"[PRESET LOAD DEBUG]   disable_shadows: {preset_data.get('disable_shadows', 'NOT FOUND')}")
                        print(f"[PRESET LOAD DEBUG]   disable_reflections: {preset_data.get('disable_reflections', 'NOT FOUND')}")
                        print(f"[PRESET LOAD DEBUG]   disable_backfacing: {preset_data.get('disable_backfacing', 'NOT FOUND')}")
                    else:
                        print(f"[PRESET LOAD DEBUG] Legacy file does not exist: {legacy_file}")
                        self.report({'ERROR'}, f"Preset not found in any storage location: {self.preset_name}")
                        return {'CANCELLED'}
            
            if not preset_data:
                print(f"[PRESET LOAD DEBUG] No preset data found!")
                self.report({'ERROR'}, f"No data found for preset: {self.preset_name}")
                return {'CANCELLED'}
            
            props.is_updating = True
            
            # Skip loading text to prevent overwriting current text
            # props.text = preset_data.get('text', props.text)
            props.texture_width = preset_data.get('texture_width', props.texture_width)
            props.texture_height = preset_data.get('texture_height', props.texture_height)
            props.font_size = preset_data.get('font_size', props.font_size)
            props.padding = preset_data.get('padding', props.padding)
            props.text_color = preset_data.get('text_color', props.text_color)
            props.background_transparent = preset_data.get('background_transparent', props.background_transparent)
            props.background_color = preset_data.get('background_color', props.background_color)
            props.font_path = preset_data.get('font_path', props.font_path)
            props.custom_font_path = preset_data.get('custom_font_path', props.custom_font_path)
            props.use_custom_font = preset_data.get('use_custom_font', props.use_custom_font)
            props.text_align = preset_data.get('text_align', props.text_align)
            props.prepend_text = preset_data.get('prepend_text', props.prepend_text)
            props.append_text = preset_data.get('append_text', props.append_text)
            props.prepend_margin = preset_data.get('prepend_margin', props.prepend_margin)
            props.append_margin = preset_data.get('append_margin', props.append_margin)
            props.prepend_append_layout = preset_data.get('prepend_append_layout', props.prepend_append_layout)
            
            # DETAILED LOGGING: Shader property loading with before/after values
            print(f"[PRESET LOAD DEBUG] Loading shader properties...")
            
            old_shader_type = props.shader_type
            props.shader_type = preset_data.get('shader_type', props.shader_type)
            print(f"[PRESET LOAD DEBUG] shader_type: {old_shader_type} -> {props.shader_type}")
            
            old_shader_connection = props.shader_connection
            props.shader_connection = preset_data.get('shader_connection', props.shader_connection)
            print(f"[PRESET LOAD DEBUG] shader_connection: {old_shader_connection} -> {props.shader_connection}")
            
            old_use_alpha = props.use_alpha
            props.use_alpha = preset_data.get('use_alpha', props.use_alpha)
            print(f"[PRESET LOAD DEBUG] use_alpha: {old_use_alpha} -> {props.use_alpha}")
            
            old_emission_strength = props.emission_strength
            props.emission_strength = preset_data.get('emission_strength', props.emission_strength)
            print(f"[PRESET LOAD DEBUG] emission_strength: {old_emission_strength} -> {props.emission_strength}")
            
            old_auto_assign = props.auto_assign_material
            props.auto_assign_material = preset_data.get('auto_assign_material', props.auto_assign_material)
            print(f"[PRESET LOAD DEBUG] auto_assign_material: {old_auto_assign} -> {props.auto_assign_material}")
            
            # CRITICAL SHADER PROPERTIES - Most detailed logging
            print(f"[PRESET LOAD DEBUG] ==================== CRITICAL SHADER PROPERTIES ====================")
            
            old_disable_shadows = props.disable_shadows
            new_disable_shadows = preset_data.get('disable_shadows', props.disable_shadows)
            props.disable_shadows = new_disable_shadows
            print(f"[PRESET LOAD DEBUG] disable_shadows:")
            print(f"[PRESET LOAD DEBUG]   OLD VALUE: {old_disable_shadows} (type: {type(old_disable_shadows)})")
            print(f"[PRESET LOAD DEBUG]   PRESET DATA: {preset_data.get('disable_shadows', 'NOT FOUND')} (type: {type(preset_data.get('disable_shadows', 'N/A'))})")
            print(f"[PRESET LOAD DEBUG]   NEW VALUE: {props.disable_shadows} (type: {type(props.disable_shadows)})")
            
            old_disable_reflections = props.disable_reflections
            new_disable_reflections = preset_data.get('disable_reflections', props.disable_reflections)
            props.disable_reflections = new_disable_reflections
            print(f"[PRESET LOAD DEBUG] disable_reflections:")
            print(f"[PRESET LOAD DEBUG]   OLD VALUE: {old_disable_reflections} (type: {type(old_disable_reflections)})")
            print(f"[PRESET LOAD DEBUG]   PRESET DATA: {preset_data.get('disable_reflections', 'NOT FOUND')} (type: {type(preset_data.get('disable_reflections', 'N/A'))})")
            print(f"[PRESET LOAD DEBUG]   NEW VALUE: {props.disable_reflections} (type: {type(props.disable_reflections)})")
            
            old_disable_backfacing = props.disable_backfacing
            new_disable_backfacing = preset_data.get('disable_backfacing', props.disable_backfacing)
            props.disable_backfacing = new_disable_backfacing
            print(f"[PRESET LOAD DEBUG] disable_backfacing:")
            print(f"[PRESET LOAD DEBUG]   OLD VALUE: {old_disable_backfacing} (type: {type(old_disable_backfacing)})")
            print(f"[PRESET LOAD DEBUG]   PRESET DATA: {preset_data.get('disable_backfacing', 'NOT FOUND')} (type: {type(preset_data.get('disable_backfacing', 'N/A'))})")
            print(f"[PRESET LOAD DEBUG]   NEW VALUE: {props.disable_backfacing} (type: {type(props.disable_backfacing)})")
            
            print(f"[PRESET LOAD DEBUG] ================================================================")
            
            props.generate_normal_map = preset_data.get('generate_normal_map', props.generate_normal_map)
            props.normal_map_strength = preset_data.get('normal_map_strength', props.normal_map_strength)
            props.normal_map_blur_radius = preset_data.get('normal_map_blur_radius', props.normal_map_blur_radius)
            props.normal_map_invert = preset_data.get('normal_map_invert', props.normal_map_invert)
            
            # Load multiline settings
            props.enable_multiline = preset_data.get('enable_multiline', props.enable_multiline)
            props.line_height = preset_data.get('line_height', props.line_height)
            props.text_alignment = preset_data.get('text_alignment', props.text_alignment)
            props.auto_wrap = preset_data.get('auto_wrap', props.auto_wrap)
            props.wrap_width = preset_data.get('wrap_width', props.wrap_width)
            props.max_lines = preset_data.get('max_lines', props.max_lines)
            props.line_spacing_pixels = preset_data.get('line_spacing_pixels', props.line_spacing_pixels)
            
            # Load stroke settings
            props.enable_stroke = preset_data.get('enable_stroke', props.enable_stroke)
            props.stroke_width = preset_data.get('stroke_width', props.stroke_width)
            props.stroke_position = preset_data.get('stroke_position', props.stroke_position)
            props.stroke_color = preset_data.get('stroke_color', props.stroke_color)
            
            # Load shadow/glow settings
            props.enable_shadow = preset_data.get('enable_shadow', props.enable_shadow)
            props.shadow_offset_x = preset_data.get('shadow_offset_x', props.shadow_offset_x)
            props.shadow_offset_y = preset_data.get('shadow_offset_y', props.shadow_offset_y)
            props.shadow_blur = preset_data.get('shadow_blur', props.shadow_blur)
            props.shadow_color = preset_data.get('shadow_color', props.shadow_color)
            props.enable_glow = preset_data.get('enable_glow', props.enable_glow)
            props.glow_blur = preset_data.get('glow_blur', props.glow_blur)
            props.glow_color = preset_data.get('glow_color', props.glow_color)
            
            # Load tiling settings
            props.enable_tiling = preset_data.get('enable_tiling', props.enable_tiling)
            props.tiling_mode = preset_data.get('tiling_mode', props.tiling_mode)
            props.tile_count_x = preset_data.get('tile_count_x', props.tile_count_x)
            props.tile_count_y = preset_data.get('tile_count_y', props.tile_count_y)
            props.tile_spacing = preset_data.get('tile_spacing', props.tile_spacing)
            props.expand_tiling_canvas = preset_data.get('expand_tiling_canvas', props.expand_tiling_canvas)
            
            # Load text fitting settings
            props.enable_text_fitting = preset_data.get('enable_text_fitting', props.enable_text_fitting)
            props.text_fitting_mode = preset_data.get('text_fitting_mode', props.text_fitting_mode)
            props.text_fitting_margin = preset_data.get('text_fitting_margin', props.text_fitting_margin)
            props.min_font_size = preset_data.get('min_font_size', props.min_font_size)
            props.max_font_size = preset_data.get('max_font_size', props.max_font_size)
            
            # Load overlay data
            props.image_overlays.clear()
            overlay_data_list = preset_data.get('image_overlays', [])
            for overlay_data in overlay_data_list:
                overlay = props.image_overlays.add()
                overlay.name = overlay_data.get('name', 'Overlay')
                overlay.image_path = overlay_data.get('image_path', '')
                overlay.x_position = overlay_data.get('x_position', 0.5)
                overlay.y_position = overlay_data.get('y_position', 0.5)
                overlay.scale = overlay_data.get('scale', 1.0)
                overlay.rotation = overlay_data.get('rotation', 0.0)
                overlay.z_index = overlay_data.get('z_index', 1)
                overlay.enabled = overlay_data.get('enabled', True)
            
            props.is_updating = False
            
            # FINAL VERIFICATION: Check what the properties actually are after loading
            print(f"[PRESET LOAD DEBUG] ==================== FINAL VERIFICATION ====================")
            print(f"[PRESET LOAD DEBUG] Properties AFTER loading preset '{self.preset_name}':")
            print(f"[PRESET LOAD DEBUG]   disable_shadows: {props.disable_shadows} (type: {type(props.disable_shadows)})")
            print(f"[PRESET LOAD DEBUG]   disable_reflections: {props.disable_reflections} (type: {type(props.disable_reflections)})")
            print(f"[PRESET LOAD DEBUG]   disable_backfacing: {props.disable_backfacing} (type: {type(props.disable_backfacing)})")
            print(f"[PRESET LOAD DEBUG] ================================================================")
            
            # Always generate preview after loading preset
            generate_preview(context)
            
            source_info = {
                'BLEND_FILE': "from blend file",
                'PERSISTENT_FILE': "from persistent storage",
                'LOCAL_FILE': "from legacy storage"
            }.get(source, "from unknown source")
            self.report({'INFO'}, f"✅ Loaded preset '{self.preset_name}' {source_info}")
            
        except (OSError, IOError) as e:
            props.is_updating = False
            self.report({'ERROR'}, f"Failed to read preset file: {str(e)}")
            return {'CANCELLED'}
        except json.JSONDecodeError as e:
            props.is_updating = False
            self.report({'ERROR'}, f"Invalid preset file format: {str(e)}")
            return {'CANCELLED'}
        except Exception as e:
            props.is_updating = False
            self.report({'ERROR'}, f"Unexpected error loading preset: {str(e)}")
            return {'CANCELLED'}
        
        return {'FINISHED'}

class TEXT_TEXTURE_OT_delete_preset(Operator):
    """Delete preset"""
    bl_idname = "text_texture.delete_preset"
    bl_label = "Delete Preset"
    bl_description = "Delete selected preset"
    bl_options = {'REGISTER', 'UNDO'}
    
    preset_name: StringProperty()
    
    def execute(self, context):
        props = context.scene.text_texture_props
        
        try:
            # Delete from both blend file and local storage
            blend_deleted = delete_blend_file_preset(self.preset_name)
            
            preset_file = os.path.join(get_preset_path(), f"{self.preset_name}.json")
            local_deleted = False
            if os.path.exists(preset_file):
                os.remove(preset_file)
                local_deleted = True
            
            if not blend_deleted and not local_deleted:
                self.report({'WARNING'}, f"Preset '{self.preset_name}' not found in blend file or local storage")
            
            # Remove from UI list
            for i, preset in enumerate(props.presets):
                if preset.name == self.preset_name:
                    props.presets.remove(i)
                    break
            
            delete_info = []
            if blend_deleted:
                delete_info.append("blend file")
            if local_deleted:
                delete_info.append("local storage")
            
            location_text = " and ".join(delete_info) if delete_info else "nowhere (not found)"
            self.report({'INFO'}, f"🗑️ Deleted preset '{self.preset_name}' from {location_text}")
            
        except (OSError, IOError) as e:
            self.report({'ERROR'}, f"Failed to delete preset file: {str(e)}")
            return {'CANCELLED'}
        except Exception as e:
            self.report({'ERROR'}, f"Unexpected error deleting preset: {str(e)}")
            return {'CANCELLED'}
        
        return {'FINISHED'}
    
    def invoke(self, context, event):
        # Add confirmation dialog for better UX
        return context.window_manager.invoke_confirm(self, event)
    
    def draw(self, context):
        layout = self.layout
        layout.label(text=f"Delete preset '{self.preset_name}'?")
        layout.label(text="This action cannot be undone.", icon='ERROR')

class TEXT_TEXTURE_OT_open_panel(Operator):
    """Open Text Texture Generator panel"""
    bl_idname = "text_texture.open_panel"
    bl_label = "Open Text Texture Panel"
    bl_description = "Open the Text Texture Generator panel in the sidebar"
    bl_options = {'REGISTER', 'UNDO'}
    
    def execute(self, context):
        # Ensure the sidebar is visible
        for area in context.screen.areas:
            if area.type == 'VIEW_3D':
                for space in area.spaces:
                    if space.type == 'VIEW_3D':
                        space.show_region_ui = True
                        # Switch to Tool tab
                        area.tag_redraw()
                        self.report({'INFO'}, "Text Texture panel opened in 3D View sidebar (press N if not visible)")
                        return {'FINISHED'}
            elif area.type == 'NODE_EDITOR':
                for space in area.spaces:
                    if space.type == 'NODE_EDITOR':
                        space.show_region_ui = True
                        area.tag_redraw()
                        self.report({'INFO'}, "Text Texture panel opened in Shader Editor sidebar")
                        return {'FINISHED'}
        
        self.report({'WARNING'}, "Please open a 3D View or Shader Editor first")
        return {'CANCELLED'}

class TEXT_TEXTURE_OT_refresh_fonts(Operator):
    """Refresh fonts"""
    bl_idname = "text_texture.refresh_fonts"
    bl_label = "Refresh Fonts"
    bl_description = "Refresh font list"
    bl_options = {'REGISTER', 'UNDO'}
    
    def execute(self, context):
        if hasattr(get_font_enum_items, 'cached_fonts'):
            del get_font_enum_items.cached_fonts
        get_font_enum_items.cached_fonts = get_system_fonts()
        self.report({'INFO'}, f"Found {len(get_font_enum_items.cached_fonts)} fonts")
        return {'FINISHED'}

class TEXT_TEXTURE_OT_refresh_presets(Operator):
    """Refresh presets from blend file and local storage"""
    bl_idname = "text_texture.refresh_presets"
    bl_label = "Refresh Presets"
    bl_description = "Reload presets from blend file and local storage"
    bl_options = {'REGISTER', 'UNDO'}
    
    def execute(self, context):
        try:
            # Reload presets
            initialize_presets()
            
            props = context.scene.text_texture_props
            blend_presets = get_blend_file_presets()
            total_presets = len(props.presets)
            blend_count = len(blend_presets)
            local_count = total_presets - blend_count
            
            self.report({'INFO'}, f"✅ Refreshed {total_presets} presets ({blend_count} from blend file, {local_count} local)")
            
            # Force UI redraw
            for area in context.screen.areas:
                area.tag_redraw()
            
            return {'FINISHED'}
            
        except Exception as e:
            self.report({'ERROR'}, f"Failed to refresh presets: {str(e)}")
            return {'CANCELLED'}

class TEXT_TEXTURE_OT_add_overlay(Operator):
    """Add new image overlay"""
    bl_idname = "text_texture.add_overlay"
    bl_label = "Add Overlay"
    bl_description = "Add new image overlay"
    bl_options = {'REGISTER', 'UNDO'}
    
    def execute(self, context):
        props = context.scene.text_texture_props
        
        overlay = props.image_overlays.add()
        overlay.name = f"Overlay {len(props.image_overlays)}"
        props.active_overlay_index = len(props.image_overlays) - 1
        
        # Force UI redraw
        for area in context.screen.areas:
            area.tag_redraw()
        
        self.report({'INFO'}, f"Added overlay: {overlay.name}")
        return {'FINISHED'}

class TEXT_TEXTURE_OT_remove_overlay(Operator):
    """Remove active image overlay"""
    bl_idname = "text_texture.remove_overlay"
    bl_label = "Remove Overlay"
    bl_description = "Remove active image overlay"
    bl_options = {'REGISTER', 'UNDO'}
    
    def execute(self, context):
        props = context.scene.text_texture_props
        
        if props.image_overlays and props.active_overlay_index < len(props.image_overlays):
            overlay_name = props.image_overlays[props.active_overlay_index].name
            props.image_overlays.remove(props.active_overlay_index)
            
            # Update active index
            if props.active_overlay_index >= len(props.image_overlays) and props.image_overlays:
                props.active_overlay_index = len(props.image_overlays) - 1
            
            # Force UI redraw
            for area in context.screen.areas:
                area.tag_redraw()
            
            self.report({'INFO'}, f"Removed overlay: {overlay_name}")
        else:
            self.report({'WARNING'}, "No overlay to remove")
        
        return {'FINISHED'}

class TEXT_TEXTURE_OT_set_anchor_point(Operator):
    """Set anchor point for text positioning"""
    bl_idname = "text_texture.set_anchor_point"
    bl_label = "Set Anchor Point"
    bl_description = "Set text anchor position"
    bl_options = {'REGISTER', 'UNDO'}
    
    anchor_point: StringProperty()
    
    def execute(self, context):
        props = context.scene.text_texture_props
        props.anchor_point = self.anchor_point
        return {'FINISHED'}

class TEXT_TEXTURE_OT_sync_margins(Operator):
    """Sync margin values when linked"""
    bl_idname = "text_texture.sync_margins"
    bl_label = "Sync Margins"
    bl_description = "Synchronize margin values"
    bl_options = {'REGISTER', 'UNDO'}
    
    margin_type: StringProperty()
    
    def execute(self, context):
        props = context.scene.text_texture_props
        sync_margin_values(props, self.margin_type)
        return {'FINISHED'}

class TEXT_TEXTURE_OT_export_presets(Operator):
    """Export all presets to a file for backup"""
    bl_idname = "text_texture.export_presets"
    bl_label = "Export Presets"
    bl_description = "Export all presets to a file for backup"
    bl_options = {'REGISTER', 'UNDO'}
    
    filepath: StringProperty(
        name="File Path",
        description="Path to save preset backup file",
        subtype='FILE_PATH',
        default="text_texture_presets_backup.json"
    )
    
    def execute(self, context):
        try:
            props = context.scene.text_texture_props
            
            # Collect all presets from all sources
            all_presets = {}
            
            # Get blend file presets
            blend_presets = get_blend_file_presets()
            for name, data in blend_presets.items():
                all_presets[name] = {
                    "data": data,
                    "source": "BLEND_FILE"
                }
            
            # Get persistent presets
            persistent_dir = get_persistent_preset_path()
            if os.path.exists(persistent_dir):
                for filename in os.listdir(persistent_dir):
                    if filename.endswith('.json'):
                        preset_name = os.path.splitext(filename)[0]
                        if preset_name not in all_presets:  # Don't override blend file presets
                            try:
                                with open(os.path.join(persistent_dir, filename), 'r') as f:
                                    preset_data = json.load(f)
                                all_presets[preset_name] = {
                                    "data": preset_data,
                                    "source": "PERSISTENT_FILE"
                                }
                            except (json.JSONDecodeError, OSError) as e:
                                print(f"[TTG] Error reading persistent preset {preset_name}: {e}")
            
            # Get legacy presets
            legacy_dir = get_preset_path()
            if os.path.exists(legacy_dir) and legacy_dir != persistent_dir:
                for filename in os.listdir(legacy_dir):
                    if filename.endswith('.json'):
                        preset_name = os.path.splitext(filename)[0]
                        if preset_name not in all_presets:  # Don't override higher priority presets
                            try:
                                with open(os.path.join(legacy_dir, filename), 'r') as f:
                                    preset_data = json.load(f)
                                all_presets[preset_name] = {
                                    "data": preset_data,
                                    "source": "LOCAL_FILE"
                                }
                            except (json.JSONDecodeError, OSError) as e:
                                print(f"[TTG] Error reading legacy preset {preset_name}: {e}")
            
            if not all_presets:
                self.report({'WARNING'}, "No presets found to export")
                return {'CANCELLED'}
            
            # Create export data with metadata
            export_data = {
                "format_version": "1.0",
                "addon_version": bl_info["version"],
                "export_timestamp": time.time(),
                "presets": all_presets
            }
            
            # Write to file
            with open(self.filepath, 'w') as f:
                json.dump(export_data, f, indent=2)
            
            self.report({'INFO'}, f"✅ Exported {len(all_presets)} presets to {os.path.basename(self.filepath)}")
            return {'FINISHED'}
            
        except Exception as e:
            self.report({'ERROR'}, f"Failed to export presets: {str(e)}")
            return {'CANCELLED'}
    
    def invoke(self, context, event):
        # Set default filename with timestamp
        import datetime
        timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
        self.filepath = f"text_texture_presets_backup_{timestamp}.json"
        
        context.window_manager.fileselect_add(self)
        return {'RUNNING_MODAL'}

class TEXT_TEXTURE_OT_import_presets(Operator):
    """Import presets from a backup file"""
    bl_idname = "text_texture.import_presets"
    bl_label = "Import Presets"
    bl_description = "Import presets from a backup file"
    bl_options = {'REGISTER', 'UNDO'}
    
    filepath: StringProperty(
        name="File Path",
        description="Path to preset backup file",
        subtype='FILE_PATH'
    )
    
    import_mode: EnumProperty(
        name="Import Mode",
        description="How to handle existing presets with same names",
        items=[
            ('SKIP', 'Skip Existing', 'Skip presets that already exist'),
            ('OVERWRITE', 'Overwrite', 'Overwrite existing presets'),
            ('RENAME', 'Rename New', 'Rename imported presets if they conflict'),
        ],
        default='SKIP'
    )
    
    import_to_blend: BoolProperty(
        name="Import to Blend File",
        description="Import presets to blend file (recommended for portability)",
        default=True
    )
    
    def execute(self, context):
        try:
            if not os.path.exists(self.filepath):
                self.report({'ERROR'}, "Backup file not found")
                return {'CANCELLED'}
            
            props = context.scene.text_texture_props
            
            # Read backup file
            with open(self.filepath, 'r') as f:
                import_data = json.load(f)
            
            # Validate format
            if "presets" not in import_data:
                self.report({'ERROR'}, "Invalid backup file format")
                return {'CANCELLED'}
            
            imported_presets = import_data["presets"]
            if not imported_presets:
                self.report({'WARNING'}, "No presets found in backup file")
                return {'CANCELLED'}
            
            # Get existing presets to check for conflicts
            existing_blend_presets = get_blend_file_presets()
            existing_preset_names = set(existing_blend_presets.keys())
            
            # Also check persistent storage
            persistent_dir = get_persistent_preset_path()
            if os.path.exists(persistent_dir):
                for filename in os.listdir(persistent_dir):
                    if filename.endswith('.json'):
                        existing_preset_names.add(os.path.splitext(filename)[0])
            
            imported_count = 0
            skipped_count = 0
            errors = []
            
            for preset_name, preset_info in imported_presets.items():
                preset_data = preset_info.get("data", {})
                
                # Handle conflicts
                final_name = preset_name
                if preset_name in existing_preset_names:
                    if self.import_mode == 'SKIP':
                        skipped_count += 1
                        continue
                    elif self.import_mode == 'RENAME':
                        counter = 1
                        base_name = preset_name
                        while final_name in existing_preset_names:
                            final_name = f"{base_name}_imported_{counter}"
                            counter += 1
                    # OVERWRITE mode uses original name
                
                try:
                    # Import to blend file or persistent storage based on setting
                    if self.import_to_blend:
                        success = save_blend_file_preset(final_name, preset_data)
                        if not success:
                            raise Exception("Failed to save to blend file")
                    else:
                        # Save to persistent storage
                        persistent_file = os.path.join(get_persistent_preset_path(), f"{final_name}.json")
                        with open(persistent_file, 'w') as f:
                            json.dump(preset_data, f, indent=2)
                    
                    imported_count += 1
                    
                    # Add to UI list if not already present
                    existing_preset = None
                    for preset in props.presets:
                        if preset.name == final_name:
                            existing_preset = preset
                            break
                    
                    if not existing_preset:
                        new_preset = props.presets.add()
                        new_preset.name = final_name
                        new_preset.source = 'BLEND_FILE' if self.import_to_blend else 'PERSISTENT_FILE'
                    else:
                        existing_preset.source = 'BLEND_FILE' if self.import_to_blend else 'PERSISTENT_FILE'
                    
                except Exception as e:
                    errors.append(f"{preset_name}: {str(e)}")
            
            # Report results
            if imported_count > 0:
                storage_location = "blend file" if self.import_to_blend else "persistent storage"
                self.report({'INFO'}, f"✅ Imported {imported_count} presets to {storage_location}")
                if skipped_count > 0:
                    self.report({'INFO'}, f"⏭️ Skipped {skipped_count} existing presets")
            else:
                self.report({'WARNING'}, "No presets were imported")
            
            if errors:
                self.report({'WARNING'}, f"Errors importing {len(errors)} presets - check console")
                for error in errors[:5]:  # Show first 5 errors
                    print(f"[TTG] Import error: {error}")
            
            return {'FINISHED'}
            
        except Exception as e:
            self.report({'ERROR'}, f"Failed to import presets: {str(e)}")
            return {'CANCELLED'}
    
    def invoke(self, context, event):
        context.window_manager.fileselect_add(self)
        return {'RUNNING_MODAL'}
    
    def draw(self, context):
        layout = self.layout
        layout.prop(self, "import_mode")
        layout.prop(self, "import_to_blend")

class TEXT_TEXTURE_OT_show_migration_report(Operator):
    """Show migration report"""
    bl_idname = "text_texture.show_migration_report"
    bl_label = "Show Migration Report"
    bl_description = "Display migration report with details about preset migration"
    bl_options = {'REGISTER', 'UNDO'}
    
    def execute(self, context):
        try:
            import json
            import os
            persistent_dir = os.path.dirname(get_persistent_preset_path())
            migration_report_file = os.path.join(persistent_dir, "migration_report.json")
            
            if os.path.exists(migration_report_file):
                with open(migration_report_file, 'r') as f:
                    migration_data = json.load(f)
                
                migrated_count = migration_data.get('migrated_count', 0)
                total_found = migration_data.get('total_found', 0)
                errors = migration_data.get('errors', [])
                backup_path = migration_data.get('backup_path', 'Unknown')
                
                self.report({'INFO'}, f"Migration Report: {migrated_count}/{total_found} presets migrated successfully")
                
                if errors:
                    error_summary = f"{len(errors)} errors occurred during migration"
                    self.report({'WARNING'}, error_summary)
                    print(f"[TTG] Migration errors:")
                    for error in errors[:10]:  # Show first 10 errors
                        print(f"[TTG] - {error}")
                else:
                    self.report({'INFO'}, "No migration errors")
                
                if backup_path != 'Unknown':
                    print(f"[TTG] Migration backup created at: {backup_path}")
            else:
                self.report({'INFO'}, "No migration report found - migration may not have been needed")
            
            return {'FINISHED'}
        except Exception as e:
            self.report({'ERROR'}, f"Error reading migration report: {e}")
            return {'CANCELLED'}


# ============================================================================
# BATCH PROCESSING OPERATORS
# ============================================================================

class TEXT_TEXTURE_OT_batch_generate(Operator):
    """Generate textures for multiple words/phrases in batch"""
    bl_idname = "text_texture.batch_generate"
    bl_label = "Generate Batch"
    bl_description = "Generate textures for all words in the batch list"
    bl_options = {'REGISTER', 'UNDO'}
    
    def execute(self, context):
        props = context.scene.text_texture_props
        
        # Parse word list
        if not props.batch_word_list.strip():
            self.report({'ERROR'}, "Word list is empty")
            return {'CANCELLED'}
        
        # Split by commas and clean up words
        words = [word.strip() for word in props.batch_word_list.split(',')]
        words = [word for word in words if word]  # Remove empty strings
        
        if not words:
            self.report({'ERROR'}, "No valid words found in list")
            return {'CANCELLED'}
        
        # Store original text
        original_text = props.text
        
        # Create output directory if it doesn't exist and auto-save is enabled
        if props.batch_auto_save:
            output_path = bpy.path.abspath(props.batch_output_folder)
            import os
            try:
                os.makedirs(output_path, exist_ok=True)
            except OSError as e:
                self.report({'ERROR'}, f"Failed to create output directory: {e}")
                return {'CANCELLED'}
        
        # Process each word
        total_words = len(words)
        successful_generations = 0
        
        for i, word in enumerate(words):
            # Update progress
            props.batch_progress = int((i / total_words) * 100)
            
            # Set current word as text input
            props.text = word
            
            try:
                # Generate texture using existing generation operator
                result = bpy.ops.text_texture.generate()
                if result == {'FINISHED'}:
                    successful_generations += 1
                    
                    # Save if auto-save is enabled
                    if props.batch_auto_save and context.scene.text_texture_image:
                        # Create safe filename
                        safe_word = "".join(c for c in word if c.isalnum() or c in (' ', '-', '_')).rstrip()
                        safe_word = safe_word.replace(' ', '_')
                        filename = f"{props.batch_filename_prefix}{safe_word}.png"
                        filepath = os.path.join(output_path, filename)
                        
                        # Save the image
                        try:
                            context.scene.text_texture_image.filepath_raw = filepath
                            context.scene.text_texture_image.file_format = 'PNG'
                            context.scene.text_texture_image.save()
                        except Exception as save_error:
                            self.report({'WARNING'}, f"Failed to save '{word}': {save_error}")
                
            except Exception as gen_error:
                self.report({'WARNING'}, f"Failed to generate texture for '{word}': {gen_error}")
            
            # Update UI to show progress
            for area in context.screen.areas:
                area.tag_redraw()
        
        # Restore original text
        props.text = original_text
        props.batch_progress = 100
        
        # Final report
        if successful_generations == total_words:
            self.report({'INFO'}, f"✅ Successfully generated {successful_generations} textures")
        elif successful_generations > 0:
            self.report({'WARNING'}, f"Generated {successful_generations}/{total_words} textures (some failed)")
        else:
            self.report({'ERROR'}, "Failed to generate any textures")
        
        return {'FINISHED'}


class TEXT_TEXTURE_OT_import_word_list(Operator):
    """Import word list from text file"""
    bl_idname = "text_texture.import_word_list"
    bl_label = "Import Word List"
    bl_description = "Import word list from a text file"
    bl_options = {'REGISTER', 'UNDO'}
    
    filepath: StringProperty(
        name="File Path",
        description="Path to text file containing word list",
        subtype='FILE_PATH'
    )
    
    filter_glob: StringProperty(
        default="*.txt",
        options={'HIDDEN'}
    )
    
    def execute(self, context):
        props = context.scene.text_texture_props
        
        try:
            with open(self.filepath, 'r', encoding='utf-8') as file:
                content = file.read()
                
                # Support both comma-separated and line-separated words
                if ',' in content:
                    words = [word.strip() for word in content.split(',')]
                else:
                    words = [line.strip() for line in content.splitlines()]
                
                # Filter out empty words
                words = [word for word in words if word]
                
                if words:
                    props.batch_word_list = ', '.join(words)
                    self.report({'INFO'}, f"✅ Imported {len(words)} words")
                else:
                    self.report({'WARNING'}, "No words found in file")
                    
        except Exception as e:
            self.report({'ERROR'}, f"Failed to import file: {str(e)}")
            return {'CANCELLED'}
        
        return {'FINISHED'}
    
    def invoke(self, context, event):
        context.window_manager.fileselect_add(self)
        return {'RUNNING_MODAL'}


class TEXT_TEXTURE_OT_export_word_list(Operator):
    """Export word list to text file"""
    bl_idname = "text_texture.export_word_list"
    bl_label = "Export Word List"
    bl_description = "Export current word list to a text file"
    bl_options = {'REGISTER', 'UNDO'}
    
    filepath: StringProperty(
        name="File Path",
        description="Path to save text file",
        subtype='FILE_PATH',
        default="word_list.txt"
    )
    
    filter_glob: StringProperty(
        default="*.txt",
        options={'HIDDEN'}
    )
    
    def execute(self, context):
        props = context.scene.text_texture_props
        
        if not props.batch_word_list.strip():
            self.report({'ERROR'}, "Word list is empty")
            return {'CANCELLED'}
        
        try:
            words = [word.strip() for word in props.batch_word_list.split(',')]
            words = [word for word in words if word]  # Remove empty strings
            
            with open(self.filepath, 'w', encoding='utf-8') as file:
                for word in words:
                    file.write(word + '\n')
            
            self.report({'INFO'}, f"✅ Exported {len(words)} words to {os.path.basename(self.filepath)}")
            
        except Exception as e:
            self.report({'ERROR'}, f"Failed to export file: {str(e)}")
            return {'CANCELLED'}
        
        return {'FINISHED'}
    
    def invoke(self, context, event):
        context.window_manager.fileselect_add(self)
        return {'RUNNING_MODAL'}


class TEXT_TEXTURE_OT_save_batch_template(Operator):
    """Save current settings as batch template"""
    bl_idname = "text_texture.save_batch_template"
    bl_label = "Save Batch Template"
    bl_description = "Save current settings as a reusable batch template"
    bl_options = {'REGISTER', 'UNDO'}
    
    def execute(self, context):
        props = context.scene.text_texture_props
        
        if not props.batch_template_name.strip():
            self.report({'ERROR'}, "Template name is required")
            return {'CANCELLED'}
        
        # Create template data including all relevant settings
        template_data = {
            # Core text settings
            'font_size': props.font_size,
            'font_color': list(props.font_color),
            'background_transparent': props.background_transparent,
            'background_color': list(props.background_color),
            'font_path': props.font_path,
            'custom_font_path': props.custom_font_path,
            'use_custom_font': props.use_custom_font,
            'text_align': props.text_align,
            'padding': props.padding,
            
            # Texture settings
            'texture_width': props.texture_width,
            'texture_height': props.texture_height,
            
            # Multiline settings
            'enable_multiline': props.enable_multiline,
            'line_height': props.line_height,
            'text_alignment': props.text_alignment,
            'auto_wrap': props.auto_wrap,
            'wrap_width': props.wrap_width,
            'max_lines': props.max_lines,
            'line_spacing_pixels': props.line_spacing_pixels,
            
            # Stroke settings
            'enable_stroke': props.enable_stroke,
            'stroke_width': props.stroke_width,
            'stroke_position': props.stroke_position,
            'stroke_color': list(props.stroke_color),
            
            # Shadow/Glow settings
            'enable_shadow': props.enable_shadow,
            'shadow_offset_x': props.shadow_offset_x,
            'shadow_offset_y': props.shadow_offset_y,
            'shadow_blur': props.shadow_blur,
            'shadow_color': list(props.shadow_color),
            'enable_glow': props.enable_glow,
            'glow_blur': props.glow_blur,
            'glow_color': list(props.glow_color),
            
            # Tiling settings
            'enable_tiling': props.enable_tiling,
            'tiling_mode': props.tiling_mode,
            'tile_count_x': props.tile_count_x,
            'tile_count_y': props.tile_count_y,
            'tile_spacing': props.tile_spacing,
            'expand_tiling_canvas': props.expand_tiling_canvas,
            
            # Batch specific settings
            'batch_output_folder': props.batch_output_folder,
            'batch_filename_prefix': props.batch_filename_prefix,
            'batch_auto_save': props.batch_auto_save,
        }
        
        # Save to presets with special prefix for batch templates
        template_key = f"batch_{props.batch_template_name}"
        
        # Save to blend file (highest priority)
        blend_success = save_blend_file_preset(template_key, template_data)
        
        # Also save to persistent storage for cross-file sharing
        persistent_success = False
        try:
            persistent_file = os.path.join(get_persistent_preset_path(), f"{template_key}.json")
            with open(persistent_file, 'w') as f:
                json.dump(template_data, f, indent=2)
            persistent_success = True
        except Exception as e:
            print(f"Warning: Failed to save batch template to persistent storage: {e}")
        
        if blend_success or persistent_success:
            storage_info = ""
            if blend_success and persistent_success:
                storage_info = " (saved to blend file + persistent storage)"
            elif blend_success:
                storage_info = " (saved to blend file)"
            elif persistent_success:
                storage_info = " (saved to persistent storage)"
            
            self.report({'INFO'}, f"✅ Saved batch template: {props.batch_template_name}{storage_info}")
        else:
            self.report({'ERROR'}, "Failed to save batch template")
            return {'CANCELLED'}
        
        return {'FINISHED'}


class TEXT_TEXTURE_OT_load_batch_template(Operator):
    """Load batch template settings"""
    bl_idname = "text_texture.load_batch_template"
    bl_label = "Load Batch Template"
    bl_description = "Load previously saved batch template"
    bl_options = {'REGISTER', 'UNDO'}
    
    def execute(self, context):
        props = context.scene.text_texture_props
        
        if not props.batch_template_name.strip():
            self.report({'ERROR'}, "Template name is required")
            return {'CANCELLED'}
        
        template_key = f"batch_{props.batch_template_name}"
        
        # Three-tier loading: Blend File > Persistent File > Legacy File
        template_data = None
        source = 'UNKNOWN'
        
        # Try blend file first
        blend_presets = get_blend_file_presets()
        if template_key in blend_presets:
            template_data = blend_presets[template_key]
            source = 'BLEND_FILE'
        else:
            # Try persistent storage
            persistent_file = os.path.join(get_persistent_preset_path(), f"{template_key}.json")
            if os.path.exists(persistent_file):
                try:
                    with open(persistent_file, 'r') as f:
                        template_data = json.load(f)
                    source = 'PERSISTENT_FILE'
                except Exception as e:
                    self.report({'ERROR'}, f"Failed to read template: {e}")
                    return {'CANCELLED'}
        
        if not template_data:
            self.report({'ERROR'}, f"Batch template not found: {props.batch_template_name}")
            return {'CANCELLED'}
        
        # Apply template settings
        props.is_updating = True
        
        try:
            # Apply all settings from template
            for key, value in template_data.items():
                if hasattr(props, key):
                    setattr(props, key, value)
            
            props.is_updating = False
            
            # Update live preview
            generate_preview(context)
            
            source_info = {
                'BLEND_FILE': "from blend file",
                'PERSISTENT_FILE': "from persistent storage",
                'LOCAL_FILE': "from legacy storage"
            }.get(source, "from unknown source")
            
            self.report({'INFO'}, f"✅ Loaded batch template: {props.batch_template_name} {source_info}")
            
        except Exception as e:
            props.is_updating = False
            self.report({'ERROR'}, f"Failed to apply template: {e}")
            return {'CANCELLED'}
        
        return {'FINISHED'}


# ============================================================================
# SHARED UI COMPONENTS
# ============================================================================

def draw_collapsible_header(layout, prop_name, text, icon='TRIA_DOWN'):
    """Draw a collapsible section header with expand/collapse arrow"""
    props = bpy.context.scene.text_texture_props
    expanded = getattr(props, prop_name)
    
    box = layout.box()
    header_row = box.row()
    
    # Draw arrow icon and toggle expanded state
    arrow_icon = 'TRIA_DOWN' if expanded else 'TRIA_RIGHT'
    header_row.prop(props, prop_name, text="", icon=arrow_icon, emboss=False)
    header_row.label(text=text, icon=icon)
    
    return box if expanded else None

def draw_text_input_section(layout, props):
    """Draw the main text input field"""
    layout.prop(props, "text", text="")

def draw_font_dropdown_section(layout, props):
    """Draw basic font dropdown for top level"""
    row = layout.row(align=True)
    row.prop(props, "font_path", text="")
    row.operator("text_texture.refresh_fonts", text="", icon='FILE_REFRESH')

def draw_generate_button(layout):
    """Draw the main generate button"""
    row = layout.row()
    row.scale_y = 1.5
    row.operator("text_texture.generate", text="Generate Text Texture", icon='IMAGE_DATA')

def draw_image_overlays_section(layout, props):
    """Draw image overlays controls"""
    # Add/Remove buttons
    row = layout.row(align=True)
    row.operator("text_texture.add_overlay", text="Add Image Overlay", icon='ADD')
    if props.image_overlays:
        row.operator("text_texture.remove_overlay", text="Remove", icon='REMOVE')
    
    # Overlay list
    if props.image_overlays:
        layout.separator()
        for i, overlay in enumerate(props.image_overlays):
            overlay_row = layout.row()
            overlay_row.prop(overlay, "enabled", text="")
            overlay_row.prop(overlay, "name", text="")
            
            if overlay.enabled:
                layout.prop(overlay, "image_path", text="Image File")
                
                pos_row = layout.row(align=True)
                pos_row.prop(overlay, "x_position", text="X")
                pos_row.prop(overlay, "y_position", text="Y")
                
                trans_row = layout.row(align=True)
                trans_row.prop(overlay, "scale", text="Scale")
                trans_row.prop(overlay, "rotation", text="Rotation")
                trans_row.prop(overlay, "z_index", text="Z-Level")
                
                # Add new positioning controls
                layout.separator()
                layout.prop(overlay, "positioning_mode")
                
                # Show spacing controls only for APPEND/PREPEND modes
                if overlay.positioning_mode in ['APPEND', 'PREPEND']:
                    layout.prop(overlay, "text_spacing")
                    layout.prop(overlay, "image_spacing")
                    layout.prop(overlay, "sequence_index")

def draw_positioning_section(layout, props):
    """Draw positioning and alignment controls"""
    # Dimensions moved here from top level
    layout.label(text="Dimensions:", icon='SETTINGS')
    row = layout.row(align=True)
    row.prop(props, "texture_width", text="Width")
    row.prop(props, "texture_height", text="Height")
    
    layout.separator()
    
    # Prepend/Append Text Section
    layout.label(text="Additional Text:", icon='TEXT')
    
    # Compact layout selector
    layout.prop(props, "prepend_append_layout", text="Layout")
    
    # Minimal prepend/append controls
    row = layout.row(align=True)
    row.prop(props, "prepend_text", text="Prepend")
    if props.prepend_text.strip():
        row.prop(props, "prepend_margin", text="", slider=True)
    
    row = layout.row(align=True)
    row.prop(props, "append_text", text="Append")
    if props.append_text.strip():
        row.prop(props, "append_margin", text="", slider=True)
    
    layout.separator()
    layout.prop(props, "position_mode", text="Mode")
    
    if props.position_mode == 'PRESET':
        layout.separator()
        layout.label(text="Anchor Point:", icon='ANCHOR_TOP')
        
        anchor_matrix = get_anchor_point_matrix()
        for row_idx, row in enumerate(anchor_matrix):
            grid_row = layout.row(align=True)
            grid_row.scale_y = 1.2
            for col_idx, anchor in enumerate(row):
                if anchor == props.anchor_point:
                    op = grid_row.operator("text_texture.set_anchor_point", text="●", depress=True)
                else:
                    op = grid_row.operator("text_texture.set_anchor_point", text="○")
                op.anchor_point = anchor
        
        # Margins
        layout.separator()
        margin_row = layout.row(align=True)
        margin_row.label(text="Margins:")
        margin_row.prop(props, "margins_linked", text="", icon='LINKED' if props.margins_linked else 'UNLINKED', toggle=True)
        
        margins_row = layout.row(align=True)
        margins_row.prop(props, "margin_x", text="X")
        if not props.margins_linked:
            margins_row.prop(props, "margin_y", text="Y")
        
        # Offsets
        layout.separator()
        layout.label(text="Fine-tune (pixels):")
        offset_row = layout.row(align=True)
        offset_row.prop(props, "offset_x", text="X")
        offset_row.prop(props, "offset_y", text="Y")
        
    else:  # MANUAL mode
        layout.separator()
        layout.prop(props, "manual_position_unit", text="Unit")
        manual_row = layout.row(align=True)
        manual_row.prop(props, "manual_position_x", text="X")
        manual_row.prop(props, "manual_position_y", text="Y")
    
    layout.separator()
    layout.prop(props, "constrain_to_canvas")

def draw_font_settings_section(layout, props):
    """Draw extended font settings"""
    # Font size moved here from top level
    layout.prop(props, "font_size")
    
    layout.separator()
    layout.label(text="Font Selection:")
    # Default font dropdown moved here from top level
    row = layout.row(align=True)
    row.prop(props, "font_path", text="")
    row.operator("text_texture.refresh_fonts", text="", icon='FILE_REFRESH')
    
    layout.separator()
    # Custom font controls moved here from top level
    layout.prop(props, "use_custom_font")
    if props.use_custom_font:
        layout.prop(props, "custom_font_path", text="")
    
    layout.separator()
    row = layout.row(align=True)
    row.prop(props, "padding")
    row.prop(props, "text_align")

def draw_colors_section(layout, props):
    """Draw color controls for text and background"""
    layout.label(text="Text Color:", icon='COLOR')
    layout.prop(props, "text_color", text="")
    
    layout.separator()
    layout.label(text="Background Color:", icon='IMAGE_PLANE')
    layout.prop(props, "background_transparent")
    if not props.background_transparent:
        layout.prop(props, "background_color", text="")

def draw_preview_options_section(layout, props):
    """Draw preview options (preview-specific settings only)"""
    layout.prop(props, "preview_size", text="Preview Size")
    layout.separator()
    layout.label(text="Preview Background Display:", icon='SCENE')
    layout.prop(props, "preview_bg_type", text="Background")
    if props.preview_bg_type == 'color':
        layout.prop(props, "preview_bg_color", text="Preview Display Color")
    elif props.preview_bg_type == 'gradient':
        layout.prop(props, "preview_bg_color", text="Color 1")
        layout.prop(props, "preview_bg_color2", text="Color 2")

def draw_presets_section(layout, props):
    """Draw presets save/load controls"""
    # Save preset section
    save_row = layout.row(align=True)
    save_row.prop(props, "new_preset_name", text="", placeholder="Preset Name")
    save_row.operator("text_texture.save_preset", text="Save", icon='FILE_NEW')
    
    layout.separator()
    
    # Import/Export and refresh buttons
    backup_row = layout.row(align=True)
    backup_row.operator("text_texture.export_presets", text="Export Backup", icon='EXPORT')
    backup_row.operator("text_texture.import_presets", text="Import Backup", icon='IMPORT')
    
    utility_row = layout.row(align=True)
    utility_row.operator("text_texture.refresh_presets", text="Refresh", icon='FILE_REFRESH')
    utility_row.operator("text_texture.show_migration_report", text="Migration Report", icon='INFO')
    
    layout.separator()
    
    # Load presets section
    if props.presets:
        layout.label(text=f"📁 Saved Presets ({len(props.presets)}):", icon='PRESET')
        
        # Separate presets by source
        blend_presets = [p for p in props.presets if p.source == 'BLEND_FILE']
        local_presets = [p for p in props.presets if p.source == 'LOCAL_FILE']
        
        # Show blend file presets first
        if blend_presets:
            layout.label(text="🎯 Blend File Presets (travel with file):", icon='FILE_BLEND')
            for preset in blend_presets:
                row = layout.row()
                load_btn = row.operator("text_texture.load_preset", text=f"📋 {preset.name}")
                load_btn.preset_name = preset.name
                
                # Quick overwrite button
                overwrite_btn = row.operator("text_texture.save_preset", text="", icon='FILE_REFRESH')
                overwrite_btn.overwrite = True
                # Set the preset name so it can be overwritten
                overwrite_btn.preset_name = preset.name
                
                delete_btn = row.operator("text_texture.delete_preset", text="", icon='TRASH')
                delete_btn.preset_name = preset.name
        
        # Show local presets second
        if local_presets:
            if blend_presets:
                layout.separator()
            layout.label(text="💻 Local Presets (this machine only):", icon='DISK_DRIVE')
            for preset in local_presets:
                row = layout.row()
                load_btn = row.operator("text_texture.load_preset", text=f"📋 {preset.name}")
                load_btn.preset_name = preset.name
                
                # Quick overwrite button
                overwrite_btn = row.operator("text_texture.save_preset", text="", icon='FILE_REFRESH')
                overwrite_btn.overwrite = True
                # Set the preset name so it can be overwritten
                overwrite_btn.preset_name = preset.name
                
                delete_btn = row.operator("text_texture.delete_preset", text="", icon='TRASH')
                delete_btn.preset_name = preset.name
    else:
        layout.label(text="📂 No presets saved yet", icon='INFO')
        layout.label(text="💡 Save your first preset above!", icon='LIGHT_DATA')
        layout.separator()
        info_box = layout.box()
        info_box.label(text="ℹ️ Presets are now saved in the blend file", icon='INFO')
        info_box.label(text="They will travel with your .blend file!")

def draw_shader_section(layout, props):
    """Draw shader generation controls"""
    # Create Shader Button (as requested)
    row = layout.row()
    row.scale_y = 1.5
    row.operator("text_texture.generate_shader", text="Create Shader", icon='MATERIAL')
    
    layout.separator()
    
    # Clean minimalist controls
    col = layout.column(align=True)
    col.prop(props, "shader_type", text="Type")
    
    # Show relevant options based on shader type
    if props.shader_type == 'PRINCIPLED':
        col.prop(props, "shader_connection", text="Connect")
        if props.shader_connection == 'EMISSION':
            col.prop(props, "emission_strength", text="Strength")
    elif props.shader_type == 'EMISSION':
        col.prop(props, "emission_strength", text="Strength")
    
    layout.separator()
    
    # Simple toggles
    col = layout.column(align=True)
    col.prop(props, "use_alpha", text="Use Alpha Channel")
    col.prop(props, "auto_assign_material", text="Auto-Assign to Object")
    
    # Clean status info
    if props.auto_assign_material:
        layout.separator()
        info_row = layout.row()
        info_row.scale_y = 0.8
        try:
            context = bpy.context
            if context and context.active_object and context.active_object.type in ['MESH', 'CURVE']:
                info_row.label(text=f"→ {context.active_object.name}", icon='CHECKMARK')
            else:
                info_row.label(text="Select mesh/curve object", icon='INFO')
        except:
            info_row.label(text="Select mesh/curve object", icon='INFO')

def draw_shader_options_only(layout, props):
    """Draw only shader options without the Create button - for collapsible section"""
    # Clean grid layout for shader options
    grid = layout.grid_flow(row_major=True, columns=2, align=True)
    
    grid.prop(props, "shader_type", text="Type")
    
    if props.shader_type == 'PRINCIPLED':
        grid.prop(props, "shader_connection", text="Connect")
        if props.shader_connection == 'EMISSION':
            grid.prop(props, "emission_strength", text="Strength")
    elif props.shader_type == 'EMISSION':
        grid.prop(props, "emission_strength", text="Strength")
    
    # Simple row for toggles
    layout.separator()
    row = layout.row(align=True)
    row.prop(props, "use_alpha", toggle=True)
    row.prop(props, "auto_assign_material", toggle=True)
    
    # Shadow, reflection, and backfacing toggles
    row = layout.row(align=True)
    row.prop(props, "disable_shadows", toggle=True)
    row.prop(props, "disable_reflections", toggle=True)
    row.prop(props, "disable_backfacing", toggle=True)

def draw_normal_maps_section(layout, props):
    """Draw normal map generation controls"""
    # Main toggle for normal map generation
    layout.prop(props, "generate_normal_map", text="Generate Normal Map", icon='TEXTURE')
    
    # Show controls only if normal map generation is enabled
    if props.generate_normal_map:
        layout.separator()
        
        # Normal map settings in a clean layout
        col = layout.column(align=True)
        col.prop(props, "normal_map_strength", text="Strength", slider=True)
        col.prop(props, "normal_map_blur_radius", text="Blur Radius", slider=True)
        
        layout.separator()
        layout.prop(props, "normal_map_invert", text="Invert (Emboss ↔ Deboss)")
        
        # Info box with usage tips
        layout.separator()
        info_box = layout.box()
        info_box.scale_y = 0.8
        info_box.label(text="💡 Normal maps add depth and detail", icon='INFO')
        info_box.label(text="• Higher strength = more pronounced effect")
        info_box.label(text="• Blur smooths sharp edges")
        info_box.label(text="• Invert changes raised/recessed effect")

def draw_text_fitting_section(layout, props):
    """Draw text fitting controls"""
    # Main toggle for text fitting
    layout.prop(props, "enable_text_fitting", text="Enable Text Fitting", icon='FULLSCREEN_ENTER')
    
    # Show controls only if text fitting is enabled
    if props.enable_text_fitting:
        layout.separator()
        
        # Fitting mode selection
        layout.prop(props, "text_fitting_mode", text="Fitting Mode")
        
        layout.separator()
        
        # Margin setting
        layout.prop(props, "text_fitting_margin", text="Margin", slider=True)
        
        layout.separator()
        
        # Font size limits
        col = layout.column(align=True)
        col.label(text="Font Size Limits:", icon='RESTRICT_SELECT_ON')
        row = col.row(align=True)
        row.prop(props, "min_font_size", text="Min")
        row.prop(props, "max_font_size", text="Max")
        
        # Info box with usage tips
        layout.separator()
        info_box = layout.box()
        info_box.scale_y = 0.8
        info_box.label(text="💡 Text fitting automatically sizes text", icon='INFO')
        
        if props.text_fitting_mode == 'FIT_WIDTH':
            info_box.label(text="• Fits text width to canvas width")
        elif props.text_fitting_mode == 'FIT_HEIGHT':
            info_box.label(text="• Fits text height to canvas height")
        elif props.text_fitting_mode == 'FIT_BOTH':
            info_box.label(text="• Fits both width and height (may distort)")
        elif props.text_fitting_mode == 'FIT_CONTAIN':
            info_box.label(text="• Fits entirely within canvas (preserves ratio)")
        
        info_box.label(text="• Respects min/max font size limits")

def draw_multiline_section(layout, props):
    """Draw multiline text controls"""
    # Main toggle for multiline text
    layout.prop(props, "enable_multiline", text="Enable Multiline", icon='TEXT')
    
    # Show controls only if multiline is enabled
    if props.enable_multiline:
        layout.separator()
        
        # Line height and alignment settings
        col = layout.column(align=True)
        col.prop(props, "line_height", text="Line Height", slider=True)
        col.prop(props, "text_alignment", text="Alignment")
        
        layout.separator()
        
        # Auto wrap settings
        layout.prop(props, "auto_wrap", text="Auto Wrap")
        if props.auto_wrap:
            layout.prop(props, "wrap_width", text="Wrap Width (0 = texture width)")
        
        layout.separator()
        
        # Line limits and spacing
        col = layout.column(align=True)
        col.prop(props, "max_lines", text="Max Lines (0 = unlimited)")
        col.prop(props, "line_spacing_pixels", text="Line Spacing (px)")
        
        # Info box with usage tips
        layout.separator()
        info_box = layout.box()
        info_box.scale_y = 0.8
        info_box.label(text="💡 Multiline text supports:", icon='INFO')
        info_box.label(text="• Use \\n for manual line breaks")
        info_box.label(text="• Auto-wrap fits text to width")
        info_box.label(text="• Alignment works per line")
        info_box.label(text="• Line height controls spacing")

def draw_stroke_section(layout, props):
    """Draw stroke and outline controls"""
    # Main toggle for stroke
    layout.prop(props, "enable_stroke", text="Enable Stroke", icon='MOD_OUTLINE')
    
    # Show controls only if stroke is enabled
    if props.enable_stroke:
        layout.separator()
        
        # Stroke width and color
        col = layout.column(align=True)
        col.prop(props, "stroke_width", text="Width", slider=True)
        col.prop(props, "stroke_color", text="Color")
        
        layout.separator()
        
        # Stroke position
        layout.prop(props, "stroke_position", text="Position")
        
        # Info box with usage tips
        layout.separator()
        info_box = layout.box()
        info_box.scale_y = 0.8
        info_box.label(text="💡 Stroke adds outline to text:", icon='INFO')
        
        if props.stroke_position == 'OUTER':
            info_box.label(text="• Outer: Expands text bounds outward")
        elif props.stroke_position == 'INNER':
            info_box.label(text="• Inner: Shrinks text, stroke inside")
        elif props.stroke_position == 'CENTER':
            info_box.label(text="• Center: Stroke on text edge")
        
        info_box.label(text="• Width 0 = no stroke")
        info_box.label(text="• Works with multiline text")

def draw_shadow_glow_section(layout, props):
    """Draw shadow and glow effects controls"""
    # Drop shadow section
    layout.prop(props, "enable_shadow", text="Enable Drop Shadow", icon='SHADERFX')
    
    if props.enable_shadow:
        layout.separator()
        
        # Shadow offset controls in a row
        offset_row = layout.row(align=True)
        offset_row.prop(props, "shadow_offset_x", text="X Offset")
        offset_row.prop(props, "shadow_offset_y", text="Y Offset")
        
        # Shadow properties
        col = layout.column(align=True)
        col.prop(props, "shadow_blur", text="Shadow Blur", slider=True)
        col.prop(props, "shadow_color", text="Shadow Color")
    
    layout.separator()
    
    # Glow section
    layout.prop(props, "enable_glow", text="Enable Glow", icon='LIGHT_SUN')
    
    if props.enable_glow:
        layout.separator()
        
        # Glow properties
        col = layout.column(align=True)
        col.prop(props, "glow_blur", text="Glow Blur", slider=True)
        col.prop(props, "glow_color", text="Glow Color")
    
    # Info box with usage tips
    if props.enable_shadow or props.enable_glow:
        layout.separator()
        info_box = layout.box()
        info_box.scale_y = 0.8
        info_box.label(text="💡 Shadow & Glow effects:", icon='INFO')
        
        if props.enable_shadow:
            info_box.label(text="• Drop shadow: offset + blur for depth")
        if props.enable_glow:
            info_box.label(text="• Glow: blur without offset for aura effect")
        
        info_box.label(text="• Higher blur = softer effect")
        info_box.label(text="• Alpha controls effect intensity")

def draw_tiling_section(layout, props):
    """Draw tiling system controls"""
    # Main toggle for tiling
    layout.prop(props, "enable_tiling", text="Enable Tiling", icon='GRID')
    
    # Show controls only if tiling is enabled
    if props.enable_tiling:
        layout.separator()
        
        # Tiling mode selection
        layout.prop(props, "tiling_mode", text="Tiling Mode")
        
        layout.separator()
        
        # Tile count controls
        col = layout.column(align=True)
        col.label(text="Tile Count:", icon='DUPLICATE')
        
        # Show appropriate controls based on mode
        if props.tiling_mode in ['TILE', 'MIRROR']:
            # Both X and Y for full tiling modes
            row = col.row(align=True)
            row.prop(props, "tile_count_x", text="X")
            row.prop(props, "tile_count_y", text="Y")
        elif props.tiling_mode == 'REPEAT_X':
            # Only X for horizontal repeat
            col.prop(props, "tile_count_x", text="X Count")
        elif props.tiling_mode == 'REPEAT_Y':
            # Only Y for vertical repeat
            col.prop(props, "tile_count_y", text="Y Count")
        
        layout.separator()
        
        # Canvas and spacing options
        col = layout.column(align=True)
        col.prop(props, "expand_tiling_canvas", text="Expand Canvas")
        col.prop(props, "tile_spacing", text="Tile Spacing")
        
        # Info box with usage tips
        layout.separator()
        info_box = layout.box()
        info_box.scale_y = 0.8
        info_box.label(text="💡 Tiling creates repeated patterns:", icon='INFO')
        
        if props.tiling_mode == 'TILE':
            info_box.label(text="• Tile: Simple repetition in grid pattern")
        elif props.tiling_mode == 'MIRROR':
            info_box.label(text="• Mirror: Alternating normal/mirrored tiles")
        elif props.tiling_mode == 'REPEAT_X':
            info_box.label(text="• Repeat X: Horizontal tiling only")
        elif props.tiling_mode == 'REPEAT_Y':
            info_box.label(text="• Repeat Y: Vertical tiling only")
        
        if props.expand_tiling_canvas:
            info_box.label(text="• Expand Canvas: Enlarges texture to fit tiles")
        else:
            info_box.label(text="• Scale Down: Fits tiles in original size")

def draw_batch_processing_section(layout, props):
    """Draw batch processing controls"""
    # Word list management
    layout.label(text="Word List:", icon='TEXT')
    layout.prop(props, "batch_word_list", text="")
    
    # Import/Export buttons for word list
    row = layout.row(align=True)
    row.operator("text_texture.import_word_list", text="Import List", icon='IMPORT')
    row.operator("text_texture.export_word_list", text="Export List", icon='EXPORT')
    
    layout.separator()
    
    # Output settings
    layout.label(text="Output Settings:", icon='FILE_FOLDER')
    layout.prop(props, "batch_output_folder", text="Output Folder")
    
    row = layout.row(align=True)
    row.prop(props, "batch_filename_prefix", text="Prefix")
    row.prop(props, "batch_auto_save", text="Auto Save", toggle=True)
    
    layout.separator()
    
    # Template management
    layout.label(text="Template Management:", icon='PRESET')
    layout.prop(props, "batch_template_name", text="Template Name")
    
    row = layout.row(align=True)
    row.operator("text_texture.save_batch_template", text="Save Template", icon='FILE_NEW')
    row.operator("text_texture.load_batch_template", text="Load Template", icon='FILE_FOLDER')
    
    layout.separator()
    
    # Progress display (show only when processing)
    if props.batch_progress > 0 and props.batch_progress < 100:
        layout.prop(props, "batch_progress", text="Progress", slider=True)
        layout.separator()
    
    # Batch generation button
    batch_row = layout.row()
    batch_row.scale_y = 1.5
    batch_row.operator("text_texture.batch_generate", text="Generate Batch", icon='PLAY')
    
    # Info box with usage tips
    layout.separator()
    info_box = layout.box()
    info_box.scale_y = 0.8
    info_box.label(text="💡 Batch Processing:", icon='INFO')
    info_box.label(text="• Enter comma-separated words/phrases")
    info_box.label(text="• Templates save all current settings")
    info_box.label(text="• Auto-save creates PNG files in output folder")
    info_box.label(text="• Progress bar shows generation status")

# ============================================================================
# MENUS
# ============================================================================

def menu_func_node_editor(self, context):
    """Add menu item to Node Editor"""
    self.layout.separator()
    self.layout.label(text="Text Texture Generator", icon='TEXT')
    self.layout.operator("text_texture.generate", text="Generate Text Texture", icon='IMAGE_DATA')

def menu_func_view3d(self, context):
    """Add menu item to 3D View"""
    self.layout.separator()
    self.layout.operator("text_texture.generate", text="Generate Text Texture", icon='TEXT')

def menu_func_image(self, context):
    """Add to Image menu"""
    self.layout.separator()
    self.layout.operator("text_texture.generate", text="Generate Text Texture", icon='TEXT')

# ============================================================================
# UI PANELS
# ============================================================================

class TEXT_TEXTURE_PT_panel(Panel):
    """Main Panel in Shader Editor"""
    bl_label = "Text Texture Generator"
    bl_idname = "TEXT_TEXTURE_PT_panel"
    bl_space_type = 'NODE_EDITOR'
    bl_region_type = 'UI'
    bl_category = "Tool"
    
    @classmethod
    def poll(cls, context):
        # DEBUG: Log poll method calls
        print(f"[TTG DEBUG] Panel poll called - space_data: {context.space_data}")
        print(f"[TTG DEBUG] space_data.type: {context.space_data.type if context.space_data else 'None'}")
        if hasattr(context.space_data, 'tree_type'):
            print(f"[TTG DEBUG] tree_type: {context.space_data.tree_type}")
        else:
            print("[TTG DEBUG] No tree_type attribute")
        
        # More permissive poll condition to ensure panel shows up
        if context.space_data.type == 'NODE_EDITOR':
            if hasattr(context.space_data, 'tree_type'):
                result = context.space_data.tree_type == 'ShaderNodeTree'
                print(f"[TTG DEBUG] Poll result: {result}")
                return result
            else:
                # Allow panel even without tree_type (sometimes happens)
                print("[TTG DEBUG] Poll result: True (no tree_type but NODE_EDITOR)")
                return True
        
        print("[TTG DEBUG] Poll result: False (not NODE_EDITOR)")
        return False
    
    def draw(self, context):
        layout = self.layout
        props = context.scene.text_texture_props
        
        # TOP LEVEL - Always Visible Controls
        
        # Text Input
        box = layout.box()
        box.label(text="Text:", icon='TEXT')
        draw_text_input_section(box, props)
        
        
        # Generate Button
        layout.separator(factor=0.5)
        draw_generate_button(layout)
        
        # CREATE SHADER BUTTON AT TOP LEVEL - AS REQUESTED
        layout.separator(factor=0.5)
        create_shader_row = layout.row()
        create_shader_row.scale_y = 1.5
        create_shader_row.operator("text_texture.generate_shader", text="Create Shader", icon='MATERIAL')
        
        # Preview Button at top level
        layout.separator(factor=0.5)
        preview_row = layout.row()
        preview_row.scale_y = 1.5
        preview_row.operator("text_texture.view_preview_zoom", text="Open Preview Window", icon='WINDOW')
        
        layout.separator(factor=1.2)
        
        # COLLAPSIBLE SECTIONS
        
        # Multiline Section (collapsed by default)
        multiline_section = draw_collapsible_header(layout, "expand_multiline", "Multiline Text", 'TEXT')
        if multiline_section:
            draw_multiline_section(multiline_section, props)
        
        # Shader Section (collapsed by default) - ONLY OPTIONS, NOT THE BUTTON
        shader_section = draw_collapsible_header(layout, "expand_shader", "Shader Options", 'SETTINGS')
        if shader_section:
            draw_shader_options_only(shader_section, props)
        
        # Image Overlays Section (collapsed by default)
        overlay_section = draw_collapsible_header(layout, "expand_image_overlays", "Image Overlays", 'IMAGE_DATA')
        if overlay_section:
            draw_image_overlays_section(overlay_section, props)
        
        # Positioning & Layout Section (collapsed by default)
        position_section = draw_collapsible_header(layout, "expand_positioning", "Positioning & Layout", 'ANCHOR_CENTER')
        if position_section:
            draw_positioning_section(position_section, props)
        
        # Font Settings Section (collapsed by default)
        font_section = draw_collapsible_header(layout, "expand_font_settings", "Font Settings", 'FONT_DATA')
        if font_section:
            draw_font_settings_section(font_section, props)
        
        # Text Fitting Section (collapsed by default)
        text_fitting_section = draw_collapsible_header(layout, "expand_text_fitting", "Text Fitting", 'FULLSCREEN_ENTER')
        if text_fitting_section:
            draw_text_fitting_section(text_fitting_section, props)
        
        # Stroke Section (collapsed by default)
        stroke_section = draw_collapsible_header(layout, "expand_stroke", "Stroke & Outlines", 'MOD_OUTLINE')
        if stroke_section:
            draw_stroke_section(stroke_section, props)
        
        # Shadow & Glow Section (collapsed by default)
        shadow_glow_section = draw_collapsible_header(layout, "expand_shadow_glow", "Shadow & Glow Effects", 'SHADERFX')
        if shadow_glow_section:
            draw_shadow_glow_section(shadow_glow_section, props)
        
        # Tiling Section (collapsed by default)
        tiling_section = draw_collapsible_header(layout, "expand_tiling", "Tiling System", 'GRID')
        if tiling_section:
            draw_tiling_section(tiling_section, props)
        
        # Normal Maps Section (collapsed by default)
        normal_maps_section = draw_collapsible_header(layout, "expand_normal_maps", "Normal Maps", 'TEXTURE')
        if normal_maps_section:
            draw_normal_maps_section(normal_maps_section, props)
        
        # Colors Section (collapsed by default)
        colors_section = draw_collapsible_header(layout, "expand_colors", "Colors", 'COLOR')
        if colors_section:
            draw_colors_section(colors_section, props)
        
        # Presets Section (collapsed by default)
        presets_section = draw_collapsible_header(layout, "expand_presets", "Presets", 'PRESET')
        if presets_section:
            draw_presets_section(presets_section, props)

class TEXT_TEXTURE_PT_panel_3d(Panel):
    """3D View Panel - Access from sidebar (N key)"""
    bl_label = "Text Texture Generator"
    bl_idname = "TEXT_TEXTURE_PT_panel_3d"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = "Tool"
    bl_options = {'DEFAULT_CLOSED'}
    
    def draw(self, context):
        layout = self.layout
        props = context.scene.text_texture_props
        
        # TOP LEVEL - Always Visible Controls
        
        # Text Input
        box = layout.box()
        box.label(text="Text:", icon='TEXT')
        draw_text_input_section(box, props)
        
        
        # Generate Button
        layout.separator(factor=0.5)
        draw_generate_button(layout)
        
        # CREATE SHADER BUTTON AT TOP LEVEL - AS REQUESTED
        layout.separator(factor=0.5)
        create_shader_row = layout.row()
        create_shader_row.scale_y = 1.5
        create_shader_row.operator("text_texture.generate_shader", text="Create Shader", icon='MATERIAL')
        
        # Preview Button at top level
        layout.separator(factor=0.5)
        preview_row = layout.row()
        preview_row.scale_y = 1.5
        preview_row.operator("text_texture.view_preview_zoom", text="Open Preview Window", icon='WINDOW')
        
        layout.separator(factor=1.2)
        
        # COLLAPSIBLE SECTIONS
        
        # Multiline Section (collapsed by default)
        multiline_section = draw_collapsible_header(layout, "expand_multiline", "Multiline Text", 'TEXT')
        if multiline_section:
            draw_multiline_section(multiline_section, props)
        
        # Shader Section (collapsed by default) - ONLY OPTIONS, NOT THE BUTTON
        shader_section = draw_collapsible_header(layout, "expand_shader", "Shader Options", 'SETTINGS')
        if shader_section:
            draw_shader_options_only(shader_section, props)
        
        # Image Overlays Section (collapsed by default)
        overlay_section = draw_collapsible_header(layout, "expand_image_overlays", "Image Overlays", 'IMAGE_DATA')
        if overlay_section:
            draw_image_overlays_section(overlay_section, props)
        
        # Positioning & Layout Section (collapsed by default)
        position_section = draw_collapsible_header(layout, "expand_positioning", "Positioning & Layout", 'ANCHOR_CENTER')
        if position_section:
            draw_positioning_section(position_section, props)
        
        # Font Settings Section (collapsed by default)
        font_section = draw_collapsible_header(layout, "expand_font_settings", "Font Settings", 'FONT_DATA')
        if font_section:
            draw_font_settings_section(font_section, props)
        
        # Text Fitting Section (collapsed by default)
        text_fitting_section = draw_collapsible_header(layout, "expand_text_fitting", "Text Fitting", 'FULLSCREEN_ENTER')
        if text_fitting_section:
            draw_text_fitting_section(text_fitting_section, props)
        
        # Stroke Section (collapsed by default)
        stroke_section = draw_collapsible_header(layout, "expand_stroke", "Stroke & Outlines", 'MOD_OUTLINE')
        if stroke_section:
            draw_stroke_section(stroke_section, props)
        
        # Shadow & Glow Section (collapsed by default)
        shadow_glow_section = draw_collapsible_header(layout, "expand_shadow_glow", "Shadow & Glow Effects", 'SHADERFX')
        if shadow_glow_section:
            draw_shadow_glow_section(shadow_glow_section, props)
        
        # Tiling Section (collapsed by default)
        tiling_section = draw_collapsible_header(layout, "expand_tiling", "Tiling System", 'GRID')
        if tiling_section:
            draw_tiling_section(tiling_section, props)
        
        # Batch Processing Section (collapsed by default)
        batch_section = draw_collapsible_header(layout, "expand_batch_processing", "Batch Processing", 'LINENUMBERS_ON')
        if batch_section:
            draw_batch_processing_section(batch_section, props)
        
        # Normal Maps Section (collapsed by default)
        normal_maps_section = draw_collapsible_header(layout, "expand_normal_maps", "Normal Maps", 'TEXTURE')
        if normal_maps_section:
            draw_normal_maps_section(normal_maps_section, props)
        
        # Colors Section (collapsed by default)
        colors_section = draw_collapsible_header(layout, "expand_colors", "Colors", 'COLOR')
        if colors_section:
            draw_colors_section(colors_section, props)
        
        # Presets Section (collapsed by default)
        presets_section = draw_collapsible_header(layout, "expand_presets", "Presets", 'PRESET')
        if presets_section:
            draw_presets_section(presets_section, props)

class TEXT_TEXTURE_PT_panel_properties(Panel):
    """Properties Panel"""
    bl_label = "Text Texture Generator"
    bl_idname = "TEXT_TEXTURE_PT_panel_properties"
    bl_space_type = 'PROPERTIES'
    bl_region_type = 'WINDOW'
    bl_context = "material"
    bl_category = "Tool"
    
    def draw(self, context):
        layout = self.layout
        props = context.scene.text_texture_props
        
        # TOP LEVEL - Always Visible Controls
        
        # Text Input
        box = layout.box()
        box.label(text="Text:", icon='TEXT')
        draw_text_input_section(box, props)
        
        
        # Generate Button
        layout.separator(factor=0.5)
        draw_generate_button(layout)
        
        # Preview Button at top level
        layout.separator(factor=0.5)
        preview_row = layout.row()
        preview_row.scale_y = 1.5
        preview_row.operator("text_texture.view_preview_zoom", text="Open Preview Window", icon='WINDOW')
        
        layout.separator(factor=1.2)
        
        # COLLAPSIBLE SECTIONS
        
        # Multiline Section (collapsed by default)
        multiline_section = draw_collapsible_header(layout, "expand_multiline", "Multiline Text", 'TEXT')
        if multiline_section:
            draw_multiline_section(multiline_section, props)
        
        # Image Overlays Section (collapsed by default)
        overlay_section = draw_collapsible_header(layout, "expand_image_overlays", "Image Overlays", 'IMAGE_DATA')
        if overlay_section:
            draw_image_overlays_section(overlay_section, props)
        
        # Positioning & Layout Section (collapsed by default)
        position_section = draw_collapsible_header(layout, "expand_positioning", "Positioning & Layout", 'ANCHOR_CENTER')
        if position_section:
            draw_positioning_section(position_section, props)
        
        # Font Settings Section (collapsed by default)
        font_section = draw_collapsible_header(layout, "expand_font_settings", "Font Settings", 'FONT_DATA')
        if font_section:
            draw_font_settings_section(font_section, props)
        
        # Text Fitting Section (collapsed by default)
        text_fitting_section = draw_collapsible_header(layout, "expand_text_fitting", "Text Fitting", 'FULLSCREEN_ENTER')
        if text_fitting_section:
            draw_text_fitting_section(text_fitting_section, props)
        
        # Stroke Section (collapsed by default)
        stroke_section = draw_collapsible_header(layout, "expand_stroke", "Stroke & Outlines", 'MOD_OUTLINE')
        if stroke_section:
            draw_stroke_section(stroke_section, props)
        
        # Shadow & Glow Section (collapsed by default)
        shadow_glow_section = draw_collapsible_header(layout, "expand_shadow_glow", "Shadow & Glow Effects", 'SHADERFX')
        if shadow_glow_section:
            draw_shadow_glow_section(shadow_glow_section, props)
        
        # Tiling Section (collapsed by default)
        tiling_section = draw_collapsible_header(layout, "expand_tiling", "Tiling System", 'GRID')
        if tiling_section:
            draw_tiling_section(tiling_section, props)
        
        # Batch Processing Section (collapsed by default)
        batch_section = draw_collapsible_header(layout, "expand_batch_processing", "Batch Processing", 'LINENUMBERS_ON')
        if batch_section:
            draw_batch_processing_section(batch_section, props)
        
        # Normal Maps Section (collapsed by default)
        normal_maps_section = draw_collapsible_header(layout, "expand_normal_maps", "Normal Maps", 'TEXTURE')
        if normal_maps_section:
            draw_normal_maps_section(normal_maps_section, props)
        
        # Colors Section (collapsed by default)
        colors_section = draw_collapsible_header(layout, "expand_colors", "Colors", 'COLOR')
        if colors_section:
            draw_colors_section(colors_section, props)
        
        # Presets Section (collapsed by default)
        presets_section = draw_collapsible_header(layout, "expand_presets", "Presets", 'PRESET')
        if presets_section:
            draw_presets_section(presets_section, props)

# ============================================================================
# REGISTRATION
# ============================================================================

classes = (
    TEXT_TEXTURE_ImageOverlay,
    TEXT_TEXTURE_Preset,
    TEXT_TEXTURE_Properties,
    TEXT_TEXTURE_OT_generate,
    TEXT_TEXTURE_OT_generate_shader,
    TEXT_TEXTURE_OT_refresh_preview,
    TEXT_TEXTURE_OT_view_preview_zoom,
    TEXT_TEXTURE_OT_add_overlay,
    TEXT_TEXTURE_OT_remove_overlay,
    TEXT_TEXTURE_OT_open_panel,
    TEXT_TEXTURE_OT_save_preset,
    TEXT_TEXTURE_OT_save_preset_enter,
    TEXT_TEXTURE_OT_set_anchor_point,
    TEXT_TEXTURE_OT_sync_margins,
    TEXT_TEXTURE_OT_load_preset,
    TEXT_TEXTURE_OT_delete_preset,
    TEXT_TEXTURE_OT_refresh_fonts,
    TEXT_TEXTURE_OT_refresh_presets,
    TEXT_TEXTURE_OT_export_presets,
    TEXT_TEXTURE_OT_import_presets,
    TEXT_TEXTURE_OT_show_migration_report,
    TEXT_TEXTURE_OT_batch_generate,
    TEXT_TEXTURE_OT_import_word_list,
    TEXT_TEXTURE_OT_export_word_list,
    TEXT_TEXTURE_OT_save_batch_template,
    TEXT_TEXTURE_OT_load_batch_template,
    TEXT_TEXTURE_PT_panel,
    TEXT_TEXTURE_PT_panel_3d,
    TEXT_TEXTURE_PT_panel_properties,
)

def register():
    print("[TTG DEBUG] ========== ADDON REGISTRATION START ==========")
    for cls in classes:
        print(f"[TTG DEBUG] Registering class: {cls.__name__}")
        try:
            bpy.utils.register_class(cls)
            print(f"[TTG DEBUG] Successfully registered: {cls.__name__}")
        except Exception as e:
            print(f"[TTG DEBUG] FAILED to register {cls.__name__}: {e}")
    
    print("[TTG DEBUG] Registering scene properties...")
    bpy.types.Scene.text_texture_props = PointerProperty(type=TEXT_TEXTURE_Properties)
    print("[TTG DEBUG] Scene properties registered successfully")
    
    # CRITICAL FIX: Force UI refresh after registration
    print("[TTG DEBUG] Forcing UI refresh...")
    try:
        # Update all areas to ensure panels are properly displayed
        for window in bpy.context.window_manager.windows:
            for area in window.screen.areas:
                area.tag_redraw()
        print("[TTG DEBUG] UI refresh completed successfully")
    except Exception as e:
        print(f"[TTG DEBUG] UI refresh failed: {e}")
    
    # Add to menus
    bpy.types.NODE_MT_add.append(menu_func_node_editor)
    bpy.types.VIEW3D_MT_add.append(menu_func_view3d)
    bpy.types.VIEW3D_MT_mesh_add.append(menu_func_view3d)
    bpy.types.NODE_MT_node.append(menu_func_node_editor)
    
    # Add to Image menu if available
    if hasattr(bpy.types, 'IMAGE_MT_image'):
        bpy.types.IMAGE_MT_image.append(menu_func_image)
    
    # Initialize presets after registration to prevent race conditions
    # Use a timer to ensure the scene is ready
    def delayed_preset_init():
        initialize_presets()
        return None  # Stop timer
    
    print("[TTG DEBUG] Registration completed - scheduling preset initialization")
    bpy.app.timers.register(delayed_preset_init, first_interval=0.1)
    
    # Add handler to reload presets when blend file changes
    @bpy.app.handlers.persistent
    def on_file_load(dummy):
        """Reload presets when a blend file is loaded"""
        try:
            print("[TTG] Reloading presets after file load...")
            def delayed_reload():
                initialize_presets()
                return None  # Stop timer
            bpy.app.timers.register(delayed_reload, first_interval=0.1)
        except Exception as e:
            print(f"[TTG] Error reloading presets: {e}")
    
    # Register the file load handler
    if on_file_load not in bpy.app.handlers.load_post:
        bpy.app.handlers.load_post.append(on_file_load)
    
    print("[TTG DEBUG] ========== ADDON REGISTRATION END ==========")

def unregister():
    # Clean up preview
    try:
        if bpy.context.scene:
            props = bpy.context.scene.text_texture_props
            if props and props.preview_image:
                bpy.data.images.remove(props.preview_image)
    except Exception:
        pass
    
    if "TextTexturePreview" in bpy.data.images:
        bpy.data.images.remove(bpy.data.images["TextTexturePreview"])
    
    # Remove file load handler
    try:
        for handler in bpy.app.handlers.load_post[:]:
            if handler.__name__ == 'on_file_load' and 'TTG' in str(handler):
                bpy.app.handlers.load_post.remove(handler)
    except Exception as e:
        print(f"Error removing file load handler: {e}")
    
    # Remove from menus
    bpy.types.NODE_MT_add.remove(menu_func_node_editor)
    bpy.types.VIEW3D_MT_add.remove(menu_func_view3d)
    bpy.types.VIEW3D_MT_mesh_add.remove(menu_func_view3d)
    bpy.types.NODE_MT_node.remove(menu_func_node_editor)
    
    if hasattr(bpy.types, 'IMAGE_MT_image'):
        bpy.types.IMAGE_MT_image.remove(menu_func_image)
    
    for cls in reversed(classes):
        bpy.utils.unregister_class(cls)
    
    del bpy.types.Scene.text_texture_props

if __name__ == "__main__":
    register()