from pathlib import Path
from PIL import Image, ImageTk, ImageChops, ImageDraw
import copy
import importlib.util
import json
import subprocess
import sys
import tkinter as tk
from tkinter import colorchooser, messagebox, ttk


ROOT = Path(__file__).resolve().parents[2]
APP = Path(__file__).resolve().parent
GENERATED = APP / 'generated'
AUTOSAVE = APP / 'autosave.racecore.json'
LIVE_CONFIG = APP / 'live-link-ui.json'
V12_PATH = ROOT / 'outputs' / 'RaceCore_Studio_v1.2' / 'racecore_studio_v12.py'
V12_AUTOSAVE = V12_PATH.parent / 'autosave.racecore.json'

spec = importlib.util.spec_from_file_location('racecore_v12', V12_PATH)
v12 = importlib.util.module_from_spec(spec)
spec.loader.exec_module(v12)

# Gebruik voor v1.3 een eigen werkmap, terwijl alle bewezen v1.2-code behouden blijft.
v12.APP = APP
v12.GENERATED = GENERATED
v12.AUTOSAVE = AUTOSAVE
v12.LIVE_CONFIG = LIVE_CONFIG
v12.v11.APP = APP
v12.v11.GENERATED = GENERATED
v12.v11.AUTOSAVE = AUTOSAVE
v12.v11.LIVE_CONFIG = LIVE_CONFIG

COLORS = v12.COLORS
SHAPES = ('Rechte lijn', 'Dubbele lijn', 'Chevron', 'Breed vlak')


