from __future__ import annotations

import shutil
import struct
import re
from dataclasses import asdict, dataclass, field
from pathlib import Path
from typing import Any, Protocol

from liverylab_core.proto import (
    ProtoField,
    build_fields,
    is_printable_utf8,
    parse_fields,
    patch_strings_recursive_exact,
)


DRIVER_FINALSTATE_FIELD = 12
DRIVER_DESIGN_SLOT_TOKEN = "DRIVER"
DRIVER_FILE_SUFFIXES = {".driverfinalstate", ".material", ".mesh", ".texture", ".texturemips"}
PATCHABLE_SUFFIXES = {".driverfinalstate", ".material", ".mesh", ".texture", ".design", ".visualcarpreset"}
DRIVER_VISUAL_MESH_CATEGORIES = {"helmet", "suit", "gloves"}
DRIVER_VISUAL_MATERIAL_CATEGORIES = {"helmet", "helmetGlass", "suit", "gloves"}
DRIVER_REF_TOKEN_PREFIXES: dict[str, tuple[str, ...]] = {
    "helmet": ("helmet_",),
    "helmetGlass": ("helmet_",),
    "suit": ("suit_",),
    "gloves": ("glove_", "gloves_"),
}
SUIT_LAYER_PARAMETER_NAMES = {
    "Red_Basecolor",
    "Green_Basecolor",
    "Blue_Basecolor",
    "Red_Enable",
    "Green_Enable",
    "Blue_Enable",
    "RedOverrideReplace_BaseColor",
    "GreenOverrideReplace_BaseColor",
    "BlueOverrideReplace_BaseColor",
}
DRIVER_PNG_ONLY_NEUTRAL_PARAMETERS: dict[str, float | list[float]] = {
    "Base_Basecolor": [1.0, 1.0, 1.0],
    "Base_HasBaseColorMap": 1.0,
    "HasBaseColorMap": 1.0,
    "ksBaseColor": [1.0, 1.0, 1.0, 1.0],
    "ksHasBaseColorMap": 1.0,
    "Base_HasNormalMap": 0.0,
    "Base_NormalScale": 0.0,
    "Base_HasMetalnessMap": 0.0,
    "Base_Metalness": 0.0,
    "Base_HasRoughnessMap": 0.0,
    "Base_Roughness": 0.800000011920929,
    "Base_HasAmbientOcclusionMap": 0.0,
    "Base_HasClearCoatRoughnessMap": 0.0,
    "Base_ClearCoatRoughness": 0.0,
    "Base_HasClearCoatMap": 0.0,
    "Base_ClearCoat": 0.0,
    "Base_HasClearCoatNormalMap": 0.0,
    "Base_ClearCoatNormalScale": 0.0,
    "Base_HasAnisotropyMap": 0.0,
    "HasNormalMap": 0.0,
    "NormalScale": 0.0,
    "HasMetalnessMap": 0.0,
    "Metalness": 0.0,
    "HasRoughnessMap": 0.0,
    "Roughness": 0.800000011920929,
    "HasAmbientOcclusionMap": 0.0,
    "HasAnisotropyMap": 0.0,
    "ksHasNormalMap": 0.0,
    "ksNormalScale": 0.0,
    "ksHasMetalnessMap": 0.0,
    "ksMetalness": 0.0,
    "ksHasRoughnessMap": 0.0,
    "ksRoughness": 0.800000011920929,
    "ksHasAOMap": 0.0,
    "Red_Basecolor": [1.0, 1.0, 1.0],
    "Green_Basecolor": [1.0, 1.0, 1.0],
    "Blue_Basecolor": [1.0, 1.0, 1.0],
    "Red_Enable": 0.0,
    "Green_Enable": 0.0,
    "Blue_Enable": 0.0,
    "RedOverrideReplace_BaseColor": 0.0,
    "GreenOverrideReplace_BaseColor": 0.0,
    "BlueOverrideReplace_BaseColor": 0.0,
    "Splatmap_UseConstant": 0.0,
    "Modulation_BaseBaseColor": 1.0,
    "Red_HasNormalMap": 0.0,
    "Green_HasNormalMap": 0.0,
    "Blue_HasNormalMap": 0.0,
    "Red_NormalScale": 0.0,
    "Green_NormalScale": 0.0,
    "Blue_NormalScale": 0.0,
    "RedOverrideCombine_Normal": 0.0,
    "GreenOverrideCombine_Normal": 0.0,
    "BlueOverrideCombine_Normal": 0.0,
    "Red_HasAnisotropyMap": 0.0,
    "Green_HasAnisotropyMap": 0.0,
    "Blue_HasAnisotropyMap": 0.0,
    "Red_Anisotropy": 0.0,
    "Green_Anisotropy": 0.0,
    "Blue_Anisotropy": 0.0,
    "RedOverrideReplace_Anisotropy": 0.0,
    "GreenOverrideReplace_Anisotropy": 0.0,
    "BlueOverrideReplace_Anisotropy": 0.0,
    "RedOverrideMultiply_Anisotropy": 0.0,
    "GreenOverrideMultiply_Anisotropy": 0.0,
    "BlueOverrideMultiply_Anisotropy": 0.0,
}
DRIVER_LAYER_REPAIR_PARAMETER_NAMES = set(DRIVER_PNG_ONLY_NEUTRAL_PARAMETERS)
DRIVER_HELMET_PNG_ONLY_TEXTURE_REPLACEMENTS: dict[str, str] = {
    "content\\cars\\common_assets\\textures\\carbon\\T_CarbonFiber_Twill_N.texture": "editor\\textures\\default_material\\normal2.texture",
    "content\\cars\\common_assets\\textures\\carbon\\T_CarbonFiber_Twill_AN.texture": "editor\\textures\\default_material\\roughness2.texture",
    "content\\cars\\common_assets\\textures\\carpaint\\flakes_NM.texture": "editor\\textures\\default_material\\normal2.texture",
    "content\\cars\\common_assets\\textures\\cloth\\detail_FabricPlain_NM.texture": "editor\\textures\\default_material\\normal2.texture",
    "content\\cars\\common_assets\\textures\\cloth\\detail_strap_NM.texture": "editor\\textures\\default_material\\normal2.texture",
    "content\\cars\\common_assets\\textures\\cloth\\detail_strap_AN.texture": "editor\\textures\\default_material\\roughness2.texture",
    "content\\cars\\common_assets\\textures\\plastic\\detail_rubber_NM.texture": "editor\\textures\\default_material\\normal2.texture",
}
DRIVER_HELMET_PNG_ONLY_TEXTURE_SLOTS: dict[str, str] = {
    "Base_NormalMap": "editor\\textures\\default_material\\normal2.texture",
    "NormalMap": "editor\\textures\\default_material\\normal2.texture",
    "txNormal": "editor\\textures\\default_material\\normal2.texture",
    "Red_NormalMap": "editor\\textures\\default_material\\normal2.texture",
    "Green_NormalMap": "editor\\textures\\default_material\\normal2.texture",
    "Blue_NormalMap": "editor\\textures\\default_material\\normal2.texture",
    "Red_AnisotropyMap": "editor\\textures\\default_material\\roughness2.texture",
    "Green_AnisotropyMap": "editor\\textures\\default_material\\roughness2.texture",
    "Blue_AnisotropyMap": "editor\\textures\\default_material\\roughness2.texture",
    "RoughnessMap": "editor\\textures\\default_material\\roughness2.texture",
    "txRoughness": "editor\\textures\\default_material\\roughness2.texture",
}


