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, 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'
V13_PATH = ROOT / 'outputs' / 'RaceCore_Studio_v1.3' / 'racecore_studio_v13.py'
V13_AUTOSAVE = V13_PATH.parent / 'autosave.racecore.json'

spec = importlib.util.spec_from_file_location('racecore_v13', V13_PATH)
v13 = importlib.util.module_from_spec(spec)
spec.loader.exec_module(v13)

# Alle geerfde onderdelen schrijven in de eigen v1.4-map.
for module in (v13, v13.v12, v13.v12.v11):
    module.APP = APP
    module.GENERATED = GENERATED
    module.AUTOSAVE = AUTOSAVE
    module.LIVE_CONFIG = LIVE_CONFIG

COLORS = v13.COLORS
SHAPES = ('Afgerond vlak', 'Rechthoek', 'Ovaal', 'Driehoek', 'Ruit')
QUICK_AREAS = {
    'Motorkap': (3310, 2470, 90, False),
    'Dak / engine cover': (2090, 2440, 0, False),
    'Zijkant links': (2050, 1220, 0, False),
    'Beide zijkanten': (2050, 1220, 0, True),
    'Achtervleugel': (3370, 315, 0, False),
}


def shape_asset(layer):
    width = max(20, int(layer.get('width', 900)))
    height = max(20, int(layer.get('height', 420)))
    opacity = max(5, min(100, int(layer.get('opacity', 100))))
    roundness = max(0, min(100, int(layer.get('roundness', 35))))
    colour = v13.v12.v11.v10.v09.v08.base.rgb(layer.get('colour', '#22D3EE'))
    fill = (*colour, round(255 * opacity / 100))
    pad = max(24, round(max(width, height) * .04))
    image = Image.new('RGBA', (width + pad * 2, height + pad * 2), (0, 0, 0, 0))
    draw = ImageDraw.Draw(image)
    box = (pad, pad, pad + width, pad + height)
    shape = layer.get('shape', 'Afgerond vlak')
    if shape == 'Ovaal':
        draw.ellipse(box, fill=fill)
    elif shape == 'Driehoek':
        draw.polygon(
            [(pad + width // 2, pad), (pad + width, pad + height), (pad, pad + height)],
            fill=fill,
        )
    elif shape == 'Ruit':
        draw.polygon(
            [(pad + width // 2, pad), (pad + width, pad + height // 2),
             (pad + width // 2, pad + height), (pad, pad + height // 2)],
            fill=fill,
        )
    elif shape == 'Rechthoek':
        draw.rectangle(box, fill=fill)
    else:
        radius = round(min(width, height) * roundness / 200)
        draw.rounded_rectangle(box, radius=radius, fill=fill)
    return image.rotate(
        float(layer.get('rotation', 0)), expand=True,
        resample=Image.Resampling.BICUBIC,
    )


class ShapePlacementEditor:
    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_shape(studio.shape_layers[index]))
        self.background = studio.editor_background(exclude_shape=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 Vormen & Kleurvlakken 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=300)
        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.area = tk.StringVar(value='Motorkap')
        self.colour = tk.StringVar(value=self.layer['colour'])
        self.width = tk.IntVar(value=self.layer['width'])
        self.height = tk.IntVar(value=self.layer['height'])
        self.rotation = tk.IntVar(value=self.layer['rotation'])
        self.opacity = tk.IntVar(value=self.layer['opacity'])
        self.roundness = tk.IntVar(value=self.layer['roundness'])
        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='VORM / KLEURVLAK', font=('Segoe UI', 14, 'bold'),
            fg=COLORS.CYAN, bg=COLORS.PANEL,
        ).pack(anchor='w', padx=18, pady=(15, 7))
        tk.Entry(self.controls, textvariable=self.name, font=('Segoe UI', 10)).pack(
            fill='x', padx=18, pady=(0, 8)
        )
        self.label('Snelle plaatsing')
        area_combo = ttk.Combobox(
            self.controls, textvariable=self.area, values=list(QUICK_AREAS),
            state='readonly', font=('Segoe UI', 9),
        )
        area_combo.pack(fill='x', padx=18, pady=(0, 7))
        area_combo.bind('<<ComboboxSelected>>', self.choose_area)

        self.label('Vorm')
        shape_combo = ttk.Combobox(
            self.controls, textvariable=self.shape, values=SHAPES,
            state='readonly', font=('Segoe UI', 9),
        )
        shape_combo.pack(fill='x', padx=18, pady=(0, 7))
        shape_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, 7))
        self.update_colour_button()

        self.slider('Breedte', self.width, 50, 2400)
        self.slider('Hoogte', self.height, 50, 1800)
        self.slider('Rotatie', self.rotation, -180, 180)
        self.slider('Ronde hoeken', self.roundness, 0, 100)
        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=(5, 1))
        tk.Label(
            self.controls, textvariable=self.coord_text, font=('Consolas', 9),
            fg='#93C5FD', bg=COLORS.PANEL,
        ).pack(anchor='w', padx=18, pady=(4, 1))
        tk.Label(
            self.controls,
            text='Klik of sleep om te plaatsen.\nMuiswiel = formaat\nShift + wiel = draaien',
            font=('Segoe UI', 9), fg=COLORS.MUTED, bg=COLORS.PANEL,
            justify='left',
        ).pack(anchor='w', padx=18, pady=4)

        tk.Frame(self.controls, bg=COLORS.PANEL).pack(fill='both', expand=True)
        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 kleurvlak')[1]
        if colour:
            self.colour.set(colour.upper())
            self.update_colour_button()
            self.redraw()

    def update_colour_button(self):
        r, g, b = v13.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 choose_area(self, _event=None):
        x, y, rotation, paired = QUICK_AREAS[self.area.get()]
        self.layer['centerX'] = x
        self.layer['centerY'] = y
        self.rotation.set(rotation)
        self.paired.set(paired)
        self.redraw()

    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 & 0x0001:
            self.rotation.set(max(-180, min(180, self.rotation.get() + direction * 5)))
        else:
            factor = 1.06 if direction > 0 else .94
            self.width.set(max(50, min(2400, round(self.width.get() * factor))))
            self.height.set(max(50, min(1800, round(self.height.get() * factor))))
        self.redraw()

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

    def working_layer(self):
        layer = copy.deepcopy(self.layer)
        layer.update({
            'name': self.name.get().strip() or 'Nieuw kleurvlak',
            'shape': self.shape.get(), 'colour': self.colour.get(),
            'width': self.width.get(), 'height': self.height.get(),
            'rotation': self.rotation.get(), 'opacity': self.opacity.get(),
            'roundness': self.roundness.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 v13.stripe_instances(layer, self.texture_size):
            rendered = copy.deepcopy(layer)
            rendered['rotation'] = rotation
            item = shape_asset(rendered)
            x, y = round(cx - item.width / 2), round(cy - 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))
            )
            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, canvas_h = max(300, self.canvas.winfo_width()), max(300, self.canvas.winfo_height())
        texture_w, texture_h = self.texture_size
        self.display_scale = min((canvas_w - 36) / texture_w, (canvas_h - 36) / texture_h)
        display_w = max(1, round(texture_w * self.display_scale))
        display_h = max(1, round(texture_h * self.display_scale))
        ox, oy = round((canvas_w - display_w) / 2), 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.shape_layers[self.index] = self.working_layer()
        self.is_new = False
        self.win.destroy()
        self.studio.commit_shapes(self.index, 'Kleurvlak opgeslagen')

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


