from pathlib import Path
import copy
import importlib.util
import json
import math
import random
import subprocess
import sys
import tkinter as tk
from tkinter import colorchooser, ttk

from PIL import Image, ImageChops, ImageDraw, ImageEnhance, ImageFilter


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'
V20_PATH = ROOT / 'outputs' / 'RaceCore_Studio_v2.0' / 'racecore_studio_v20.py'
V21_AUTOSAVE = ROOT / 'outputs' / 'RaceCore_Studio_v2.1' / 'autosave.racecore.json'
PANEL_MASK_DIR = ROOT / 'outputs' / 'Panel_Masks_v0.1' / 'masks'

spec = importlib.util.spec_from_file_location('racecore_v20_for_v22', V20_PATH)
v20 = importlib.util.module_from_spec(spec)
spec.loader.exec_module(v20)

for module in (
    v20, v20.v19, v20.v19.v18, v20.v19.v18.v17,
    v20.v19.v18.v17.v16, v20.v19.v18.v17.v16.v15,
    v20.v19.v18.v17.v16.v15.v14,
    v20.v19.v18.v17.v16.v15.v14.v13,
    v20.v19.v18.v17.v16.v15.v14.v13.v12,
    v20.v19.v18.v17.v16.v15.v14.v13.v12.v11,
    v20.v19.v18.v17.v16.v15.v14.v13.v12.v11.v10,
    v20.v19.v18.v17.v16.v15.v14.v13.v12.v11.v10.v09,
):
    module.APP = APP
    module.GENERATED = GENERATED
    module.AUTOSAVE = AUTOSAVE
    module.LIVE_CONFIG = LIVE_CONFIG

COLORS = v20.COLORS
ENGINE = v20.v19.v18.v17
BASE = v20.v19.v18.v17.v16.v15.v14.v13.v12.v11.v10.v09.v08.base

PANELS = (
    ('door_left', 'Deur links'),
    ('door_right', 'Deur rechts'),
    ('roof', 'Dak'),
    ('bonnet', 'Motorkap'),
    ('engine_cover', 'Motorafdekking'),
    ('front_fender_left', 'Voorscherm links'),
    ('front_fender_right', 'Voorscherm rechts'),
    ('rear_quarter_left', 'Achterste zijpaneel links'),
    ('rear_quarter_right', 'Achterste zijpaneel rechts'),
    ('side_skirt_left', 'Sideskirt links'),
    ('side_skirt_right', 'Sideskirt rechts'),
    ('front_bumper_outer', 'Voorbumper buitenzijde'),
    ('front_splitter', 'Frontsplitter'),
    ('front_bumper_left', 'Voorbumperhoek links'),
    ('front_bumper_right', 'Voorbumperhoek rechts'),
    ('rear_bumper_center', 'Achterbumper midden'),
    ('rear_bumper_left', 'Achterbumper links'),
    ('rear_bumper_right', 'Achterbumper rechts'),
    ('rear_wing_top', 'Achtervleugel bovenzijde'),
    ('rear_wing_bottom', 'Achtervleugel onderzijde'),
)
PANEL_LABELS = dict(PANELS)
PATTERN_DEFAULTS = {
    'enabled': False,
    'kind': 'Camo - vloeiend',
    'colour1': '#14213D',
    'colour2': '#22D3EE',
    'colour3': '#F8FAFC',
    'scale': 55,
    'opacity': 100,
    'seed': 296,
}


def safe_colour(value, fallback='#FFFFFF'):
    value = str(value or '').upper()
    if len(value) == 7 and value.startswith('#'):
        try:
            int(value[1:], 16)
            return value
        except ValueError:
            pass
    return fallback