def png_only_neutral_parameters_for_category(category: str) -> dict[str, float | list[float]]:
    clean = str(category or "").strip()
    if clean == "helmet":
        return DRIVER_PNG_ONLY_NEUTRAL_PARAMETERS
    if clean in {"suit", "gloves"}:
        return {
            "Base_Basecolor": [1.0, 1.0, 1.0],
            "ksBaseColor": [1.0, 1.0, 1.0, 1.0],
            "Red_Basecolor": [1.0, 1.0, 1.0],
            "Green_Basecolor": [1.0, 1.0, 1.0],
            "Blue_Basecolor": [1.0, 1.0, 1.0],
            "RedOverrideReplace_BaseColor": 0.0,
            "GreenOverrideReplace_BaseColor": 0.0,
            "BlueOverrideReplace_BaseColor": 0.0,
            "Modulation_BaseBaseColor": 1.0,
        }
    return {}


def png_only_texture_replacements_for_category(category: str) -> dict[str, str]:
    if str(category or "").strip() == "helmet":
        return DRIVER_HELMET_PNG_ONLY_TEXTURE_REPLACEMENTS
    return {}


def png_only_texture_slots_for_category(category: str) -> dict[str, str]:
    if str(category or "").strip() == "helmet":
        return DRIVER_HELMET_PNG_ONLY_TEXTURE_SLOTS
    return {}


class WriterLike(Protocol):
    def copy_file(self, source: str | Path, target: str | Path, label: str = "", backup: bool = True) -> Any: ...

    def write_bytes(self, target: str | Path, data: bytes, label: str = "", backup: bool = True) -> Any: ...


@dataclass
class DriverTextureSlot:
    material_ref: str
    slot_name: str
    texture_ref: str
    target_ref: str
    category: str
    source_path: str
    target_path: str
    mips_source_path: str = ""
    mips_target_path: str = ""
    exists: bool = False
    mips_exists: bool = False


@dataclass
class DriverMaterialRef:
    source_ref: str
    target_ref: str
    category: str
    source_path: str
    target_path: str
    exists: bool = False
    parent_material_ref: str = ""
    texture_slots: list[DriverTextureSlot] = field(default_factory=list)
    suit_layer_parameters: dict[str, Any] = field(default_factory=dict)


@dataclass
class DriverDesignSlotRef:
    slot_id: str
    source_ref: str
    target_ref: str
    source_path: str
    target_path: str
    exists: bool = False
    texture_slots: list[DriverTextureSlot] = field(default_factory=list)


@dataclass
class DriverCopyEntry:
    source_ref: str
    target_ref: str
    source_path: str
    target_path: str
    kind: str
    exists: bool


@dataclass
class DriverSkinPlan:
    car_content_path: str
    skin_id: str
    source_visual_ref: str = ""
    target_visual_ref: str = ""
    source_driverfinalstate_ref: str = ""
    target_driverfinalstate_ref: str = ""
    source_driverfinalstate_path: str = ""
    target_driverfinalstate_path: str = ""
    driver_materials: list[DriverMaterialRef] = field(default_factory=list)
    design_slots: list[DriverDesignSlotRef] = field(default_factory=list)
    replacements: dict[str, str] = field(default_factory=dict)
    copy_entries: list[DriverCopyEntry] = field(default_factory=list)
    warnings: list[str] = field(default_factory=list)

    def to_dict(self) -> dict[str, Any]:
        return asdict(self)


def norm_slashes(value: str | None) -> str:
    return str(value or "").replace("/", "\\").strip()


def split_content_path(path: str | None) -> list[str]:
    return [part for part in norm_slashes(path).split("\\") if part]


def normalize_game_rel_path(path: str | None) -> str:
    parts = split_content_path(path)
    while len(parts) >= 4 and [part.lower() for part in parts[:4]] == ["content", "cars", "content", "cars"]:
        parts = parts[2:]
    return norm_slashes("\\".join(parts))


