from pathlib import Path
from PIL import Image, ImageTk, ImageChops, ImageDraw
import copy
import importlib.util
import tkinter as tk
from tkinter import 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'
V11_PATH = ROOT / 'outputs' / 'RaceCore_Studio_v1.1' / 'racecore_studio_v11.py'
V11_AUTOSAVE = V11_PATH.parent / 'autosave.racecore.json'

spec = importlib.util.spec_from_file_location('racecore_v11', V11_PATH)
v11 = importlib.util.module_from_spec(spec)
spec.loader.exec_module(v11)

# Geef de bewezen v1.1-engine een eigen veilige v1.2 werkmap.
v11.APP = APP
v11.GENERATED = GENERATED
v11.AUTOSAVE = AUTOSAVE
v11.LIVE_CONFIG = LIVE_CONFIG

COLORS = v11.v10.v09.v08.base
PLACEMENTS = v11.v10.v09.PLACEMENTS

# Een echt leeg ontwerp moet ook zonder automatisch lijnpatroon kunnen starten.
# We voegen de keuze toe aan dezelfde presetlijst die de bestaande UI gebruikt.
PRESETS = v11.v10.v09.v08.PRESETS
if 'Geen strepen' not in PRESETS:
    PRESETS = {
        'Geen strepen': {'include': [], 'islands': None},
        **PRESETS,
    }
    v11.v10.v09.v08.PRESETS = PRESETS