def stripe_asset(layer):
    """Maak een transparante, roteerbare vectorstreep voor de UV-textuur."""
    length = max(40, int(layer.get('length', 900)))
    width = max(4, int(layer.get('width', 70)))
    gap = max(0, int(layer.get('gap', 45)))
    shape = layer.get('shape', 'Rechte lijn')
    opacity = max(5, min(100, int(layer.get('opacity', 100))))
    colour = v12.v11.v10.v09.v08.base.rgb(layer.get('colour', '#22D3EE'))
    fill = (*colour, round(255 * opacity / 100))
    pad = max(20, width * 2)

    if shape == 'Chevron':
        height = max(width * 5, round(length * .55))
        image = Image.new('RGBA', (length + pad * 2, height + pad * 2), (0, 0, 0, 0))
        draw = ImageDraw.Draw(image)
        draw.line(
            [(pad, pad), (pad + length // 2, pad + height), (pad + length, pad)],
            fill=fill, width=width, joint='curve',
        )
    else:
        count = 2 if shape == 'Dubbele lijn' else 1
        actual_width = max(width, 220) if shape == 'Breed vlak' else width
        content_h = actual_width if count == 1 else actual_width * 2 + gap
        image = Image.new(
            'RGBA', (length + pad * 2, content_h + pad * 2), (0, 0, 0, 0)
        )
        draw = ImageDraw.Draw(image)
        if count == 1:
            y0 = pad
            draw.rounded_rectangle(
                (pad, y0, pad + length, y0 + actual_width),
                radius=min(actual_width // 2, 36), fill=fill,
            )
        else:
            for y0 in (pad, pad + actual_width + gap):
                draw.rounded_rectangle(
                    (pad, y0, pad + length, y0 + actual_width),
                    radius=min(actual_width // 2, 36), fill=fill,
                )
    return image.rotate(
        float(layer.get('rotation', 0)), expand=True,
        resample=Image.Resampling.BICUBIC,
    )


def stripe_instances(layer, texture_size):
    width, height = texture_size
    x = max(0, min(width, int(layer.get('centerX', 3310))))
    y = max(0, min(height, int(layer.get('centerY', 2470))))
    result = [(x, y, float(layer.get('rotation', 0)))]
    if layer.get('paired', False):
        # De twee grote zijpanelen van deze Ferrari liggen rond Y=1220 en Y=3420.
        paired_y = max(0, min(height, 4640 - y))
        result.append((x, paired_y, float(layer.get('rotation', 0)) + 180))
    return result


class StripePlacementEditor:
    def __init__(self, studio, index, is_new=False):
        self.studio = studio
        self.index = index
        self.is_new = is_new
        self.layer = copy.deepcopy(studio.normalise_stripe(studio.stripe_layers[index]))
        self.texture_size = (4096, 4096)
        self.background = studio.editor_background(exclude_stripe=index)
        self.texture_size = self.background.size
        self.editable_mask = studio.editable_mask(self.texture_size)

        self.win = tk.Toplevel(studio)
        self.win.title('RaceCore Strepen & Lijnen Editor')
        self.win.geometry('1220x850')
        self.win.minsize(980, 700)
        self.win.configure(bg=COLORS.NAVY)
        self.win.transient(studio)
        self.win.grab_set()
        self.win.protocol('WM_DELETE_WINDOW', self.cancel)

        self.canvas = tk.Canvas(
            self.win, bg='#030914', highlightthickness=0, cursor='crosshair'
        )
        self.canvas.pack(side='left', fill='both', expand=True, padx=(16, 8), pady=16)
        self.controls = tk.Frame(self.win, bg=COLORS.PANEL, width=290)
        self.controls.pack(side='right', fill='y', padx=(8, 16), pady=16)
        self.controls.pack_propagate(False)

        self.name = tk.StringVar(value=self.layer['name'])
        self.shape = tk.StringVar(value=self.layer['shape'])
        self.colour = tk.StringVar(value=self.layer['colour'])
        self.length = tk.IntVar(value=self.layer['length'])
        self.width = tk.IntVar(value=self.layer['width'])
        self.gap = tk.IntVar(value=self.layer['gap'])
        self.rotation = tk.IntVar(value=self.layer['rotation'])
        self.opacity = tk.IntVar(value=self.layer['opacity'])
        self.paired = tk.BooleanVar(value=self.layer['paired'])
        self.coord_text = tk.StringVar()
        self.photo = None
        self.display_scale = 1.0
        self.display_origin = (0, 0)
        self.display_cache_key = None
        self.display_background = None
        self.display_editable = None
        self.dragging = False

        self.build_controls()
        self.bind_events()
        self.win.after(60, self.redraw)

    def build_controls(self):
        tk.Label(
            self.controls, text='STREEP BEWERKEN', font=('Segoe UI', 14, 'bold'),
            fg=COLORS.CYAN, bg=COLORS.PANEL,
        ).pack(anchor='w', padx=18, pady=(16, 8))
        tk.Entry(
            self.controls, textvariable=self.name, font=('Segoe UI', 10),
        ).pack(fill='x', padx=18, pady=(0, 10))

        self.label('Vorm')
        combo = ttk.Combobox(
            self.controls, textvariable=self.shape, values=SHAPES,
            state='readonly', font=('Segoe UI', 9),
        )
        combo.pack(fill='x', padx=18, pady=(0, 8))
        combo.bind('<<ComboboxSelected>>', lambda _event: self.redraw())

        self.label('Kleur')
        self.colour_button = tk.Button(
            self.controls, textvariable=self.colour, command=self.pick_colour,
            font=('Consolas', 10, 'bold'), relief='flat', pady=7,
        )
        self.colour_button.pack(fill='x', padx=18, pady=(0, 8))
        self.update_colour_button()

        self.slider('Lengte', self.length, 100, 2400)
        self.slider('Breedte', self.width, 8, 500)
        self.slider('Ruimte dubbele lijn', self.gap, 0, 300)
        self.slider('Rotatie', self.rotation, -180, 180)
        self.slider('Dekking', self.opacity, 5, 100)

        tk.Checkbutton(
            self.controls, text='Kopie op andere zijkant', variable=self.paired,
            command=self.redraw, font=('Segoe UI', 9, 'bold'),
            fg=COLORS.TEXT, bg=COLORS.PANEL, selectcolor='#253656',
            activebackground=COLORS.PANEL, activeforeground=COLORS.TEXT,
        ).pack(anchor='w', padx=14, pady=(6, 2))
        tk.Label(
            self.controls, textvariable=self.coord_text, font=('Consolas', 9),
            fg='#93c5fd', bg=COLORS.PANEL, justify='left',
        ).pack(anchor='w', padx=18, pady=(5, 2))
        tk.Label(
            self.controls,
            text='Klik of sleep om te plaatsen.\nMuiswiel = breedte\nShift + wiel = draaien\nCtrl + wiel = lengte',
            font=('Segoe UI', 9), fg=COLORS.MUTED, bg=COLORS.PANEL,
            justify='left',
        ).pack(anchor='w', padx=18, pady=5)

        tk.Frame(self.controls, bg=COLORS.PANEL).pack(fill='both', expand=True)
        tk.Button(
            self.controls, text='NAAR MOTORKAP', command=self.reset_position,
            font=('Segoe UI', 9, 'bold'), fg=COLORS.TEXT, bg='#334155',
            relief='flat', pady=8,
        ).pack(fill='x', padx=18, pady=(0, 6))
        tk.Button(
            self.controls, text='ANNULEREN', command=self.cancel,
            font=('Segoe UI', 9, 'bold'), fg=COLORS.TEXT, bg='#475569',
            relief='flat', pady=8,
        ).pack(fill='x', padx=18, pady=(0, 6))
        tk.Button(
            self.controls, text='OPSLAAN & PREVIEW', command=self.save,
            font=('Segoe UI', 10, 'bold'), fg=COLORS.NAVY, bg=COLORS.CYAN,
            relief='flat', pady=11,
        ).pack(fill='x', padx=18, pady=(0, 16))

    def label(self, text):
        tk.Label(
            self.controls, text=text, font=('Segoe UI', 9),
            fg=COLORS.TEXT, bg=COLORS.PANEL,
        ).pack(anchor='w', padx=18, pady=(2, 3))

    def slider(self, title, variable, low, high):
        row = tk.Frame(self.controls, bg=COLORS.PANEL)
        row.pack(fill='x', padx=18, pady=(2, 0))
        tk.Label(row, text=title, font=('Segoe UI', 9), fg=COLORS.TEXT,
                 bg=COLORS.PANEL).pack(side='left')
        tk.Label(row, textvariable=variable, font=('Segoe UI', 9, 'bold'),
                 fg=COLORS.CYAN, bg=COLORS.PANEL).pack(side='right')
        tk.Scale(
            self.controls, from_=low, to=high, variable=variable,
            command=lambda _value: self.redraw(), orient='horizontal',
            bg=COLORS.PANEL, fg=COLORS.TEXT, troughcolor='#253656',
            activebackground=COLORS.CYAN, highlightthickness=0, showvalue=False,
        ).pack(fill='x', padx=14)

    def pick_colour(self):
        colour = colorchooser.askcolor(self.colour.get(), title='Kies streepkleur')[1]
        if colour:
            self.colour.set(colour.upper())
            self.update_colour_button()
            self.redraw()

    def update_colour_button(self):
        r, g, b = v12.v11.v10.v09.v08.base.rgb(self.colour.get())
        fg = '#000000' if (r * 299 + g * 587 + b * 114) > 150000 else '#FFFFFF'
        self.colour_button.configure(
            bg=self.colour.get(), fg=fg,
            activebackground=self.colour.get(), activeforeground=fg,
        )

    def bind_events(self):
        self.canvas.bind('<Configure>', lambda _event: self.redraw())
        self.canvas.bind('<Button-1>', self.pointer_down)
        self.canvas.bind('<B1-Motion>', self.pointer_move)
        self.canvas.bind('<ButtonRelease-1>', lambda _event: setattr(self, 'dragging', False))
        self.canvas.bind('<MouseWheel>', self.mouse_wheel)
        self.win.bind('<Left>', lambda event: self.nudge(-self.nudge_size(event), 0))
        self.win.bind('<Right>', lambda event: self.nudge(self.nudge_size(event), 0))
        self.win.bind('<Up>', lambda event: self.nudge(0, -self.nudge_size(event)))
        self.win.bind('<Down>', lambda event: self.nudge(0, self.nudge_size(event)))
        self.win.bind('<Escape>', lambda _event: self.cancel())
        self.win.bind('<Return>', lambda _event: self.save())

    @staticmethod
    def nudge_size(event):
        return 32 if event.state & 0x0001 else 8

    def pointer_to_uv(self, event):
        ox, oy = self.display_origin
        return (
            (event.x - ox) / self.display_scale,
            (event.y - oy) / self.display_scale,
        )

    def set_center(self, x, y):
        texture_w, texture_h = self.texture_size
        self.layer['centerX'] = max(0, min(texture_w, round(x)))
        self.layer['centerY'] = max(0, min(texture_h, round(y)))
        self.redraw()

    def pointer_down(self, event):
        self.dragging = True
        self.set_center(*self.pointer_to_uv(event))

    def pointer_move(self, event):
        if self.dragging:
            self.set_center(*self.pointer_to_uv(event))

    def mouse_wheel(self, event):
        direction = 1 if event.delta > 0 else -1
        if event.state & 0x0004:
            self.length.set(max(100, min(2400, self.length.get() + direction * 30)))
        elif event.state & 0x0001:
            self.rotation.set(max(-180, min(180, self.rotation.get() + direction * 5)))
        else:
            self.width.set(max(8, min(500, self.width.get() + direction * 5)))
        self.redraw()

    def nudge(self, dx, dy):
        self.set_center(self.layer['centerX'] + dx, self.layer['centerY'] + dy)

    def reset_position(self):
        self.layer['centerX'] = 3310
        self.layer['centerY'] = 2470
        self.rotation.set(90)
        self.redraw()

    def working_layer(self):
        layer = copy.deepcopy(self.layer)
        layer.update({
            'name': self.name.get().strip() or 'Nieuwe streep',
            'shape': self.shape.get(),
            'colour': self.colour.get(),
            'length': self.length.get(),
            'width': self.width.get(),
            'gap': self.gap.get(),
            'rotation': self.rotation.get(),
            'opacity': self.opacity.get(),
            'paired': self.paired.get(),
        })
        return layer

    def compose_display(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()
        layer = self.working_layer()
        bounds = None
        for cx, cy, rotation in stripe_instances(layer, self.texture_size):
            shown_layer = copy.deepcopy(layer)
            shown_layer['rotation'] = rotation
            item = stripe_asset(shown_layer)
            x = round(cx - item.width / 2)
            y = round(cy - item.height / 2)
            shown_w = max(1, round(item.width * self.display_scale))
            shown_h = max(1, round(item.height * self.display_scale))
            shown = item.resize((shown_w, shown_h), 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))
            )
            alpha = ImageChops.multiply(overlay.getchannel('A'), self.display_editable)
            overlay.putalpha(alpha)
            canvas = Image.alpha_composite(canvas, overlay)
            if bounds is None:
                bounds = (x, y, x + item.width, y + item.height)
        return canvas, bounds

    def redraw(self):
        if not self.canvas.winfo_exists():
            return
        canvas_w = max(300, self.canvas.winfo_width())
        canvas_h = max(300, self.canvas.winfo_height())
        texture_w, texture_h = self.texture_size
        margin = 18
        self.display_scale = min(
            (canvas_w - margin * 2) / texture_w,
            (canvas_h - margin * 2) / texture_h,
        )
        display_w = max(1, round(texture_w * self.display_scale))
        display_h = max(1, round(texture_h * self.display_scale))
        ox = round((canvas_w - display_w) / 2)
        oy = round((canvas_h - display_h) / 2)
        self.display_origin = (ox, oy)
        display, bounds = self.compose_display(display_w, display_h)
        self.photo = ImageTk.PhotoImage(display)
        self.canvas.delete('all')
        self.canvas.create_image(ox, oy, image=self.photo, anchor='nw')
        self.canvas.create_rectangle(
            ox, oy, ox + display_w, oy + display_h,
            outline='#1e3a5f', width=1,
        )
        if bounds:
            x0, y0, x1, y1 = bounds
            self.canvas.create_rectangle(
                ox + x0 * self.display_scale, oy + y0 * self.display_scale,
                ox + x1 * self.display_scale, oy + y1 * self.display_scale,
                outline=COLORS.CYAN, width=2, dash=(6, 3),
            )
        cx, cy = self.layer['centerX'], self.layer['centerY']
        sx, sy = ox + cx * self.display_scale, oy + cy * self.display_scale
        self.canvas.create_line(sx - 8, sy, sx + 8, sy, fill='#FFFFFF', width=2)
        self.canvas.create_line(sx, sy - 8, sx, sy + 8, fill='#FFFFFF', width=2)
        self.coord_text.set(f'Positie: X {cx} / Y {cy}')

    def save(self):
        self.studio.stripe_layers[self.index] = self.working_layer()
        self.is_new = False
        self.win.destroy()
        self.studio.commit_stripes(self.index, 'Streep opgeslagen')

    def cancel(self):
        if self.is_new and self.index < len(self.studio.stripe_layers):
            self.studio.stripe_layers.pop(self.index)
            self.studio.refresh_stripes(max(0, self.index - 1))
        self.win.destroy()


class StudioV13(v12.StudioV12):
    def __init__(self):
        self.stripe_layers = []
        self.suppressed_preview_callbacks = 0
        migrate = not AUTOSAVE.exists() and V12_AUTOSAVE.exists()
        super().__init__()
        self.title('RaceCore Livery Studio v1.3 - Stripes & Lines')
        if migrate:
            self.load_path(V12_AUTOSAVE, record=False)
            self.autosave()

    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  v1.3')
        toolbar = self.winfo_children()[1]
        tk.Button(
            toolbar, text='Strepen / lijnen', command=self.stripe_dialog,
            font=('Segoe UI', 9, 'bold'), fg=COLORS.NAVY, bg='#A7F3D0',
            activebackground='#D1FAE5', relief='flat', padx=14,
        ).pack(side='left', padx=(0, 8), pady=6)
        self.rename_ui_text(self, 'Accentkleur', 'Preset-streepkleur')
        self.rename_ui_text(self, 'VECTOR STRIPES', 'SNELLE STREEPPRESETS')

    def rename_ui_text(self, parent, old, new):
        for child in parent.winfo_children():
            if isinstance(child, tk.Label) and child.cget('text') == old:
                child.configure(text=new)
            self.rename_ui_text(child, old, new)

    @staticmethod
    def normalise_stripe(layer):
        defaults = {
            'id': 'stripe', 'name': 'Nieuwe streep', 'shape': 'Rechte lijn',
            'colour': '#22D3EE', 'length': 900, 'width': 70, 'gap': 45,
            'rotation': 90, 'centerX': 3310, 'centerY': 2470,
            'opacity': 100, 'paired': False, 'visible': True, 'locked': False,
        }
        for key, value in defaults.items():
            layer.setdefault(key, value)
        return layer

    def snapshot(self):
        state = super().snapshot()
        state['stripeLayers'] = copy.deepcopy(self.stripe_layers)
        return state

    def apply_state(self, state, record=False):
        state = copy.deepcopy(state)
        self.stripe_layers = [
            self.normalise_stripe(item) for item in state.get('stripeLayers', [])
        ]
        super().apply_state(state, record)
        if hasattr(self, 'stripe_list'):
            self.refresh_stripes()

    def new_blank_project(self):
        self.stripe_layers = []
        super().new_blank_project()

    def stripe_dialog(self):
        win = tk.Toplevel(self)
        win.title('RaceCore Strepen & Lijnen Studio')
        win.geometry('830x560')
        win.minsize(760, 500)
        win.configure(bg=COLORS.PANEL)
        win.transient(self)
        win.grab_set()

        top = tk.Frame(win, bg=COLORS.PANEL)
        top.pack(fill='x', padx=22, pady=(18, 10))
        tk.Label(
            top, text='STREPEN & LIJNEN', font=('Segoe UI', 15, 'bold'),
            fg=COLORS.CYAN, bg=COLORS.PANEL,
        ).pack(side='left')
        self.stripe_summary = tk.StringVar()
        tk.Label(
            top, textvariable=self.stripe_summary, font=('Segoe UI', 9),
            fg=COLORS.MUTED, bg=COLORS.PANEL,
        ).pack(side='right')

        frame = tk.Frame(win, bg=COLORS.PANEL)
        frame.pack(fill='both', expand=True, padx=22)
        self.stripe_list = tk.Listbox(
            frame, font=('Segoe UI', 10), bg='#08152c', fg=COLORS.TEXT,
            selectbackground='#2563eb', relief='flat', activestyle='none',
            exportselection=False,
        )
        self.stripe_list.pack(side='left', fill='both', expand=True)
        self.stripe_list.bind('<Double-Button-1>', lambda _event: self.edit_stripe())
        self.stripe_list.bind('<Delete>', lambda _event: self.delete_stripe())
        self.stripe_list.bind('<space>', lambda _event: self.toggle_stripe())
        self.stripe_list.bind('<Control-d>', lambda _event: self.duplicate_stripe())

        side = tk.Frame(frame, bg=COLORS.PANEL, width=180)
        side.pack(side='right', fill='y', padx=(12, 0))
        actions = [
            ('+ Streep toevoegen', self.add_stripe, COLORS.CYAN),
            ('Visueel bewerken', self.edit_stripe, '#334155'),
            ('Dupliceren', self.duplicate_stripe, '#334155'),
            ('Andere zijde aan/uit', self.pair_stripe, '#334155'),
            ('Naar voren', lambda: self.move_stripe(1), '#334155'),
            ('Naar achteren', lambda: self.move_stripe(-1), '#334155'),
            ('Tonen / verbergen', self.toggle_stripe, '#334155'),
            ('Vergrendelen', self.lock_stripe, '#334155'),
            ('Verwijderen', self.delete_stripe, '#7f1d1d'),
        ]
        for text, command, colour in actions:
            tk.Button(
                side, text=text, command=command, font=('Segoe UI', 9, 'bold'),
                fg=(COLORS.NAVY if colour == COLORS.CYAN else '#F8FAFC'),
                bg=colour, relief='flat', pady=7,
            ).pack(fill='x', pady=(0, 5))

        tk.Label(
            win,
            text='Tip: begin met Geen strepen bij Design Preset en bouw daarna je eigen ontwerp.',
            font=('Segoe UI', 9), fg=COLORS.MUTED, bg=COLORS.PANEL,
        ).pack(anchor='w', padx=22, pady=(8, 0))
        tk.Button(
            win, text='SLUITEN', command=win.destroy,
            font=('Segoe UI', 10, 'bold'), fg=COLORS.NAVY, bg=COLORS.CYAN,
            relief='flat', pady=10,
        ).pack(fill='x', padx=22, pady=16)
        self.refresh_stripes()
        self.stripe_list.focus_set()

    def selected_stripe(self):
        selected = self.stripe_list.curselection() if hasattr(self, 'stripe_list') else ()
        return selected[0] if selected else None

    def refresh_stripes(self, select=None):
        if not hasattr(self, 'stripe_list'):
            return
        self.stripe_list.delete(0, 'end')
        visible = 0
        for index, raw in enumerate(self.stripe_layers):
            layer = self.normalise_stripe(raw)
            visible += int(layer['visible'])
            eye = '[aan]' if layer['visible'] else '[uit]'
            paired = ' | andere zijde' if layer['paired'] else ''
            locked = ' | vergrendeld' if layer['locked'] else ''
            self.stripe_list.insert(
                'end',
                f"{eye}  {index + 1:02d}  {layer['name']}  |  {layer['shape']}  |  "
                f"{layer['width']} px{paired}{locked}",
            )
        self.stripe_summary.set(
            f'{len(self.stripe_layers)} lagen | {visible} zichtbaar'
        )
        if self.stripe_layers:
            index = min(
                select if select is not None else len(self.stripe_layers) - 1,
                len(self.stripe_layers) - 1,
            )
            self.stripe_list.selection_set(index)
            self.stripe_list.see(index)

    def commit_stripes(self, select=None, label='Strepen bijgewerkt'):
        self.refresh_stripes(select)
        self.status.set(label)
        self.changed()

    def add_stripe(self):
        self.preset.set('Geen strepen')
        layer = self.normalise_stripe({
            'id': f'stripe-{len(self.stripe_layers) + 1}',
            'name': f'Streep {len(self.stripe_layers) + 1}',
            'colour': self.accent.get(),
        })
        self.stripe_layers.append(layer)
        index = len(self.stripe_layers) - 1
        self.refresh_stripes(index)
        StripePlacementEditor(self, index, is_new=True)

    def edit_stripe(self):
        index = self.selected_stripe()
        if index is None:
            self.status.set('Kies eerst een streeplaag')
            return
        if self.normalise_stripe(self.stripe_layers[index])['locked']:
            self.status.set('Ontgrendel deze streep eerst')
            return
        StripePlacementEditor(self, index)

    def duplicate_stripe(self):
        index = self.selected_stripe()
        if index is None:
            return
        layer = copy.deepcopy(self.normalise_stripe(self.stripe_layers[index]))
        layer['id'] = f"{layer['id']}-copy"
        layer['name'] += ' kopie'
        layer['centerX'] = min(4096, layer['centerX'] + 80)
        layer['centerY'] = min(4096, layer['centerY'] + 80)
        layer['locked'] = False
        self.stripe_layers.insert(index + 1, layer)
        self.commit_stripes(index + 1, 'Streep gedupliceerd')

    def pair_stripe(self):
        index = self.selected_stripe()
        if index is None:
            return
        layer = self.normalise_stripe(self.stripe_layers[index])
        if layer['locked']:
            self.status.set('Ontgrendel deze streep eerst')
            return
        layer['paired'] = not layer['paired']
        self.commit_stripes(index, 'Andere zijde aangepast')

    def toggle_stripe(self):
        index = self.selected_stripe()
        if index is None:
            return
        layer = self.normalise_stripe(self.stripe_layers[index])
        layer['visible'] = not layer['visible']
        self.commit_stripes(index, 'Zichtbaarheid aangepast')

    def lock_stripe(self):
        index = self.selected_stripe()
        if index is None:
            return
        layer = self.normalise_stripe(self.stripe_layers[index])
        layer['locked'] = not layer['locked']
        self.commit_stripes(index, 'Vergrendeling aangepast')

    def delete_stripe(self):
        index = self.selected_stripe()
        if index is None:
            return
        if self.normalise_stripe(self.stripe_layers[index])['locked']:
            self.status.set('Een vergrendelde streep kan niet worden verwijderd')
            return
        self.stripe_layers.pop(index)
        self.commit_stripes(max(0, index - 1), 'Streep verwijderd')

    def move_stripe(self, delta):
        index = self.selected_stripe()
        if index is None:
            return
        if self.normalise_stripe(self.stripe_layers[index])['locked']:
            self.status.set('Ontgrendel deze streep eerst')
            return
        new_index = max(0, min(len(self.stripe_layers) - 1, index + delta))
        if new_index != index:
            self.stripe_layers.insert(new_index, self.stripe_layers.pop(index))
            self.commit_stripes(new_index, 'Laagvolgorde aangepast')

    def editable_mask(self, size):
        body = v12.v11.v10.v09.v08.base.load_l('carbon.png')
        return ImageChops.subtract(body, self.protected_mask(size))

    @staticmethod
    def paste_item(canvas, item, center_x, center_y, editable):
        overlay = Image.new('RGBA', canvas.size, (0, 0, 0, 0))
        x = round(center_x - item.width / 2)
        y = round(center_y - item.height / 2)
        overlay.alpha_composite(item, (x, y))
        alpha = ImageChops.multiply(overlay.getchannel('A'), editable)
        overlay.putalpha(alpha)
        return Image.alpha_composite(canvas, overlay)

    def compose_stripes(self, canvas, exclude=None):
        editable = self.editable_mask(canvas.size)
        for index, raw in enumerate(self.stripe_layers):
            if index == exclude:
                continue
            layer = self.normalise_stripe(raw)
            if not layer['visible']:
                continue
            for center_x, center_y, rotation in stripe_instances(layer, canvas.size):
                rendered = copy.deepcopy(layer)
                rendered['rotation'] = rotation
                canvas = self.paste_item(
                    canvas, stripe_asset(rendered), center_x, center_y, editable
                )
        return canvas

    def compose_logos(self, canvas):
        editable = self.editable_mask(canvas.size)
        for raw in self.logo_layers:
            layer = self.normalise_layer(raw)
            if not layer['visible'] or not Path(layer['path']).exists():
                continue
            logo = Image.open(layer['path']).convert('RGBA')
            max_width = round(720 * layer['scale'] / 100)
            logo.thumbnail((max_width, round(max_width * .65)), Image.Resampling.LANCZOS)
            if layer['opacity'] < 100:
                alpha = logo.getchannel('A').point(
                    lambda value: value * layer['opacity'] // 100
                )
                logo.putalpha(alpha)
            for center_x, center_y, base_rotation in v12.PLACEMENTS[layer['position']]:
                item = logo.rotate(
                    base_rotation + layer['rotation'], expand=True,
                    resample=Image.Resampling.BICUBIC,
                )
                canvas = self.paste_item(
                    canvas, item,
                    center_x + layer['offsetX'], center_y + layer['offsetY'],
                    editable,
                )
        return canvas

    def editor_background(self, exclude_stripe=None):
        candidates = [
            v12.v11.v10.v09.GENERATED / 'customskin.tga',
            v12.v11.v10.v09.v08.GENERATED / 'customskin.tga',
            v12.v11.v10.v09.v08.base.SOURCE,
        ]
        for path in candidates:
            if path.exists():
                canvas = Image.open(path).convert('RGBA')
                break
        else:
            canvas = Image.new('RGBA', (4096, 4096), (6, 15, 31, 255))
        canvas = self.compose_stripes(canvas, exclude=exclude_stripe)
        return self.compose_logos(canvas)

    def _worker(self, apply):
        try:
            old_enabled = self.logo_enabled
            self.logo_enabled = False
            try:
                # De oudere motor bouwt twee tussenstappen; toon alleen het v1.3-eindresultaat.
                self.suppressed_preview_callbacks += 2
                v12.v11.v10.v09.StudioV09._worker(self, False)
            finally:
                self.logo_enabled = old_enabled
            source = v12.v11.v10.v09.GENERATED / 'customskin.tga'
            canvas = Image.open(source).convert('RGBA')
            canvas = self.compose_stripes(canvas)
            canvas = self.compose_logos(canvas)

            GENERATED.mkdir(parents=True, exist_ok=True)
            rgb = canvas.convert('RGB')
            rgb.save(GENERATED / 'customskin.tga', format='TGA', compression=None)
            Image.new('RGB', canvas.size, (0, 0, 0)).save(
                GENERATED / 'customskin_region.tga', format='TGA', compression=None
            )
            preview = rgb.copy()
            preview.thumbnail((880, 680))
            preview.save(GENERATED / 'preview.png', optimize=True)
            self.autosave()
            label = (
                f'Preview gereed | {len(self.stripe_layers)} streeplagen | '
                f'{sum(1 for item in self.logo_layers if item.get("visible", True))} logos'
            )
            if apply:
                result = subprocess.run(
                    [sys.executable, str(v12.v11.v10.v09.v08.base.LIVE_LINK),
                     str(LIVE_CONFIG)],
                    capture_output=True, text=True, timeout=60,
                )
                if result.returncode:
                    raise RuntimeError(result.stderr.strip() or result.stdout.strip())
                event = json.loads(result.stdout)
                label = 'Live in LMU' if event['status'] == 'installed' else 'LMU is al bijgewerkt'
            self.after(0, lambda: self.show_preview(label))
        except Exception as error:
            self.after(0, lambda text=str(error): self.show_error(text))

    def show_preview(self, label):
        if label == 'Preview gereed' and self.suppressed_preview_callbacks > 0:
            self.suppressed_preview_callbacks -= 1
            return
        super().show_preview(label)


if __name__ == '__main__':
    StudioV13().mainloop()