def _normalize_exact_text(value: str) -> str:
    return value.replace("/", "\\") if ("\\" in value or "/" in value) else value


def _rel_to_disk(root: Path, rel: str) -> Path:
    parts = split_content_path(rel)
    return root / Path(*parts)


def resolve_content_path(root: str | Path, rel: str | None) -> Path:
    root_path = Path(root)
    parts = split_content_path(rel)
    candidates = [root_path / Path(*parts)] if parts else [root_path]
    if parts and parts[0].lower() == "content":
        candidates.append(root_path / Path(*parts[1:]))
        candidates.append(root_path.parent / Path(*parts))
    if "skins" in [part.lower() for part in parts]:
        lower_parts = [part.lower() for part in parts]
        index = lower_parts.index("skins")
        candidates.append(root_path / Path(*parts[index:]))
    for candidate in candidates:
        if candidate.exists():
            return candidate
    return candidates[0]


def content_to_disk(root: str | Path, rel: str | None) -> Path:
    return _rel_to_disk(Path(root), normalize_game_rel_path(rel))


def collect_proto_strings(raw: bytes, prefix: str = "", depth: int = 0, max_depth: int = 8) -> list[tuple[str, str]]:
    if depth > max_depth:
        return []
    try:
        fields = parse_fields(raw)
    except Exception:
        return []
    found: list[tuple[str, str]] = []
    for field in fields:
        if field.wire != 2 or not isinstance(field.value, (bytes, bytearray)):
            continue
        path = f"{prefix}.{field.number}" if prefix else str(field.number)
        text = is_printable_utf8(field.value)
        if text is not None:
            found.append((path, text))
        else:
            found.extend(collect_proto_strings(bytes(field.value), path, depth + 1, max_depth))
    return found


def _scan_ascii_strings(data: bytes, min_len: int = 4) -> list[str]:
    rows: list[str] = []
    start: int | None = None
    for index, byte in enumerate(data + b"\x00"):
        if 32 <= byte <= 126:
            if start is None:
                start = index
            continue
        if start is not None and index - start >= min_len:
            rows.append(data[start:index].decode("ascii", errors="replace"))
        start = None
    return rows


def _content_refs_from_text(text: str, suffixes: set[str]) -> list[str]:
    endings = sorted(suffixes, key=len, reverse=True)
    if not endings:
        return []
    pattern = re.compile(r"content\\[A-Za-z0-9_ .\\/-]+?(?:" + "|".join(re.escape(suffix) for suffix in endings) + r")", re.I)
    return [normalize_game_rel_path(match.group(0)) for match in pattern.finditer(text)]


def _content_refs_with_suffix(raw: bytes, suffixes: set[str]) -> list[str]:
    refs: list[str] = []
    seen: set[str] = set()
    source_texts = [text for _path, text in collect_proto_strings(raw)]
    source_texts.extend(_scan_ascii_strings(raw))
    for text in source_texts:
        candidates = _content_refs_from_text(text, suffixes)
        normalized_text = normalize_game_rel_path(text)
        if normalized_text.lower().startswith("content\\"):
            candidates.append(normalized_text)
        for normalized in candidates:
            lower = normalized.lower()
            if not lower.startswith("content\\"):
                continue
            if not any(lower.endswith(suffix) for suffix in suffixes):
                continue
            if lower in seen:
                continue
            seen.add(lower)
            refs.append(normalized)
    return refs


def extract_visual_driverfinalstate_ref(data: bytes) -> str | None:
    for field in parse_fields(data):
        if field.number != DRIVER_FINALSTATE_FIELD or field.wire != 2:
            continue
        text = is_printable_utf8(field.value)
        if text and text.lower().endswith(".driverfinalstate"):
            return normalize_game_rel_path(text)
    return None


def patch_visual_driverfinalstate_ref(data: bytes, driverfinalstate_ref: str) -> bytes:
    target = normalize_game_rel_path(driverfinalstate_ref)
    fields = parse_fields(data)
    changed = False
    written = False
    new_fields: list[ProtoField] = []
    for field in fields:
        if field.number == DRIVER_FINALSTATE_FIELD and field.wire == 2:
            written = True
            current = is_printable_utf8(field.value) or ""
            if norm_slashes(current) != target:
                new_fields.append(ProtoField(field.number, field.wire, target.encode("utf-8")))
                changed = True
            else:
                new_fields.append(field)
            continue
        new_fields.append(field)
    if not written:
        new_fields.append(ProtoField(DRIVER_FINALSTATE_FIELD, 2, target.encode("utf-8")))
        changed = True
    return build_fields(new_fields) if changed else data


def extract_driverfinalstate_material_refs(data: bytes) -> list[str]:
    return _content_refs_with_suffix(data, {".material"})


def extract_driverfinalstate_mesh_refs(data: bytes) -> list[str]:
    return _content_refs_with_suffix(data, {".mesh"})


def extract_mesh_material_refs(data: bytes) -> list[str]:
    return _content_refs_with_suffix(data, {".material"})


def extract_design_driver_slots(design_data: bytes) -> dict[str, list[str]]:
    slots: dict[str, list[str]] = {}
    for field in parse_fields(design_data):
        if field.number != 3 or field.wire != 2:
            continue
        try:
            slot_fields = parse_fields(bytes(field.value))
        except Exception:
            continue
        slot_id = ""
        material_ref = ""
        for slot_field in slot_fields:
            if slot_field.wire != 2:
                continue
            text = is_printable_utf8(slot_field.value)
            if slot_field.number == 1 and text:
                slot_id = text
            elif slot_field.number == 3 and text:
                material_ref = normalize_game_rel_path(text)
        if slot_id and material_ref and DRIVER_DESIGN_SLOT_TOKEN in slot_id.upper():
            slots.setdefault(slot_id, []).append(material_ref)
    return slots


