{
    "sourceFile": "configuration.py",
    "activeCommit": 0,
    "commits": [
        {
            "activePatchIndex": 2,
            "patches": [
                {
                    "date": 1745314026627,
                    "content": "Index: \n===================================================================\n--- \n+++ \n"
                },
                {
                    "date": 1745315881107,
                    "content": "Index: \n===================================================================\n--- \n+++ \n@@ -90,9 +90,8 @@\n         self.preset_name = preset_name\n         self._core_settings: dict = self._load_core_config()\n         self._preset_settings: dict = self._load_preset(preset_name)\n         self._validate_configs()\n-        log.debug(f\"Loaded source_naming config: {self._preset_settings.get('source_naming', 'Not Found')}\") # DEBUG: Log loaded source_naming\n         self._compile_regex_patterns() # Compile regex after validation\n         log.info(f\"Configuration loaded successfully using preset: '{self.preset_name}'\")\n \n \n"
                },
                {
                    "date": 1745506645928,
                    "content": "Index: \n===================================================================\n--- \n+++ \n@@ -1,418 +1,418 @@\n-# configuration.py\n-\n-import json\n-import os\n-import importlib.util\n-from pathlib import Path\n-import logging\n-import re # Import the regex module\n-\n-log = logging.getLogger(__name__) # Use logger defined in main.py\n-\n-# --- Constants ---\n-# Assumes config.py and presets/ are relative to this file's location\n-BASE_DIR = Path(__file__).parent\n-CORE_CONFIG_PATH = BASE_DIR / \"config.py\"\n-PRESETS_DIR = BASE_DIR / \"presets\"\n-\n-# --- Custom Exception ---\n-class ConfigurationError(Exception):\n-    \"\"\"Custom exception for configuration loading errors.\"\"\"\n-    pass\n-\n-# --- Helper Functions ---\n-def _get_base_map_type(target_map_string: str) -> str:\n-   \"\"\"Extracts the base map type (e.g., 'COL') from a potentially numbered string ('COL-1').\"\"\"\n-   # Use regex to find the leading alphabetical part\n-   match = re.match(r\"([a-zA-Z]+)\", target_map_string)\n-   if match:\n-       return match.group(1).upper()\n-   # Fallback if no number suffix or unexpected format\n-   return target_map_string.upper()\n-\n-def _fnmatch_to_regex(pattern: str) -> str:\n-    \"\"\"\n-    Converts an fnmatch pattern to a regex pattern string.\n-    Handles basic wildcards (*, ?) and escapes other regex special characters.\n-    \"\"\"\n-    i, n = 0, len(pattern)\n-    res = ''\n-    while i < n:\n-        c = pattern[i]\n-        i = i + 1\n-        if c == '*':\n-            res = res + '.*'\n-        elif c == '?':\n-            res = res + '.'\n-        elif c == '[':\n-            j = i\n-            if j < n and pattern[j] == '!':\n-                j = j + 1\n-            if j < n and pattern[j] == ']':\n-                j = j + 1\n-            while j < n and pattern[j] != ']':\n-                j = j + 1\n-            if j >= n:\n-                res = res + '\\\\['\n-            else:\n-                stuff = pattern[i:j].replace('\\\\','\\\\\\\\')\n-                i = j + 1\n-                if stuff[0] == '!':\n-                    stuff = '^' + stuff[1:]\n-                elif stuff[0] == '^':\n-                    stuff = '\\\\' + stuff\n-                res = '%s[%s]' % (res, stuff)\n-        else:\n-            res = res + re.escape(c)\n-    # We want to find the pattern anywhere in the filename for flexibility,\n-    # so don't anchor with ^$ by default. Anchoring might be needed for specific cases.\n-    # Let's return the core pattern and let the caller decide on anchoring if needed.\n-    # For filename matching, we usually want to find the pattern, not match the whole string.\n-    return res\n-\n-\n-# --- Configuration Class ---\n-class Configuration:\n-    \"\"\"\n-    Loads and provides access to core settings combined with a specific preset.\n-    \"\"\"\n-    def __init__(self, preset_name: str):\n-        \"\"\"\n-        Loads core config and the specified preset file.\n-\n-        Args:\n-            preset_name: The name of the preset (without .json extension).\n-\n-        Raises:\n-            ConfigurationError: If core config or preset cannot be loaded/validated.\n-        \"\"\"\n-        log.debug(f\"Initializing Configuration with preset: '{preset_name}'\")\n-        self.preset_name = preset_name\n-        self._core_settings: dict = self._load_core_config()\n-        self._preset_settings: dict = self._load_preset(preset_name)\n-        self._validate_configs()\n-        self._compile_regex_patterns() # Compile regex after validation\n-        log.info(f\"Configuration loaded successfully using preset: '{self.preset_name}'\")\n-\n-\n-    def _compile_regex_patterns(self):\n-        \"\"\"Compiles regex patterns from config/preset for faster matching.\"\"\"\n-        log.debug(\"Compiling regex patterns from configuration...\")\n-        self.compiled_extra_regex: list[re.Pattern] = []\n-        self.compiled_model_regex: list[re.Pattern] = []\n-        self.compiled_bit_depth_regex_map: dict[str, re.Pattern] = {}\n-        # Map: base_map_type -> list of tuples: (compiled_regex, original_keyword, rule_index)\n-        self.compiled_map_keyword_regex: dict[str, list[tuple[re.Pattern, str, int]]] = {}\n-        # Store the original rule order for priority checking later if needed (can be removed if index is stored in tuple)\n-        # self._map_type_rule_order: list[dict] = [] # Keep for now, might be useful elsewhere\n-\n-        # Compile Extra Patterns (case-insensitive)\n-        for pattern in self.move_to_extra_patterns:\n-            try:\n-                # Use the raw fnmatch pattern directly if it's simple enough for re.search\n-                # Or convert using helper if needed. Let's try direct search first.\n-                # We want to find the pattern *within* the filename.\n-                regex_str = _fnmatch_to_regex(pattern) # Convert wildcards\n-                self.compiled_extra_regex.append(re.compile(regex_str, re.IGNORECASE))\n-            except re.error as e:\n-                log.warning(f\"Failed to compile 'extra' regex pattern '{pattern}': {e}. Skipping pattern.\")\n-\n-        # Compile Model Patterns (case-insensitive)\n-        model_patterns = self.asset_category_rules.get('model_patterns', [])\n-        for pattern in model_patterns:\n-             try:\n-                 regex_str = _fnmatch_to_regex(pattern)\n-                 self.compiled_model_regex.append(re.compile(regex_str, re.IGNORECASE))\n-             except re.error as e:\n-                 log.warning(f\"Failed to compile 'model' regex pattern '{pattern}': {e}. Skipping pattern.\")\n-\n-        # Compile Bit Depth Variant Patterns (case-sensitive recommended)\n-        for map_type, pattern in self.source_bit_depth_variants.items():\n-            try:\n-                # These often rely on specific suffixes, so anchoring might be better?\n-                # Let's stick to the converted pattern for now, assuming it ends with suffix.\n-                regex_str = _fnmatch_to_regex(pattern) # e.g., \".*_DISP16.*\"\n-                # If the original pattern ended with *, remove the trailing '.*' for suffix matching\n-                if pattern.endswith('*'):\n-                    regex_str = regex_str.removesuffix('.*') # e.g., \".*_DISP16\"\n-                    # Fallback for < 3.9: if regex_str.endswith('.*'): regex_str = regex_str[:-2]\n-\n-                # Use the fnmatch-converted regex directly, allowing matches anywhere in the filename\n-                # This is less strict than anchoring to the end with \\\\.[^.]+$\n-                final_regex_str = regex_str # Use the result from _fnmatch_to_regex\n-                self.compiled_bit_depth_regex_map[map_type] = re.compile(final_regex_str, re.IGNORECASE) # Added IGNORECASE\n-                log.debug(f\"  Compiled bit depth variant for '{map_type}' as regex (IGNORECASE): {final_regex_str}\")\n-            except re.error as e:\n-                log.warning(f\"Failed to compile 'bit depth' regex pattern '{pattern}' for map type '{map_type}': {e}. Skipping pattern.\")\n-\n-        # Compile Map Type Keywords (case-insensitive) based on the new structure\n-        separator = re.escape(self.source_naming_separator) # Escape separator for regex\n-        # Use defaultdict to easily append to lists for the same base type\n-        from collections import defaultdict\n-        temp_compiled_map_regex = defaultdict(list)\n-\n-        for rule_index, mapping_rule in enumerate(self.map_type_mapping):\n-            # Validate rule structure (dictionary with target_type and keywords)\n-            if not isinstance(mapping_rule, dict) or \\\n-               'target_type' not in mapping_rule or \\\n-               'keywords' not in mapping_rule or \\\n-               not isinstance(mapping_rule['keywords'], list):\n-                log.warning(f\"Skipping invalid map_type_mapping rule at index {rule_index}: {mapping_rule}. Expected dict with 'target_type' and 'keywords' list.\")\n-                continue\n-\n-            target_type = mapping_rule['target_type'].upper() # Use the base type directly\n-            source_keywords = mapping_rule['keywords']\n-\n-            # Store the rule for potential priority access later (optional, index is now in tuple)\n-            # self._map_type_rule_order.append(mapping_rule)\n-\n-            if target_type not in self.standard_map_types:\n-                 log.warning(f\"Map rule {rule_index} uses target_type '{target_type}' which is not in core config STANDARD_MAP_TYPES. Classification might be incomplete.\")\n-                 # Continue processing anyway, as it might be intended for merging etc.\n-\n-            # Compile keywords for this rule and store with context\n-            for keyword in source_keywords:\n-                if not isinstance(keyword, str):\n-                     log.warning(f\"Skipping non-string keyword '{keyword}' in rule {rule_index} for target '{target_type}'.\")\n-                     continue\n-                try:\n-                    # Match keyword potentially surrounded by separators or start/end\n-                    # Handle potential wildcards within the keyword using fnmatch conversion\n-                    kw_regex_part = _fnmatch_to_regex(keyword)\n-                    # Build regex to match the keyword part, anchored by separators or string boundaries\n-                    # Use non-capturing groups (?:...)\n-                    # Capture the keyword part itself for potential use later if needed (group 1)\n-                    regex_str = rf\"(?:^|{separator})({kw_regex_part})(?:$|{separator})\"\n-                    compiled_regex = re.compile(regex_str, re.IGNORECASE)\n-                    # Append tuple: (compiled_regex, original_keyword, rule_index)\n-                    temp_compiled_map_regex[target_type].append((compiled_regex, keyword, rule_index))\n-                    log.debug(f\"  Compiled keyword '{keyword}' (rule {rule_index}) for target '{target_type}' as regex: {regex_str}\")\n-                except re.error as e:\n-                    log.warning(f\"Failed to compile map keyword regex '{keyword}' for target type '{target_type}': {e}. Skipping keyword.\")\n-\n-        # Assign the compiled regex dictionary\n-        self.compiled_map_keyword_regex = dict(temp_compiled_map_regex)\n-        log.debug(f\"Compiled map keyword regex keys: {list(self.compiled_map_keyword_regex.keys())}\")\n-\n-        log.debug(\"Finished compiling regex patterns.\")\n-\n-\n-    def _load_core_config(self) -> dict:\n-        \"\"\"Loads settings from the core config.py file.\"\"\"\n-        log.debug(f\"Loading core config from: {CORE_CONFIG_PATH}\")\n-        if not CORE_CONFIG_PATH.is_file():\n-            raise ConfigurationError(f\"Core configuration file not found: {CORE_CONFIG_PATH}\")\n-        try:\n-            spec = importlib.util.spec_from_file_location(\"core_config\", CORE_CONFIG_PATH)\n-            if spec is None or spec.loader is None:\n-                 raise ConfigurationError(f\"Could not create module spec for {CORE_CONFIG_PATH}\")\n-            core_config_module = importlib.util.module_from_spec(spec)\n-            # Define default values for core settings in case they are missing in config.py\n-            default_core_settings = {\n-                'TARGET_FILENAME_PATTERN': \"{base_name}_{map_type}_{resolution}.{ext}\",\n-                'STANDARD_MAP_TYPES': [],\n-                'EXTRA_FILES_SUBDIR': \"Extra\",\n-                'METADATA_FILENAME': \"metadata.json\",\n-                'IMAGE_RESOLUTIONS': {},\n-                'ASPECT_RATIO_DECIMALS': 2,\n-                'MAP_BIT_DEPTH_RULES': {\"DEFAULT\": \"respect\"},\n-                'OUTPUT_FORMAT_16BIT_PRIMARY': \"png\",\n-                'OUTPUT_FORMAT_16BIT_FALLBACK': \"png\",\n-                'OUTPUT_FORMAT_8BIT': \"png\",\n-                'MAP_MERGE_RULES': [],\n-                'CALCULATE_STATS_RESOLUTION': \"1K\",\n-                'DEFAULT_ASSET_CATEGORY': \"Texture\",\n-                'TEMP_DIR_PREFIX': \"_PROCESS_ASSET_\",\n-                # --- Additions ---\n-                'JPG_QUALITY': 95, # Default JPG quality\n-                'RESOLUTION_THRESHOLD_FOR_JPG': 4096, # Default threshold\n-                'RESPECT_VARIANT_MAP_TYPES': [], # Default for map types that always get suffix\n-                'FORCE_LOSSLESS_MAP_TYPES': [] # Default for map types that must be lossless\n-            }\n-            # Load attributes from module, using defaults if missing\n-            settings = default_core_settings.copy()\n-            spec.loader.exec_module(core_config_module)\n-            for name in default_core_settings:\n-                 if hasattr(core_config_module, name):\n-                      settings[name] = getattr(core_config_module, name)\n-\n-            log.debug(f\"Core config loaded successfully.\")\n-            return settings\n-        except Exception as e:\n-            raise ConfigurationError(f\"Failed to load core configuration from {CORE_CONFIG_PATH}: {e}\")\n-\n-    def _load_preset(self, preset_name: str) -> dict:\n-        \"\"\"Loads the specified preset JSON file.\"\"\"\n-        log.debug(f\"Loading preset: '{preset_name}' from {PRESETS_DIR}\")\n-        if not PRESETS_DIR.is_dir():\n-             raise ConfigurationError(f\"Presets directory not found: {PRESETS_DIR}\")\n-\n-        preset_file = PRESETS_DIR / f\"{preset_name}.json\"\n-        if not preset_file.is_file():\n-            raise ConfigurationError(f\"Preset file not found: {preset_file}\")\n-\n-        try:\n-            with open(preset_file, 'r', encoding='utf-8') as f:\n-                preset_data = json.load(f)\n-            log.debug(f\"Preset '{preset_name}' loaded successfully.\")\n-            return preset_data\n-        except json.JSONDecodeError as e:\n-            raise ConfigurationError(f\"Failed to parse preset file {preset_file}: Invalid JSON - {e}\")\n-        except Exception as e:\n-            raise ConfigurationError(f\"Failed to read preset file {preset_file}: {e}\")\n-\n-    def _validate_configs(self):\n-        \"\"\"Performs basic validation checks on loaded settings.\"\"\"\n-        log.debug(\"Validating loaded configurations...\")\n-        # Preset validation\n-        required_preset_keys = [\n-            \"preset_name\", \"supplier_name\", \"source_naming\", \"map_type_mapping\",\n-            \"asset_category_rules\", \"archetype_rules\", \"move_to_extra_patterns\"\n-        ]\n-        for key in required_preset_keys:\n-            if key not in self._preset_settings:\n-                raise ConfigurationError(f\"Preset '{self.preset_name}' is missing required key: '{key}'.\")\n-\n-        # Validate map_type_mapping structure (new format)\n-        if not isinstance(self._preset_settings['map_type_mapping'], list):\n-             raise ConfigurationError(f\"Preset '{self.preset_name}': 'map_type_mapping' must be a list.\")\n-        for index, rule in enumerate(self._preset_settings['map_type_mapping']):\n-             if not isinstance(rule, dict):\n-                  raise ConfigurationError(f\"Preset '{self.preset_name}': Rule at index {index} in 'map_type_mapping' must be a dictionary.\")\n-             if 'target_type' not in rule or not isinstance(rule['target_type'], str):\n-                  raise ConfigurationError(f\"Preset '{self.preset_name}': Rule at index {index} in 'map_type_mapping' is missing 'target_type' string.\")\n-             if 'keywords' not in rule or not isinstance(rule['keywords'], list):\n-                  raise ConfigurationError(f\"Preset '{self.preset_name}': Rule at index {index} in 'map_type_mapping' is missing 'keywords' list.\")\n-             for kw_index, keyword in enumerate(rule['keywords']):\n-                  if not isinstance(keyword, str):\n-                       raise ConfigurationError(f\"Preset '{self.preset_name}': Keyword at index {kw_index} in rule {index} ('{rule['target_type']}') must be a string.\")\n-\n-\n-        # Core validation (check types or specific values if needed)\n-        if not isinstance(self._core_settings.get('TARGET_FILENAME_PATTERN'), str):\n-             raise ConfigurationError(\"Core config 'TARGET_FILENAME_PATTERN' must be a string.\")\n-        if not isinstance(self._core_settings.get('IMAGE_RESOLUTIONS'), dict):\n-             raise ConfigurationError(\"Core config 'IMAGE_RESOLUTIONS' must be a dictionary.\")\n-        if not isinstance(self._core_settings.get('STANDARD_MAP_TYPES'), list):\n-             raise ConfigurationError(\"Core config 'STANDARD_MAP_TYPES' must be a list.\")\n-        # Add more checks as necessary\n-        log.debug(\"Configuration validation passed.\")\n-\n-\n-    # --- Accessor Methods/Properties ---\n-    # Use @property for direct access, methods for potentially complex lookups/defaults\n-\n-    @property\n-    def supplier_name(self) -> str:\n-        return self._preset_settings.get('supplier_name', 'DefaultSupplier')\n-        \n-    @property\n-    def default_asset_category(self) -> str:\n-        \"\"\"Gets the default asset category from core settings.\"\"\"\n-        # Provide a fallback default just in case it's missing from config.py\n-        return self._core_settings.get('DEFAULT_ASSET_CATEGORY', 'Texture')\n-\n-    @property\n-    def target_filename_pattern(self) -> str:\n-        return self._core_settings['TARGET_FILENAME_PATTERN'] # Assumes validation passed\n-\n-    @property\n-    def image_resolutions(self) -> dict[str, int]:\n-        return self._core_settings['IMAGE_RESOLUTIONS']\n-\n-    @property\n-    def standard_map_types(self) -> list[str]:\n-        return self._core_settings['STANDARD_MAP_TYPES']\n-\n-    @property\n-    def map_type_mapping(self) -> list:\n-        return self._preset_settings['map_type_mapping']\n-\n-    @property\n-    def source_naming_separator(self) -> str:\n-        return self._preset_settings.get('source_naming', {}).get('separator', '_')\n-\n-    @property\n-    def source_naming_indices(self) -> dict:\n-        return self._preset_settings.get('source_naming', {}).get('part_indices', {})\n-\n-    @property\n-    def source_glossiness_keywords(self) -> list:\n-        return self._preset_settings.get('source_naming', {}).get('glossiness_keywords', [])\n-\n-    @property\n-    def source_bit_depth_variants(self) -> dict:\n-         return self._preset_settings.get('source_naming', {}).get('bit_depth_variants', {})\n-\n-    @property\n-    def archetype_rules(self) -> list:\n-         return self._preset_settings['archetype_rules']\n-\n-    @property\n-    def asset_category_rules(self) -> dict:\n-         return self._preset_settings['asset_category_rules']\n-\n-    @property\n-    def move_to_extra_patterns(self) -> list:\n-        return self._preset_settings['move_to_extra_patterns']\n-\n-    @property\n-    def extra_files_subdir(self) -> str:\n-        return self._core_settings['EXTRA_FILES_SUBDIR']\n-\n-    @property\n-    def metadata_filename(self) -> str:\n-        return self._core_settings['METADATA_FILENAME']\n-\n-    @property\n-    def calculate_stats_resolution(self) -> str:\n-        return self._core_settings['CALCULATE_STATS_RESOLUTION']\n-\n-    @property\n-    def map_merge_rules(self) -> list:\n-         return self._core_settings['MAP_MERGE_RULES']\n-\n-    @property\n-    def aspect_ratio_decimals(self) -> int:\n-         return self._core_settings['ASPECT_RATIO_DECIMALS']\n-\n-    @property\n-    def temp_dir_prefix(self) -> str:\n-         return self._core_settings['TEMP_DIR_PREFIX']\n-\n-    @property\n-    def jpg_quality(self) -> int:\n-        \"\"\"Gets the configured JPG quality level.\"\"\"\n-        return self._core_settings.get('JPG_QUALITY', 95) # Use default if somehow missing\n-\n-    @property\n-    def resolution_threshold_for_jpg(self) -> int:\n-        \"\"\"Gets the pixel dimension threshold for using JPG for 8-bit images.\"\"\"\n-        return self._core_settings.get('RESOLUTION_THRESHOLD_FOR_JPG', 4096)\n-\n-    @property\n-    def respect_variant_map_types(self) -> list:\n-        \"\"\"Gets the list of map types that should always respect variant numbering.\"\"\"\n-        # Ensure it returns a list, even if missing from config.py (though defaults should handle it)\n-        return self._core_settings.get('RESPECT_VARIANT_MAP_TYPES', [])\n-\n-    @property\n-    def force_lossless_map_types(self) -> list:\n-        \"\"\"Gets the list of map types that must always be saved losslessly.\"\"\"\n-        return self._core_settings.get('FORCE_LOSSLESS_MAP_TYPES', [])\n-\n-    def get_bit_depth_rule(self, map_type: str) -> str:\n-        \"\"\"Gets the bit depth rule ('respect' or 'force_8bit') for a given standard map type.\"\"\"\n-        rules = self._core_settings.get('MAP_BIT_DEPTH_RULES', {})\n-        default_rule = rules.get('DEFAULT', 'respect')\n-        return rules.get(map_type, default_rule)\n-\n-    def get_16bit_output_formats(self) -> tuple[str, str]:\n-        \"\"\"Gets the primary and fallback format names for 16-bit output.\"\"\"\n-        primary = self._core_settings.get('OUTPUT_FORMAT_16BIT_PRIMARY', 'png')\n-        fallback = self._core_settings.get('OUTPUT_FORMAT_16BIT_FALLBACK', 'png')\n-        return primary.lower(), fallback.lower()\n-\n-    def get_8bit_output_format(self) -> str:\n-        \"\"\"Gets the format name for 8-bit output.\"\"\"\n+# configuration.py\r\n+\r\n+import json\r\n+import os\r\n+import importlib.util\r\n+from pathlib import Path\r\n+import logging\r\n+import re # Import the regex module\r\n+\r\n+log = logging.getLogger(__name__) # Use logger defined in main.py\r\n+\r\n+# --- Constants ---\r\n+# Assumes config.py and presets/ are relative to this file's location\r\n+BASE_DIR = Path(__file__).parent\r\n+CORE_CONFIG_PATH = BASE_DIR / \"config.py\"\r\n+PRESETS_DIR = BASE_DIR / \"presets\"\r\n+\r\n+# --- Custom Exception ---\r\n+class ConfigurationError(Exception):\r\n+    \"\"\"Custom exception for configuration loading errors.\"\"\"\r\n+    pass\r\n+\r\n+# --- Helper Functions ---\r\n+def _get_base_map_type(target_map_string: str) -> str:\r\n+   \"\"\"Extracts the base map type (e.g., 'COL') from a potentially numbered string ('COL-1').\"\"\"\r\n+   # Use regex to find the leading alphabetical part\r\n+   match = re.match(r\"([a-zA-Z]+)\", target_map_string)\r\n+   if match:\r\n+       return match.group(1).upper()\r\n+   # Fallback if no number suffix or unexpected format\r\n+   return target_map_string.upper()\r\n+\r\n+def _fnmatch_to_regex(pattern: str) -> str:\r\n+    \"\"\"\r\n+    Converts an fnmatch pattern to a regex pattern string.\r\n+    Handles basic wildcards (*, ?) and escapes other regex special characters.\r\n+    \"\"\"\r\n+    i, n = 0, len(pattern)\r\n+    res = ''\r\n+    while i < n:\r\n+        c = pattern[i]\r\n+        i = i + 1\r\n+        if c == '*':\r\n+            res = res + '.*'\r\n+        elif c == '?':\r\n+            res = res + '.'\r\n+        elif c == '[':\r\n+            j = i\r\n+            if j < n and pattern[j] == '!':\r\n+                j = j + 1\r\n+            if j < n and pattern[j] == ']':\r\n+                j = j + 1\r\n+            while j < n and pattern[j] != ']':\r\n+                j = j + 1\r\n+            if j >= n:\r\n+                res = res + '\\\\['\r\n+            else:\r\n+                stuff = pattern[i:j].replace('\\\\','\\\\\\\\')\r\n+                i = j + 1\r\n+                if stuff[0] == '!':\r\n+                    stuff = '^' + stuff[1:]\r\n+                elif stuff[0] == '^':\r\n+                    stuff = '\\\\' + stuff\r\n+                res = '%s[%s]' % (res, stuff)\r\n+        else:\r\n+            res = res + re.escape(c)\r\n+    # We want to find the pattern anywhere in the filename for flexibility,\r\n+    # so don't anchor with ^$ by default. Anchoring might be needed for specific cases.\r\n+    # Let's return the core pattern and let the caller decide on anchoring if needed.\r\n+    # For filename matching, we usually want to find the pattern, not match the whole string.\r\n+    return res\r\n+\r\n+\r\n+# --- Configuration Class ---\r\n+class Configuration:\r\n+    \"\"\"\r\n+    Loads and provides access to core settings combined with a specific preset.\r\n+    \"\"\"\r\n+    def __init__(self, preset_name: str):\r\n+        \"\"\"\r\n+        Loads core config and the specified preset file.\r\n+\r\n+        Args:\r\n+            preset_name: The name of the preset (without .json extension).\r\n+\r\n+        Raises:\r\n+            ConfigurationError: If core config or preset cannot be loaded/validated.\r\n+        \"\"\"\r\n+        log.debug(f\"Initializing Configuration with preset: '{preset_name}'\")\r\n+        self.preset_name = preset_name\r\n+        self._core_settings: dict = self._load_core_config()\r\n+        self._preset_settings: dict = self._load_preset(preset_name)\r\n+        self._validate_configs()\r\n+        self._compile_regex_patterns() # Compile regex after validation\r\n+        log.info(f\"Configuration loaded successfully using preset: '{self.preset_name}'\")\r\n+\r\n+\r\n+    def _compile_regex_patterns(self):\r\n+        \"\"\"Compiles regex patterns from config/preset for faster matching.\"\"\"\r\n+        log.debug(\"Compiling regex patterns from configuration...\")\r\n+        self.compiled_extra_regex: list[re.Pattern] = []\r\n+        self.compiled_model_regex: list[re.Pattern] = []\r\n+        self.compiled_bit_depth_regex_map: dict[str, re.Pattern] = {}\r\n+        # Map: base_map_type -> list of tuples: (compiled_regex, original_keyword, rule_index)\r\n+        self.compiled_map_keyword_regex: dict[str, list[tuple[re.Pattern, str, int]]] = {}\r\n+        # Store the original rule order for priority checking later if needed (can be removed if index is stored in tuple)\r\n+        # self._map_type_rule_order: list[dict] = [] # Keep for now, might be useful elsewhere\r\n+\r\n+        # Compile Extra Patterns (case-insensitive)\r\n+        for pattern in self.move_to_extra_patterns:\r\n+            try:\r\n+                # Use the raw fnmatch pattern directly if it's simple enough for re.search\r\n+                # Or convert using helper if needed. Let's try direct search first.\r\n+                # We want to find the pattern *within* the filename.\r\n+                regex_str = _fnmatch_to_regex(pattern) # Convert wildcards\r\n+                self.compiled_extra_regex.append(re.compile(regex_str, re.IGNORECASE))\r\n+            except re.error as e:\r\n+                log.warning(f\"Failed to compile 'extra' regex pattern '{pattern}': {e}. Skipping pattern.\")\r\n+\r\n+        # Compile Model Patterns (case-insensitive)\r\n+        model_patterns = self.asset_category_rules.get('model_patterns', [])\r\n+        for pattern in model_patterns:\r\n+             try:\r\n+                 regex_str = _fnmatch_to_regex(pattern)\r\n+                 self.compiled_model_regex.append(re.compile(regex_str, re.IGNORECASE))\r\n+             except re.error as e:\r\n+                 log.warning(f\"Failed to compile 'model' regex pattern '{pattern}': {e}. Skipping pattern.\")\r\n+\r\n+        # Compile Bit Depth Variant Patterns (case-sensitive recommended)\r\n+        for map_type, pattern in self.source_bit_depth_variants.items():\r\n+            try:\r\n+                # These often rely on specific suffixes, so anchoring might be better?\r\n+                # Let's stick to the converted pattern for now, assuming it ends with suffix.\r\n+                regex_str = _fnmatch_to_regex(pattern) # e.g., \".*_DISP16.*\"\r\n+                # If the original pattern ended with *, remove the trailing '.*' for suffix matching\r\n+                if pattern.endswith('*'):\r\n+                    regex_str = regex_str.removesuffix('.*') # e.g., \".*_DISP16\"\r\n+                    # Fallback for < 3.9: if regex_str.endswith('.*'): regex_str = regex_str[:-2]\r\n+\r\n+                # Use the fnmatch-converted regex directly, allowing matches anywhere in the filename\r\n+                # This is less strict than anchoring to the end with \\\\.[^.]+$\r\n+                final_regex_str = regex_str # Use the result from _fnmatch_to_regex\r\n+                self.compiled_bit_depth_regex_map[map_type] = re.compile(final_regex_str, re.IGNORECASE) # Added IGNORECASE\r\n+                log.debug(f\"  Compiled bit depth variant for '{map_type}' as regex (IGNORECASE): {final_regex_str}\")\r\n+            except re.error as e:\r\n+                log.warning(f\"Failed to compile 'bit depth' regex pattern '{pattern}' for map type '{map_type}': {e}. Skipping pattern.\")\r\n+\r\n+        # Compile Map Type Keywords (case-insensitive) based on the new structure\r\n+        separator = re.escape(self.source_naming_separator) # Escape separator for regex\r\n+        # Use defaultdict to easily append to lists for the same base type\r\n+        from collections import defaultdict\r\n+        temp_compiled_map_regex = defaultdict(list)\r\n+\r\n+        for rule_index, mapping_rule in enumerate(self.map_type_mapping):\r\n+            # Validate rule structure (dictionary with target_type and keywords)\r\n+            if not isinstance(mapping_rule, dict) or \\\r\n+               'target_type' not in mapping_rule or \\\r\n+               'keywords' not in mapping_rule or \\\r\n+               not isinstance(mapping_rule['keywords'], list):\r\n+                log.warning(f\"Skipping invalid map_type_mapping rule at index {rule_index}: {mapping_rule}. Expected dict with 'target_type' and 'keywords' list.\")\r\n+                continue\r\n+\r\n+            target_type = mapping_rule['target_type'].upper() # Use the base type directly\r\n+            source_keywords = mapping_rule['keywords']\r\n+\r\n+            # Store the rule for potential priority access later (optional, index is now in tuple)\r\n+            # self._map_type_rule_order.append(mapping_rule)\r\n+\r\n+            if target_type not in self.standard_map_types:\r\n+                 log.warning(f\"Map rule {rule_index} uses target_type '{target_type}' which is not in core config STANDARD_MAP_TYPES. Classification might be incomplete.\")\r\n+                 # Continue processing anyway, as it might be intended for merging etc.\r\n+\r\n+            # Compile keywords for this rule and store with context\r\n+            for keyword in source_keywords:\r\n+                if not isinstance(keyword, str):\r\n+                     log.warning(f\"Skipping non-string keyword '{keyword}' in rule {rule_index} for target '{target_type}'.\")\r\n+                     continue\r\n+                try:\r\n+                    # Match keyword potentially surrounded by separators or start/end\r\n+                    # Handle potential wildcards within the keyword using fnmatch conversion\r\n+                    kw_regex_part = _fnmatch_to_regex(keyword)\r\n+                    # Build regex to match the keyword part, anchored by separators or string boundaries\r\n+                    # Use non-capturing groups (?:...)\r\n+                    # Capture the keyword part itself for potential use later if needed (group 1)\r\n+                    regex_str = rf\"(?:^|{separator})({kw_regex_part})(?:$|{separator})\"\r\n+                    compiled_regex = re.compile(regex_str, re.IGNORECASE)\r\n+                    # Append tuple: (compiled_regex, original_keyword, rule_index)\r\n+                    temp_compiled_map_regex[target_type].append((compiled_regex, keyword, rule_index))\r\n+                    log.debug(f\"  Compiled keyword '{keyword}' (rule {rule_index}) for target '{target_type}' as regex: {regex_str}\")\r\n+                except re.error as e:\r\n+                    log.warning(f\"Failed to compile map keyword regex '{keyword}' for target type '{target_type}': {e}. Skipping keyword.\")\r\n+\r\n+        # Assign the compiled regex dictionary\r\n+        self.compiled_map_keyword_regex = dict(temp_compiled_map_regex)\r\n+        log.debug(f\"Compiled map keyword regex keys: {list(self.compiled_map_keyword_regex.keys())}\")\r\n+\r\n+        log.debug(\"Finished compiling regex patterns.\")\r\n+\r\n+\r\n+    def _load_core_config(self) -> dict:\r\n+        \"\"\"Loads settings from the core config.py file.\"\"\"\r\n+        log.debug(f\"Loading core config from: {CORE_CONFIG_PATH}\")\r\n+        if not CORE_CONFIG_PATH.is_file():\r\n+            raise ConfigurationError(f\"Core configuration file not found: {CORE_CONFIG_PATH}\")\r\n+        try:\r\n+            spec = importlib.util.spec_from_file_location(\"core_config\", CORE_CONFIG_PATH)\r\n+            if spec is None or spec.loader is None:\r\n+                 raise ConfigurationError(f\"Could not create module spec for {CORE_CONFIG_PATH}\")\r\n+            core_config_module = importlib.util.module_from_spec(spec)\r\n+            # Define default values for core settings in case they are missing in config.py\r\n+            default_core_settings = {\r\n+                'TARGET_FILENAME_PATTERN': \"{base_name}_{map_type}_{resolution}.{ext}\",\r\n+                'STANDARD_MAP_TYPES': [],\r\n+                'EXTRA_FILES_SUBDIR': \"Extra\",\r\n+                'METADATA_FILENAME': \"metadata.json\",\r\n+                'IMAGE_RESOLUTIONS': {},\r\n+                'ASPECT_RATIO_DECIMALS': 2,\r\n+                'MAP_BIT_DEPTH_RULES': {\"DEFAULT\": \"respect\"},\r\n+                'OUTPUT_FORMAT_16BIT_PRIMARY': \"png\",\r\n+                'OUTPUT_FORMAT_16BIT_FALLBACK': \"png\",\r\n+                'OUTPUT_FORMAT_8BIT': \"png\",\r\n+                'MAP_MERGE_RULES': [],\r\n+                'CALCULATE_STATS_RESOLUTION': \"1K\",\r\n+                'DEFAULT_ASSET_CATEGORY': \"Texture\",\r\n+                'TEMP_DIR_PREFIX': \"_PROCESS_ASSET_\",\r\n+                # --- Additions ---\r\n+                'JPG_QUALITY': 95, # Default JPG quality\r\n+                'RESOLUTION_THRESHOLD_FOR_JPG': 4096, # Default threshold\r\n+                'RESPECT_VARIANT_MAP_TYPES': [], # Default for map types that always get suffix\r\n+                'FORCE_LOSSLESS_MAP_TYPES': [] # Default for map types that must be lossless\r\n+            }\r\n+            # Load attributes from module, using defaults if missing\r\n+            settings = default_core_settings.copy()\r\n+            spec.loader.exec_module(core_config_module)\r\n+            for name in default_core_settings:\r\n+                 if hasattr(core_config_module, name):\r\n+                      settings[name] = getattr(core_config_module, name)\r\n+\r\n+            log.debug(f\"Core config loaded successfully.\")\r\n+            return settings\r\n+        except Exception as e:\r\n+            raise ConfigurationError(f\"Failed to load core configuration from {CORE_CONFIG_PATH}: {e}\")\r\n+\r\n+    def _load_preset(self, preset_name: str) -> dict:\r\n+        \"\"\"Loads the specified preset JSON file.\"\"\"\r\n+        log.debug(f\"Loading preset: '{preset_name}' from {PRESETS_DIR}\")\r\n+        if not PRESETS_DIR.is_dir():\r\n+             raise ConfigurationError(f\"Presets directory not found: {PRESETS_DIR}\")\r\n+\r\n+        preset_file = PRESETS_DIR / f\"{preset_name}.json\"\r\n+        if not preset_file.is_file():\r\n+            raise ConfigurationError(f\"Preset file not found: {preset_file}\")\r\n+\r\n+        try:\r\n+            with open(preset_file, 'r', encoding='utf-8') as f:\r\n+                preset_data = json.load(f)\r\n+            log.debug(f\"Preset '{preset_name}' loaded successfully.\")\r\n+            return preset_data\r\n+        except json.JSONDecodeError as e:\r\n+            raise ConfigurationError(f\"Failed to parse preset file {preset_file}: Invalid JSON - {e}\")\r\n+        except Exception as e:\r\n+            raise ConfigurationError(f\"Failed to read preset file {preset_file}: {e}\")\r\n+\r\n+    def _validate_configs(self):\r\n+        \"\"\"Performs basic validation checks on loaded settings.\"\"\"\r\n+        log.debug(\"Validating loaded configurations...\")\r\n+        # Preset validation\r\n+        required_preset_keys = [\r\n+            \"preset_name\", \"supplier_name\", \"source_naming\", \"map_type_mapping\",\r\n+            \"asset_category_rules\", \"archetype_rules\", \"move_to_extra_patterns\"\r\n+        ]\r\n+        for key in required_preset_keys:\r\n+            if key not in self._preset_settings:\r\n+                raise ConfigurationError(f\"Preset '{self.preset_name}' is missing required key: '{key}'.\")\r\n+\r\n+        # Validate map_type_mapping structure (new format)\r\n+        if not isinstance(self._preset_settings['map_type_mapping'], list):\r\n+             raise ConfigurationError(f\"Preset '{self.preset_name}': 'map_type_mapping' must be a list.\")\r\n+        for index, rule in enumerate(self._preset_settings['map_type_mapping']):\r\n+             if not isinstance(rule, dict):\r\n+                  raise ConfigurationError(f\"Preset '{self.preset_name}': Rule at index {index} in 'map_type_mapping' must be a dictionary.\")\r\n+             if 'target_type' not in rule or not isinstance(rule['target_type'], str):\r\n+                  raise ConfigurationError(f\"Preset '{self.preset_name}': Rule at index {index} in 'map_type_mapping' is missing 'target_type' string.\")\r\n+             if 'keywords' not in rule or not isinstance(rule['keywords'], list):\r\n+                  raise ConfigurationError(f\"Preset '{self.preset_name}': Rule at index {index} in 'map_type_mapping' is missing 'keywords' list.\")\r\n+             for kw_index, keyword in enumerate(rule['keywords']):\r\n+                  if not isinstance(keyword, str):\r\n+                       raise ConfigurationError(f\"Preset '{self.preset_name}': Keyword at index {kw_index} in rule {index} ('{rule['target_type']}') must be a string.\")\r\n+\r\n+\r\n+        # Core validation (check types or specific values if needed)\r\n+        if not isinstance(self._core_settings.get('TARGET_FILENAME_PATTERN'), str):\r\n+             raise ConfigurationError(\"Core config 'TARGET_FILENAME_PATTERN' must be a string.\")\r\n+        if not isinstance(self._core_settings.get('IMAGE_RESOLUTIONS'), dict):\r\n+             raise ConfigurationError(\"Core config 'IMAGE_RESOLUTIONS' must be a dictionary.\")\r\n+        if not isinstance(self._core_settings.get('STANDARD_MAP_TYPES'), list):\r\n+             raise ConfigurationError(\"Core config 'STANDARD_MAP_TYPES' must be a list.\")\r\n+        # Add more checks as necessary\r\n+        log.debug(\"Configuration validation passed.\")\r\n+\r\n+\r\n+    # --- Accessor Methods/Properties ---\r\n+    # Use @property for direct access, methods for potentially complex lookups/defaults\r\n+\r\n+    @property\r\n+    def supplier_name(self) -> str:\r\n+        return self._preset_settings.get('supplier_name', 'DefaultSupplier')\r\n+        \r\n+    @property\r\n+    def default_asset_category(self) -> str:\r\n+        \"\"\"Gets the default asset category from core settings.\"\"\"\r\n+        # Provide a fallback default just in case it's missing from config.py\r\n+        return self._core_settings.get('DEFAULT_ASSET_CATEGORY', 'Texture')\r\n+\r\n+    @property\r\n+    def target_filename_pattern(self) -> str:\r\n+        return self._core_settings['TARGET_FILENAME_PATTERN'] # Assumes validation passed\r\n+\r\n+    @property\r\n+    def image_resolutions(self) -> dict[str, int]:\r\n+        return self._core_settings['IMAGE_RESOLUTIONS']\r\n+\r\n+    @property\r\n+    def standard_map_types(self) -> list[str]:\r\n+        return self._core_settings['STANDARD_MAP_TYPES']\r\n+\r\n+    @property\r\n+    def map_type_mapping(self) -> list:\r\n+        return self._preset_settings['map_type_mapping']\r\n+\r\n+    @property\r\n+    def source_naming_separator(self) -> str:\r\n+        return self._preset_settings.get('source_naming', {}).get('separator', '_')\r\n+\r\n+    @property\r\n+    def source_naming_indices(self) -> dict:\r\n+        return self._preset_settings.get('source_naming', {}).get('part_indices', {})\r\n+\r\n+    @property\r\n+    def source_glossiness_keywords(self) -> list:\r\n+        return self._preset_settings.get('source_naming', {}).get('glossiness_keywords', [])\r\n+\r\n+    @property\r\n+    def source_bit_depth_variants(self) -> dict:\r\n+         return self._preset_settings.get('source_naming', {}).get('bit_depth_variants', {})\r\n+\r\n+    @property\r\n+    def archetype_rules(self) -> list:\r\n+         return self._preset_settings['archetype_rules']\r\n+\r\n+    @property\r\n+    def asset_category_rules(self) -> dict:\r\n+         return self._preset_settings['asset_category_rules']\r\n+\r\n+    @property\r\n+    def move_to_extra_patterns(self) -> list:\r\n+        return self._preset_settings['move_to_extra_patterns']\r\n+\r\n+    @property\r\n+    def extra_files_subdir(self) -> str:\r\n+        return self._core_settings['EXTRA_FILES_SUBDIR']\r\n+\r\n+    @property\r\n+    def metadata_filename(self) -> str:\r\n+        return self._core_settings['METADATA_FILENAME']\r\n+\r\n+    @property\r\n+    def calculate_stats_resolution(self) -> str:\r\n+        return self._core_settings['CALCULATE_STATS_RESOLUTION']\r\n+\r\n+    @property\r\n+    def map_merge_rules(self) -> list:\r\n+         return self._core_settings['MAP_MERGE_RULES']\r\n+\r\n+    @property\r\n+    def aspect_ratio_decimals(self) -> int:\r\n+         return self._core_settings['ASPECT_RATIO_DECIMALS']\r\n+\r\n+    @property\r\n+    def temp_dir_prefix(self) -> str:\r\n+         return self._core_settings['TEMP_DIR_PREFIX']\r\n+\r\n+    @property\r\n+    def jpg_quality(self) -> int:\r\n+        \"\"\"Gets the configured JPG quality level.\"\"\"\r\n+        return self._core_settings.get('JPG_QUALITY', 95) # Use default if somehow missing\r\n+\r\n+    @property\r\n+    def resolution_threshold_for_jpg(self) -> int:\r\n+        \"\"\"Gets the pixel dimension threshold for using JPG for 8-bit images.\"\"\"\r\n+        return self._core_settings.get('RESOLUTION_THRESHOLD_FOR_JPG', 4096)\r\n+\r\n+    @property\r\n+    def respect_variant_map_types(self) -> list:\r\n+        \"\"\"Gets the list of map types that should always respect variant numbering.\"\"\"\r\n+        # Ensure it returns a list, even if missing from config.py (though defaults should handle it)\r\n+        return self._core_settings.get('RESPECT_VARIANT_MAP_TYPES', [])\r\n+\r\n+    @property\r\n+    def force_lossless_map_types(self) -> list:\r\n+        \"\"\"Gets the list of map types that must always be saved losslessly.\"\"\"\r\n+        return self._core_settings.get('FORCE_LOSSLESS_MAP_TYPES', [])\r\n+\r\n+    def get_bit_depth_rule(self, map_type: str) -> str:\r\n+        \"\"\"Gets the bit depth rule ('respect' or 'force_8bit') for a given standard map type.\"\"\"\r\n+        rules = self._core_settings.get('MAP_BIT_DEPTH_RULES', {})\r\n+        default_rule = rules.get('DEFAULT', 'respect')\r\n+        return rules.get(map_type, default_rule)\r\n+\r\n+    def get_16bit_output_formats(self) -> tuple[str, str]:\r\n+        \"\"\"Gets the primary and fallback format names for 16-bit output.\"\"\"\r\n+        primary = self._core_settings.get('OUTPUT_FORMAT_16BIT_PRIMARY', 'png')\r\n+        fallback = self._core_settings.get('OUTPUT_FORMAT_16BIT_FALLBACK', 'png')\r\n+        return primary.lower(), fallback.lower()\r\n+\r\n+    def get_8bit_output_format(self) -> str:\r\n+        \"\"\"Gets the format name for 8-bit output.\"\"\"\r\n         return self._core_settings.get('OUTPUT_FORMAT_8BIT', 'png').lower()\n\\ No newline at end of file\n"
                }
            ],
            "date": 1745314026627,
            "name": "Commit-0",
            "content": "# configuration.py\n\nimport json\nimport os\nimport importlib.util\nfrom pathlib import Path\nimport logging\nimport re # Import the regex module\n\nlog = logging.getLogger(__name__) # Use logger defined in main.py\n\n# --- Constants ---\n# Assumes config.py and presets/ are relative to this file's location\nBASE_DIR = Path(__file__).parent\nCORE_CONFIG_PATH = BASE_DIR / \"config.py\"\nPRESETS_DIR = BASE_DIR / \"presets\"\n\n# --- Custom Exception ---\nclass ConfigurationError(Exception):\n    \"\"\"Custom exception for configuration loading errors.\"\"\"\n    pass\n\n# --- Helper Functions ---\ndef _get_base_map_type(target_map_string: str) -> str:\n   \"\"\"Extracts the base map type (e.g., 'COL') from a potentially numbered string ('COL-1').\"\"\"\n   # Use regex to find the leading alphabetical part\n   match = re.match(r\"([a-zA-Z]+)\", target_map_string)\n   if match:\n       return match.group(1).upper()\n   # Fallback if no number suffix or unexpected format\n   return target_map_string.upper()\n\ndef _fnmatch_to_regex(pattern: str) -> str:\n    \"\"\"\n    Converts an fnmatch pattern to a regex pattern string.\n    Handles basic wildcards (*, ?) and escapes other regex special characters.\n    \"\"\"\n    i, n = 0, len(pattern)\n    res = ''\n    while i < n:\n        c = pattern[i]\n        i = i + 1\n        if c == '*':\n            res = res + '.*'\n        elif c == '?':\n            res = res + '.'\n        elif c == '[':\n            j = i\n            if j < n and pattern[j] == '!':\n                j = j + 1\n            if j < n and pattern[j] == ']':\n                j = j + 1\n            while j < n and pattern[j] != ']':\n                j = j + 1\n            if j >= n:\n                res = res + '\\\\['\n            else:\n                stuff = pattern[i:j].replace('\\\\','\\\\\\\\')\n                i = j + 1\n                if stuff[0] == '!':\n                    stuff = '^' + stuff[1:]\n                elif stuff[0] == '^':\n                    stuff = '\\\\' + stuff\n                res = '%s[%s]' % (res, stuff)\n        else:\n            res = res + re.escape(c)\n    # We want to find the pattern anywhere in the filename for flexibility,\n    # so don't anchor with ^$ by default. Anchoring might be needed for specific cases.\n    # Let's return the core pattern and let the caller decide on anchoring if needed.\n    # For filename matching, we usually want to find the pattern, not match the whole string.\n    return res\n\n\n# --- Configuration Class ---\nclass Configuration:\n    \"\"\"\n    Loads and provides access to core settings combined with a specific preset.\n    \"\"\"\n    def __init__(self, preset_name: str):\n        \"\"\"\n        Loads core config and the specified preset file.\n\n        Args:\n            preset_name: The name of the preset (without .json extension).\n\n        Raises:\n            ConfigurationError: If core config or preset cannot be loaded/validated.\n        \"\"\"\n        log.debug(f\"Initializing Configuration with preset: '{preset_name}'\")\n        self.preset_name = preset_name\n        self._core_settings: dict = self._load_core_config()\n        self._preset_settings: dict = self._load_preset(preset_name)\n        self._validate_configs()\n        log.debug(f\"Loaded source_naming config: {self._preset_settings.get('source_naming', 'Not Found')}\") # DEBUG: Log loaded source_naming\n        self._compile_regex_patterns() # Compile regex after validation\n        log.info(f\"Configuration loaded successfully using preset: '{self.preset_name}'\")\n\n\n    def _compile_regex_patterns(self):\n        \"\"\"Compiles regex patterns from config/preset for faster matching.\"\"\"\n        log.debug(\"Compiling regex patterns from configuration...\")\n        self.compiled_extra_regex: list[re.Pattern] = []\n        self.compiled_model_regex: list[re.Pattern] = []\n        self.compiled_bit_depth_regex_map: dict[str, re.Pattern] = {}\n        # Map: base_map_type -> list of tuples: (compiled_regex, original_keyword, rule_index)\n        self.compiled_map_keyword_regex: dict[str, list[tuple[re.Pattern, str, int]]] = {}\n        # Store the original rule order for priority checking later if needed (can be removed if index is stored in tuple)\n        # self._map_type_rule_order: list[dict] = [] # Keep for now, might be useful elsewhere\n\n        # Compile Extra Patterns (case-insensitive)\n        for pattern in self.move_to_extra_patterns:\n            try:\n                # Use the raw fnmatch pattern directly if it's simple enough for re.search\n                # Or convert using helper if needed. Let's try direct search first.\n                # We want to find the pattern *within* the filename.\n                regex_str = _fnmatch_to_regex(pattern) # Convert wildcards\n                self.compiled_extra_regex.append(re.compile(regex_str, re.IGNORECASE))\n            except re.error as e:\n                log.warning(f\"Failed to compile 'extra' regex pattern '{pattern}': {e}. Skipping pattern.\")\n\n        # Compile Model Patterns (case-insensitive)\n        model_patterns = self.asset_category_rules.get('model_patterns', [])\n        for pattern in model_patterns:\n             try:\n                 regex_str = _fnmatch_to_regex(pattern)\n                 self.compiled_model_regex.append(re.compile(regex_str, re.IGNORECASE))\n             except re.error as e:\n                 log.warning(f\"Failed to compile 'model' regex pattern '{pattern}': {e}. Skipping pattern.\")\n\n        # Compile Bit Depth Variant Patterns (case-sensitive recommended)\n        for map_type, pattern in self.source_bit_depth_variants.items():\n            try:\n                # These often rely on specific suffixes, so anchoring might be better?\n                # Let's stick to the converted pattern for now, assuming it ends with suffix.\n                regex_str = _fnmatch_to_regex(pattern) # e.g., \".*_DISP16.*\"\n                # If the original pattern ended with *, remove the trailing '.*' for suffix matching\n                if pattern.endswith('*'):\n                    regex_str = regex_str.removesuffix('.*') # e.g., \".*_DISP16\"\n                    # Fallback for < 3.9: if regex_str.endswith('.*'): regex_str = regex_str[:-2]\n\n                # Use the fnmatch-converted regex directly, allowing matches anywhere in the filename\n                # This is less strict than anchoring to the end with \\\\.[^.]+$\n                final_regex_str = regex_str # Use the result from _fnmatch_to_regex\n                self.compiled_bit_depth_regex_map[map_type] = re.compile(final_regex_str, re.IGNORECASE) # Added IGNORECASE\n                log.debug(f\"  Compiled bit depth variant for '{map_type}' as regex (IGNORECASE): {final_regex_str}\")\n            except re.error as e:\n                log.warning(f\"Failed to compile 'bit depth' regex pattern '{pattern}' for map type '{map_type}': {e}. Skipping pattern.\")\n\n        # Compile Map Type Keywords (case-insensitive) based on the new structure\n        separator = re.escape(self.source_naming_separator) # Escape separator for regex\n        # Use defaultdict to easily append to lists for the same base type\n        from collections import defaultdict\n        temp_compiled_map_regex = defaultdict(list)\n\n        for rule_index, mapping_rule in enumerate(self.map_type_mapping):\n            # Validate rule structure (dictionary with target_type and keywords)\n            if not isinstance(mapping_rule, dict) or \\\n               'target_type' not in mapping_rule or \\\n               'keywords' not in mapping_rule or \\\n               not isinstance(mapping_rule['keywords'], list):\n                log.warning(f\"Skipping invalid map_type_mapping rule at index {rule_index}: {mapping_rule}. Expected dict with 'target_type' and 'keywords' list.\")\n                continue\n\n            target_type = mapping_rule['target_type'].upper() # Use the base type directly\n            source_keywords = mapping_rule['keywords']\n\n            # Store the rule for potential priority access later (optional, index is now in tuple)\n            # self._map_type_rule_order.append(mapping_rule)\n\n            if target_type not in self.standard_map_types:\n                 log.warning(f\"Map rule {rule_index} uses target_type '{target_type}' which is not in core config STANDARD_MAP_TYPES. Classification might be incomplete.\")\n                 # Continue processing anyway, as it might be intended for merging etc.\n\n            # Compile keywords for this rule and store with context\n            for keyword in source_keywords:\n                if not isinstance(keyword, str):\n                     log.warning(f\"Skipping non-string keyword '{keyword}' in rule {rule_index} for target '{target_type}'.\")\n                     continue\n                try:\n                    # Match keyword potentially surrounded by separators or start/end\n                    # Handle potential wildcards within the keyword using fnmatch conversion\n                    kw_regex_part = _fnmatch_to_regex(keyword)\n                    # Build regex to match the keyword part, anchored by separators or string boundaries\n                    # Use non-capturing groups (?:...)\n                    # Capture the keyword part itself for potential use later if needed (group 1)\n                    regex_str = rf\"(?:^|{separator})({kw_regex_part})(?:$|{separator})\"\n                    compiled_regex = re.compile(regex_str, re.IGNORECASE)\n                    # Append tuple: (compiled_regex, original_keyword, rule_index)\n                    temp_compiled_map_regex[target_type].append((compiled_regex, keyword, rule_index))\n                    log.debug(f\"  Compiled keyword '{keyword}' (rule {rule_index}) for target '{target_type}' as regex: {regex_str}\")\n                except re.error as e:\n                    log.warning(f\"Failed to compile map keyword regex '{keyword}' for target type '{target_type}': {e}. Skipping keyword.\")\n\n        # Assign the compiled regex dictionary\n        self.compiled_map_keyword_regex = dict(temp_compiled_map_regex)\n        log.debug(f\"Compiled map keyword regex keys: {list(self.compiled_map_keyword_regex.keys())}\")\n\n        log.debug(\"Finished compiling regex patterns.\")\n\n\n    def _load_core_config(self) -> dict:\n        \"\"\"Loads settings from the core config.py file.\"\"\"\n        log.debug(f\"Loading core config from: {CORE_CONFIG_PATH}\")\n        if not CORE_CONFIG_PATH.is_file():\n            raise ConfigurationError(f\"Core configuration file not found: {CORE_CONFIG_PATH}\")\n        try:\n            spec = importlib.util.spec_from_file_location(\"core_config\", CORE_CONFIG_PATH)\n            if spec is None or spec.loader is None:\n                 raise ConfigurationError(f\"Could not create module spec for {CORE_CONFIG_PATH}\")\n            core_config_module = importlib.util.module_from_spec(spec)\n            # Define default values for core settings in case they are missing in config.py\n            default_core_settings = {\n                'TARGET_FILENAME_PATTERN': \"{base_name}_{map_type}_{resolution}.{ext}\",\n                'STANDARD_MAP_TYPES': [],\n                'EXTRA_FILES_SUBDIR': \"Extra\",\n                'METADATA_FILENAME': \"metadata.json\",\n                'IMAGE_RESOLUTIONS': {},\n                'ASPECT_RATIO_DECIMALS': 2,\n                'MAP_BIT_DEPTH_RULES': {\"DEFAULT\": \"respect\"},\n                'OUTPUT_FORMAT_16BIT_PRIMARY': \"png\",\n                'OUTPUT_FORMAT_16BIT_FALLBACK': \"png\",\n                'OUTPUT_FORMAT_8BIT': \"png\",\n                'MAP_MERGE_RULES': [],\n                'CALCULATE_STATS_RESOLUTION': \"1K\",\n                'DEFAULT_ASSET_CATEGORY': \"Texture\",\n                'TEMP_DIR_PREFIX': \"_PROCESS_ASSET_\",\n                # --- Additions ---\n                'JPG_QUALITY': 95, # Default JPG quality\n                'RESOLUTION_THRESHOLD_FOR_JPG': 4096, # Default threshold\n                'RESPECT_VARIANT_MAP_TYPES': [], # Default for map types that always get suffix\n                'FORCE_LOSSLESS_MAP_TYPES': [] # Default for map types that must be lossless\n            }\n            # Load attributes from module, using defaults if missing\n            settings = default_core_settings.copy()\n            spec.loader.exec_module(core_config_module)\n            for name in default_core_settings:\n                 if hasattr(core_config_module, name):\n                      settings[name] = getattr(core_config_module, name)\n\n            log.debug(f\"Core config loaded successfully.\")\n            return settings\n        except Exception as e:\n            raise ConfigurationError(f\"Failed to load core configuration from {CORE_CONFIG_PATH}: {e}\")\n\n    def _load_preset(self, preset_name: str) -> dict:\n        \"\"\"Loads the specified preset JSON file.\"\"\"\n        log.debug(f\"Loading preset: '{preset_name}' from {PRESETS_DIR}\")\n        if not PRESETS_DIR.is_dir():\n             raise ConfigurationError(f\"Presets directory not found: {PRESETS_DIR}\")\n\n        preset_file = PRESETS_DIR / f\"{preset_name}.json\"\n        if not preset_file.is_file():\n            raise ConfigurationError(f\"Preset file not found: {preset_file}\")\n\n        try:\n            with open(preset_file, 'r', encoding='utf-8') as f:\n                preset_data = json.load(f)\n            log.debug(f\"Preset '{preset_name}' loaded successfully.\")\n            return preset_data\n        except json.JSONDecodeError as e:\n            raise ConfigurationError(f\"Failed to parse preset file {preset_file}: Invalid JSON - {e}\")\n        except Exception as e:\n            raise ConfigurationError(f\"Failed to read preset file {preset_file}: {e}\")\n\n    def _validate_configs(self):\n        \"\"\"Performs basic validation checks on loaded settings.\"\"\"\n        log.debug(\"Validating loaded configurations...\")\n        # Preset validation\n        required_preset_keys = [\n            \"preset_name\", \"supplier_name\", \"source_naming\", \"map_type_mapping\",\n            \"asset_category_rules\", \"archetype_rules\", \"move_to_extra_patterns\"\n        ]\n        for key in required_preset_keys:\n            if key not in self._preset_settings:\n                raise ConfigurationError(f\"Preset '{self.preset_name}' is missing required key: '{key}'.\")\n\n        # Validate map_type_mapping structure (new format)\n        if not isinstance(self._preset_settings['map_type_mapping'], list):\n             raise ConfigurationError(f\"Preset '{self.preset_name}': 'map_type_mapping' must be a list.\")\n        for index, rule in enumerate(self._preset_settings['map_type_mapping']):\n             if not isinstance(rule, dict):\n                  raise ConfigurationError(f\"Preset '{self.preset_name}': Rule at index {index} in 'map_type_mapping' must be a dictionary.\")\n             if 'target_type' not in rule or not isinstance(rule['target_type'], str):\n                  raise ConfigurationError(f\"Preset '{self.preset_name}': Rule at index {index} in 'map_type_mapping' is missing 'target_type' string.\")\n             if 'keywords' not in rule or not isinstance(rule['keywords'], list):\n                  raise ConfigurationError(f\"Preset '{self.preset_name}': Rule at index {index} in 'map_type_mapping' is missing 'keywords' list.\")\n             for kw_index, keyword in enumerate(rule['keywords']):\n                  if not isinstance(keyword, str):\n                       raise ConfigurationError(f\"Preset '{self.preset_name}': Keyword at index {kw_index} in rule {index} ('{rule['target_type']}') must be a string.\")\n\n\n        # Core validation (check types or specific values if needed)\n        if not isinstance(self._core_settings.get('TARGET_FILENAME_PATTERN'), str):\n             raise ConfigurationError(\"Core config 'TARGET_FILENAME_PATTERN' must be a string.\")\n        if not isinstance(self._core_settings.get('IMAGE_RESOLUTIONS'), dict):\n             raise ConfigurationError(\"Core config 'IMAGE_RESOLUTIONS' must be a dictionary.\")\n        if not isinstance(self._core_settings.get('STANDARD_MAP_TYPES'), list):\n             raise ConfigurationError(\"Core config 'STANDARD_MAP_TYPES' must be a list.\")\n        # Add more checks as necessary\n        log.debug(\"Configuration validation passed.\")\n\n\n    # --- Accessor Methods/Properties ---\n    # Use @property for direct access, methods for potentially complex lookups/defaults\n\n    @property\n    def supplier_name(self) -> str:\n        return self._preset_settings.get('supplier_name', 'DefaultSupplier')\n        \n    @property\n    def default_asset_category(self) -> str:\n        \"\"\"Gets the default asset category from core settings.\"\"\"\n        # Provide a fallback default just in case it's missing from config.py\n        return self._core_settings.get('DEFAULT_ASSET_CATEGORY', 'Texture')\n\n    @property\n    def target_filename_pattern(self) -> str:\n        return self._core_settings['TARGET_FILENAME_PATTERN'] # Assumes validation passed\n\n    @property\n    def image_resolutions(self) -> dict[str, int]:\n        return self._core_settings['IMAGE_RESOLUTIONS']\n\n    @property\n    def standard_map_types(self) -> list[str]:\n        return self._core_settings['STANDARD_MAP_TYPES']\n\n    @property\n    def map_type_mapping(self) -> list:\n        return self._preset_settings['map_type_mapping']\n\n    @property\n    def source_naming_separator(self) -> str:\n        return self._preset_settings.get('source_naming', {}).get('separator', '_')\n\n    @property\n    def source_naming_indices(self) -> dict:\n        return self._preset_settings.get('source_naming', {}).get('part_indices', {})\n\n    @property\n    def source_glossiness_keywords(self) -> list:\n        return self._preset_settings.get('source_naming', {}).get('glossiness_keywords', [])\n\n    @property\n    def source_bit_depth_variants(self) -> dict:\n         return self._preset_settings.get('source_naming', {}).get('bit_depth_variants', {})\n\n    @property\n    def archetype_rules(self) -> list:\n         return self._preset_settings['archetype_rules']\n\n    @property\n    def asset_category_rules(self) -> dict:\n         return self._preset_settings['asset_category_rules']\n\n    @property\n    def move_to_extra_patterns(self) -> list:\n        return self._preset_settings['move_to_extra_patterns']\n\n    @property\n    def extra_files_subdir(self) -> str:\n        return self._core_settings['EXTRA_FILES_SUBDIR']\n\n    @property\n    def metadata_filename(self) -> str:\n        return self._core_settings['METADATA_FILENAME']\n\n    @property\n    def calculate_stats_resolution(self) -> str:\n        return self._core_settings['CALCULATE_STATS_RESOLUTION']\n\n    @property\n    def map_merge_rules(self) -> list:\n         return self._core_settings['MAP_MERGE_RULES']\n\n    @property\n    def aspect_ratio_decimals(self) -> int:\n         return self._core_settings['ASPECT_RATIO_DECIMALS']\n\n    @property\n    def temp_dir_prefix(self) -> str:\n         return self._core_settings['TEMP_DIR_PREFIX']\n\n    @property\n    def jpg_quality(self) -> int:\n        \"\"\"Gets the configured JPG quality level.\"\"\"\n        return self._core_settings.get('JPG_QUALITY', 95) # Use default if somehow missing\n\n    @property\n    def resolution_threshold_for_jpg(self) -> int:\n        \"\"\"Gets the pixel dimension threshold for using JPG for 8-bit images.\"\"\"\n        return self._core_settings.get('RESOLUTION_THRESHOLD_FOR_JPG', 4096)\n\n    @property\n    def respect_variant_map_types(self) -> list:\n        \"\"\"Gets the list of map types that should always respect variant numbering.\"\"\"\n        # Ensure it returns a list, even if missing from config.py (though defaults should handle it)\n        return self._core_settings.get('RESPECT_VARIANT_MAP_TYPES', [])\n\n    @property\n    def force_lossless_map_types(self) -> list:\n        \"\"\"Gets the list of map types that must always be saved losslessly.\"\"\"\n        return self._core_settings.get('FORCE_LOSSLESS_MAP_TYPES', [])\n\n    def get_bit_depth_rule(self, map_type: str) -> str:\n        \"\"\"Gets the bit depth rule ('respect' or 'force_8bit') for a given standard map type.\"\"\"\n        rules = self._core_settings.get('MAP_BIT_DEPTH_RULES', {})\n        default_rule = rules.get('DEFAULT', 'respect')\n        return rules.get(map_type, default_rule)\n\n    def get_16bit_output_formats(self) -> tuple[str, str]:\n        \"\"\"Gets the primary and fallback format names for 16-bit output.\"\"\"\n        primary = self._core_settings.get('OUTPUT_FORMAT_16BIT_PRIMARY', 'png')\n        fallback = self._core_settings.get('OUTPUT_FORMAT_16BIT_FALLBACK', 'png')\n        return primary.lower(), fallback.lower()\n\n    def get_8bit_output_format(self) -> str:\n        \"\"\"Gets the format name for 8-bit output.\"\"\"\n        return self._core_settings.get('OUTPUT_FORMAT_8BIT', 'png').lower()"
        }
    ]
}