class StudioV22(v20.StudioV20):
    def __init__(self):
        self.panel_colours = {}
        self.carbon_panels = set()
        self.body_pattern = copy.deepcopy(PATTERN_DEFAULTS)
        self._panel_mask_cache = {}
        migrate = not AUTOSAVE.exists() and V21_AUTOSAVE.exists()
        super().__init__()
        self.title('RaceCore Livery Studio v2.2 - Carrosserie & Materialen')
        if migrate:
            # Bestaande ontwerpen blijven behouden. Alleen de verwijderde Slimme
            # Start-bediening wordt niet meer aangeboden.
            self.load_path(V21_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  v2.2')

        toolbar = self.winfo_children()[1]
        button = tk.Menubutton(
            toolbar, text='CARROSSERIE  ▾', font=('Segoe UI', 9, 'bold'),
            fg=COLORS.NAVY, bg='#67E8F9', activebackground='#A5F3FC',
            activeforeground=COLORS.NAVY, relief='flat', padx=12,
            cursor='hand2',
        )
        menu = tk.Menu(
            button, tearoff=False, font=('Segoe UI', 10), bg='#F8FAFC',
            fg='#0F172A', activebackground='#CFFAFE', activeforeground='#0F172A',
            relief='flat', bd=1,
        )
        menu.add_command(label='Paneelkleuren & carbon', command=self.panel_dialog)
        menu.add_command(label='Patroon over de hele auto', command=self.pattern_dialog)
        button.configure(menu=menu)
        before = self.toolbar_menus[0][0] if self.toolbar_menus else None
        button.pack(side='left', padx=(7, 0), pady=6, before=before)
        self.carrosserie_menu = (button, menu)

        if self.toolbar_menus:
            design_menu = self.toolbar_menus[0][1]
            design_menu.insert_command(
                0, label='Paneelkleuren & carbon', command=self.panel_dialog)
            design_menu.insert_command(
                1, label='Full-body patroon / camo', command=self.pattern_dialog)
            design_menu.insert_separator(2)

    def snapshot(self):
        state = super().snapshot()
        state['schemaVersion'] = 3
        state['panelColours'] = copy.deepcopy(self.panel_colours)
        state['carbonPanels'] = sorted(self.carbon_panels)
        state['bodyPattern'] = copy.deepcopy(self.body_pattern)
        return state

    def apply_state(self, state, record=False):
        state = copy.deepcopy(state)
        self.panel_colours = {
            key: safe_colour(value)
            for key, value in state.get('panelColours', {}).items()
            if key in PANEL_LABELS and value
        }
        self.carbon_panels = {
            key for key in state.get('carbonPanels', []) if key in PANEL_LABELS
        }
        pattern = copy.deepcopy(PATTERN_DEFAULTS)
        pattern.update(state.get('bodyPattern', {}))
        pattern['colour1'] = safe_colour(pattern['colour1'], PATTERN_DEFAULTS['colour1'])
        pattern['colour2'] = safe_colour(pattern['colour2'], PATTERN_DEFAULTS['colour2'])
        pattern['colour3'] = safe_colour(pattern['colour3'], PATTERN_DEFAULTS['colour3'])
        pattern['scale'] = max(10, min(100, int(pattern.get('scale', 55))))
        pattern['opacity'] = max(10, min(100, int(pattern.get('opacity', 100))))
        pattern['seed'] = int(pattern.get('seed', 296))
        self.body_pattern = pattern
        super().apply_state(state, record)

    def new_blank_project(self):
        self.panel_colours = {}
        self.carbon_panels = set()
        self.body_pattern = copy.deepcopy(PATTERN_DEFAULTS)
        super().new_blank_project()

    def panel_dialog(self):
        win = tk.Toplevel(self)
        win.title('RaceCore Carrosseriepanelen & Carbon')
        win.geometry('980x690')
        win.minsize(900, 620)
        win.configure(bg=COLORS.PANEL)
        win.transient(self)
        win.grab_set()

        draft_colours = copy.deepcopy(self.panel_colours)
        draft_carbon = set(self.carbon_panels)

        top = tk.Frame(win, bg=COLORS.PANEL)
        top.pack(fill='x', padx=24, pady=(18, 10))
        tk.Label(top, text='CARROSSERIEPANELEN', font=('Segoe UI', 16, 'bold'),
                 fg=COLORS.CYAN, bg=COLORS.PANEL).pack(anchor='w')
        tk.Label(
            top,
            text=('Alle hoofdpanelen staan los in de lijst. Kies één onderdeel en geef '
                  'alleen dat paneel een kleur of het officiële LMU-carbonmateriaal.'),
            font=('Segoe UI', 9), fg=COLORS.MUTED, bg=COLORS.PANEL,
            justify='left', wraplength=830,
        ).pack(anchor='w', pady=(4, 0))

        content = tk.Frame(win, bg=COLORS.PANEL)
        content.pack(fill='both', expand=True, padx=24, pady=8)
        list_card = tk.Frame(content, bg='#08152C')
        list_card.pack(side='left', fill='both', expand=True, padx=(0, 12))
        editor = tk.Frame(content, bg='#101F3C', width=330)
        editor.pack(side='right', fill='y')
        editor.pack_propagate(False)

        tree_frame = tk.Frame(list_card, bg='#08152C')
        tree_frame.pack(fill='both', expand=True, padx=12, pady=12)
        tree = ttk.Treeview(
            tree_frame, columns=('colour', 'carbon'), show='headings',
            selectmode='browse', height=18,
        )
        tree.heading('colour', text='Paneel en kleur')
        tree.heading('carbon', text='LMU-materiaal')
        tree.column('colour', width=330, minwidth=250)
        tree.column('carbon', width=130, minwidth=110, anchor='center')
        scroll = ttk.Scrollbar(tree_frame, orient='vertical', command=tree.yview)
        tree.configure(yscrollcommand=scroll.set)
        tree.pack(side='left', fill='both', expand=True)
        scroll.pack(side='right', fill='y')

        selected_title = tk.StringVar(value=PANELS[0][1])
        selected_info = tk.StringVar()
        tk.Label(editor, text='GESELECTEERD PANEEL', font=('Segoe UI', 9, 'bold'),
                 fg='#93C5FD', bg='#101F3C').pack(anchor='w', padx=20,
                                                  pady=(20, 5))
        tk.Label(editor, textvariable=selected_title, font=('Segoe UI', 14, 'bold'),
                 fg=COLORS.TEXT, bg='#101F3C', wraplength=285,
                 justify='left').pack(anchor='w', padx=20)
        tk.Label(editor, textvariable=selected_info, font=('Segoe UI', 9),
                 fg=COLORS.MUTED, bg='#101F3C', wraplength=285,
                 justify='left').pack(anchor='w', padx=20, pady=(4, 18))

        swatch = tk.Button(editor, font=('Consolas', 10, 'bold'), relief='flat',
                           pady=13, cursor='hand2')
        swatch.pack(fill='x', padx=20)
        carbon_button = tk.Button(editor, font=('Segoe UI', 10, 'bold'),
                                  relief='flat', pady=13, cursor='hand2')
        carbon_button.pack(fill='x', padx=20, pady=(10, 0))
        tk.Label(
            editor,
            text=('Carbon wordt niet in de lak getekend. RaceCore schrijft voor dit '
                  'ene paneel rood naar customskin_region.tga; LMU vervangt daar de '
                  'lak door zijn standaard carbon preset.'),
            font=('Segoe UI', 8), fg='#BFDBFE', bg='#101F3C', wraplength=285,
            justify='left',
        ).pack(anchor='w', padx=20, pady=(18, 0))

        def selected_key():
            selection = tree.selection()
            return selection[0] if selection else PANELS[0][0]

        def refresh_tree(select=None):
            current = select or selected_key()
            tree.delete(*tree.get_children())
            for key, label in PANELS:
                colour = draft_colours.get(key, 'Basiskleur')
                material = 'CARBON' if key in draft_carbon else 'Lak'
                tree.insert('', 'end', iid=key,
                            values=(f'{label}   —   {colour}', material))
            if current in PANEL_LABELS:
                tree.selection_set(current)
                tree.focus(current)
                tree.see(current)
            refresh_editor()

        def refresh_editor(*_args):
            key = selected_key()
            colour = draft_colours.get(key)
            selected_title.set(PANEL_LABELS[key])
            selected_info.set(
                'Eigen paneelkleur actief.' if colour
                else 'Gebruikt momenteel de algemene basiskleur.'
            )
            display = colour or self.primary.get()
            rgb = tuple(int(display[i:i + 2], 16) for i in (1, 3, 5))
            fg = '#000000' if sum(rgb) > 430 else '#FFFFFF'
            swatch.configure(
                text=(f'KLEUR WIJZIGEN  {colour}' if colour
                      else 'EIGEN KLEUR KIEZEN'),
                command=choose, bg=display, fg=fg,
                activebackground=display, activeforeground=fg,
            )
            carbon = key in draft_carbon
            carbon_button.configure(
                text=('CARBON AAN  —  KLIK OM UIT TE ZETTEN' if carbon
                      else 'CARBON UIT  —  KLIK OM AAN TE ZETTEN'),
                command=toggle_carbon,
                bg=('#86EFAC' if carbon else '#334155'),
                fg=(COLORS.NAVY if carbon else COLORS.TEXT),
                activebackground=('#BBF7D0' if carbon else '#475569'),
            )

        def choose():
            key = selected_key()
            initial = draft_colours.get(key, self.primary.get())
            value = colorchooser.askcolor(
                initial, title=f'Kleur voor {PANEL_LABELS[key]}')[1]
            if value:
                draft_colours[key] = value.upper()
                refresh_tree(key)

        def reset_one():
            key = selected_key()
            draft_colours.pop(key, None)
            refresh_tree(key)

        def toggle_carbon():
            key = selected_key()
            if key in draft_carbon:
                draft_carbon.remove(key)
            else:
                draft_carbon.add(key)
            refresh_tree(key)

        tree.bind('<<TreeviewSelect>>', refresh_editor)
        tree.bind('<Double-1>', lambda _event: choose())
        tk.Button(editor, text='KLEUR TERUG NAAR BASIS', command=reset_one,
                  font=('Segoe UI', 9, 'bold'), fg=COLORS.TEXT, bg='#475569',
                  relief='flat', pady=10).pack(fill='x', padx=20, pady=(10, 0))
        refresh_tree(PANELS[0][0])

        actions = tk.Frame(win, bg=COLORS.PANEL)
        actions.pack(fill='x', padx=24, pady=(7, 18))

        def reset_all():
            draft_colours.clear()
            draft_carbon.clear()
            refresh_tree()

        def apply():
            self.panel_colours = copy.deepcopy(draft_colours)
            self.carbon_panels = set(draft_carbon)
            self.changed()
            self.status.set(
                f'{len(self.panel_colours)} paneelkleuren | '
                f'{len(self.carbon_panels)} carbonpanelen')
            win.destroy()

        tk.Button(actions, text='ALLES TERUG NAAR BASIS', command=reset_all,
                  font=('Segoe UI', 9, 'bold'), fg=COLORS.TEXT, bg='#475569',
                  relief='flat', padx=14, pady=10).pack(side='left')
        tk.Button(actions, text='TOEPASSEN', command=apply,
                  font=('Segoe UI', 10, 'bold'), fg=COLORS.NAVY, bg=COLORS.CYAN,
                  relief='flat', padx=24, pady=10).pack(side='right')
        tk.Button(actions, text='ANNULEREN', command=win.destroy,
                  font=('Segoe UI', 9, 'bold'), fg=COLORS.TEXT, bg='#334155',
                  relief='flat', padx=18, pady=10).pack(side='right', padx=8)

    def pattern_dialog(self):
        win = tk.Toplevel(self)
        win.title('RaceCore Full-body Patroon')
        win.geometry('760x590')
        win.minsize(700, 540)
        win.configure(bg=COLORS.PANEL)
        win.transient(self)
        win.grab_set()

        draft = copy.deepcopy(self.body_pattern)
        enabled = tk.BooleanVar(value=bool(draft['enabled']))
        kind = tk.StringVar(value=draft['kind'])
        scale = tk.IntVar(value=int(draft['scale']))
        opacity = tk.IntVar(value=int(draft['opacity']))
        seed = tk.IntVar(value=int(draft['seed']))

        tk.Label(win, text='PATROON OVER DE HELE AUTO', font=('Segoe UI', 16, 'bold'),
                 fg=COLORS.CYAN, bg=COLORS.PANEL).pack(anchor='w', padx=24,
                                                       pady=(20, 4))
        tk.Label(
            win,
            text=('Het patroon volgt automatisch alle beschilderbare carrosseriedelen. '
                  'Nummerplaten, vaste stickers en displays blijven beschermd.'),
            font=('Segoe UI', 9), fg=COLORS.MUTED, bg=COLORS.PANEL,
            wraplength=700, justify='left',
        ).pack(anchor='w', padx=24, pady=(0, 14))

        card = tk.Frame(win, bg='#08152C')
        card.pack(fill='both', expand=True, padx=24)
        tk.Checkbutton(
            card, text='PATROON INSCHAKELEN', variable=enabled,
            font=('Segoe UI', 11, 'bold'), fg='#86EFAC', bg='#08152C',
            selectcolor='#253656', activebackground='#08152C',
            activeforeground='#86EFAC',
        ).pack(anchor='w', padx=20, pady=(18, 10))

        form = tk.Frame(card, bg='#08152C')
        form.pack(fill='x', padx=20)
        tk.Label(form, text='Patroon', font=('Segoe UI', 10), fg=COLORS.TEXT,
                 bg='#08152C').grid(row=0, column=0, sticky='w', pady=7)
        ttk.Combobox(form, textvariable=kind,
                     values=('Camo - vloeiend', 'Camo - digitaal'),
                     state='readonly', width=28).grid(row=0, column=1, sticky='ew',
                                                      padx=(18, 0), pady=7)
        form.grid_columnconfigure(1, weight=1)
        colour_buttons = {}

        def choose_colour(key):
            value = colorchooser.askcolor(draft[key], title='Kies patroonkleur')[1]
            if value:
                draft[key] = value.upper()
                draw_colour(key)

        def draw_colour(key):
            value = draft[key]
            colour_buttons[key].configure(text=value, bg=value,
                                          activebackground=value)

        for index, key in enumerate(('colour1', 'colour2', 'colour3'), start=1):
            tk.Label(form, text=f'Kleur {index}', font=('Segoe UI', 10),
                     fg=COLORS.TEXT, bg='#08152C').grid(row=index, column=0,
                                                       sticky='w', pady=7)
            button = tk.Button(form, command=lambda k=key: choose_colour(k),
                               font=('Consolas', 9, 'bold'), fg='#FFFFFF',
                               relief='flat', pady=7)
            button.grid(row=index, column=1, sticky='ew', padx=(18, 0), pady=5)
            colour_buttons[key] = button
            draw_colour(key)

        def scale_row(row, title, variable, low, high, suffix):
            line = tk.Frame(form, bg='#08152C')
            line.grid(row=row, column=0, columnspan=2, sticky='ew', pady=6)
            tk.Label(line, text=title, width=15, anchor='w', font=('Segoe UI', 10),
                     fg=COLORS.TEXT, bg='#08152C').pack(side='left')
            label = tk.Label(line, width=7, font=('Segoe UI', 9, 'bold'),
                             fg=COLORS.CYAN, bg='#08152C')
            label.pack(side='right')
            slider = tk.Scale(line, from_=low, to=high, orient='horizontal',
                              variable=variable, showvalue=False, bg='#08152C',
                              troughcolor='#253656', highlightthickness=0,
                              activebackground=COLORS.CYAN)
            slider.pack(side='left', fill='x', expand=True, padx=8)
            def update(*_args):
                label.configure(text=f'{variable.get()}{suffix}')
            variable.trace_add('write', update)
            update()

        scale_row(4, 'Patroongrootte', scale, 10, 100, '%')
        scale_row(5, 'Dekking', opacity, 10, 100, '%')

        random_row = tk.Frame(form, bg='#08152C')
        random_row.grid(row=6, column=0, columnspan=2, sticky='ew', pady=(9, 4))
        tk.Label(random_row, text='Variatie', width=15, anchor='w',
                 font=('Segoe UI', 10), fg=COLORS.TEXT,
                 bg='#08152C').pack(side='left')
        tk.Label(random_row, textvariable=seed, width=8, font=('Consolas', 9),
                 fg=COLORS.MUTED, bg='#08152C').pack(side='left')
        tk.Button(random_row, text='NIEUWE VARIANT',
                  command=lambda: seed.set(random.randint(1, 999999)),
                  font=('Segoe UI', 8, 'bold'), fg=COLORS.NAVY, bg='#A7F3D0',
                  relief='flat', padx=12).pack(side='right')

        actions = tk.Frame(win, bg=COLORS.PANEL)
        actions.pack(fill='x', padx=24, pady=18)

        def disable():
            self.body_pattern = copy.deepcopy(PATTERN_DEFAULTS)
            self.changed()
            self.status.set('Full-body patroon uitgeschakeld')
            win.destroy()

        def apply():
            draft.update({
                'enabled': enabled.get(), 'kind': kind.get(),
                'scale': scale.get(), 'opacity': opacity.get(), 'seed': seed.get(),
            })
            self.body_pattern = copy.deepcopy(draft)
            self.changed()
            self.status.set('Full-body patroon bijgewerkt')
            win.destroy()

        tk.Button(actions, text='PATROON UIT', command=disable,
                  font=('Segoe UI', 9, 'bold'), fg=COLORS.TEXT, bg='#475569',
                  relief='flat', padx=16, pady=10).pack(side='left')
        tk.Button(actions, text='TOEPASSEN', command=apply,
                  font=('Segoe UI', 10, 'bold'), fg=COLORS.NAVY, bg=COLORS.CYAN,
                  relief='flat', padx=24, pady=10).pack(side='right')
        tk.Button(actions, text='ANNULEREN', command=win.destroy,
                  font=('Segoe UI', 9, 'bold'), fg=COLORS.TEXT, bg='#334155',
                  relief='flat', padx=18, pady=10).pack(side='right', padx=8)

    def panel_mask(self, key, size):
        cache_key = (key, size)
        if cache_key in self._panel_mask_cache:
            return self._panel_mask_cache[cache_key].copy()
        path = PANEL_MASK_DIR / f'{key}.png'
        if not path.exists():
            return Image.new('L', size, 0)
        mask = Image.open(path).convert('L')
        if mask.size != size:
            mask = mask.resize(size, Image.Resampling.NEAREST)
        self._panel_mask_cache[cache_key] = mask.copy()
        return mask

    @staticmethod
    def shade_colour(colour, shade):
        solid = Image.new('RGB', shade.size, BASE.rgb(colour))
        return ImageEnhance.Brightness(ImageChops.multiply(solid, shade)).enhance(2.15)

    def compose_panel_colours(self, canvas, shade, editable):
        for key, colour in self.panel_colours.items():
            if key not in PANEL_LABELS:
                continue
            mask = ImageChops.multiply(self.panel_mask(key, canvas.size), editable)
            tinted = self.shade_colour(colour, shade)
            canvas = Image.composite(tinted.convert('RGBA'), canvas, mask)
        return canvas

    def make_pattern(self, size):
        data = self.body_pattern
        work_size = (1024, 1024)
        colours = [BASE.rgb(data[f'colour{index}']) for index in (1, 2, 3)]
        image = Image.new('RGB', work_size, colours[0])
        draw = ImageDraw.Draw(image)
        rng = random.Random(int(data['seed']))
        factor = max(0.25, int(data['scale']) / 55)
        if data['kind'] == 'Camo - digitaal':
            cell = max(12, int(42 * factor))
            for y in range(-cell, 1024 + cell, cell):
                for x in range(-cell, 1024 + cell, cell):
                    if rng.random() < 0.58:
                        colour = colours[1] if rng.random() < 0.62 else colours[2]
                        width = cell * rng.choice((1, 1, 2, 3))
                        height = cell * rng.choice((1, 1, 2))
                        draw.rectangle((x, y, x + width, y + height), fill=colour)
        else:
            count = max(55, int(170 / factor))
            for _index in range(count):
                cx, cy = rng.randint(-180, 1204), rng.randint(-180, 1204)
                width = rng.randint(int(70 * factor), int(240 * factor))
                height = rng.randint(int(45 * factor), int(155 * factor))
                colour = colours[1] if rng.random() < 0.62 else colours[2]
                points = []
                corners = rng.randint(7, 11)
                for corner in range(corners):
                    angle = (corner / corners) * 6.283185
                    wobble = rng.uniform(0.68, 1.18)
                    points.append((
                        cx + int(width * 0.5 * wobble * math.cos(angle)),
                        cy + int(height * 0.5 * wobble * math.sin(angle)),
                    ))
                draw.polygon(points, fill=colour)
            image = image.filter(ImageFilter.GaussianBlur(max(2, int(8 * factor))))
        return image.resize(size, Image.Resampling.LANCZOS)

    def compose_pattern(self, canvas, shade, editable):
        if not self.body_pattern.get('enabled'):
            return canvas
        pattern = self.make_pattern(canvas.size)
        shaded = ImageEnhance.Brightness(
            ImageChops.multiply(pattern, shade)).enhance(2.15).convert('RGBA')
        opacity = int(255 * int(self.body_pattern['opacity']) / 100)
        mask = ImageChops.multiply(editable, Image.new('L', canvas.size, opacity))
        return Image.composite(shaded, canvas, mask)

    def carbon_region_map(self, size, editable):
        carbon = Image.new('L', size, 0)
        for key in self.carbon_panels:
            carbon = ImageChops.lighter(carbon, self.panel_mask(key, size))
        carbon = ImageChops.multiply(carbon, editable)
        # LMU-regiokleuren zijn materiaalcodes, geen gewone kleurverlopen.
        # Houd de uitvoer daarom exact zwart of rood, ook langs paneelranden.
        carbon = carbon.point(lambda value: 255 if value >= 128 else 0)
        region = Image.new('RGB', size, (0, 0, 0))
        return Image.composite(Image.new('RGB', size, (255, 0, 0)), region, carbon), carbon

    @staticmethod
    def carbon_preview(canvas, mask):
        if not mask.getbbox():
            return canvas
        overlay = Image.new('RGBA', canvas.size, (11, 18, 28, 210))
        grid = Image.new('RGBA', canvas.size, (0, 0, 0, 0))
        draw = ImageDraw.Draw(grid)
        step = 26
        for offset in range(-canvas.size[1], canvas.size[0], step):
            draw.line((offset, 0, offset + canvas.size[1], canvas.size[1]),
                      fill=(90, 110, 126, 100), width=5)
            draw.line((offset + 10, 0, offset + 10 + canvas.size[1], canvas.size[1]),
                      fill=(5, 10, 18, 120), width=5)
        woven = Image.alpha_composite(overlay, grid)
        return Image.composite(woven, canvas, mask)

    def _worker(self, apply):
        try:
            old_enabled = self.logo_enabled
            self.logo_enabled = False
            try:
                self.suppressed_preview_callbacks += 2
                v20.v19.v18.v17.v16.v15.v14.v13.v12.v11.v10.v09.StudioV09._worker(
                    self, False)
            finally:
                self.logo_enabled = old_enabled

            source_path = GENERATED / 'customskin.tga'
            canvas = Image.open(source_path).convert('RGBA')
            original = Image.open(BASE.SOURCE).convert('RGB')
            shade_gray = original.convert('L')
            shade = Image.merge('RGB', (shade_gray, shade_gray, shade_gray))
            editable = self.editable_mask(canvas.size)

            canvas = self.compose_panel_colours(canvas, shade, editable)
            canvas = self.compose_pattern(canvas, shade, editable)
            canvas = self.compose_shapes(canvas)
            canvas = self.compose_stripes(canvas)
            canvas = self.compose_texts(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)
            region, carbon_mask = self.carbon_region_map(canvas.size, editable)
            region.save(GENERATED / 'customskin_region.tga', format='TGA', compression=None)

            preview = self.carbon_preview(canvas, carbon_mask).convert('RGB')
            preview.thumbnail((880, 680))
            preview.save(GENERATED / 'preview.png', optimize=True)
            self.autosave()
            pattern_label = ' | patroon aan' if self.body_pattern.get('enabled') else ''
            label = (f'Preview gereed | {len(self.panel_colours)} paneelkleuren | '
                     f'{len(self.carbon_panels)} carbonpanelen{pattern_label}')
            if apply:
                result = subprocess.run(
                    [sys.executable, str(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__':
    StudioV22().mainloop()