class VisualPlacementEditor:
    """Directe 2D UV-plaatsing voor een enkele logo-laag."""

    def __init__(self, studio, index):
        self.studio = studio
        self.index = index
        self.layer = copy.deepcopy(studio.normalise_layer(studio.logo_layers[index]))
        self.layer.setdefault('position', 'Roof / engine cover')
        self.layer.setdefault('scale', 55)
        self.layer.setdefault('rotation', 0)
        self.layer.setdefault('offsetX', 0)
        self.layer.setdefault('offsetY', 0)
        self.layer.setdefault('opacity', 100)

        self.win = tk.Toplevel(studio)
        self.win.title('RaceCore Visual Placement Editor')
        self.win.geometry('1180x820')
        self.win.minsize(940, 680)
        self.win.configure(bg=COLORS.NAVY)
        self.win.transient(studio)
        self.win.grab_set()

        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=270)
        self.controls.pack(side='right', fill='y', padx=(8, 16), pady=16)
        self.controls.pack_propagate(False)

        self.position = tk.StringVar(value=self.layer['position'])
        self.scale = tk.IntVar(value=self.layer['scale'])
        self.rotation = tk.IntVar(value=self.layer['rotation'])
        self.opacity = tk.IntVar(value=self.layer['opacity'])
        self.coord_text = tk.StringVar()
        self.info_text = tk.StringVar(value='Klik of sleep het logo naar de juiste plek.')

        self.texture_size = (4096, 4096)
        self.background = self.load_background()
        self.editable_mask = ImageChops.subtract(
            v11.v10.v09.v08.base.load_l('carbon.png'),
            studio.protected_mask(self.texture_size),
        )
        self.logo_source = Image.open(self.layer['path']).convert('RGBA')
        self.photo = None
        self.display_cache_key = None
        self.display_background = None
        self.display_editable = None
        self.display_scale = 1.0
        self.display_origin = (0, 0)
        self.dragging = False

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

    def load_background(self):
        candidates = [
            v11.v10.v09.GENERATED / 'customskin.tga',
            v11.v10.v09.v08.GENERATED / 'customskin.tga',
            v11.v10.v09.v08.base.SOURCE,
        ]
        for path in candidates:
            if path.exists():
                image = Image.open(path).convert('RGBA')
                self.texture_size = image.size
                return image
        return Image.new('RGBA', self.texture_size, (6, 15, 31, 255))

    def build_controls(self):
        tk.Label(
            self.controls, text='VISUEEL PLAATSEN', font=('Segoe UI', 14, 'bold'),
            fg=COLORS.CYAN, bg=COLORS.PANEL,
        ).pack(anchor='w', padx=18, pady=(18, 2))
        tk.Label(
            self.controls, text=self.layer['name'], font=('Segoe UI', 10, 'bold'),
            fg=COLORS.TEXT, bg=COLORS.PANEL, wraplength=225, justify='left',
        ).pack(anchor='w', padx=18, pady=(0, 14))

        self.label('Startgebied')
        combo = ttk.Combobox(
            self.controls, textvariable=self.position, values=list(PLACEMENTS),
            state='readonly', font=('Segoe UI', 9),
        )
        combo.pack(fill='x', padx=18, pady=(0, 10))
        combo.bind('<<ComboboxSelected>>', self.change_position)

        self.slider('Formaat', self.scale, 10, 180)
        self.slider('Rotatie', self.rotation, -180, 180)
        self.slider('Dekking', self.opacity, 5, 100)

        tk.Label(
            self.controls, textvariable=self.coord_text, font=('Consolas', 9),
            fg='#93c5fd', bg=COLORS.PANEL, justify='left',
        ).pack(anchor='w', padx=18, pady=(10, 4))
        tk.Label(
            self.controls,
            text='BEDIENING\n- Klik/sleep: verplaatsen\n- Muiswiel: formaat\n- Shift + wiel: draaien\n- Pijltjes: precies verplaatsen',
            font=('Segoe UI', 9), fg=COLORS.MUTED, bg=COLORS.PANEL,
            justify='left',
        ).pack(anchor='w', padx=18, pady=8)
        tk.Label(
            self.controls, textvariable=self.info_text, font=('Segoe UI', 9, 'bold'),
            fg='#86efac', bg=COLORS.PANEL, wraplength=230, justify='left',
        ).pack(anchor='w', padx=18, pady=8)

        tk.Frame(self.controls, bg=COLORS.PANEL).pack(fill='both', expand=True)
        tk.Button(
            self.controls, text='TERUG NAAR STARTPUNT', command=self.reset_position,
            font=('Segoe UI', 9, 'bold'), fg=COLORS.TEXT, bg='#334155',
            relief='flat', pady=9,
        ).pack(fill='x', padx=18, pady=(0, 7))
        tk.Button(
            self.controls, text='ANNULEREN', command=self.win.destroy,
            font=('Segoe UI', 9, 'bold'), fg=COLORS.TEXT, bg='#475569',
            relief='flat', pady=9,
        ).pack(fill='x', padx=18, pady=(0, 7))
        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, 18))

    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=(3, 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 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.win.destroy())
        self.win.bind('<Return>', lambda _event: self.save())

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

    def anchor(self):
        return PLACEMENTS[self.position.get()][0]

    def uv_center(self):
        x, y, _rotation = self.anchor()
        return x + int(self.layer['offsetX']), y + int(self.layer['offsetY'])

    def set_uv_center(self, x, y):
        anchor_x, anchor_y, _rotation = self.anchor()
        width, height = self.texture_size
        x = max(0, min(width, round(x)))
        y = max(0, min(height, round(y)))
        self.layer['offsetX'] = x - anchor_x
        self.layer['offsetY'] = y - anchor_y
        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 pointer_down(self, event):
        self.dragging = True
        self.set_uv_center(*self.pointer_to_uv(event))

    def pointer_move(self, event):
        if self.dragging:
            self.set_uv_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() + 5 * direction)))
        else:
            self.scale.set(max(10, min(180, self.scale.get() + 3 * direction)))
        self.redraw()

    def nudge(self, dx, dy):
        x, y = self.uv_center()
        self.set_uv_center(x + dx, y + dy)

    def change_position(self, _event=None):
        self.layer['offsetX'] = 0
        self.layer['offsetY'] = 0
        self.redraw()

    def reset_position(self):
        self.layer['offsetX'] = 0
        self.layer['offsetY'] = 0
        self.redraw()

    def transformed_logo(self, base_rotation=0):
        logo = self.logo_source.copy()
        max_width = round(720 * self.scale.get() / 100)
        logo.thumbnail((max_width, round(max_width * .65)), Image.Resampling.LANCZOS)
        if self.opacity.get() < 100:
            alpha = logo.getchannel('A').point(
                lambda value: value * self.opacity.get() // 100
            )
            logo.putalpha(alpha)
        return logo.rotate(
            base_rotation + self.rotation.get(), expand=True,
            resample=Image.Resampling.BICUBIC,
        )

    def compose_display_preview(self, display_w, display_h):
        cache_key = (display_w, display_h)
        if cache_key != self.display_cache_key:
            self.display_cache_key = cache_key
            self.display_background = self.background.resize(
                (display_w, display_h), Image.Resampling.LANCZOS
            )
            self.display_editable = self.editable_mask.resize(
                (display_w, display_h), Image.Resampling.LANCZOS
            )
        canvas = self.display_background.copy()
        bounds = None
        for anchor_x, anchor_y, base_rotation in PLACEMENTS[self.position.get()]:
            item = self.transformed_logo(base_rotation)
            x = round(anchor_x + self.layer['offsetX'] - item.width / 2)
            y = round(anchor_y + self.layer['offsetY'] - 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)
            shown_x = round(x * self.display_scale)
            shown_y = round(y * self.display_scale)
            overlay = Image.new('RGBA', (display_w, display_h), (0, 0, 0, 0))
            overlay.alpha_composite(shown, (shown_x, shown_y))
            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
        width = max(300, self.canvas.winfo_width())
        height = max(300, self.canvas.winfo_height())
        texture_w, texture_h = self.texture_size
        margin = 18
        self.display_scale = min(
            (width - margin * 2) / texture_w,
            (height - 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((width - display_w) / 2)
        oy = round((height - display_h) / 2)
        self.display_origin = (ox, oy)

        display, bounds = self.compose_display_preview(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.uv_center()
            screen_x = ox + cx * self.display_scale
            screen_y = oy + cy * self.display_scale
            self.canvas.create_line(screen_x - 8, screen_y, screen_x + 8, screen_y,
                                    fill='#ffffff', width=2)
            self.canvas.create_line(screen_x, screen_y - 8, screen_x, screen_y + 8,
                                    fill='#ffffff', width=2)
        cx, cy = self.uv_center()
        self.coord_text.set(
            f'Positie: X {cx} / Y {cy}\n'
            f'Correctie: {self.layer["offsetX"]:+d}, {self.layer["offsetY"]:+d}'
        )

    def save(self):
        self.layer['position'] = self.position.get()
        self.layer['scale'] = self.scale.get()
        self.layer['rotation'] = self.rotation.get()
        self.layer['opacity'] = self.opacity.get()
        self.studio.logo_layers[self.index] = self.layer
        self.win.destroy()
        self.studio.commit_layers(self.index, 'Logo visueel geplaatst')


class StudioV12(v11.StudioV11):
    def __init__(self):
        migration = not AUTOSAVE.exists() and V11_AUTOSAVE.exists()
        super().__init__()
        self.title('RaceCore Livery Studio v1.2 - Visual Placement')
        if migration:
            self.load_path(V11_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.2')

    def new_blank_project(self):
        super().new_blank_project()
        self.preset.set('Geen strepen')
        self.changed()
        self.status.set('Nieuw blanco project zonder strepen')

    def add_layer(self):
        before = len(self.logo_layers)
        super().add_layer()
        if len(self.logo_layers) > before:
            self.edit_layer()

    def edit_layer(self):
        idx = self.selected()
        if idx is None:
            self.status.set('Kies eerst een logo-laag')
            return
        layer = self.normalise_layer(self.logo_layers[idx])
        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
        VisualPlacementEditor(self, idx)


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