def _fixed32_to_float(raw: bytes) -> float:
    return struct.unpack("<f", bytes(raw))[0]


def _float_to_fixed32(value: float) -> bytes:
    return struct.pack("<f", float(value))


def _decode_material_value(raw: bytes) -> Any:
    if raw == b"":
        return None
    try:
        fields = parse_fields(raw)
    except Exception:
        text = is_printable_utf8(raw)
        return text if text is not None else {"raw_hex": raw.hex(" ")}
    if len(fields) == 1 and fields[0].number == 1 and fields[0].wire == 5:
        return _fixed32_to_float(fields[0].value)
    if len(fields) == 1 and fields[0].number == 2 and fields[0].wire == 2:
        text = is_printable_utf8(fields[0].value)
        if text is not None:
            return text
    if len(fields) == 1 and fields[0].wire == 2:
        try:
            nested = parse_fields(bytes(fields[0].value))
        except Exception:
            nested = []
        values = [
            _fixed32_to_float(field.value)
            for field in nested
            if field.wire == 5 and isinstance(field.value, (bytes, bytearray)) and len(field.value) == 4
        ]
        if values:
            return values
    return None


def _material_parse_entry(raw: bytes) -> tuple[str | None, Any, bytes | None]:
    fields = parse_fields(raw)
    name = None
    value_raw = None
    value_varint = None
    for field in fields:
        if field.number == 1 and field.wire == 2 and isinstance(field.value, bytes):
            name = is_printable_utf8(field.value)
        elif field.number == 2:
            if field.wire == 2 and isinstance(field.value, bytes):
                value_raw = bytes(field.value)
            elif field.wire == 0:
                value_varint = int(field.value)
    if value_raw is not None:
        return name, _decode_material_value(value_raw), value_raw
    if value_varint is not None:
        return name, value_varint, None
    return name, None, None


def inspect_driver_material_data(material_data: bytes) -> dict[str, Any]:
    result: dict[str, Any] = {
        "shader": "",
        "parentMaterial": "",
        "textureSlots": [],
        "parameters": [],
        "suitLayerParameters": {},
    }
    for field in parse_fields(material_data):
        if field.number == 1 and field.wire == 2:
            result["shader"] = is_printable_utf8(field.value) or ""
        elif field.number == 20 and field.wire == 2:
            result["parentMaterial"] = normalize_game_rel_path(is_printable_utf8(field.value) or "")
        elif field.number == 5 and field.wire == 2:
            name, value, _raw = _material_parse_entry(bytes(field.value))
            if isinstance(value, str) and normalize_game_rel_path(value).lower().endswith(".texture"):
                result["textureSlots"].append({"name": name or "", "value": normalize_game_rel_path(value)})
        elif field.number == 4 and field.wire == 2:
            name, value, _raw = _material_parse_entry(bytes(field.value))
            result["parameters"].append({"name": name or "", "value": value})
            if name in SUIT_LAYER_PARAMETER_NAMES:
                result["suitLayerParameters"][name] = value
    return result


def inspect_driver_material(material_path: str | Path) -> dict[str, Any]:
    path = Path(material_path)
    result = inspect_driver_material_data(path.read_bytes())
    result["file"] = str(path)
    result["sizeBytes"] = path.stat().st_size if path.exists() else 0
    return result


def classify_driver_ref(ref: str, slot_name: str = "") -> str:
    value = f"{ref}\\{slot_name}".lower()
    name = Path(ref.replace("\\", "/")).name.lower()
    if "glove" in value:
        return "gloves"
    if "suit" in value or "drivers" in value:
        return "suit"
    if "glass" in value or "visor" in value:
        return "helmetGlass"
    if "helmet" in value:
        return "helmet"
    if name.endswith(".driverfinalstate"):
        return "driverFinalState"
    return "driver"


def driver_ref_variant_tokens(ref: str, category: str = "") -> set[str]:
    prefixes = DRIVER_REF_TOKEN_PREFIXES.get(category) or tuple(prefix for values in DRIVER_REF_TOKEN_PREFIXES.values() for prefix in values)
    text = norm_slashes(ref).replace("/", "\\").lower()
    tokens: set[str] = set()
    for match in re.finditer(r"[a-z]+_\d+", text):
        token = match.group(0)
        if any(token.startswith(prefix) for prefix in prefixes):
            tokens.add(token)
    return tokens


def source_driver_mesh_should_localize(mesh_ref: str, driver_material_refs: list[str] | tuple[str, ...]) -> bool:
    mesh_category = classify_driver_ref(mesh_ref)
    if mesh_category not in DRIVER_VISUAL_MESH_CATEGORIES:
        return False
    category_materials = [ref for ref in driver_material_refs if classify_driver_ref(ref) in {mesh_category, "helmetGlass" if mesh_category == "helmet" else mesh_category}]
    if not category_materials:
        return False
    mesh_tokens = driver_ref_variant_tokens(mesh_ref, mesh_category)
    if not mesh_tokens:
        return True
    material_tokens: set[str] = set()
    for material_ref in category_materials:
        material_tokens.update(driver_ref_variant_tokens(material_ref, classify_driver_ref(material_ref)))
    return not material_tokens or bool(mesh_tokens & material_tokens)


