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

ROOT = Path(__file__).resolve().parents[2]
APP = Path(__file__).resolve().parent
GENERATED = APP / 'generated'
AUTOSAVE = APP / 'autosave.racecore.json'
V10_PATH = ROOT / 'outputs' / 'RaceCore_Studio_v1.0' / 'racecore_studio_v10.py'
spec = importlib.util.spec_from_file_location('racecore_v10', V10_PATH)
v10 = importlib.util.module_from_spec(spec)
spec.loader.exec_module(v10)
LIVE_CONFIG = APP / 'live-link-ui.json'
GPU_BRIDGE = ROOT / 'outputs' / 'GPU_Placement_Proof_v2.0' / 'gpu-placement.json'


class StudioV11(v10.StudioV10):
    def __init__(self):
        super().__init__()
        self.title('RaceCore Livery Studio v1.1 - Layer Studio')
        self.bind_all('<Control-z>', lambda _e: self.undo())
        self.bind_all('<Control-y>', lambda _e: self.redo())
        if AUTOSAVE.exists():
            self.load_path(AUTOSAVE, record=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  v1.1')

    @staticmethod
    def normalise_layer(layer):
        layer.setdefault('rotation', 0)
        layer.setdefault('offsetX', 0)
        layer.setdefault('offsetY', 0)
        layer.setdefault('opacity', 100)
        layer.setdefault('visible', True)
        layer.setdefault('locked', False)
        return layer

    def autosave(self):
        APP.mkdir(parents=True, exist_ok=True)
        AUTOSAVE.write_text(json.dumps(self.snapshot(), indent=2), encoding='utf-8')

    def apply_state(self, state, record=False):
        state = copy.deepcopy(state)
        state['logoLayers'] = [self.normalise_layer(x) for x in state.get('logoLayers', [])]
        super().apply_state(state, record)
        if hasattr(self, 'layer_list'):
            self.refresh_layers()

    def poll_gpu_bridge(self):
        try:
            if GPU_BRIDGE.exists():
                stamp = GPU_BRIDGE.stat().st_mtime_ns
                if stamp > self.gpu_bridge_mtime:
                    self.gpu_bridge_mtime = stamp
                    data = json.loads(GPU_BRIDGE.read_text(encoding='utf-8'))
                    incoming = data['layer']
                    path = Path(incoming['path'])
                    if path.exists():
                        self.logo_layers.append(self.normalise_layer({
                            'id': f'gpu-{stamp}', 'name': path.stem, 'path': str(path),
                            'position': incoming['position'], 'scale': incoming['scale'],
                            'rotation': incoming.get('rotation', 0), 'visible': True,
                        }))
                        self.commit_layers(len(self.logo_layers) - 1, 'GPU-laag ontvangen')
        except Exception as err:
            self.status.set('GPU Bridge: ' + str(err))
        self.after(600, self.poll_gpu_bridge)

    def logo_dialog(self):
        win = tk.Toplevel(self)
        win.title('RaceCore Layer Studio')
        win.geometry('780x520')
        win.minsize(720, 470)
        win.configure(bg=v10.v09.v08.base.PANEL)
        win.transient(self)
        win.grab_set()

        top = tk.Frame(win, bg=v10.v09.v08.base.PANEL)
        top.pack(fill='x', padx=22, pady=(18, 10))
        tk.Label(top, text='LAYER STUDIO', font=('Segoe UI', 15, 'bold'),
                 fg=v10.v09.v08.base.CYAN, bg=v10.v09.v08.base.PANEL).pack(side='left')
        self.layer_summary = tk.StringVar()
        tk.Label(top, textvariable=self.layer_summary, font=('Segoe UI', 9),
                 fg=v10.v09.v08.base.MUTED, bg=v10.v09.v08.base.PANEL).pack(side='right')

        frame = tk.Frame(win, bg=v10.v09.v08.base.PANEL)
        frame.pack(fill='both', expand=True, padx=22)
        self.layer_list = tk.Listbox(
            frame, font=('Segoe UI', 10), bg='#08152c', fg=v10.v09.v08.base.TEXT,
            selectbackground='#2563eb', relief='flat', activestyle='none', exportselection=False,
        )
        self.layer_list.pack(side='left', fill='both', expand=True)
        self.layer_list.bind('<Double-Button-1>', lambda _e: self.edit_layer())
        self.layer_list.bind('<Delete>', lambda _e: self.delete_layer())
        self.layer_list.bind('<space>', lambda _e: self.toggle_layer())
        self.layer_list.bind('<Control-d>', lambda _e: self.duplicate_layer())
        self.layer_list.bind('<Control-Up>', lambda _e: self.move_layer(-1))
        self.layer_list.bind('<Control-Down>', lambda _e: self.move_layer(1))

        side = tk.Frame(frame, bg=v10.v09.v08.base.PANEL, width=170)
        side.pack(side='right', fill='y', padx=(12, 0))
        actions = [
            ('+ Logo toevoegen', self.add_layer, v10.v09.v08.base.CYAN),
            ('Bewerken', self.edit_layer, '#334155'),
            ('Dupliceren  Ctrl+D', self.duplicate_layer, '#334155'),
            ('Omhoog  Ctrl+↑', lambda: self.move_layer(-1), '#334155'),
            ('Omlaag  Ctrl+↓', lambda: self.move_layer(1), '#334155'),
            ('Tonen/verbergen  Spatie', self.toggle_layer, '#334155'),
            ('Vergrendelen', self.lock_layer, '#334155'),
            ('Verwijderen  Delete', self.delete_layer, '#7f1d1d'),
        ]
        for text, cmd, colour in actions:
            tk.Button(side, text=text, command=cmd, font=('Segoe UI', 9, 'bold'),
                      fg=('#07152f' if colour == v10.v09.v08.base.CYAN else '#f8fafc'),
                      bg=colour, relief='flat', pady=8).pack(fill='x', pady=(0, 6))

        tk.Label(win, text='Dubbelklik = bewerken  ·  Spatie = zichtbaar  ·  Delete = verwijderen',
                 font=('Segoe UI', 9), fg=v10.v09.v08.base.MUTED,
                 bg=v10.v09.v08.base.PANEL).pack(anchor='w', padx=22, pady=(8, 0))
        tk.Button(win, text='SLUITEN', command=win.destroy, font=('Segoe UI', 10, 'bold'),
                  fg=v10.v09.v08.base.NAVY, bg=v10.v09.v08.base.CYAN,
                  relief='flat', pady=10).pack(fill='x', padx=22, pady=16)
        self.refresh_layers()
        self.layer_list.focus_set()

    def refresh_layers(self, select=None):
        if not hasattr(self, 'layer_list'):
            return
        self.layer_list.delete(0, 'end')
        visible = 0
        for i, raw in enumerate(self.logo_layers):
            layer = self.normalise_layer(raw)
            visible += int(layer['visible'])
            eye = '●' if layer['visible'] else '○'
            lock = '  🔒' if layer['locked'] else ''
            paired = '  ↔' if layer['position'] == 'Side panels (paired)' else ''
            self.layer_list.insert(
                'end',
                f"{eye}  {i + 1:02d}  {layer['name']}  ·  {layer['position']}  ·  "
                f"{layer['scale']}%  ·  {layer['opacity']}% dekking{paired}{lock}",
            )
        self.layer_summary.set(f'{len(self.logo_layers)} lagen · {visible} zichtbaar')
        if self.logo_layers:
            idx = min(select if select is not None else len(self.logo_layers) - 1,
                      len(self.logo_layers) - 1)
            self.layer_list.selection_set(idx)
            self.layer_list.see(idx)

    def commit_layers(self, select=None, label='Lagen bijgewerkt'):
        self.refresh_layers(select)
        self.status.set(label)
        self.changed()

    def add_layer(self):
        path = filedialog.askopenfilename(filetypes=[('Afbeeldingen', '*.png *.webp'), ('PNG', '*.png')])
        if not path:
            return
        try:
            with Image.open(path) as im:
                if 'A' not in im.getbands():
                    messagebox.showwarning('RaceCore', 'Deze afbeelding heeft geen transparantiekanaal.')
            self.logo_layers.append(self.normalise_layer({
                'id': f'logo-{len(self.logo_layers) + 1}', 'name': Path(path).stem,
                'path': path, 'position': 'Roof / engine cover', 'scale': 55,
            }))
            self.commit_layers(len(self.logo_layers) - 1, 'Logo toegevoegd')
        except Exception as err:
            messagebox.showerror('RaceCore', str(err))

    def edit_layer(self):
        idx = self.selected()
        if idx is None:
            return
        layer = self.normalise_layer(self.logo_layers[idx])
        if layer['locked']:
            self.status.set('Ontgrendel deze laag eerst')
            return
        dlg = tk.Toplevel(self)
        dlg.title('Laag bewerken')
        dlg.geometry('470x510')
        dlg.configure(bg=v10.v09.v08.base.PANEL)
        dlg.transient(self)
        dlg.grab_set()
        vars_ = {
            'name': tk.StringVar(value=layer['name']),
            'position': tk.StringVar(value=layer['position']),
            'scale': tk.IntVar(value=layer['scale']),
            'rotation': tk.IntVar(value=layer['rotation']),
            'offsetX': tk.IntVar(value=layer['offsetX']),
            'offsetY': tk.IntVar(value=layer['offsetY']),
            'opacity': tk.IntVar(value=layer['opacity']),
        }
        tk.Label(dlg, text='LAAG BEWERKEN', font=('Segoe UI', 13, 'bold'),
                 fg=v10.v09.v08.base.CYAN, bg=v10.v09.v08.base.PANEL).pack(anchor='w', padx=20, pady=(18, 8))
        tk.Entry(dlg, textvariable=vars_['name'], font=('Segoe UI', 10)).pack(fill='x', padx=20, pady=5)
        ttk.Combobox(dlg, textvariable=vars_['position'], values=list(v10.v09.PLACEMENTS),
                     state='readonly').pack(fill='x', padx=20, pady=5)
        for key, label, low, high in [
            ('scale', 'Formaat', 10, 180), ('rotation', 'Rotatie', -180, 180),
            ('offsetX', 'Horizontale correctie', -800, 800),
            ('offsetY', 'Verticale correctie', -800, 800), ('opacity', 'Dekking', 5, 100),
        ]:
            row = tk.Frame(dlg, bg=v10.v09.v08.base.PANEL)
            row.pack(fill='x', padx=20, pady=(5, 0))
            tk.Label(row, text=label, font=('Segoe UI', 9), fg=v10.v09.v08.base.TEXT,
                     bg=v10.v09.v08.base.PANEL).pack(side='left')
            tk.Label(row, textvariable=vars_[key], font=('Segoe UI', 9, 'bold'),
                     fg=v10.v09.v08.base.CYAN, bg=v10.v09.v08.base.PANEL).pack(side='right')
            tk.Scale(dlg, from_=low, to=high, variable=vars_[key], orient='horizontal',
                     bg=v10.v09.v08.base.PANEL, fg=v10.v09.v08.base.TEXT,
                     troughcolor='#253656', highlightthickness=0, showvalue=False).pack(fill='x', padx=16)

        def done():
            layer.update({k: value.get() for k, value in vars_.items()})
            self.logo_layers[idx] = layer
            dlg.destroy()
            self.commit_layers(idx, 'Laag bijgewerkt')

        tk.Button(dlg, text='OPSLAAN & PREVIEW', command=done, font=('Segoe UI', 10, 'bold'),
                  fg=v10.v09.v08.base.NAVY, bg=v10.v09.v08.base.CYAN,
                  relief='flat', pady=10).pack(fill='x', padx=20, pady=14)

    def duplicate_layer(self):
        idx = self.selected()
        if idx is None:
            return
        item = copy.deepcopy(self.normalise_layer(self.logo_layers[idx]))
        item['id'] = f"{item['id']}-copy"
        item['name'] += ' kopie'
        item['locked'] = False
        self.logo_layers.insert(idx + 1, item)
        self.commit_layers(idx + 1, 'Laag gedupliceerd')

    def delete_layer(self):
        idx = self.selected()
        if idx is None:
            return
        if self.normalise_layer(self.logo_layers[idx])['locked']:
            self.status.set('Vergrendelde laag kan niet worden verwijderd')
            return
        self.logo_layers.pop(idx)
        self.commit_layers(max(0, idx - 1), 'Laag verwijderd')

    def toggle_layer(self):
        idx = self.selected()
        if idx is None:
            return
        layer = self.normalise_layer(self.logo_layers[idx])
        layer['visible'] = not layer['visible']
        self.commit_layers(idx, 'Zichtbaarheid aangepast')

    def lock_layer(self):
        idx = self.selected()
        if idx is None:
            return
        layer = self.normalise_layer(self.logo_layers[idx])
        layer['locked'] = not layer['locked']
        self.commit_layers(idx, 'Laag vergrendeld' if layer['locked'] else 'Laag ontgrendeld')

    def move_layer(self, delta):
        idx = self.selected()
        if idx is None:
            return
        if self.normalise_layer(self.logo_layers[idx])['locked']:
            self.status.set('Ontgrendel deze laag eerst')
            return
        new = max(0, min(len(self.logo_layers) - 1, idx + delta))
        if new != idx:
            self.logo_layers.insert(new, self.logo_layers.pop(idx))
            self.commit_layers(new, 'Laagvolgorde aangepast')

    def _worker(self, apply):
        try:
            old_enabled = self.logo_enabled
            self.logo_enabled = False
            try:
                v10.v09.StudioV09._worker(self, False)
            finally:
                self.logo_enabled = old_enabled
            source = v10.v09.GENERATED / 'customskin.tga'
            canvas = Image.open(source).convert('RGBA')
            size = canvas.size
            editable = ImageChops.subtract(v10.v09.v08.base.load_l('carbon.png'), self.protected_mask(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 cx, cy, base_rotation in v10.v09.PLACEMENTS[layer['position']]:
                    item = logo.rotate(base_rotation + layer['rotation'], expand=True,
                                       resample=Image.Resampling.BICUBIC)
                    overlay = Image.new('RGBA', size, (0, 0, 0, 0))
                    x = round(cx + layer['offsetX'] - item.width / 2)
                    y = round(cy + layer['offsetY'] - item.height / 2)
                    overlay.alpha_composite(item, (x, y))
                    alpha = ImageChops.multiply(overlay.getchannel('A'), editable)
                    overlay.putalpha(alpha)
                    canvas = Image.alpha_composite(canvas, overlay)
            GENERATED.mkdir(parents=True, exist_ok=True)
            rgb = canvas.convert('RGB')
            rgb.save(GENERATED / 'customskin.tga', format='TGA', compression=None)
            Image.new('RGB', 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 · {sum(1 for x in self.logo_layers if x.get('visible', True))} zichtbare lagen"
            if apply:
                result = subprocess.run([sys.executable, str(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 err:
            self.after(0, lambda text=str(err): self.show_error(text))

    def show_preview(self, label):
        path = GENERATED / 'preview.png'
        if not path.exists():
            return
        image = Image.open(path)
        self.preview_photo = ImageTk.PhotoImage(image)
        self.preview_label.configure(image=self.preview_photo, text='')
        self.status.set(label)


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