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

from PIL import Image, ImageChops, ImageDraw, ImageTk


APP = Path(__file__).resolve().parent
V32_PATH = APP / "racecore_studio_v32.py"
if str(APP) not in sys.path:
    sys.path.insert(0, str(APP))

spec = importlib.util.spec_from_file_location("racecore_v32_for_v33", V32_PATH)
v32 = importlib.util.module_from_spec(spec)
spec.loader.exec_module(v32)

from core.canvas_transform import handle_points
from core.focus_canvas import FocusCanvasRenderer, FocusViewport
from core.live_link_runner import run_live_link
from core.multi_selection import combined_bounds


class StudioV33(v32.StudioV32):
    """Rustig full-resolution ontwerpvlak met onafhankelijk zoomen en pannen."""

    def __init__(self):
        self.focus_viewport = FocusViewport()
        self.focus_renderer = None
        self.focus_master = None
        self.focus_cache = None
        self.focus_cache_lock = threading.Lock()
        self.focus_pan_start = None
        self.focus_resize_job = None
        self.render_result_queue = queue.SimpleQueue()
        self.render_poll_job = None
        self._last_overlay_context = None
        super().__init__()
        if self.focus_renderer is None:
            self.focus_renderer = FocusCanvasRenderer(self.pipeline)
        self.title("RaceCore Livery Studio v3.3 - Focus Canvas")
        self._schedule_render_result_poll()
        if self.focus_cache is None:
            self.generate(False)

    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  v3.3")

        for widget_name in ("direct_hint", "keyboard_hint", "snap_tools"):
            widget = getattr(self, widget_name, None)
            if widget is not None:
                widget.place_forget()

        self.preview_label.configure(bg="#07090C", cursor="crosshair")
        self.focus_mode = tk.StringVar(master=self, value="design")
        self.focus_show_uv = tk.BooleanVar(master=self, value=True)
        self.focus_show_stickers = tk.BooleanVar(master=self, value=True)
        self.focus_zoom_text = tk.StringVar(master=self, value="100%")

        viewer = self.preview_label.master
        tools = tk.Frame(viewer, bg="#10131A", bd=1, relief="solid")
        tools.place(relx=1.0, x=-18, y=16, anchor="ne")
        self.focus_tools = tools
        self.design_button = self._mode_button(tools, "ONTWERP", "design")
        self.lmu_button = self._mode_button(tools, "LMU", "lmu")
        self._separator(tools)
        self.uv_toggle = tk.Checkbutton(
            tools, text="UV", variable=self.focus_show_uv,
            command=self.focus_overlay_changed, indicatoron=False,
            font=("Segoe UI", 8, "bold"), fg="#E5E7EB", bg="#252A35",
            activeforeground="#FFFFFF", activebackground="#374151",
            selectcolor="#6D28D9", relief="flat", padx=8, pady=5,
        )
        self.uv_toggle.pack(side="left", padx=(3, 0), pady=3)
        self.sticker_toggle = tk.Checkbutton(
            tools, text="STICKERS", variable=self.focus_show_stickers,
            command=self.focus_overlay_changed, indicatoron=False,
            font=("Segoe UI", 8, "bold"), fg="#E5E7EB", bg="#252A35",
            activeforeground="#FFFFFF", activebackground="#374151",
            selectcolor="#6D28D9", relief="flat", padx=8, pady=5,
        )
        self.sticker_toggle.pack(side="left", padx=(3, 0), pady=3)
        self._separator(tools)
        self._focus_button(tools, "PASSEND", self.focus_fit)
        self._focus_button(tools, "−", lambda: self.focus_zoom_step(1 / 1.25), width=2)
        tk.Label(
            tools, textvariable=self.focus_zoom_text,
            font=("Segoe UI", 8, "bold"), fg="#C4B5FD", bg="#10131A",
            width=5,
        ).pack(side="left", padx=2)
        self._focus_button(tools, "+", lambda: self.focus_zoom_step(1.25), width=2)
        tools.lift()
        self._refresh_mode_buttons()

        self.preview_label.bind("<MouseWheel>", self.focus_mousewheel, add="+")
        self.preview_label.bind("<ButtonPress-3>", self.focus_pan_press, add="+")
        self.preview_label.bind("<B3-Motion>", self.focus_pan_drag, add="+")
        self.preview_label.bind("<ButtonRelease-3>", self.focus_pan_release, add="+")
        self.preview_label.bind("<Configure>", self.focus_resized, add="+")

    def _mode_button(self, parent, text, value):
        button = tk.Radiobutton(
            parent, text=text, value=value, variable=self.focus_mode,
            command=self.focus_mode_changed, indicatoron=False,
            font=("Segoe UI", 8, "bold"), fg="#F8FAFC", bg="#252A35",
            activeforeground="#FFFFFF", activebackground="#374151",
            selectcolor="#7E22CE", relief="flat", padx=9, pady=5,
        )
        button.pack(side="left", padx=(3, 0), pady=3)
        return button

    @staticmethod
    def _separator(parent):
        tk.Frame(parent, width=1, height=20, bg="#374151").pack(
            side="left", padx=5, pady=5
        )

    @staticmethod
    def _focus_button(parent, text, command, width=0):
        button = tk.Button(
            parent, text=text, command=command, width=width,
            font=("Segoe UI", 8, "bold"), fg="#F8FAFC", bg="#252A35",
            activeforeground="#FFFFFF", activebackground="#374151",
            relief="flat", padx=7, pady=5,
        )
        button.pack(side="left", padx=(3, 0), pady=3)
        return button

    def _ensure_focus_renderer(self):
        if self.focus_renderer is None:
            self.focus_renderer = FocusCanvasRenderer(self.pipeline)
        return self.focus_renderer

    def _worker(self, state, apply, request_id):
        """Maakt naast de bestaande export twee full-resolution editorbeelden."""
        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

                renderer = self._ensure_focus_renderer()
                design = Image.alpha_composite(
                    renderer.neutral_base(), creative
                )
                # Een expliciet naar voren gehaald carbonpaneel hoort bij de
                # laagvolgorde en moet daarom ook direct in ONTWERP zichtbaar zijn.
                if result.foreground.getbbox():
                    front_carbon = ImageChops.multiply(
                        carbon, result.foreground
                    )
                    design = self.pipeline.carbon.preview(
                        design, front_carbon
                    ).convert("RGBA")
                lmu = self.pipeline.carbon.preview(
                    canvas, carbon
                ).convert("RGBA")
                # De hoofdwerkruimte gebruikt de beelden hierboven rechtstreeks
                # uit het geheugen. Twee 4K-TGA's (ruim 96 MB) bij iedere kleine
                # bewerking naar schijf schrijven maakte vooral logo's onnodig
                # traag. Alleen APPLY TO LMU heeft die bestanden werkelijk nodig.
                if apply:
                    self.pipeline.save(canvas, region, carbon)
                with self.focus_cache_lock:
                    self.focus_cache = (request_id, design, lmu)
                self._render_completed = request_id

                pattern = (
                    " | patroon aan"
                    if state.get("bodyPattern", {}).get("enabled") else ""
                )
                label = (
                    f"Focus Canvas 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"
                    )
            self.render_result_queue.put(
                ("preview", request_id, bool(apply), label)
            )
        except Exception as error:
            self.render_result_queue.put(
                ("error", request_id, bool(apply), str(error))
            )

    def _schedule_render_result_poll(self):
        if self.render_poll_job is None and self.winfo_exists():
            self.render_poll_job = self.after(40, self._poll_render_results)

    def _poll_render_results(self):
        """Verwerk workerresultaten uitsluitend vanuit de Tk-hoofdthread."""
        self.render_poll_job = None
        try:
            while True:
                kind, request_id, apply, payload = self.render_result_queue.get_nowait()
                if not apply and request_id != self._render_request:
                    continue
                if kind == "preview":
                    self.show_preview(payload)
                else:
                    self.show_error(payload)
        except queue.Empty:
            pass
        except tk.TclError:
            return
        self._schedule_render_result_poll()

    def _publish_preview_image(self, image):
        """Ververs een bestaand Tk-beeld in plaats van het widget om te wisselen."""
        current = getattr(self, "preview_photo", None)
        if current is not None:
            try:
                if current.width() == image.width and current.height() == image.height:
                    current.paste(image)
                    return current
            except (AttributeError, RuntimeError, tk.TclError):
                pass
        self.preview_photo = ImageTk.PhotoImage(image)
        self.preview_label.configure(image=self.preview_photo, text="")
        return self.preview_photo

    def _fallback_focus_cache(self):
        renderer = self._ensure_focus_renderer()
        state = self.snapshot()
        design = self._compose_state_layers(renderer.neutral_base(), state)
        path = self.pipeline.paths.generated / "customskin.tga"
        if path.exists():
            with Image.open(path) as opened:
                lmu = opened.convert("RGBA")
        else:
            lmu = design.copy()
        return self._render_completed, design, lmu

    def show_preview(self, label):
        with self.focus_cache_lock:
            cache = self.focus_cache
        if cache is None:
            cache = self._fallback_focus_cache()
        _request_id, design, lmu = cache
        state = self.snapshot()
        if self.focus_mode.get() == "design":
            self.focus_master = self._ensure_focus_renderer().compose_design_view(
                design,
                show_uv=self.focus_show_uv.get(),
                show_stickers=self.focus_show_stickers.get(),
                hidden_official_decals=state.get(
                    "hiddenOfficialDecalGroups", ()
                ),
            )
        else:
            self.focus_master = lmu.copy()
            if self.focus_show_uv.get():
                self.focus_master = Image.alpha_composite(
                    self.focus_master.convert("RGBA"),
                    self._ensure_focus_renderer().uv_guide(),
                )
        self.status.set(label)
        self.show_selection_overlay()

    def focus_mode_changed(self):
        self._refresh_mode_buttons()
        self.sticker_toggle.configure(
            state="normal" if self.focus_mode.get() == "design" else "disabled"
        )
        label = (
            "Rustige ontwerpweergave"
            if self.focus_mode.get() == "design"
            else "Werkelijke LMU-materiaalweergave"
        )
        self.show_preview(label)

    def _refresh_mode_buttons(self):
        if not hasattr(self, "design_button"):
            return
        selected = self.focus_mode.get()
        self.design_button.configure(bg="#7E22CE" if selected == "design" else "#252A35")
        self.lmu_button.configure(bg="#7E22CE" if selected == "lmu" else "#252A35")

    def focus_overlay_changed(self):
        self.show_preview("Canvasweergave bijgewerkt")

    def focus_fit(self):
        self.focus_viewport.fit()
        self._update_zoom_text()
        self.show_selection_overlay()
        self.status.set("Volledige livery passend in beeld")

    def focus_zoom_step(self, factor, anchor=None):
        width = max(1, self.preview_label.winfo_width())
        height = max(1, self.preview_label.winfo_height())
        if anchor is None:
            anchor = (width / 2, height / 2)
        self.focus_viewport.set_zoom(
            self.focus_viewport.zoom * factor, anchor, (width, height)
        )
        self._update_zoom_text()
        self.show_selection_overlay()

    def focus_mousewheel(self, event):
        factor = 1.18 if event.delta > 0 else 1 / 1.18
        self.focus_zoom_step(factor, (event.x, event.y))
        return "break"

    def focus_pan_press(self, event):
        self.focus_pan_start = (event.x, event.y)
        self.preview_label.configure(cursor="fleur")
        return "break"

    def focus_pan_drag(self, event):
        if self.focus_pan_start is None:
            return "break"
        old_x, old_y = self.focus_pan_start
        self.focus_viewport.pan_pixels(
            event.x - old_x, event.y - old_y,
            self.preview_label.winfo_width(), self.preview_label.winfo_height(),
        )
        self.focus_pan_start = (event.x, event.y)
        self.show_selection_overlay()
        return "break"

    def focus_pan_release(self, _event):
        self.focus_pan_start = None
        self.preview_label.configure(cursor="crosshair")
        return "break"

    def focus_resized(self, _event):
        if self.focus_resize_job is not None:
            self.after_cancel(self.focus_resize_job)
        self.focus_resize_job = self.after(80, self._finish_focus_resize)

    def _finish_focus_resize(self):
        self.focus_resize_job = None
        self.show_selection_overlay()

    def _update_zoom_text(self):
        if hasattr(self, "focus_zoom_text"):
            self.focus_zoom_text.set(f"{round(self.focus_viewport.zoom * 100)}%")

    def preview_image_metrics(self):
        width = max(1, self.preview_label.winfo_width())
        height = max(1, self.preview_label.winfo_height())
        metrics = self.focus_viewport.metrics(width, height)
        return metrics.size, metrics.size, metrics.offset_x, metrics.offset_y

    def event_to_texture(self, event):
        return self.focus_viewport.event_to_texture(
            event.x, event.y,
            max(1, self.preview_label.winfo_width()),
            max(1, self.preview_label.winfo_height()),
        )

    def _texture_point(self, point, image_size):
        metrics = self.focus_viewport.metrics(
            max(1, self.preview_label.winfo_width()),
            max(1, self.preview_label.winfo_height()),
        )
        left, top, right, bottom = metrics.crop
        return (
            (point[0] - left) * image_size / (right - left),
            (point[1] - top) * image_size / (bottom - top),
        )

    def _draw_box(self, draw, bounds, image_size, colour, width=3, handles=False):
        left, top = self._texture_point((bounds[0], bounds[1]), image_size)
        right, bottom = self._texture_point((bounds[2], bounds[3]), image_size)
        draw.rectangle((left, top, right, bottom), outline=colour, width=width)
        if not handles:
            return
        points = handle_points(bounds)
        display = {
            name: self._texture_point(point, image_size)
            for name, point in points.items()
        }
        top_center = self._texture_point(
            ((bounds[0] + bounds[2]) / 2, bounds[1]), image_size
        )
        rotate = display["rotate"]
        draw.line((*top_center, *rotate), fill="#22D3EE", width=2)
        radius = 6
        for name in ("nw", "ne", "sw", "se"):
            x, y = display[name]
            draw.rectangle(
                (x - radius, y - radius, x + radius, y + radius),
                fill="#F8FAFC", outline="#22D3EE", width=2,
            )
        x, y = rotate
        draw.ellipse(
            (x - 7, y - 7, x + 7, y + 7),
            fill="#22D3EE", outline="#F8FAFC", width=2,
        )

    def _draw_snap_guides(self, draw, image_size):
        guides = getattr(self, "active_snap_guides", {})
        for axis, value in guides.items():
            if value is None:
                continue
            if axis == "x":
                x, _y = self._texture_point((value, 0), image_size)
                for y in range(0, image_size, 18):
                    draw.line((x, y, x, min(y + 10, image_size)), fill="#67E8F9", width=2)
            else:
                _x, y = self._texture_point((0, value), image_size)
                for x in range(0, image_size, 18):
                    draw.line((x, y, min(x + 10, image_size), y), fill="#67E8F9", width=2)

    def show_selection_overlay(self):
        if self.focus_master is None or not hasattr(self, "preview_label"):
            return
        try:
            label_width = max(1, self.preview_label.winfo_width())
            label_height = max(1, self.preview_label.winfo_height())
            image, metrics = self.focus_viewport.render(
                self.focus_master, label_width, label_height
            )
            draw = ImageDraw.Draw(image)
            self._draw_snap_guides(draw, metrics.size)
            assets = self.selection_assets() if len(self.multi_selection) > 1 else []
            if assets:
                boxes = []
                for item, _reference, _layer, placed in assets:
                    boxes.append(placed.bounds)
                    colour = "#FDE68A" if item.uid == self.preview_selected_uid else "#22D3EE"
                    self._draw_box(draw, placed.bounds, metrics.size, colour, width=2)
                group = combined_bounds(boxes)
                if group:
                    self._draw_box(draw, group, metrics.size, "#FBBF24", width=3)
            else:
                placed = self.selected_preview_asset()
                if placed is not None:
                    self._draw_box(
                        draw, placed.bounds, metrics.size, "#22D3EE",
                        width=3, handles=True,
                    )
            self._publish_preview_image(image)
            context = (len(self.multi_selection), self.preview_selected_uid)
            if context != self._last_overlay_context:
                self._last_overlay_context = context
                self.update_multi_count()
                self.update_context_tools()
                self.focus_tools.lift()
        except (OSError, ValueError, tk.TclError):
            pass

    def destroy(self):
        if self.render_poll_job is not None:
            try:
                self.after_cancel(self.render_poll_job)
            except tk.TclError:
                pass
            self.render_poll_job = None
        super().destroy()

    def update_context_tools(self):
        super().update_context_tools()
        if hasattr(self, "keyboard_hint"):
            self.keyboard_hint.place_forget()
        if hasattr(self, "direct_hint"):
            self.direct_hint.place_forget()
        if hasattr(self, "focus_tools"):
            self.focus_tools.lift()


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