def source_driver_material_for_mesh_material(
    mesh_ref: str,
    mesh_material_ref: str,
    driver_material_refs: list[str] | tuple[str, ...],
) -> str:
    mesh_category = classify_driver_ref(mesh_ref)
    material_category = classify_driver_ref(mesh_material_ref)
    compatible_categories = {material_category}
    if mesh_category == "helmet" and material_category == "helmetGlass":
        compatible_categories.add("helmetGlass")
    candidates = [ref for ref in driver_material_refs if classify_driver_ref(ref) in compatible_categories]
    if not candidates:
        return ""
    material_tokens = driver_ref_variant_tokens(mesh_material_ref, material_category)
    if material_tokens:
        for candidate in candidates:
            if material_tokens & driver_ref_variant_tokens(candidate, classify_driver_ref(candidate)):
                return candidate
    return candidates[0] if len(candidates) == 1 else ""


def _car_skin_prefix(car_content_path: str, skin_id: str) -> str:
    return norm_slashes(f"{car_content_path}\\skins\\{skin_id}").rstrip("\\")


def _is_localizable_driver_asset_ref(ref: str, car_content_path: str) -> bool:
    normalized = normalize_game_rel_path(ref).lower()
    car_prefix = norm_slashes(car_content_path).lower().rstrip("\\")
    return (
        normalized.startswith(f"{car_prefix}\\skins\\")
        or normalized.startswith(f"{car_prefix}\\materials\\")
        or normalized.startswith(f"{car_prefix}\\textures\\")
        or normalized.startswith("content\\characters\\")
    )


def _path_name(ref: str) -> str:
    return Path(norm_slashes(ref).replace("\\", "/")).name


def _path_parent_ref(ref: str) -> str:
    return norm_slashes(str(Path(norm_slashes(ref).replace("\\", "/")).parent))


def _with_suffix_ref(ref: str, suffix: str) -> str:
    return norm_slashes(str(Path(norm_slashes(ref).replace("\\", "/")).with_suffix(suffix)))


def _driver_target_ref_for_car_skin_ref(ref: str, car_content_path: str, skin_id: str) -> str | None:
    normalized = normalize_game_rel_path(ref)
    lower = normalized.lower()
    car_prefix = norm_slashes(car_content_path).lower().rstrip("\\")
    marker = f"{car_prefix}\\skins\\driver\\"
    if lower.startswith(marker):
        return norm_slashes(f"{_car_skin_prefix(car_content_path, skin_id)}\\driver\\{normalized[len(marker):]}")
    marker = f"{car_prefix}\\skins\\"
    if lower.startswith(marker):
        rest = normalized[len(marker):]
        parts = split_content_path(rest)
        if len(parts) >= 2 and parts[0].lower() == skin_id.lower() and parts[1].lower() == "driver":
            return normalized
        if len(parts) >= 2 and parts[1].lower() == "driver":
            driver_rest = "\\".join(parts[2:])
            return norm_slashes(f"{_car_skin_prefix(car_content_path, skin_id)}\\driver\\{driver_rest}")
        if len(parts) >= 2:
            return norm_slashes(f"{_car_skin_prefix(car_content_path, skin_id)}\\{parts[-1]}")
    marker = f"{car_prefix}\\materials\\"
    if lower.startswith(marker):
        return norm_slashes(f"{_car_skin_prefix(car_content_path, skin_id)}\\driver\\materials\\{_path_name(normalized)}")
    marker = f"{car_prefix}\\textures\\"
    if lower.startswith(marker):
        return norm_slashes(f"{_car_skin_prefix(car_content_path, skin_id)}\\driver\\textures\\{_path_name(normalized)}")
    return None


def target_ref_for_driver_asset(ref: str, car_content_path: str, skin_id: str, source_material_ref: str = "") -> str:
    normalized = normalize_game_rel_path(ref)
    car_target = _driver_target_ref_for_car_skin_ref(normalized, car_content_path, skin_id)
    if car_target:
        return car_target
    lower = normalized.lower()
    if lower.startswith("content\\characters\\"):
        rest = normalized[len("content\\characters\\") :]
        return norm_slashes(f"{_car_skin_prefix(car_content_path, skin_id)}\\driver\\characters\\{rest}")
    if source_material_ref:
        source_target = _driver_target_ref_for_car_skin_ref(source_material_ref, car_content_path, skin_id)
        if source_target:
            return norm_slashes(f"{_path_parent_ref(source_target)}\\{_path_name(normalized)}")
    return norm_slashes(f"{_car_skin_prefix(car_content_path, skin_id)}\\driver\\misc\\{_path_name(normalized)}")


def target_ref_for_design_driver_material(ref: str, car_content_path: str, skin_id: str) -> str:
    normalized = normalize_game_rel_path(ref)
    return norm_slashes(f"{_car_skin_prefix(car_content_path, skin_id)}\\{_path_name(normalized)}")


def _add_replacement(replacements: dict[str, str], old_ref: str, new_ref: str) -> None:
    old_norm = normalize_game_rel_path(old_ref)
    new_norm = normalize_game_rel_path(new_ref)
    if old_norm and new_norm and old_norm.lower() != new_norm.lower():
        replacements[_normalize_exact_text(old_norm)] = new_norm


def _add_copy_entry(entries: list[DriverCopyEntry], root: Path, target_root: Path, source_ref: str, target_ref: str, kind: str) -> None:
    source = resolve_content_path(root, source_ref)
    target = content_to_disk(target_root, target_ref)
    key = str(target).lower()
    if any(str(Path(entry.target_path)).lower() == key for entry in entries):
        return
    entries.append(
        DriverCopyEntry(
            source_ref=normalize_game_rel_path(source_ref),
            target_ref=normalize_game_rel_path(target_ref),
            source_path=str(source),
            target_path=str(target),
            kind=kind,
            exists=source.exists(),
        )
    )


