from pathlib import Path
import copy
import importlib.util
import json
import subprocess
import sys
import threading
import tkinter as tk

from PIL import Image, ImageChops


ROOT = Path(__file__).resolve().parents[2]
APP = Path(__file__).resolve().parent
V23_PATH = ROOT / "outputs" / "RaceCore_Studio_v2.3" / "racecore_studio_v23.py"
if str(APP) not in sys.path:
    sys.path.insert(0, str(APP))

from core.runtime_config import ensure_runtime_storage

RUNTIME_PATHS = ensure_runtime_storage(APP)
GENERATED = RUNTIME_PATHS.generated
AUTOSAVE = RUNTIME_PATHS.autosaves / "legacy.racecore.json"
LIVE_CONFIG = RUNTIME_PATHS.live_config
V23_AUTOSAVE = AUTOSAVE

spec = importlib.util.spec_from_file_location("racecore_v23_for_v24", V23_PATH)
v23 = importlib.util.module_from_spec(spec)
spec.loader.exec_module(v23)

from core.creative_layer_engine import CreativeLayerEngine, logo_above_carbon
from core.live_link_runner import run_live_link
from core.paths import StudioPaths
from core.render_pipeline import RenderPipeline


V18 = v23.v22.v20.v19.v18
V17 = V18.v17
V14 = V17.v16.v15.v14
V13 = V14.v13
V12 = V13.v12


def legacy_modules():
    v22 = v23.v22
    return (
        v23, v22, v22.v20, v22.v20.v19, v22.v20.v19.v18,
        v22.v20.v19.v18.v17, v22.v20.v19.v18.v17.v16,
        v22.v20.v19.v18.v17.v16.v15,
        v22.v20.v19.v18.v17.v16.v15.v14,
        v22.v20.v19.v18.v17.v16.v15.v14.v13,
        v22.v20.v19.v18.v17.v16.v15.v14.v13.v12,
        v22.v20.v19.v18.v17.v16.v15.v14.v13.v12.v11,
        v22.v20.v19.v18.v17.v16.v15.v14.v13.v12.v11.v10,
        v22.v20.v19.v18.v17.v16.v15.v14.v13.v12.v11.v10.v09,
    )


# De oude schermonderdelen bewaren alleen projectinstellingen. Alle beeldbewerking
# loopt vanaf v2.4 uitsluitend door de losse modules in de map core.
for module in legacy_modules():
    module.APP = APP
    module.GENERATED = GENERATED
    module.AUTOSAVE = AUTOSAVE
    module.LIVE_CONFIG = LIVE_CONFIG
    if hasattr(module, "ASSET_DIR"):
        module.ASSET_DIR = RUNTIME_PATHS.asset_library
    if hasattr(module, "ASSET_DB"):
        module.ASSET_DB = RUNTIME_PATHS.asset_database

v23.V22_AUTOSAVE = V23_AUTOSAVE
v23.FRESH_TEMPLATE = APP / "assets" / v23.OFFICIAL_LMU_TEMPLATE.name
v23.FRESH_BLANK = v23.FRESH_TEMPLATE
v23.v22.BASE.SOURCE = v23.FRESH_TEMPLATE
v23.v22.v20.v19.v18.v17.v16.v15.v14.v13.v12.v11.v10.v09.BLANK_SOURCE = (
    v23.FRESH_TEMPLATE
)