class LogoPlacementEditorV14(v13.v12.VisualPlacementEditor):
    def load_background(self):
        image = self.studio.editor_background(exclude_logo=self.index)
        self.texture_size = image.size
        return image


class StudioV14(v13.StudioV13):
    def __init__(self):
        self.shape_layers = []
        migrate = not AUTOSAVE.exists() and V13_AUTOSAVE.exists()
        super().__init__()
        self.title('RaceCore Livery Studio v1.4 - Shapes & Accents')
        if migrate:
            self.load_path(V13_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.4')
        toolbar = self.winfo_children()[1]
        tk.Button(
            toolbar, text='Vormen / kleurvlakken', command=self.shape_dialog,
            font=('Segoe UI', 9, 'bold'), fg=COLORS.NAVY, bg='#FDE68A',
            activebackground='#FEF3C7', relief='flat', padx=14,
        ).pack(side='left', padx=(0, 8), pady=6)

    @staticmethod
    def normalise_shape(layer):
        defaults = {
            'id': 'shape', 'name': 'Nieuw kleurvlak', 'shape': 'Afgerond vlak',
            'colour': '#22D3EE', 'width': 900, 'height': 420,
            'rotation': 90, 'centerX': 3310, 'centerY': 2470,
            'roundness': 35, '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['shapeLayers'] = copy.deepcopy(self.shape_layers)
        return state

    def apply_state(self, state, record=False):
        state = copy.deepcopy(state)
        self.shape_layers = [
            self.normalise_shape(item) for item in state.get('shapeLayers', [])
        ]
        super().apply_state(state, record)
        if hasattr(self, 'shape_list'):
            self.refresh_shapes()

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

    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
        LogoPlacementEditorV14(self, index)

    def shape_dialog(self):
        win = tk.Toplevel(self)
        win.title('RaceCore Vormen & Kleurvlakken')
        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='VORMEN & KLEURVLAKKEN', font=('Segoe UI', 15, 'bold'),
                 fg=COLORS.CYAN, bg=COLORS.PANEL).pack(side='left')
        self.shape_summary = tk.StringVar()
        tk.Label(top, textvariable=self.shape_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.shape_list = tk.Listbox(
            frame, font=('Segoe UI', 10), bg='#08152C', fg=COLORS.TEXT,
            selectbackground='#2563EB', relief='flat', activestyle='none',
            exportselection=False,
        )
        self.shape_list.pack(side='left', fill='both', expand=True)
        self.shape_list.bind('<Double-Button-1>', lambda _event: self.edit_shape())
        self.shape_list.bind('<Delete>', lambda _event: self.delete_shape())
        self.shape_list.bind('<space>', lambda _event: self.toggle_shape())
        self.shape_list.bind('<Control-d>', lambda _event: self.duplicate_shape())
        side = tk.Frame(frame, bg=COLORS.PANEL, width=185)
        side.pack(side='right', fill='y', padx=(12, 0))
        actions = [
            ('+ Kleurvlak toevoegen', self.add_shape, COLORS.CYAN),
            ('Visueel bewerken', self.edit_shape, '#334155'),
            ('Dupliceren', self.duplicate_shape, '#334155'),
            ('Andere zijde aan/uit', self.pair_shape, '#334155'),
            ('Naar voren', lambda: self.move_shape(1), '#334155'),
            ('Naar achteren', lambda: self.move_shape(-1), '#334155'),
            ('Tonen / verbergen', self.toggle_shape, '#334155'),
            ('Vergrendelen', self.lock_shape, '#334155'),
            ('Verwijderen', self.delete_shape, '#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='Kleurvlakken komen automatisch onder strepen en sponsorlogo\'s.',
            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_shapes()
        self.shape_list.focus_set()

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

    def refresh_shapes(self, select=None):
        if not hasattr(self, 'shape_list'):
            return
        self.shape_list.delete(0, 'end')
        visible = 0
        for index, raw in enumerate(self.shape_layers):
            layer = self.normalise_shape(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.shape_list.insert(
                'end', f"{eye}  {index + 1:02d}  {layer['name']}  |  "
                       f"{layer['shape']}{paired}{locked}",
            )
        self.shape_summary.set(f'{len(self.shape_layers)} lagen | {visible} zichtbaar')
        if self.shape_layers:
            index = min(select if select is not None else len(self.shape_layers) - 1,
                        len(self.shape_layers) - 1)
            self.shape_list.selection_set(index)
            self.shape_list.see(index)

    def commit_shapes(self, select=None, label='Kleurvlakken bijgewerkt'):
        self.refresh_shapes(select)
        self.status.set(label)
        self.changed()

    def add_shape(self):
        layer = self.normalise_shape({
            'id': f'shape-{len(self.shape_layers) + 1}',
            'name': f'Kleurvlak {len(self.shape_layers) + 1}',
            'colour': self.accent.get(),
        })
        self.shape_layers.append(layer)
        index = len(self.shape_layers) - 1
        self.refresh_shapes(index)
        ShapePlacementEditor(self, index, is_new=True)

    def edit_shape(self):
        index = self.selected_shape()
        if index is None:
            self.status.set('Kies eerst een kleurvlak')
            return
        if self.normalise_shape(self.shape_layers[index])['locked']:
            self.status.set('Ontgrendel dit kleurvlak eerst')
            return
        ShapePlacementEditor(self, index)

    def duplicate_shape(self):
        index = self.selected_shape()
        if index is None:
            return
        layer = copy.deepcopy(self.normalise_shape(self.shape_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.shape_layers.insert(index + 1, layer)
        self.commit_shapes(index + 1, 'Kleurvlak gedupliceerd')

    def pair_shape(self):
        index = self.selected_shape()
        if index is None:
            return
        layer = self.normalise_shape(self.shape_layers[index])
        if layer['locked']:
            self.status.set('Ontgrendel dit kleurvlak eerst')
            return
        layer['paired'] = not layer['paired']
        self.commit_shapes(index, 'Andere zijde aangepast')

    def toggle_shape(self):
        index = self.selected_shape()
        if index is None:
            return
        layer = self.normalise_shape(self.shape_layers[index])
        layer['visible'] = not layer['visible']
        self.commit_shapes(index, 'Zichtbaarheid aangepast')

    def lock_shape(self):
        index = self.selected_shape()
        if index is None:
            return
        layer = self.normalise_shape(self.shape_layers[index])
        layer['locked'] = not layer['locked']
        self.commit_shapes(index, 'Vergrendeling aangepast')

    def delete_shape(self):
        index = self.selected_shape()
        if index is None:
            return
        if self.normalise_shape(self.shape_layers[index])['locked']:
            self.status.set('Een vergrendeld kleurvlak kan niet worden verwijderd')
            return
        self.shape_layers.pop(index)
        self.commit_shapes(max(0, index - 1), 'Kleurvlak verwijderd')

    def move_shape(self, delta):
        index = self.selected_shape()
        if index is None:
            return
        if self.normalise_shape(self.shape_layers[index])['locked']:
            self.status.set('Ontgrendel dit kleurvlak eerst')
            return
        new_index = max(0, min(len(self.shape_layers) - 1, index + delta))
        if new_index != index:
            self.shape_layers.insert(new_index, self.shape_layers.pop(index))
            self.commit_shapes(new_index, 'Laagvolgorde aangepast')

    def compose_shapes(self, canvas, exclude=None):
        editable = self.editable_mask(canvas.size)
        for index, raw in enumerate(self.shape_layers):
            if index == exclude:
                continue
            layer = self.normalise_shape(raw)
            if not layer['visible']:
                continue
            for cx, cy, rotation in v13.stripe_instances(layer, canvas.size):
                rendered = copy.deepcopy(layer)
                rendered['rotation'] = rotation
                canvas = self.paste_item(canvas, shape_asset(rendered), cx, cy, editable)
        return canvas

    def compose_logos(self, canvas, exclude=None):
        editable = self.editable_mask(canvas.size)
        for index, raw in enumerate(self.logo_layers):
            if index == exclude:
                continue
            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 v13.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, exclude_shape=None, exclude_logo=None):
        candidates = [
            v13.v12.v11.v10.v09.GENERATED / 'customskin.tga',
            v13.v12.v11.v10.v09.v08.GENERATED / 'customskin.tga',
            v13.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_shapes(canvas, exclude=exclude_shape)
        canvas = self.compose_stripes(canvas, exclude=exclude_stripe)
        return self.compose_logos(canvas, exclude=exclude_logo)

    def _worker(self, apply):
        try:
            old_enabled = self.logo_enabled
            self.logo_enabled = False
            try:
                self.suppressed_preview_callbacks += 2
                v13.v12.v11.v10.v09.StudioV09._worker(self, False)
            finally:
                self.logo_enabled = old_enabled
            source = v13.v12.v11.v10.v09.GENERATED / 'customskin.tga'
            canvas = Image.open(source).convert('RGBA')
            canvas = self.compose_shapes(canvas)
            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.shape_layers)} kleurvlakken | '
                     f'{len(self.stripe_layers)} strepen | {len(self.logo_layers)} logos')
            if apply:
                result = subprocess.run(
                    [sys.executable, str(v13.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))


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