def _material_texture_slots(
    root: Path,
    target_root: Path,
    material_ref: str,
    material_data: bytes,
    car_content_path: str,
    skin_id: str,
    replacements: dict[str, str],
    copy_entries: list[DriverCopyEntry],
) -> list[DriverTextureSlot]:
    material_info = inspect_driver_material_data(material_data)
    slots: list[DriverTextureSlot] = []
    for slot in material_info.get("textureSlots", []):
        slot_name = str(slot.get("name") or "")
        texture_ref = normalize_game_rel_path(str(slot.get("value") or ""))
        if not texture_ref:
            continue
        if not _is_localizable_driver_asset_ref(texture_ref, car_content_path):
            continue
        target_ref = target_ref_for_driver_asset(texture_ref, car_content_path, skin_id, source_material_ref=material_ref)
        _add_replacement(replacements, texture_ref, target_ref)
        _add_copy_entry(copy_entries, root, target_root, texture_ref, target_ref, "texture")
        source_path = resolve_content_path(root, texture_ref)
        target_path = content_to_disk(target_root, target_ref)
        mips_source = source_path.with_suffix(".texturemips")
        mips_target_ref = _with_suffix_ref(target_ref, ".texturemips")
        mips_target = content_to_disk(target_root, mips_target_ref)
        if mips_source.exists():
            _add_copy_entry(copy_entries, root, target_root, normalize_game_rel_path(str(mips_source.relative_to(root))) if _is_under_root(mips_source, root) else texture_ref.replace(".texture", ".texturemips"), mips_target_ref, "texturemips")
        slots.append(
            DriverTextureSlot(
                material_ref=material_ref,
                slot_name=slot_name,
                texture_ref=texture_ref,
                target_ref=target_ref,
                category=classify_driver_ref(texture_ref, slot_name),
                source_path=str(source_path),
                target_path=str(target_path),
                mips_source_path=str(mips_source),
                mips_target_path=str(mips_target),
                exists=source_path.exists(),
                mips_exists=mips_source.exists(),
            )
        )
    return slots


def _is_under_root(path: Path, root: Path) -> bool:
    try:
        path.resolve().relative_to(root.resolve())
        return True
    except Exception:
        return False


def build_driver_skin_plan(
    game_root: str | Path,
    target_root: str | Path,
    car_content_path: str,
    skin_id: str,
    visual_data: bytes,
    design_data: bytes | None = None,
    source_visual_ref: str = "",
    target_visual_ref: str = "",
) -> DriverSkinPlan:
    root = Path(game_root)
    output_root = Path(target_root)
    car_ref = norm_slashes(car_content_path).rstrip("\\")
    plan = DriverSkinPlan(
        car_content_path=car_ref,
        skin_id=skin_id,
        source_visual_ref=normalize_game_rel_path(source_visual_ref),
        target_visual_ref=normalize_game_rel_path(target_visual_ref),
    )
    driver_ref = extract_visual_driverfinalstate_ref(visual_data)
    if not driver_ref:
        plan.warnings.append("Visual preset has no driver final state field.")
        return plan

    target_driver_ref = target_ref_for_driver_asset(driver_ref, car_ref, skin_id)
    plan.source_driverfinalstate_ref = driver_ref
    plan.target_driverfinalstate_ref = target_driver_ref
    plan.source_driverfinalstate_path = str(resolve_content_path(root, driver_ref))
    plan.target_driverfinalstate_path = str(content_to_disk(output_root, target_driver_ref))
    _add_replacement(plan.replacements, driver_ref, target_driver_ref)
    _add_copy_entry(plan.copy_entries, root, output_root, driver_ref, target_driver_ref, "driverfinalstate")

    source_driver_path = resolve_content_path(root, driver_ref)
    if not source_driver_path.exists():
        plan.warnings.append(f"Driver final state not found: {driver_ref}")
    else:
        source_driver_data = source_driver_path.read_bytes()
        driver_material_refs = extract_driverfinalstate_material_refs(source_driver_data)
        target_material_by_category: dict[str, str] = {}
        for material_ref in driver_material_refs:
            target_material_ref = target_ref_for_driver_asset(material_ref, car_ref, skin_id)
            category = classify_driver_ref(material_ref)
            if category in DRIVER_VISUAL_MATERIAL_CATEGORIES and category not in target_material_by_category:
                target_material_by_category[category] = target_material_ref
            _add_replacement(plan.replacements, material_ref, target_material_ref)
            _add_copy_entry(plan.copy_entries, root, output_root, material_ref, target_material_ref, "material")
            material_path = resolve_content_path(root, material_ref)
            material_info = inspect_driver_material(material_path) if material_path.exists() else {}
            material_data = material_path.read_bytes() if material_path.exists() else b""
            texture_slots = (
                _material_texture_slots(root, output_root, material_ref, material_data, car_ref, skin_id, plan.replacements, plan.copy_entries)
                if material_data
                else []
            )
            plan.driver_materials.append(
                DriverMaterialRef(
                    source_ref=material_ref,
                    target_ref=target_material_ref,
                    category=category,
                    source_path=str(material_path),
                    target_path=str(content_to_disk(output_root, target_material_ref)),
                    exists=material_path.exists(),
                    parent_material_ref=str(material_info.get("parentMaterial") or ""),
                    texture_slots=texture_slots,
                    suit_layer_parameters=dict(material_info.get("suitLayerParameters") or {}),
                )
            )
        for mesh_ref in extract_driverfinalstate_mesh_refs(source_driver_data):
            mesh_category = classify_driver_ref(mesh_ref)
            if not source_driver_mesh_should_localize(mesh_ref, driver_material_refs):
                continue
            target_mesh_ref = target_ref_for_driver_asset(mesh_ref, car_ref, skin_id)
            _add_replacement(plan.replacements, mesh_ref, target_mesh_ref)
            _add_copy_entry(plan.copy_entries, root, output_root, mesh_ref, target_mesh_ref, "mesh")
            mesh_path = resolve_content_path(root, mesh_ref)
            mesh_material_refs = extract_mesh_material_refs(mesh_path.read_bytes()) if mesh_path.exists() else []
            for material_ref in mesh_material_refs:
                material_category = classify_driver_ref(material_ref)
                if material_category not in DRIVER_VISUAL_MATERIAL_CATEGORIES:
                    continue
                source_material_ref = source_driver_material_for_mesh_material(mesh_ref, material_ref, driver_material_refs)
                if source_material_ref:
                    _add_replacement(plan.replacements, material_ref, target_ref_for_driver_asset(source_material_ref, car_ref, skin_id))
                    continue
                target_material_ref = target_ref_for_driver_asset(material_ref, car_ref, skin_id)
                _add_replacement(plan.replacements, material_ref, target_material_ref)
                _add_copy_entry(plan.copy_entries, root, output_root, material_ref, target_material_ref, "mesh_material")
                material_path = resolve_content_path(root, material_ref)
                material_info = inspect_driver_material(material_path) if material_path.exists() else {}
                material_data = material_path.read_bytes() if material_path.exists() else b""
                texture_slots = (
                    _material_texture_slots(root, output_root, material_ref, material_data, car_ref, skin_id, plan.replacements, plan.copy_entries)
                    if material_data
                    else []
                )
                plan.driver_materials.append(
                    DriverMaterialRef(
                        source_ref=material_ref,
                        target_ref=target_material_ref,
                        category=classify_driver_ref(material_ref),
                        source_path=str(material_path),
                        target_path=str(content_to_disk(output_root, target_material_ref)),
                        exists=material_path.exists(),
                        parent_material_ref=str(material_info.get("parentMaterial") or ""),
                        texture_slots=texture_slots,
                        suit_layer_parameters=dict(material_info.get("suitLayerParameters") or {}),
                    )
                )

    if design_data is not None:
        for slot_id, material_refs in extract_design_driver_slots(design_data).items():
            for material_ref in material_refs:
                target_material_ref = target_ref_for_design_driver_material(material_ref, car_ref, skin_id)
                _add_replacement(plan.replacements, material_ref, target_material_ref)
                _add_copy_entry(plan.copy_entries, root, output_root, material_ref, target_material_ref, "design_driver_material")
                material_path = resolve_content_path(root, material_ref)
                material_data = material_path.read_bytes() if material_path.exists() else b""
                texture_slots = (
                    _material_texture_slots(root, output_root, material_ref, material_data, car_ref, skin_id, plan.replacements, plan.copy_entries)
                    if material_data
                    else []
                )
                plan.design_slots.append(
                    DriverDesignSlotRef(
                        slot_id=slot_id,
                        source_ref=material_ref,
                        target_ref=target_material_ref,
                        source_path=str(material_path),
                        target_path=str(content_to_disk(output_root, target_material_ref)),
                        exists=material_path.exists(),
                        texture_slots=texture_slots,
                    )
                )

    missing = [entry.source_ref for entry in plan.copy_entries if not entry.exists]
    if missing:
        plan.warnings.append(f"Missing driver source file(s): {len(missing)}")
    return plan