class LogoPlacementEditorV24(V18.LogoPlacementEditorV18):
    """Laat een gespiegeld logo-paar samen of per kant verplaatsen."""

    def __init__(self, studio, index):
        layer = studio.normalise_layer(copy.deepcopy(studio.logo_layers[index]))
        self.separate_pair = tk.BooleanVar(
            master=studio, value=layer.get("pairMode") == "separate"
        )
        self.active_pair_side = tk.IntVar(master=studio, value=0)
        self.pair_controls = []
        super().__init__(studio, index)
        self.win.title("RaceCore Logo plaatsen - samen of per kant")
        self._refresh_pair_controls()

    def build_controls(self):
        super().build_controls()
        children = self.controls.winfo_children()
        before = children[-4] if len(children) >= 4 else None
        box = tk.LabelFrame(
            self.controls, text=" LOGO-PAAR ", font=("Segoe UI", 9, "bold"),
            fg="#67E8F9", bg="#101F3C", bd=1, relief="solid",
        )
        box.pack(fill="x", padx=18, pady=(6, 8), before=before)
        self.pair_box = box
        self.pair_toggle = tk.Checkbutton(
            box, text="Logo's apart verplaatsen",
            variable=self.separate_pair, command=self._pair_mode_changed,
            font=("Segoe UI", 9), fg="#F8FAFC", bg="#101F3C",
            activeforeground="#F8FAFC", activebackground="#101F3C",
            selectcolor="#08152C", anchor="w",
        )
        self.pair_toggle.pack(fill="x", padx=9, pady=(7, 3))
        row = tk.Frame(box, bg="#101F3C")
        row.pack(fill="x", padx=7, pady=(0, 7))
        self.pair_controls = [
            tk.Radiobutton(
                row, text="Kant 1", variable=self.active_pair_side, value=0,
                command=self.redraw, font=("Segoe UI", 9), fg="#F8FAFC",
                bg="#101F3C", activeforeground="#F8FAFC",
                activebackground="#101F3C", selectcolor="#08152C",
            ),
            tk.Radiobutton(
                row, text="Kant 2", variable=self.active_pair_side, value=1,
                command=self.redraw, font=("Segoe UI", 9), fg="#F8FAFC",
                bg="#101F3C", activeforeground="#F8FAFC",
                activebackground="#101F3C", selectcolor="#08152C",
            ),
        ]
        for button in self.pair_controls:
            button.pack(side="left", expand=True)

    def _placements(self):
        return V12.PLACEMENTS[self.position.get()]

    def _is_pair(self):
        return len(self._placements()) > 1

    def _ensure_pair_offsets(self):
        placements = self._placements()
        existing = self.layer.get("pairOffsets", [])
        if len(existing) == len(placements):
            return
        dx = int(self.layer.get("offsetX", 0))
        dy = int(self.layer.get("offsetY", 0))
        offsets = []
        for index in range(len(placements)):
            px, py = dx, dy
            if index and self.layer.get("mirrorPairedOffsets"):
                px, py = -px, -py
            offsets.append({"x": px, "y": py})
        self.layer["pairOffsets"] = offsets

    def _pair_mode_changed(self):
        if self.separate_pair.get() and self._is_pair():
            self._ensure_pair_offsets()
            self.layer["pairMode"] = "separate"
            self.info_text.set("Kies Kant 1 of Kant 2 en verplaats alleen dat logo.")
        else:
            if self.layer.get("pairMode") == "separate":
                self._ensure_pair_offsets()
                active = min(self.active_pair_side.get(), len(self._placements()) - 1)
                offset = self.layer["pairOffsets"][active]
                self.layer["offsetX"] = int(offset["x"])
                self.layer["offsetY"] = int(offset["y"])
            self.layer["pairMode"] = "together"
            self.info_text.set("Beide kanten worden als één gespiegeld paar verplaatst.")
        self._refresh_pair_controls()
        self.redraw()

    def _refresh_pair_controls(self):
        paired = self._is_pair()
        self.pair_toggle.configure(state="normal" if paired else "disabled")
        separate = paired and self.separate_pair.get()
        for button in self.pair_controls:
            button.configure(state="normal" if separate else "disabled")
        if not paired:
            self.separate_pair.set(False)
            self.layer["pairMode"] = "together"

    def _offset_for(self, index):
        if self.separate_pair.get() and self._is_pair():
            self._ensure_pair_offsets()
            offset = self.layer["pairOffsets"][index]
            return int(offset["x"]), int(offset["y"])
        dx = int(self.layer.get("offsetX", 0))
        dy = int(self.layer.get("offsetY", 0))
        if index and self.layer.get("mirrorPairedOffsets"):
            return -dx, -dy
        return dx, dy

    def uv_center(self):
        index = (
            min(self.active_pair_side.get(), len(self._placements()) - 1)
            if self.separate_pair.get() and self._is_pair() else 0
        )
        anchor_x, anchor_y, _rotation = self._placements()[index]
        dx, dy = self._offset_for(index)
        return anchor_x + dx, anchor_y + dy

    def set_uv_center(self, x, y):
        index = (
            min(self.active_pair_side.get(), len(self._placements()) - 1)
            if self.separate_pair.get() and self._is_pair() else 0
        )
        anchor_x, anchor_y, _rotation = self._placements()[index]
        width, height = self.texture_size
        dx = max(0, min(width, round(x))) - anchor_x
        dy = max(0, min(height, round(y))) - anchor_y
        if self.separate_pair.get() and self._is_pair():
            self._ensure_pair_offsets()
            self.layer["pairOffsets"][index] = {"x": dx, "y": dy}
        else:
            self.layer["offsetX"] = dx
            self.layer["offsetY"] = dy
        self.redraw()

    def change_position(self, _event=None):
        self.layer["offsetX"] = 0
        self.layer["offsetY"] = 0
        self.layer["pairOffsets"] = []
        self.active_pair_side.set(0)
        self._refresh_pair_controls()
        self.redraw()

    def reset_position(self):
        if self.separate_pair.get() and self._is_pair():
            self._ensure_pair_offsets()
            index = min(self.active_pair_side.get(), len(self._placements()) - 1)
            self.layer["pairOffsets"][index] = {"x": 0, "y": 0}
        else:
            self.layer["offsetX"] = 0
            self.layer["offsetY"] = 0
        self.redraw()

    def compose_display_preview(self, display_w, display_h):
        key = (display_w, display_h)
        if key != self.display_cache_key:
            self.display_cache_key = key
            self.display_background = self.background.resize(
                key, Image.Resampling.LANCZOS
            )
            self.display_editable = self.editable_mask.resize(
                key, Image.Resampling.LANCZOS
            )
        canvas = self.display_background.copy()
        active = (
            min(self.active_pair_side.get(), len(self._placements()) - 1)
            if self.separate_pair.get() and self._is_pair() else 0
        )
        bounds = None
        for index, (anchor_x, anchor_y, base_rotation) in enumerate(
            self._placements()
        ):
            item = self.transformed_logo(base_rotation)
            dx, dy = self._offset_for(index)
            x = round(anchor_x + dx - item.width / 2)
            y = round(anchor_y + dy - item.height / 2)
            shown = item.resize(
                (max(1, round(item.width * self.display_scale)),
                 max(1, round(item.height * self.display_scale))),
                Image.Resampling.LANCZOS,
            )
            overlay = Image.new("RGBA", key, (0, 0, 0, 0))
            overlay.alpha_composite(
                shown,
                (round(x * self.display_scale), round(y * self.display_scale)),
            )
            overlay.putalpha(ImageChops.multiply(
                overlay.getchannel("A"), self.display_editable
            ))
            canvas = Image.alpha_composite(canvas, overlay)
            if index == active:
                bounds = (x, y, x + item.width, y + item.height)
        return canvas, bounds

    def save(self):
        self.layer["pairMode"] = (
            "separate" if self.separate_pair.get() and self._is_pair()
            else "together"
        )
        if self.layer["pairMode"] == "separate":
            self._ensure_pair_offsets()
        super().save()


