#!/usr/bin/env python3
"""UI Designer for Complexx Raycaster - visual menu builder, exports JSON.

X,Y are CENTER-RELATIVE: 0,0 = center of canvas, -0.5,-0.5 = top-left, 0.5,0.5 = bottom-right.
"""

import copy
import json
import os
import tkinter as tk
from tkinter import colorchooser, filedialog, messagebox

ELEMENT_TYPES = ["bg_color", "bg_image", "text", "image", "image_button", "text_button"]

DEFAULTS = {
    "bg_color":     {"w": 960, "h": 600, "color": "#1a1a2e"},
    "bg_image":     {"w": 960, "h": 600, "src": "", "tint": "#ffffff"},
    "text":         {"w": 300, "h": 40, "text": "Label", "font_size": 20, "text_color": "#ffffff"},
    "image":        {"w": 200, "h": 150, "src": "", "tint": "#ffffff"},
    "image_button": {"w": 200, "h": 60, "normal_img": "", "hover_img": "", "pressed_img": "", "action": ""},
    "text_button":  {"w": 220, "h": 50, "text": "Button", "font_size": 24,
                     "text_color": "#ffffff", "bg_color": "#333355",
                     "hover_color": "#5555aa", "pressed_color": "#222244", "action": ""},
}

NID = [0]

COLOR_ATTRS = {"color", "tint", "text_color", "bg_color", "hover_color", "pressed_color"}


class Elem:
    def __init__(self, etype, cx=0, cy=0):
        NID[0] += 1
        self.id = NID[0]
        self.type = etype
        self.name = f"{etype}_{self.id}"
        self.cx = cx
        self.cy = cy
        self.z = 0
        self.flags = {}
        d = DEFAULTS.get(etype, {})
        self.w = d.get("w", 200)
        self.h = d.get("h", 100)
        for k, v in d.items():
            if not hasattr(self, k):
                setattr(self, k, v)

    def screen_xy(self, cw, ch):
        return cw / 2 + self.cx - self.w / 2, ch / 2 + self.cy - self.h / 2

    def to_json(self, cw, ch):
        d = {"type": self.type, "name": self.name, "z": self.z}
        if self.type in ("bg_color", "bg_image"):
            d["x"] = 0; d["y"] = 0; d["width"] = 1.0; d["height"] = 1.0
        else:
            d["x"] = round(self.cx / cw, 4)
            d["y"] = round(self.cy / ch, 4)
            d["width"] = round(self.w / cw, 4)
            d["height"] = round(self.h / ch, 4)
        for attr in ("color", "src", "tint", "text", "font_size", "text_color",
                      "bg_color", "hover_color", "pressed_color", "action",
                      "normal_img", "hover_img", "pressed_img"):
            if hasattr(self, attr):
                d[attr] = getattr(self, attr)
        if self.flags:
            d["flags"] = dict(self.flags)
        return d