def _copy_file(source: Path, target: Path, writer: WriterLike | None, label: str, overwrite: bool) -> None:
    if target.exists() and not overwrite:
        return
    target.parent.mkdir(parents=True, exist_ok=True)
    if writer is not None:
        writer.copy_file(source, target, label=label, backup=True)
    else:
        shutil.copy2(source, target)


def _write_bytes(target: Path, data: bytes, writer: WriterLike | None, label: str, overwrite: bool) -> None:
    if target.exists() and not overwrite and target.read_bytes() == data:
        return
    if target.exists() and not overwrite and target.read_bytes() != data:
        raise FileExistsError(f"Target file exists with different content: {target}")
    target.parent.mkdir(parents=True, exist_ok=True)
    if writer is not None:
        writer.write_bytes(target, data, label=label, backup=True)
    else:
        target.write_bytes(data)


def localize_driver_skin_assets(
    game_root: str | Path,
    target_root: str | Path,
    car_content_path: str,
    skin_id: str,
    visual_path: str | Path,
    target_visual_path: str | Path | None = None,
    design_path: str | Path | None = None,
    target_design_path: str | Path | None = None,
    overwrite: bool = False,
    writer: WriterLike | None = None,
) -> dict[str, Any]:
    root = Path(game_root)
    output_root = Path(target_root)
    visual = Path(visual_path)
    target_visual = Path(target_visual_path) if target_visual_path else visual
    design = Path(design_path) if design_path else None
    target_design = Path(target_design_path) if target_design_path else design
    visual_data = visual.read_bytes()
    design_data = design.read_bytes() if design and design.exists() else None
    plan = build_driver_skin_plan(
        root,
        output_root,
        car_content_path,
        skin_id,
        visual_data,
        design_data=design_data,
        source_visual_ref=normalize_game_rel_path(str(visual.relative_to(root))) if _is_under_root(visual, root) else "",
        target_visual_ref=normalize_game_rel_path(str(target_visual.relative_to(output_root))) if _is_under_root(target_visual, output_root) else "",
    )
    copied: list[str] = []
    patched: list[str] = []

    for entry in plan.copy_entries:
        source = Path(entry.source_path)
        target = Path(entry.target_path)
        if not source.exists():
            continue
        _copy_file(source, target, writer, f"driver skin {entry.kind}", overwrite=overwrite)
        copied.append(str(target))

    replacement_map = {_normalize_exact_text(old): new for old, new in plan.replacements.items()}
    for entry in plan.copy_entries:
        target = Path(entry.target_path)
        if not target.exists() or target.suffix.lower() not in PATCHABLE_SUFFIXES:
            continue
        raw = target.read_bytes()
        new_raw = patch_strings_recursive_exact(raw, replacement_map)
        if new_raw != raw:
            _write_bytes(target, new_raw, writer, "driver skin reference patch", overwrite=True)
            patched.append(str(target))

    if plan.target_driverfinalstate_ref:
        raw = target_visual.read_bytes() if target_visual.exists() else visual_data
        new_raw = patch_visual_driverfinalstate_ref(raw, plan.target_driverfinalstate_ref)
        if new_raw != raw:
            _write_bytes(target_visual, new_raw, writer, "visual driver final state", overwrite=True)
            patched.append(str(target_visual))

    if target_design and target_design.exists() and replacement_map:
        raw = target_design.read_bytes()
        new_raw = patch_strings_recursive_exact(raw, replacement_map)
        if new_raw != raw:
            _write_bytes(target_design, new_raw, writer, "design driver slot references", overwrite=True)
            patched.append(str(target_design))

    return {
        "plan": plan.to_dict(),
        "copied": copied,
        "patched": patched,
        "copiedCount": len(copied),
        "patchedCount": len(patched),
        "warnings": plan.warnings,
    }