class StudioV24(v23.StudioV23):
    """Bestaande gebruiksvriendelijke UI op de modulaire v2.4-renderkern."""

    def __init__(self):
        # Multi-GT3 start meteen met zijn voertuigprofiel. Daardoor hoeft een
        # releasepakket geen verouderde losse Ferrari-bronnen mee te leveren.
        self.pipeline = RenderPipeline(
            StudioPaths(APP, getattr(self, "vehicle_id", None))
        )
        self.layer_engine = CreativeLayerEngine()
        self._render_lock = threading.Lock()
        self._render_request = 0
        self._render_completed = 0
        self._preserve_original_mode = False
        super().__init__()
        self.title("RaceCore Livery Studio v2.4 - Modulaire kern")

    def build_layout(self):
        super().build_layout()
        header = self.winfo_children()[0]
        for child in header.winfo_children():
            if isinstance(child, tk.Label) and "Livery Studio" in child.cget("text"):
                child.configure(text="Livery Studio  v2.4")

    @staticmethod
    def normalise_layer(layer):
        layer = V12.StudioV12.normalise_layer(layer)
        if layer.get("pairMode") not in {"together", "separate"}:
            layer["pairMode"] = "together"
        offsets = []
        for raw in layer.get("pairOffsets", []):
            if isinstance(raw, dict):
                offsets.append({
                    "x": int(raw.get("x", 0)),
                    "y": int(raw.get("y", 0)),
                })
        layer["pairOffsets"] = offsets
        stack = str(layer.get("carbonStack", "auto")).strip().lower()
        layer["carbonStack"] = stack if stack in {"auto", "above", "below"} else "auto"
        return layer

    def edit_layer(self):
        index = self.selected()
        if index is None:
            self.status.set("Kies eerst een logo-laag")
            return
        layer = self.normalise_layer(self.logo_layers[index])
        if layer["locked"]:
            self.status.set("Ontgrendel deze laag eerst")
            return
        if not Path(layer["path"]).exists():
            self.status.set("Het afbeeldingsbestand van deze laag ontbreekt")
            return
        LogoPlacementEditorV24(self, index)

    def changed(self, *_args):
        # Elke echte bewerking verlaat de originele weergave. Tijdens herstellen
        # of projectladen blijft de opgeslagen originele-modus wel behouden.
        if not self._preserve_original_mode:
            self.original_mode = False
        return super().changed()

    def width_changed(self, _value=None):
        """Maak de snelle lijndikte onderdeel van undo en autosave."""
        self.width_label.configure(text=f"{self.width.get()}%")
        if not getattr(self, "loading", False):
            self.record_state()
            self.autosave()
        self.schedule_preview()

    def autosave(self):
        """Vervang autosave atomair zodat een onderbreking hem niet beschadigt."""
        APP.mkdir(parents=True, exist_ok=True)
        staging = AUTOSAVE.with_name(AUTOSAVE.name + ".racecore-staging")
        staging.write_text(
            json.dumps(self.snapshot(), indent=2), encoding="utf-8"
        )
        staging.replace(AUTOSAVE)

    def protected_mask(self, size):
        source = self.pipeline.source.load_rgba()
        return self.pipeline.masks.protected(
            size, source, self.snapshot().get("hiddenOfficialDecalGroups", ())
        )

    def editable_mask(self, size):
        source = self.pipeline.source.load_rgba()
        return self.pipeline.masks.editable(
            size, source, self.snapshot().get("hiddenOfficialDecalGroups", ())
        )

    def apply_state(self, state, record=False):
        previous = self._preserve_original_mode
        self._preserve_original_mode = True
        try:
            return super().apply_state(state, record)
        finally:
            self._preserve_original_mode = previous

    def restore_original_template(self):
        previous = self._preserve_original_mode
        self._preserve_original_mode = True
        try:
            return super().restore_original_template()
        finally:
            self._preserve_original_mode = previous

    def _compose_state_layers(
        self, canvas, state, exclude_stripe=None, exclude_shape=None,
        exclude_logo=None, exclude_text=None, carbon_top_only=False,
    ):
        """Bouw creatieve lagen uit dezelfde bevroren projectstatus."""
        # CreativeLayerEngine werkt lokaal en in-place voor snelheid. Geef hem
        # daarom één eigen werkcanvas, zodat de materiaalcache onaangeraakt blijft.
        canvas = canvas.convert("RGBA").copy()
        source = self.pipeline.source.load_rgba()
        editable = self.pipeline.masks.editable(
            canvas.size, source, state.get("hiddenOfficialDecalGroups", ())
        )
        canvas = self.layer_engine.vector_layers(
            canvas, copy.deepcopy(state.get("shapeLayers", [])), editable,
            self.normalise_shape, V14.shape_asset, V13.stripe_instances,
            exclude_shape,
        )
        canvas = self.layer_engine.vector_layers(
            canvas, copy.deepcopy(state.get("stripeLayers", [])), editable,
            self.normalise_stripe, V13.stripe_asset, V13.stripe_instances,
            exclude_stripe,
        )
        canvas = self.layer_engine.vector_layers(
            canvas, copy.deepcopy(state.get("textLayers", [])), editable,
            self.normalise_text, V17.text_asset, V13.stripe_instances,
            exclude_text,
        )
        return self.layer_engine.logos(
            canvas, copy.deepcopy(state.get("logoLayers", [])), editable,
            self.normalise_layer, V12.PLACEMENTS, exclude_logo,
            logo_above_carbon if carbon_top_only else None,
        )

    def editor_background(
        self, exclude_stripe=None, exclude_shape=None, exclude_logo=None,
        exclude_text=None,
    ):
        """Editorbasis zonder eerder gerenderde lagen opnieuw te stapelen."""
        state = self.snapshot()
        canvas = self.pipeline.render_materials(state).canvas
        return self._compose_state_layers(
            canvas, state, exclude_stripe, exclude_shape,
            exclude_logo, exclude_text,
        )

    def generate(self, apply):
        """Leg UI-status vast en laat alleen de nieuwste preview winnen."""
        self._render_request += 1
        request_id = self._render_request
        state = copy.deepcopy(self.snapshot())
        self.autosave()
        self.status.set("LMU-bestanden maken..." if apply else "Preview maken...")
        threading.Thread(
            target=self._worker,
            args=(state, bool(apply), request_id),
            daemon=True,
        ).start()

    def _worker(self, state, apply, request_id):
        """Eén vaste renderroute zonder Tk-aanroepen vanuit de werkthread."""
        try:
            with self._render_lock:
                if not apply and request_id != self._render_request:
                    return
                result = self.pipeline.render_materials(state)
                creative = self._compose_state_layers(
                    Image.new("RGBA", result.canvas.size, (0, 0, 0, 0)),
                    state,
                )
                above_carbon = self._compose_state_layers(
                    Image.new("RGBA", result.canvas.size, (0, 0, 0, 0)),
                    state, carbon_top_only=True,
                )
                canvas = Image.alpha_composite(
                    result.canvas.convert("RGBA"), creative
                )
                region, carbon = self.pipeline.carbon.preserve_creative_layers(
                    result.carbon, creative.getchannel("A"), result.foreground,
                    above_carbon.getchannel("A"),
                )
                if not apply and request_id != self._render_request:
                    return
                self.pipeline.save(canvas, region, carbon)
                self._render_completed = request_id
                pattern = (
                    " | patroon aan"
                    if state.get("bodyPattern", {}).get("enabled") else ""
                )
                label = (
                    f"Preview gereed | {len(state.get('panelColours', {}))} "
                    f"paneelkleuren | {len(state.get('carbonPanels', []))} "
                    f"carbonpanelen{pattern}"
                )
                if apply:
                    event = run_live_link(self.pipeline.paths)
                    label = (
                        "Live in LMU"
                        if event["status"] == "installed"
                        else "LMU is al bijgewerkt"
                    )
            if apply or request_id == self._render_request:
                self.after(0, lambda: self.show_preview(label))
        except Exception as error:
            self.after(0, lambda text=str(error): self.show_error(text))


if __name__ == "__main__":
    StudioV24().mainloop()