class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Complexx UI Designer")
        self.geometry("1400x750")
        self.configure(bg="#2b2b2b")

        self.elems = []
        self.sel = None
        self.scene = "main_menu"
        self.scale = 1.0
        self.ox = self.oy = 0
        self.drag = {"on": False, "mode": "", "handle": "",
                      "dx": 0, "dy": 0, "sx": 0, "sy": 0,
                      "ox": 0, "oy": 0, "ow": 0, "oh": 0}
        self._build()
        self.bind("<Configure>", lambda e: self.after(10, self._draw))
        self._draw()

    def _build(self):
        top = tk.Frame(self, bg="#2b2b2b")
        top.pack(fill=tk.X, padx=5, pady=4)

        tk.Label(top, text="Scene:", bg="#2b2b2b", fg="#ddd").pack(side=tk.LEFT)
        self.ent_scene = tk.Entry(top, width=18, bg="#3b3b3b", fg="#ddd", insertbackground="#ddd")
        self.ent_scene.insert(0, self.scene)
        self.ent_scene.pack(side=tk.LEFT, padx=(2, 10))
        self.ent_scene.bind("<FocusOut>", lambda e: setattr(self, "scene", self.ent_scene.get() or "main_menu"))

        tk.Label(top, text="W:", bg="#2b2b2b", fg="#ddd").pack(side=tk.LEFT)
        self.ent_w = tk.Entry(top, width=5, bg="#3b3b3b", fg="#ddd", insertbackground="#ddd")
        self.ent_w.insert(0, "960"); self.ent_w.pack(side=tk.LEFT, padx=(2, 6))
        tk.Label(top, text="H:", bg="#2b2b2b", fg="#ddd").pack(side=tk.LEFT)
        self.ent_h = tk.Entry(top, width=5, bg="#3b3b3b", fg="#ddd", insertbackground="#ddd")
        self.ent_h.insert(0, "600"); self.ent_h.pack(side=tk.LEFT, padx=(2, 10))

        tk.Label(top, text="(x,y = center-relative)", bg="#2b2b2b", fg="#666").pack(side=tk.LEFT)
        self.lbl_scale = tk.Label(top, text="100%", bg="#2b2b2b", fg="#aaa")
        self.lbl_scale.pack(side=tk.LEFT, padx=(10, 0))

        tk.Button(top, text="Export", bg="#444", fg="#ddd", command=self._export).pack(side=tk.RIGHT, padx=3)
        tk.Button(top, text="Import", bg="#444", fg="#ddd", command=self._import).pack(side=tk.RIGHT, padx=3)

        body = tk.Frame(self, bg="#2b2b2b")
        body.pack(fill=tk.BOTH, expand=True, padx=5, pady=2)

        left = tk.Frame(body, bg="#2b2b2b", width=150)
        left.pack(side=tk.LEFT, fill=tk.Y)
        tk.Label(left, text="Elements", bg="#2b2b2b", fg="#ddd", font=("", 10, "bold")).pack(pady=(0, 4))
        for t in ELEMENT_TYPES:
            tk.Button(left, text=t.replace("_", " ").title(), bg="#444", fg="#ddd",
                      command=lambda t=t: self._add(t)).pack(fill=tk.X, pady=1, padx=2)
        tk.Frame(left, bg="#2b2b2b", height=8).pack()
        tk.Button(left, text="Up", bg="#444", fg="#ddd", command=self._zup).pack(fill=tk.X, padx=2)
        tk.Button(left, text="Down", bg="#444", fg="#ddd", command=self._zdown).pack(fill=tk.X, padx=2)
        tk.Button(left, text="Duplicate", bg="#444", fg="#ddd", command=self._dup).pack(fill=tk.X, padx=2, pady=2)
        tk.Button(left, text="Delete", bg="#633", fg="#ddd", command=self._del).pack(fill=tk.X, padx=2)

        tk.Label(left, text="Tree", bg="#2b2b2b", fg="#ddd", font=("", 10, "bold")).pack(pady=(8, 4))
        self.tree = tk.Listbox(left, bg="#3b3b3b", fg="#ddd", selectbackground="#5555aa",
                               activestyle="none", height=20)
        self.tree.pack(fill=tk.BOTH, expand=True, padx=2)
        self.tree.bind("<<ListboxSelect>>", self._tree_sel)

        self.cv = tk.Canvas(body, bg="#555555", highlightthickness=0)
        self.cv.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(4, 0))
        self.cv.bind("<ButtonPress-1>", self._press)
        self.cv.bind("<B1-Motion>", self._drag)
        self.cv.bind("<ButtonRelease-1>", self._release)
        self.cv.bind("<Button-3>", self._rclick)
        self.bind("<Delete>", lambda e: self._del())

        rp = tk.Frame(body, bg="#2b2b2b", width=300)
        rp.pack(side=tk.RIGHT, fill=tk.Y, padx=(4, 0))
        tk.Label(rp, text="Properties", bg="#2b2b2b", fg="#ddd",
                 font=("", 10, "bold")).pack(pady=(0, 4))
        self.props_canvas = tk.Canvas(rp, bg="#2b2b2b", highlightthickness=0)
        self.props_canvas.pack(fill=tk.BOTH, expand=True)
        self.prop_inner = tk.Frame(self.props_canvas, bg="#2b2b2b")
        self.props_canvas.create_window((0, 0), window=self.prop_inner, anchor="nw")
        self.prop_inner.bind("<Configure>", lambda e: self.props_canvas.configure(
            scrollregion=self.props_canvas.bbox("all")))

    def _get_lw(self):
        try: return int(self.ent_w.get())
        except: return 960

    def _get_lh(self):
        try: return int(self.ent_h.get())
        except: return 600

    def _l2s(self, lx, ly):
        return int(self.ox + lx * self.scale), int(self.oy + ly * self.scale)

    def _s2l(self, sx, sy):
        return (sx - self.ox) / self.scale, (sy - self.oy) / self.scale

    def _add(self, etype, cx=None, cy=None):
        cw, ch = self._get_lw(), self._get_lh()
        if cx is None: cx = 0
        if cy is None: cy = 0
        e = Elem(etype, cx, cy)
        if etype in ("bg_color", "bg_image"):
            e.cx = 0; e.cy = 0; e.w = cw; e.h = ch
        self.elems.append(e)
        self.sel = e
        self._refresh_tree(); self._show_props(); self._draw()

    def _refresh_tree(self):
        self.tree.delete(0, tk.END)
        for e in sorted(self.elems, key=lambda x: (-x.z, x.id)):
            tag = ">" if self.sel and e.id == self.sel.id else " "
            self.tree.insert(tk.END, f"{tag} z={e.z:3d}  {e.name}")
            if self.sel and e.id == self.sel.id:
                self.tree.selection_set(tk.END)

    def _tree_sel(self, _=None):
        sel = self.tree.curselection()
        if not sel: self.sel = None; self._clear_props(); self._draw(); return
        name = self.tree.get(sel[0]).strip().split("  ", 1)[1]
        for e in self.elems:
            if e.name == name: self.sel = e; self._show_props(); self._draw(); return

    def _add_color_field(self, parent, row, label, e, attr):
        tk.Label(parent, text=label, bg="#2b2b2b", fg="#aaa", anchor="w").grid(
            row=row, column=0, sticky="w", pady=1, padx=(0, 4))
        fr = tk.Frame(parent, bg="#2b2b2b")
        fr.grid(row=row, column=1, sticky="ew", pady=1)
        var = tk.StringVar(value=str(getattr(e, attr, "#ffffff")))
        ent = tk.Entry(fr, textvariable=var, width=14, bg="#3b3b3b", fg="#ddd", insertbackground="#ddd")
        ent.pack(side=tk.LEFT, fill=tk.X, expand=True)
        swatch = tk.Canvas(fr, width=20, height=20, bg=var.get(), highlightthickness=1,
                           highlightbackground="#888", cursor="hand2")
        swatch.pack(side=tk.LEFT, padx=(4, 0))

        def pick(_):
            c = colorchooser.askcolor(initialcolor=var.get())
            if c[1]: var.set(c[1])

        swatch.bind("<Button-1>", pick)

        def on_set(_=None):
            val = var.get()
            setattr(e, attr, val)
            try: swatch.configure(bg=val)
            except: pass
            self._draw()

        var.trace_add("write", on_set)
        ent.bind("<FocusOut>", on_set)
        return var

    def _add_field(self, parent, row, label, e, attr, typ):
        tk.Label(parent, text=label, bg="#2b2b2b", fg="#aaa", anchor="w").grid(
            row=row, column=0, sticky="w", pady=1, padx=(0, 4))
        var = tk.StringVar(value=str(getattr(e, attr, "")))
        ent = tk.Entry(parent, textvariable=var, width=18, bg="#3b3b3b", fg="#ddd", insertbackground="#ddd")
        ent.grid(row=row, column=1, sticky="ew", pady=1)

        def on_set(_=None):
            raw = var.get()
            try: setattr(e, attr, typ(raw))
            except: setattr(e, attr, raw)
            self._refresh_tree(); self._draw()

        var.trace_add("write", on_set)
        ent.bind("<FocusOut>", on_set)
        return var

    def _show_props(self):
        for w in self.prop_inner.winfo_children():
            w.destroy()
        e = self.sel
        if not e: return
        f = self.prop_inner
        r = [0]

        def nxt():
            r[0] += 1; return r[0] - 1

        self._add_field(f, nxt(), "Name", e, "name", str)
        self._add_field(f, nxt(), "Z", e, "z", int)

        if e.type not in ("bg_color", "bg_image"):
            self._add_field(f, nxt(), "X (center)", e, "cx", int)
            self._add_field(f, nxt(), "Y (center)", e, "cy", int)
            self._add_field(f, nxt(), "W", e, "w", int)
            self._add_field(f, nxt(), "H", e, "h", int)

        if e.type in ("text", "text_button"):
            self._add_field(f, nxt(), "Text", e, "text", str)
            self._add_field(f, nxt(), "Font", e, "font_size", int)
        if e.type == "text":
            self._add_color_field(f, nxt(), "Color", e, "text_color")
        if e.type in ("image", "bg_image"):
            self._add_field(f, nxt(), "Source", e, "src", str)
            self._add_color_field(f, nxt(), "Tint", e, "tint")
        if e.type == "image_button":
            self._add_field(f, nxt(), "Normal", e, "normal_img", str)
            self._add_field(f, nxt(), "Hover", e, "hover_img", str)
            self._add_field(f, nxt(), "Pressed", e, "pressed_img", str)
            self._add_field(f, nxt(), "Action", e, "action", str)
        if e.type == "text_button":
            self._add_color_field(f, nxt(), "Text C", e, "text_color")
            self._add_color_field(f, nxt(), "BG C", e, "bg_color")
            self._add_color_field(f, nxt(), "Hover C", e, "hover_color")
            self._add_color_field(f, nxt(), "Press C", e, "pressed_color")
            self._add_field(f, nxt(), "Action", e, "action", str)
        if e.type == "bg_color":
            self._add_color_field(f, nxt(), "Color", e, "color")

        r[0] += 1
        tk.Label(f, text="Flags", bg="#2b2b2b", fg="#ddd", font=("", 9, "bold")).grid(
            row=nxt(), column=0, columnspan=2, sticky="w", pady=(8, 2))

        flag_keys = list(e.flags.keys())
        for fk in flag_keys:
            row_i = nxt()
            fv = tk.StringVar(value=e.flags[fk])
            tk.Label(f, text=fk, bg="#2b2b2b", fg="#aaa", anchor="w").grid(
                row=row_i, column=0, sticky="w", pady=1)
            fr = tk.Frame(f, bg="#2b2b2b")
            fr.grid(row=row_i, column=1, sticky="ew", pady=1)
            tk.Entry(fr, textvariable=fv, width=14, bg="#3b3b3b", fg="#ddd",
                     insertbackground="#ddd").pack(side=tk.LEFT, fill=tk.X, expand=True)

            def del_flag(k=fk, fv=fv):
                if k in e.flags: del e.flags[k]; self._show_props()
            tk.Button(fr, text="x", bg="#633", fg="#ddd", width=2, command=del_flag).pack(side=tk.LEFT, padx=(2, 0))

            def on_flag(_=None, k=fk, v=fv):
                e.flags[k] = v.get()
                self._draw()
            fv.trace_add("write", on_flag)

        def add_flag():
            name = tk.simpledialog.askstring("Flag", "Flag name:", parent=self) if hasattr(tk, "simpledialog") else None
            if not name:
                from tkinter import simpledialog
                name = simpledialog.askstring("Flag", "Flag name:", parent=self)
            if name:
                e.flags[name.strip()] = ""
                self._show_props()

        tk.Button(f, text="+ Add Flag", bg="#444", fg="#ddd", command=add_flag).grid(
            row=nxt(), column=0, columnspan=2, sticky="w", pady=4)

        f.columnconfigure(1, weight=1)

    def _clear_props(self):
        for w in self.prop_inner.winfo_children():
            w.destroy()

    def _draw(self):
        self.cv.delete("all")
        cw, ch = self._get_lw(), self._get_lh()
        rw = self.cv.winfo_width()
        rh = self.cv.winfo_height()
        if rw < 10: rw = 960
        if rh < 10: rh = 600
        self.scale = min(rw / cw, rh / ch)
        if self.scale < 0.01: self.scale = 0.01
        self.ox = int((rw - cw * self.scale) / 2)
        self.oy = int((rh - ch * self.scale) / 2)
        sw, sh = int(cw * self.scale), int(ch * self.scale)
        self.lbl_scale.configure(text=f"{int(self.scale * 100)}%")

        self.cv.create_rectangle(self.ox, self.oy, self.ox + sw, self.oy + sh, fill="#333", outline="#666")

        for e in sorted(self.elems, key=lambda x: (x.z, x.id)):
            sx, sy = e.screen_xy(cw, ch)
            sx0, sy0 = self._l2s(sx, sy)
            sx1, sy1 = self._l2s(sx + e.w, sy + e.h)
            is_sel = self.sel and e.id == self.sel.id

            if e.type == "bg_color":
                self.cv.create_rectangle(sx0, sy0, sx1, sy1, fill=getattr(e, "color", "#333"), outline="")
            elif e.type == "bg_image":
                self.cv.create_rectangle(sx0, sy0, sx1, sy1, fill="#2a2a3a", outline="")
                lbl = os.path.basename(getattr(e, "src", "")) or "(no bg)"
                self.cv.create_text((sx0+sx1)//2, (sy0+sy1)//2, text=f"BG\n{lbl}", fill="#6666aa")
            else:
                ol = "#00ff88" if is_sel else "#888"
                lw = 2 if is_sel else 1
                if e.type == "text":
                    self.cv.create_rectangle(sx0, sy0, sx1, sy1, fill="#222233", outline=ol, width=lw)
                    tc = getattr(e, "text_color", "#fff")
                    fs = max(8, min(20, int(getattr(e, "font_size", 14) * self.scale * 0.5)))
                    self.cv.create_text((sx0+sx1)//2, (sy0+sy1)//2, text=getattr(e, "text", ""), fill=tc, font=("", fs))
                elif e.type == "image":
                    self.cv.create_rectangle(sx0, sy0, sx1, sy1, fill="#334455", outline=ol, width=lw)
                    lbl = os.path.basename(getattr(e, "src", "")) or "(no file)"
                    self.cv.create_text((sx0+sx1)//2, (sy0+sy1)//2, text=f"IMG\n{lbl}", fill="#aaccee")
                elif e.type == "image_button":
                    self.cv.create_rectangle(sx0, sy0, sx1, sy1, fill="#445566", outline=ol, width=lw)
                    lbl = os.path.basename(getattr(e, "normal_img", "")) or "(no file)"
                    self.cv.create_text((sx0+sx1)//2, (sy0+sy1)//2, text=f"BTN\n{lbl}", fill="#aaddaa")
                elif e.type == "text_button":
                    bg = getattr(e, "bg_color", "#333355")
                    self.cv.create_rectangle(sx0, sy0, sx1, sy1, fill=bg, outline=ol, width=lw)
                    tc = getattr(e, "text_color", "#fff")
                    fs = max(8, min(20, int(getattr(e, "font_size", 14) * self.scale * 0.5)))
                    self.cv.create_text((sx0+sx1)//2, (sy0+sy1)//2, text=getattr(e, "text", "Btn"), fill=tc, font=("", fs))

            if is_sel:
                self.cv.create_text(sx0, sy0 - 10, text=e.name, fill="#00ff88", font=("", 8), anchor="w")
                for hx, hy in self._handles(e, cw, ch):
                    self.cv.create_rectangle(hx-4, hy-4, hx+4, hy+4, fill="#00ff88", outline="#003322")

        self.cv.create_text(self.ox+4, self.oy+4, text=f"{self.scene} | {cw}x{ch}", fill="#aaa", font=("", 9), anchor="nw")

    def _handles(self, e, cw, ch):
        sx, sy = e.screen_xy(cw, ch)
        sx0, sy0 = self._l2s(sx, sy)
        sx1, sy1 = self._l2s(sx + e.w, sy + e.h)
        mx, my = (sx0+sx1)//2, (sy0+sy1)//2
        return [(sx0, sy0), (sx1, sy0), (sx0, sy1), (sx1, sy1),
                (mx, sy0), (mx, sy1), (sx0, my), (sx1, my)]

    def _hit_handle(self, sx, sy, e):
        cw, ch = self._get_lw(), self._get_lh()
        for name, (hx, hy) in zip(
            ["nw", "ne", "sw", "se", "n", "s", "w", "e"], self._handles(e, cw, ch)):
            if abs(sx - hx) < 8 and abs(sy - hy) < 8:
                return name
        return None

    def _press(self, event):
        lx, ly = self._s2l(event.x, event.y)
        cw, ch = self._get_lw(), self._get_lh()
        for e in sorted(self.elems, key=lambda x: -x.z):
            if e.type in ("bg_color", "bg_image"): continue
            sx, sy = e.screen_xy(cw, ch)
            if sx <= lx <= sx + e.w and sy <= ly <= sy + e.h:
                self.sel = e
                h = self._hit_handle(event.x, event.y, e)
                if h:
                    self.drag = {"on": True, "mode": "resize", "handle": h,
                                 "sx": lx, "sy": ly,
                                 "ocx": e.cx, "ocy": e.cy, "ow": e.w, "oh": e.h,
                                 "dx": 0, "dy": 0}
                else:
                    self.drag = {"on": True, "mode": "move", "handle": "",
                                 "dx": lx - sx, "dy": ly - sy,
                                 "sx": 0, "sy": 0, "ocx": 0, "ocy": 0, "ow": 0, "oh": 0}
                self._refresh_tree(); self._show_props(); self._draw()
                return
        self.sel = None; self.drag["on"] = False
        self._refresh_tree(); self._clear_props(); self._draw()

    def _drag(self, event):
        if not self.drag["on"] or not self.sel: return
        lx, ly = self._s2l(event.x, event.y)
        e = self.sel
        cw, ch = self._get_lw(), self._get_lh()
        sx, sy = e.screen_xy(cw, ch)
        if self.drag["mode"] == "resize":
            dx = lx - self.drag["sx"]
            dy = ly - self.drag["sy"]
            h = self.drag["handle"]
            ocx, ocy = self.drag["ocx"], self.drag["ocy"]
            ow, oh = self.drag["ow"], self.drag["oh"]
            if "e" in h: e.w = max(20, int(ow + dx))
            if "w" in h:
                nw = max(20, int(ow - dx))
                e.cx = int(ocx + (ow - nw) / 2); e.w = nw
            if "s" in h: e.h = max(20, int(oh + dy))
            if "n" in h:
                nh = max(20, int(oh - dy))
                e.cy = int(ocy + (oh - nh) / 2); e.h = nh
        else:
            new_sx = lx - self.drag["dx"]
            new_sy = ly - self.drag["dy"]
            e.cx = int(new_sx + e.w / 2 - cw / 2)
            e.cy = int(new_sy + e.h / 2 - ch / 2)
        self._show_props(); self._draw()

    def _release(self, _=None):
        self.drag["on"] = False

    def _rclick(self, event):
        lx, ly = self._s2l(event.x, event.y)
        cw, ch = self._get_lw(), self._get_lh()
        hit = None
        for e in sorted(self.elems, key=lambda x: -x.z):
            if e.type in ("bg_color", "bg_image"): continue
            sx, sy = e.screen_xy(cw, ch)
            if sx <= lx <= sx + e.w and sy <= ly <= sy + e.h:
                hit = e; break
        menu = tk.Menu(self, tearoff=0)
        if hit:
            self.sel = hit; self._refresh_tree(); self._show_props(); self._draw()
            menu.add_command(label=f"Delete {hit.name}", command=self._del)
            menu.add_command(label="Duplicate", command=self._dup)
            menu.add_separator()
        for t in ELEMENT_TYPES:
            menu.add_command(label=f"Add {t.replace('_',' ').title()}",
                             command=lambda t=t: self._add(t))
        try: menu.tk_popup(event.x_root, event.y_root)
        finally: menu.grab_release()

    def _zup(self):
        if self.sel: self.sel.z += 1; self._refresh_tree(); self._show_props(); self._draw()

    def _zdown(self):
        if self.sel: self.sel.z = max(0, self.sel.z - 1); self._refresh_tree(); self._show_props(); self._draw()

    def _dup(self):
        if not self.sel: return
        e = copy.deepcopy(self.sel)
        NID[0] += 1; e.id = NID[0]; e.name = f"{e.type}_{e.id}"
        if e.type not in ("bg_color", "bg_image"): e.cx += 20; e.cy += 20
        self.elems.append(e); self.sel = e
        self._refresh_tree(); self._show_props(); self._draw()

    def _del(self):
        if not self.sel: return
        self.elems.remove(self.sel); self.sel = None
        self._refresh_tree(); self._clear_props(); self._draw()

    def _export(self):
        cw, ch = self._get_lw(), self._get_lh()
        data = {"scene": self.scene, "canvas": [cw, ch],
                "elements": [e.to_json(cw, ch) for e in sorted(self.elems, key=lambda x: (x.z, x.id))]}
        path = filedialog.asksaveasfilename(defaultextension=".json", filetypes=[("JSON", "*.json")],
                                            initialfile=f"{self.scene}.json")
        if path:
            with open(path, "w") as f: json.dump(data, f, indent=2)
            messagebox.showinfo("Done", f"Saved to {path}")

    def _import(self):
        path = filedialog.askopenfilename(filetypes=[("JSON", "*.json")])
        if not path: return
        with open(path) as f: data = json.load(f)
        self.scene = data.get("scene", "main_menu")
        self.ent_scene.delete(0, tk.END); self.ent_scene.insert(0, self.scene)
        cw, ch = data.get("canvas", [960, 600])
        self.ent_w.delete(0, tk.END); self.ent_w.insert(0, str(cw))
        self.ent_h.delete(0, tk.END); self.ent_h.insert(0, str(ch))
        self.elems.clear(); NID[0] = 0
        for ed in data.get("elements", []):
            NID[0] += 1
            et = ed.get("type", "text_button")
            e = Elem(et, int(ed.get("x", 0) * cw), int(ed.get("y", 0) * ch))
            e.name = ed.get("name", e.name)
            e.w = int(ed.get("width", 0.2) * cw)
            e.h = int(ed.get("height", 0.06) * ch)
            e.z = ed.get("z", 0)
            e.flags = dict(ed.get("flags", {}))
            for k, v in ed.items():
                if k not in ("type", "name", "x", "y", "width", "height", "z", "flags"):
                    setattr(e, k, v)
            if et in ("bg_color", "bg_image"): e.cx = 0; e.cy = 0; e.w = cw; e.h = ch
            self.elems.append(e)
        self._refresh_tree(); self._draw()


if __name__ == "__main__":
    App().mainloop()