def _encode_material_parameter_value(value: float | list[float] | tuple[float, ...], previous_raw: bytes | None = None) -> bytes:
    if isinstance(value, (list, tuple)):
        vector = [float(part) for part in value]
        nested = [ProtoField(index, 5, _float_to_fixed32(part)) for index, part in enumerate(vector, start=1)]
        outer_number = 4
        if previous_raw:
            try:
                previous_fields = parse_fields(previous_raw)
                if len(previous_fields) == 1 and previous_fields[0].wire == 2:
                    outer_number = int(previous_fields[0].number)
            except Exception:
                pass
        return build_fields([ProtoField(outer_number, 2, build_fields(nested))])
    return build_fields([ProtoField(1, 5, _float_to_fixed32(float(value)))])


def set_material_parameters_data(material_data: bytes, parameters: dict[str, float | list[float] | tuple[float, ...]]) -> bytes:
    if not parameters:
        return material_data
    changed = False
    new_top_fields: list[ProtoField] = []
    for top_field in parse_fields(material_data):
        if top_field.number != 4 or top_field.wire != 2:
            new_top_fields.append(top_field)
            continue
        try:
            entry_fields = parse_fields(bytes(top_field.value))
        except Exception:
            new_top_fields.append(top_field)
            continue
        name = None
        value_raw = None
        value_index = None
        for index, entry_field in enumerate(entry_fields):
            if entry_field.number == 1 and entry_field.wire == 2:
                name = is_printable_utf8(entry_field.value)
            elif entry_field.number == 2:
                value_index = index
                value_raw = bytes(entry_field.value) if entry_field.wire == 2 else None
        if name not in parameters:
            new_top_fields.append(top_field)
            continue
        next_value = _encode_material_parameter_value(parameters[name], value_raw)
        if value_index is None:
            entry_fields.append(ProtoField(2, 2, next_value))
            changed = True
        elif entry_fields[value_index].wire != 2 or bytes(entry_fields[value_index].value) != next_value:
            entry_fields[value_index] = ProtoField(2, 2, next_value)
            changed = True
        new_top_fields.append(ProtoField(top_field.number, top_field.wire, build_fields(entry_fields)))
    return build_fields(new_top_fields) if changed else material_data


def restore_material_parameters_from_source_data(
    material_data: bytes,
    source_material_data: bytes,
    parameter_names: set[str],
    remove_missing: bool = False,
) -> bytes:
    if not parameter_names:
        return material_data

    source_entries: dict[str, bytes] = {}
    for field in parse_fields(source_material_data):
        if field.number != 4 or field.wire != 2 or not isinstance(field.value, bytes):
            continue
        name, _value, _raw = _material_parse_entry(bytes(field.value))
        if name in parameter_names:
            source_entries[name] = bytes(field.value)

    changed = False
    seen: set[str] = set()
    new_top_fields: list[ProtoField] = []
    for top_field in parse_fields(material_data):
        if top_field.number != 4 or top_field.wire != 2 or not isinstance(top_field.value, bytes):
            new_top_fields.append(top_field)
            continue
        name, _value, _raw = _material_parse_entry(bytes(top_field.value))
        if name not in parameter_names:
            new_top_fields.append(top_field)
            continue
        seen.add(str(name))
        source_entry = source_entries.get(str(name))
        if source_entry is not None:
            if source_entry != bytes(top_field.value):
                changed = True
                new_top_fields.append(ProtoField(top_field.number, top_field.wire, source_entry))
            else:
                new_top_fields.append(top_field)
            continue
        if remove_missing:
            changed = True
            continue
        new_top_fields.append(top_field)

    for name in parameter_names:
        if name in seen or name not in source_entries:
            continue
        changed = True
        new_top_fields.append(ProtoField(4, 2, source_entries[name]))

    return build_fields(new_top_fields) if changed else material_data


def set_material_parameters_file(
    material_path: str | Path,
    parameters: dict[str, float | list[float] | tuple[float, ...]],
    writer: WriterLike | None = None,
) -> bool:
    path = Path(material_path)
    raw = path.read_bytes()
    new_raw = set_material_parameters_data(raw, parameters)
    if new_raw == raw:
        return False
    if writer is not None:
        writer.write_bytes(path, new_raw, label="driver material parameters", backup=True)
    else:
        path.write_bytes(new_raw)
    return True
