"""
Media Player  —  v4
Requirements:  pip install pygame-ce mutagen Pillow pyinstaller
Build exe:     python -m PyInstaller --onefile --windowed media_player.py
"""

import tkinter as tk
from tkinter import filedialog, colorchooser
import threading, os, sys, io, random, json

def _ensure(pkg, import_as=None):
    try: __import__(import_as or pkg)
    except ImportError:
        import subprocess
        subprocess.check_call([sys.executable, "-m", "pip", "install", pkg])

_ensure("pygame")
_ensure("mutagen")
_ensure("Pillow", "PIL")

import pygame
from mutagen import File as MutagenFile
from mutagen.id3 import ID3
from mutagen.mp4 import MP4
from mutagen.flac import FLAC
from PIL import Image, ImageTk, ImageDraw

pygame.mixer.pre_init(44100, -16, 2, 512)
pygame.mixer.init()
pygame.init()

# ── Config file (saves playlist + theme) ───────────────────────────────────────
CONFIG_FILE = os.path.join(os.path.expanduser("~"), ".mediaplayer_config.json")

def load_config():
    try:
        with open(CONFIG_FILE, "r") as f:
            return json.load(f)
    except: return {}

def save_config(data):
    try:
        with open(CONFIG_FILE, "w") as f:
            json.dump(data, f, indent=2)
    except: pass

# ── Palette (defaults, overridden by theme) ────────────────────────────────────
DEFAULT_THEME = {
    "BG":     "#0f0f0f",
    "PANEL":  "#1a1a1a",
    "RAISED": "#2a2a2a",
    "ACCENT": "#1db954",
    "TEXT":   "#ffffff",
    "TEXT2":  "#b3b3b3",
    "MUTED":  "#4a4a4a",
    "SEL":    "#2a2a2a",
}

ART_SZ   = 190
EQ_BANDS = ["60", "170", "310", "600", "1K", "3K", "6K", "12K", "14K", "16K"]
EQ_LABELS= ["BASS","BASS","MIDS","MIDS","MIDS","MIDS","TREBLE","TREBLE","TREBLE","TREBLE"]
N_BANDS  = len(EQ_BANDS)
VIZ_BARS = 30

# ── Helpers ────────────────────────────────────────────────────────────────────
def fmt(s):
    s = max(0, int(s or 0))
    return f"{s//60}:{s%60:02d}"

def clean(path):
    name = os.path.splitext(os.path.basename(path))[0]
    for suffix in ["_spotdown.org"," spotdown.org","_mp3clan","_320kbps"]:
        if name.lower().endswith(suffix.lower()):
            name = name[:-len(suffix)]
    return name.strip()

def round_img(img, r=16):
    img = img.convert("RGBA")
    mask = Image.new("L", img.size, 0)
    ImageDraw.Draw(mask).rounded_rectangle([0,0,img.size[0]-1,img.size[1]-1], radius=r, fill=255)
    img.putalpha(mask)
    return img

def get_cover(path):
    try:
        ext = os.path.splitext(path)[1].lower()
        if ext in (".mp3",".mp2",".mp1"):
            tags = ID3(path)
            for k in tags:
                if k.startswith("APIC"): return Image.open(io.BytesIO(tags[k].data))
        elif ext in (".m4a",".mp4",".aac"):
            tags = MP4(path)
            if "covr" in tags: return Image.open(io.BytesIO(bytes(tags["covr"][0])))
        elif ext == ".flac":
            tags = FLAC(path)
            if tags.pictures: return Image.open(io.BytesIO(tags.pictures[0].data))
        else:
            f = MutagenFile(path)
            if f and hasattr(f,"pictures") and f.pictures:
                return Image.open(io.BytesIO(f.pictures[0].data))
    except: pass
    return None

def get_duration(path):
    try:
        f = MutagenFile(path)
        if f and f.info: return f.info.length
    except: pass
    return 0

def make_feather_icon(size=32, color="#1db954"):
    img = Image.new("RGBA", (size, size), (0,0,0,0))
    d = ImageDraw.Draw(img)
    d.line([(4, size-4), (size-4, 4)], fill=color, width=2)
    for i in range(5):
        t = (i+1)/6
        cx = int(4 + (size-8)*t); cy = int((size-4) - (size-8)*t)
        d.line([(cx, cy), (cx-8, cy-4)], fill=color, width=1)
        d.line([(cx, cy), (cx+4, cy-8)], fill=color, width=1)
    return ImageTk.PhotoImage(img)

# ── Player ─────────────────────────────────────────────────────────────────────
class Player(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Media Player")
        self.geometry("860x660")
        self.minsize(700, 560)
        self.resizable(True, True)

        # load saved config
        cfg = load_config()
        self.theme = {**DEFAULT_THEME, **cfg.get("theme", {})}

        self.configure(bg=self.theme["BG"])

        # playback state
        self.playlist      = []
        self.current_index = -1
        self.playing       = False
        self.duration      = 0.0
        self.seeking       = False
        self.shuffle       = False
        self.repeat        = "none"
        self.volume        = 0.8
        self._seek_offset  = 0.0
        self._art_photo    = None
        self._default_art  = None
        self._eq_vals      = [0.5] * N_BANDS
        self._eq_dragging  = None
        self._eq_visible   = False   # START CLOSED
        self._viz_h        = [0.0] * VIZ_BARS
        self._viz_t        = [0.0] * VIZ_BARS
        self._show_icon    = {}

        # button flash tracking
        self._flash_after  = {}
        self._theme_win    = None   # prevent duplicate theme windows

        self._build_ui()
        self._make_default_art()
        self._set_art(None)

        # restore saved playlist
        saved = cfg.get("playlist", [])
        for path in saved:
            if os.path.exists(path):
                idx = len(self.playlist)
                self.playlist.append(path)
                self._show_icon[idx] = True
                self.listbox.insert("end", "  ♪  " + clean(path))

        self._poll()
        self._viz_loop()
        self.protocol("WM_DELETE_WINDOW", self._on_close)

    def _on_close(self):
        cfg = load_config()
        cfg["playlist"] = self.playlist
        cfg["theme"]    = self.theme
        save_config(cfg)
        self.destroy()

    # ── Default art ───────────────────────────────────────────────────────────
    def _make_default_art(self):
        img = Image.new("RGB", (ART_SZ, ART_SZ), "#181818")
        d = ImageDraw.Draw(img)
        cx, cy, r = ART_SZ//2, ART_SZ//2, ART_SZ//4
        d.ellipse([cx-r,cy-r,cx+r,cy+r], fill="#2a2a2a")
        r2 = r//3
        d.ellipse([cx-r2,cy-r2,cx+r2,cy+r2], fill="#181818")
        img = round_img(img, 16)
        self._default_art = ImageTk.PhotoImage(img)

    def _set_art(self, path):
        cover = get_cover(path) if path else None
        def apply(photo):
            self._art_photo = photo
            self.art_label.configure(image=photo)
        if cover:
            cover = cover.convert("RGB").resize((ART_SZ,ART_SZ), Image.LANCZOS)
            cover = round_img(cover, 16)
            photo = ImageTk.PhotoImage(cover)
            self.after(0, lambda: apply(photo))
        else:
            self.after(0, lambda: self.art_label.configure(image=self._default_art))

    # ── Theme helpers ─────────────────────────────────────────────────────────
    def T(self, key):
        return self.theme.get(key, DEFAULT_THEME[key])

    # ── UI ────────────────────────────────────────────────────────────────────
    def _build_ui(self):
        BG=self.T("BG"); PANEL=self.T("PANEL"); ACCENT=self.T("ACCENT")
        TEXT=self.T("TEXT"); TEXT2=self.T("TEXT2"); MUTED=self.T("MUTED")

        # title bar
        title_bar = tk.Frame(self, bg=BG, height=36)
        title_bar.pack(fill="x")
        title_bar.pack_propagate(False)

        tk.Label(title_bar, text="Media Player",
                 font=("Segoe UI",11,"bold"), fg=TEXT, bg=BG).pack(side="left", padx=14, pady=8)

        # 🎨 theme button (top right)
        theme_lbl = tk.Label(title_bar, text="🎨", font=("Segoe UI Emoji",14),
                              fg=MUTED, bg=BG, cursor="hand2", padx=10)
        theme_lbl.pack(side="right", pady=6)
        theme_lbl.bind("<Button-1>", lambda e: self._open_theme())
        theme_lbl.bind("<Enter>",    lambda e: theme_lbl.configure(fg=TEXT))
        theme_lbl.bind("<Leave>",    lambda e: theme_lbl.configure(fg=MUTED))

        try:
            self._feather = make_feather_icon(22, ACCENT)
            tk.Label(title_bar, image=self._feather, bg=BG).pack(side="right", padx=4, pady=6)
        except: pass

        # body
        body = tk.Frame(self, bg=BG)
        body.pack(fill="both", expand=True)

        # ── Left panel ────────────────────────────────────────────────────────
        left = tk.Frame(body, bg=BG, width=270)
        left.pack(side="left", fill="y")
        left.pack_propagate(False)

        self.art_label = tk.Label(left, bg=BG, bd=0)
        self.art_label.pack(pady=(10,8))

        self.title_var = tk.StringVar(value="Nothing playing")
        tk.Label(left, textvariable=self.title_var,
                 font=("Segoe UI",11,"bold"), fg=TEXT, bg=BG,
                 wraplength=240, justify="center").pack(padx=12)

        self.artist_var = tk.StringVar(value="—")
        tk.Label(left, textvariable=self.artist_var,
                 font=("Segoe UI",9), fg=TEXT2, bg=BG).pack(pady=(2,8))

        # seek
        seek_wrap = tk.Frame(left, bg=BG)
        seek_wrap.pack(fill="x", padx=18, pady=(0,2))
        self.seek_c = tk.Canvas(seek_wrap, height=6, bg=BG, highlightthickness=0, cursor="hand2")
        self.seek_c.pack(fill="x")
        self.seek_c.bind("<ButtonPress-1>",   self._sk_click)
        self.seek_c.bind("<B1-Motion>",       self._sk_drag)
        self.seek_c.bind("<ButtonRelease-1>", self._sk_release)
        self._seek_pct = 0.0

        trow = tk.Frame(left, bg=BG)
        trow.pack(fill="x", padx=18)
        self.cur_var = tk.StringVar(value="0:00")
        self.dur_var = tk.StringVar(value="0:00")
        tk.Label(trow, textvariable=self.cur_var, font=("Segoe UI",8), fg=MUTED, bg=BG).pack(side="left")
        tk.Label(trow, textvariable=self.dur_var, font=("Segoe UI",8), fg=MUTED, bg=BG).pack(side="right")

        # transport buttons — canvas icon buttons with rounded-square glow style
        brow = tk.Frame(left, bg=BG)
        brow.pack(pady=10)

        BTN_SM = 38   # small button size
        BTN_LG = 52   # large play/pause button

        self.rep_lbl  = self._icon_btn(brow, "repeat",  BTN_SM, self._toggle_repeat_one, toggle=True)
        self.prev_lbl = self._icon_btn(brow, "prev",    BTN_SM, self._prev,   flash=True)
        self.play_btn = self._icon_btn(brow, "play",    BTN_LG, self._toggle_play, is_play=True, flash=True)
        self.next_lbl = self._icon_btn(brow, "next",    BTN_SM, self._next,   flash=True)
        self.rst_lbl  = self._icon_btn(brow, "restart", BTN_SM, self._restart, flash=True)

        # keep play_lbl alias for compatibility
        self.play_lbl = self.play_btn

        # volume
        vrow = tk.Frame(left, bg=BG)
        vrow.pack(fill="x", padx=18, pady=(0,6))
        tk.Label(vrow, text="🔈", font=("Segoe UI Emoji",11), fg=MUTED, bg=BG).pack(side="left")
        self.vol_c = tk.Canvas(vrow, height=4, bg=BG, highlightthickness=0, cursor="hand2")
        self.vol_c.pack(side="left", fill="x", expand=True, padx=6)
        tk.Label(vrow, text="🔊", font=("Segoe UI Emoji",11), fg=MUTED, bg=BG).pack(side="right")
        self.vol_c.bind("<ButtonPress-1>", self._vol_click)
        self.vol_c.bind("<B1-Motion>",     self._vol_drag)
        self.vol_c.bind("<Configure>",     lambda e: self._draw_vol())

        # visualizer
        self.viz_c = tk.Canvas(left, height=52, bg=BG, highlightthickness=0)
        self.viz_c.pack(fill="x", padx=16, pady=(0,4))
        self.viz_c.bind("<Configure>", lambda e: self._draw_viz())

        # ── Divider ───────────────────────────────────────────────────────────
        tk.Frame(body, bg=self.T("MUTED"), width=1).pack(side="left", fill="y")

        # ── Right panel ───────────────────────────────────────────────────────
        right = tk.Frame(body, bg=BG)
        right.pack(side="left", fill="both", expand=True)

        # EQ section — STARTS CLOSED
        eq_outer = tk.Frame(right, bg=PANEL)
        eq_outer.pack(fill="x")

        eq_header = tk.Frame(eq_outer, bg=PANEL)
        eq_header.pack(fill="x", padx=12, pady=(8,4))
        tk.Label(eq_header, text="Equalizer", font=("Segoe UI",10,"bold"),
                 fg=TEXT, bg=PANEL).pack(side="left")

        self.eq_toggle_lbl = tk.Label(eq_header, text="▸ Show",
                                       font=("Segoe UI",8), fg=MUTED, bg=PANEL,
                                       cursor="hand2", padx=6)
        self.eq_toggle_lbl.pack(side="right")
        self.eq_toggle_lbl.bind("<Button-1>", lambda e: self._toggle_eq())

        reset_lbl = tk.Label(eq_header, text="Reset", font=("Segoe UI",8),
                              fg=MUTED, bg=PANEL, cursor="hand2", padx=6)
        reset_lbl.pack(side="right")
        reset_lbl.bind("<Button-1>", lambda e: self._eq_reset())

        # collapsible EQ body (hidden by default)
        self.eq_body = tk.Frame(eq_outer, bg=PANEL)
        # not packed — starts hidden

        self.eq_c = tk.Canvas(self.eq_body, height=110, bg=PANEL, highlightthickness=0)
        self.eq_c.pack(fill="x", padx=8, pady=(4,0))
        self.eq_c.bind("<Configure>",       lambda e: self._draw_eq())
        self.eq_c.bind("<ButtonPress-1>",   self._eq_press)
        self.eq_c.bind("<B1-Motion>",       self._eq_motion)
        self.eq_c.bind("<ButtonRelease-1>", lambda e: setattr(self, "_eq_dragging", None))

        # two rows of labels: Hz on top, type on bottom
        label_row = tk.Frame(self.eq_body, bg=PANEL)
        label_row.pack(fill="x", padx=8, pady=(2,0))
        for band in EQ_BANDS:
            tk.Label(label_row, text=band, font=("Segoe UI",7), fg=TEXT2, bg=PANEL,
                     width=4, anchor="center").pack(side="left", expand=True)

        type_row = tk.Frame(self.eq_body, bg=PANEL)
        type_row.pack(fill="x", padx=8, pady=(0,8))
        for lbl in EQ_LABELS:
            tk.Label(type_row, text=lbl, font=("Segoe UI",6), fg=MUTED, bg=PANEL,
                     width=4, anchor="center").pack(side="left", expand=True)

        # Queue
        pl_top = tk.Frame(right, bg=BG)
        pl_top.pack(fill="x", padx=14, pady=(10,6))
        tk.Label(pl_top, text="Queue", font=("Segoe UI",11,"bold"), fg=TEXT, bg=BG).pack(side="left")

        add_l = tk.Label(pl_top, text="+ Add", font=("Segoe UI",9), fg=ACCENT, bg=BG, cursor="hand2")
        add_l.pack(side="right")
        add_l.bind("<Button-1>", lambda e: self._add_files())

        clr_l = tk.Label(pl_top, text="Clear", font=("Segoe UI",9), fg=MUTED, bg=BG, cursor="hand2", padx=8)
        clr_l.pack(side="right")
        clr_l.bind("<Button-1>", lambda e: self._clear())

        lf = tk.Frame(right, bg=BG)
        lf.pack(fill="both", expand=True, padx=6, pady=(0,6))
        sb = tk.Scrollbar(lf, bg=self.T("RAISED"), troughcolor=BG, relief="flat", bd=0, width=5)
        sb.pack(side="right", fill="y")
        self.listbox = tk.Listbox(lf, bg=BG, fg=TEXT2,
                                   selectbackground=self.T("SEL"), selectforeground=TEXT,
                                   font=("Segoe UI",10), relief="flat", bd=0,
                                   activestyle="none", highlightthickness=0,
                                   yscrollcommand=sb.set, cursor="hand2")
        self.listbox.pack(fill="both", expand=True)
        sb.config(command=self.listbox.yview)
        self.listbox.bind("<Double-Button-1>", self._list_dbl)
        self.listbox.bind("<Button-1>",        self._list_click)
        self.listbox.bind("<Delete>",          self._remove_sel)

        # keyboard
        self.bind("<space>", lambda e: self._toggle_play())
        self.bind("<Right>", lambda e: self._seek_rel(5))
        self.bind("<Left>",  lambda e: self._seek_rel(-5))
        self.bind("<Up>",    lambda e: self._vol_rel(0.05))
        self.bind("<Down>",  lambda e: self._vol_rel(-0.05))

    # ── Canvas icon button factory ────────────────────────────────────────────
    def _icon_btn(self, parent, icon, size, cmd, toggle=False, flash=False, is_play=False):
        """Draw a rounded-square button with a custom icon on a Canvas."""
        pad = 6 if not is_play else 4
        c = tk.Canvas(parent, width=size, height=size, bg=self.T("BG"),
                      highlightthickness=0, cursor="hand2")
        c.pack(side="left", padx=pad)
        c._icon   = icon
        c._size   = size
        c._active = False
        c._pressed= False
        c._is_play= is_play
        c._toggle = toggle
        c._flash  = flash

        def redraw(pressed=False, active=False, hover=False):
            c.delete("all")
            ACCENT = self.T("ACCENT")
            TEXT2 = self.T("TEXT2")
            s = size

            # gray by default; accent only when toggled/flashed on; brief dim on press
            if active:
                ico_col = self._dim(ACCENT, 0.7) if pressed else ACCENT
            else:
                ico_col = self._dim(TEXT2, 0.6) if pressed else TEXT2

            # draw icon
            self._draw_icon(c, c._icon, s, ico_col, c._active)

        c._redraw = redraw

        def on_enter(e):
            pass  # gray/accent state no longer changes on hover
        def on_leave(e):
            pass
        def on_press(e):
            c._pressed = True
            redraw(pressed=True, active=c._active)
        def on_release(e):
            c._pressed = False
            redraw(active=c._active)
            if flash and not toggle:
                c._active = True; redraw(active=True)
                key = id(c)
                if key in self._flash_after: self.after_cancel(self._flash_after[key])
                self._flash_after[key] = self.after(250, lambda: (
                    setattr(c, "_active", False), redraw()))
            cmd()

        c.bind("<Enter>",          on_enter)
        c.bind("<Leave>",          on_leave)
        c.bind("<ButtonPress-1>",  on_press)
        c.bind("<ButtonRelease-1>",on_release)

        redraw()
        return c

    def _rrect(self, c, x0, y0, x1, y1, r, fill="", outline="", width=1):
        """Draw a rounded rectangle on canvas c."""
        c.create_arc(x0,y0,x0+2*r,y0+2*r, start=90,  extent=90,  fill=fill, outline=outline, width=width)
        c.create_arc(x1-2*r,y0,x1,y0+2*r, start=0,   extent=90,  fill=fill, outline=outline, width=width)
        c.create_arc(x0,y1-2*r,x0+2*r,y1, start=180, extent=90,  fill=fill, outline=outline, width=width)
        c.create_arc(x1-2*r,y1-2*r,x1,y1, start=270, extent=90,  fill=fill, outline=outline, width=width)
        c.create_rectangle(x0+r,y0,   x1-r,y1,   fill=fill, outline="")
        c.create_rectangle(x0,  y0+r, x1,  y1-r, fill=fill, outline="")
        # fix outline gaps on straight edges
        if outline and width:
            c.create_line(x0+r,y0,   x1-r,y0,   fill=outline, width=width)
            c.create_line(x0+r,y1,   x1-r,y1,   fill=outline, width=width)
            c.create_line(x0,  y0+r, x0,  y1-r, fill=outline, width=width)
            c.create_line(x1,  y0+r, x1,  y1-r, fill=outline, width=width)

    def _dim(self, hex_col, factor):
        """Darken a hex color by factor (0–1)."""
        try:
            h = hex_col.lstrip("#")
            r,g,b = int(h[0:2],16), int(h[2:4],16), int(h[4:6],16)
            return f"#{int(r*factor):02x}{int(g*factor):02x}{int(b*factor):02x}"
        except: return hex_col

    _ICON_GLYPHS = {
        "play":    "▶",
        "pause":   "⏸",
        "prev":    "⏮",
        "next":    "⏭",
        "repeat":  "⟳",
        "restart": "↺",
    }

    def _draw_icon(self, c, icon, s, color, active=False):
        """Draw the icon. Play gets a rounded custom triangle; others use plain text glyphs."""
        if icon == "play":
            import math
            cy = s/2
            # triangle corners (target points before rounding)
            p_top   = (s*0.30, s*0.20)
            p_bot   = (s*0.30, s*0.80)
            p_tip   = (s*0.76, cy)
            r = s*0.09  # corner rounding radius
            pts = []
            corners = [p_top, p_tip, p_bot]
            n = len(corners)
            for i in range(n):
                prev_p = corners[(i-1) % n]
                cur_p  = corners[i]
                next_p = corners[(i+1) % n]
                # unit vector from cur to prev, and cur to next
                v1 = (prev_p[0]-cur_p[0], prev_p[1]-cur_p[1])
                v2 = (next_p[0]-cur_p[0], next_p[1]-cur_p[1])
                l1 = math.hypot(*v1); l2 = math.hypot(*v2)
                u1 = (v1[0]/l1, v1[1]/l1); u2 = (v2[0]/l2, v2[1]/l2)
                # points pulled back from the corner along each edge
                a = (cur_p[0]+u1[0]*r, cur_p[1]+u1[1]*r)
                b = (cur_p[0]+u2[0]*r, cur_p[1]+u2[1]*r)
                pts.extend([a, cur_p, b])
            flat = [coord for pt in pts for coord in pt]
            c.create_polygon(flat, fill=color, outline="", smooth=True, splinesteps=12)
            return

        glyph = self._ICON_GLYPHS.get(icon, "?")
        font_size = int(s * 0.55)
        c.create_text(s/2, s/2, text=glyph, fill=color,
                       font=("Segoe UI Symbol", font_size))

    def _pill(self, c, x0, y0, x1, y1, r, color):
        """Draw a vertical pill (rounded rectangle) on canvas."""
        c.create_rectangle(x0, y0+r, x1, y1-r, fill=color, outline="")
        c.create_oval(x0, y0, x1, y0+2*r, fill=color, outline="")
        c.create_oval(x0, y1-2*r, x1, y1, fill=color, outline="")

    def _update_play_btn(self):
        """Redraw play button as play or pause depending on state."""
        c = self.play_btn
        c._icon = "pause" if self.playing else "play"
        c._redraw(active=False)

    # ── Theme window ──────────────────────────────────────────────────────────
    def _open_theme(self):
        # prevent opening multiple theme windows
        if self._theme_win is not None:
            try:
                self._theme_win.lift()
                self._theme_win.focus_force()
                return
            except tk.TclError:
                self._theme_win = None

        win = tk.Toplevel(self)
        self._theme_win = win
        win.title("Theme")
        win.geometry("360x520")
        win.configure(bg=self.T("BG"))
        win.resizable(False, True)
        win.protocol("WM_DELETE_WINDOW", lambda: (win.destroy(), setattr(self, "_theme_win", None)))

        tk.Label(win, text="Theme Settings", font=("Segoe UI",12,"bold"),
                 fg=self.T("TEXT"), bg=self.T("BG")).pack(pady=(14,10))

        items = [
            ("Background",    "BG"),
            ("Panel",         "PANEL"),
            ("Accent / Glow", "ACCENT"),
            ("Primary Text",  "TEXT"),
            ("Secondary Text","TEXT2"),
            ("Muted / Icons", "MUTED"),
        ]

        swatches = {}
        for label, key in items:
            row = tk.Frame(win, bg=self.T("BG"))
            row.pack(fill="x", padx=24, pady=4)
            tk.Label(row, text=label, font=("Segoe UI",10), fg=self.T("TEXT2"),
                     bg=self.T("BG"), width=16, anchor="w").pack(side="left")

            # swatch with a contrasting highlight border so BG swatch is always visible
            swatch_frame = tk.Frame(row, bg=self.T("TEXT2"), padx=1, pady=1)
            swatch_frame.pack(side="left", padx=6)
            swatch = tk.Label(swatch_frame, bg=self.theme[key], width=5, height=1,
                               cursor="hand2", relief="flat", bd=0)
            swatch.pack()
            swatches[key] = swatch

            def pick(k=key, s=swatch):
                color = colorchooser.askcolor(color=self.theme[k],
                                               title=f"Pick {k}", parent=win)
                if color and color[1]:
                    self.theme[k] = color[1]
                    s.configure(bg=color[1])

            swatch.bind("<Button-1>", lambda e, pick=pick: pick())
            swatch_frame.bind("<Button-1>", lambda e, pick=pick: pick())

        # ── Presets (scrollable) ──────────────────────────────────────────────
        tk.Label(win, text="Presets", font=("Segoe UI",9,"bold"),
                 fg=self.T("TEXT2"), bg=self.T("BG")).pack(pady=(14,4))

        presets = {
            "Dark (Default)":  DEFAULT_THEME,
            "Midnight Blue":   {**DEFAULT_THEME,"BG":"#050d1a","PANEL":"#0a1628","RAISED":"#142040","ACCENT":"#4a9eff","SEL":"#142040"},
            "Warm Sunset":     {**DEFAULT_THEME,"BG":"#1a0f08","PANEL":"#251510","RAISED":"#3a2018","ACCENT":"#ff6b35","TEXT2":"#d4a88a","SEL":"#3a2018"},
            "Purple Night":    {**DEFAULT_THEME,"BG":"#0d0a14","PANEL":"#16102a","RAISED":"#241840","ACCENT":"#a855f7","SEL":"#241840"},
            "Ocean Depth":     {**DEFAULT_THEME,"BG":"#020f18","PANEL":"#041e30","RAISED":"#073552","ACCENT":"#00d4aa","TEXT2":"#7ecfb8","SEL":"#073552"},
            "Rose Gold":       {**DEFAULT_THEME,"BG":"#1a1015","PANEL":"#26181f","RAISED":"#3d2530","ACCENT":"#e8a0b0","TEXT":"#fff0f3","TEXT2":"#c9909a","SEL":"#3d2530"},
            "Forest Green":    {**DEFAULT_THEME,"BG":"#0a120a","PANEL":"#121e12","RAISED":"#1e341e","ACCENT":"#5dba60","TEXT2":"#8fc990","SEL":"#1e341e"},
            "Neon Cyberpunk":  {**DEFAULT_THEME,"BG":"#05000f","PANEL":"#0d001f","RAISED":"#1a0035","ACCENT":"#ff00ff","TEXT2":"#cc88ff","MUTED":"#5a005a","SEL":"#1a0035"},
            "Caramel":         {**DEFAULT_THEME,"BG":"#18110a","PANEL":"#241808","RAISED":"#3d2c12","ACCENT":"#d4873a","TEXT2":"#c4a06a","SEL":"#3d2c12"},
            "Ice White":       {**DEFAULT_THEME,"BG":"#e8eef4","PANEL":"#d8e2ea","RAISED":"#c0cdd8","ACCENT":"#1a6fcf","TEXT":"#0a1520","TEXT2":"#3a5070","MUTED":"#8099b0","SEL":"#c0cdd8"},
        }

        # scrollable canvas for preset buttons
        p_canvas = tk.Canvas(win, bg=self.T("BG"), highlightthickness=0, height=160)
        p_canvas.pack(fill="x", padx=16, pady=(0,4))
        p_scroll = tk.Scrollbar(win, orient="vertical", command=p_canvas.yview,
                                 troughcolor=self.T("BG"), bg=self.T("RAISED"),
                                 relief="flat", bd=0, width=4)
        # no scrollbar visible unless needed — attach it
        p_canvas.configure(yscrollcommand=p_scroll.set)

        p_inner = tk.Frame(p_canvas, bg=self.T("BG"))
        p_canvas.create_window((0,0), window=p_inner, anchor="nw")

        # 2-column grid of preset buttons
        preset_list = list(presets.items())
        for i, (name, preset) in enumerate(preset_list):
            col = i % 2; row_i = i // 2
            # accent dot preview
            acc = preset.get("ACCENT","#888888")
            cell = tk.Frame(p_inner, bg=self.T("RAISED"), cursor="hand2")
            cell.grid(row=row_i, column=col, padx=4, pady=3, sticky="ew", ipadx=6, ipady=5)
            p_inner.columnconfigure(col, weight=1)

            dot = tk.Label(cell, bg=acc, width=2, height=1, relief="flat", bd=0)
            dot.pack(side="left", padx=(6,4))
            lbl = tk.Label(cell, text=name, font=("Segoe UI",8),
                           fg=self.T("TEXT2"), bg=self.T("RAISED"), cursor="hand2")
            lbl.pack(side="left")

            def apply_preset(p=preset, sw=swatches, d=dot, l=lbl):
                self.theme.update(p)
                for k2, s2 in sw.items():
                    s2.configure(bg=self.theme[k2])

            for widget in (cell, dot, lbl):
                widget.bind("<Button-1>", lambda e, fn=apply_preset: fn())

        p_inner.update_idletasks()
        p_canvas.configure(scrollregion=p_canvas.bbox("all"))
        p_canvas.bind("<MouseWheel>", lambda e: p_canvas.yview_scroll(-1*(e.delta//120), "units"))

        def apply_and_close():
            self._theme_win = None
            win.destroy()
            self._rebuild_ui()

        apply_btn = tk.Label(win, text="Apply & Restart UI", font=("Segoe UI",10,"bold"),
                              fg="#ffffff", bg=self.T("ACCENT"),
                              cursor="hand2", padx=16, pady=7, relief="flat", bd=0)
        apply_btn.pack(pady=12, ipadx=4)
        apply_btn.bind("<Button-1>", lambda e: apply_and_close())

    def _rebuild_ui(self):
        """Destroy all widgets and rebuild with new theme."""
        self._theme_win = None
        for w in self.winfo_children():
            w.destroy()
        self.configure(bg=self.T("BG"))
        self._build_ui()
        self._make_default_art()
        self._set_art(None)
        self._refresh_list()
        self._draw_vol()
        self._draw_seek()
        self._update_play_btn()

    # ── Draw helpers ──────────────────────────────────────────────────────────
    def _draw_seek(self):
        c = self.seek_c; w = c.winfo_width()
        if w < 2: return
        c.delete("all")
        c.configure(bg=self.T("BG"))
        c.create_rectangle(0,1,w,5, fill=self.T("RAISED"), outline="")
        fw = int(w * self._seek_pct)
        if fw > 0: c.create_rectangle(0,1,fw,5, fill=self.T("ACCENT"), outline="")
        c.create_oval(fw-6,0,fw+6,6, fill=self.T("TEXT"), outline="")

    def _draw_vol(self):
        c = self.vol_c; w = c.winfo_width()
        if w < 2: return
        c.delete("all")
        c.configure(bg=self.T("BG"))
        c.create_rectangle(0,0,w,4, fill=self.T("RAISED"), outline="")
        fw = int(w * self.volume)
        if fw > 0: c.create_rectangle(0,0,fw,4, fill=self.T("TEXT2"), outline="")

    def _sk_click(self, e):  self.seeking = True; self._sk_to(e.x)
    def _sk_drag(self, e):   self._sk_to(e.x)
    def _sk_release(self, e):
        self._sk_to(e.x)
        if self.duration > 0:
            t = self._seek_pct * self.duration
            try: pygame.mixer.music.set_pos(t); self._seek_offset = t
            except: pass
        self.seeking = False

    def _sk_to(self, x):
        w = self.seek_c.winfo_width()
        self._seek_pct = max(0.0, min(1.0, x/w))
        if self.duration > 0: self.cur_var.set(fmt(self._seek_pct * self.duration))
        self._draw_seek()

    def _vol_click(self, e): self._vol_to(e.x)
    def _vol_drag(self, e):  self._vol_to(e.x)
    def _vol_to(self, x):
        w = self.vol_c.winfo_width()
        self.volume = max(0.0, min(1.0, x/w))
        pygame.mixer.music.set_volume(self.volume)
        self._draw_vol()

    # ── Equalizer ─────────────────────────────────────────────────────────────
    def _toggle_eq(self):
        self._eq_visible = not self._eq_visible
        if self._eq_visible:
            self.eq_body.pack(fill="x")
            self.eq_toggle_lbl.configure(text="▾ Hide")
        else:
            self.eq_body.pack_forget()
            self.eq_toggle_lbl.configure(text="▸ Show")

    def _draw_eq(self):
        c = self.eq_c
        PANEL=self.T("PANEL"); RAISED=self.T("RAISED"); ACCENT=self.T("ACCENT")
        MUTED=self.T("MUTED"); TEXT=self.T("TEXT")
        w = c.winfo_width(); h = c.winfo_height()
        if w < 2: return
        c.delete("all")
        c.configure(bg=PANEL)
        slot_w = w / N_BANDS
        mid = h / 2

        c.create_line(0, mid, w, mid, fill=RAISED, width=1)
        c.create_line(0, h*0.25, w, h*0.25, fill=RAISED, dash=(2,4), width=1)
        c.create_line(0, h*0.75, w, h*0.75, fill=RAISED, dash=(2,4), width=1)
        c.create_text(4, h*0.05, text="+24", font=("Segoe UI",6), fill=MUTED, anchor="nw")
        c.create_text(4, h*0.72, text="−24", font=("Segoe UI",6), fill=MUTED, anchor="nw")

        for i, val in enumerate(self._eq_vals):
            cx = slot_w * i + slot_w / 2
            bar_h = (val - 0.5) * (h - 20)
            y_top = mid - bar_h; y_bot = mid
            if bar_h >= 0: color = ACCENT
            else: y_top = mid; y_bot = mid - bar_h; color = "#e05c5c"
            bw = max(6, slot_w * 0.35)
            c.create_rectangle(cx-bw/2, y_top+1, cx+bw/2, y_bot-1, fill=color, outline="")
            c.create_rectangle(cx-bw/2-2, mid-bar_h-5, cx+bw/2+2, mid-bar_h+5,
                                fill=TEXT, outline="", tags=f"thumb{i}")

    def _eq_press(self, e):
        slot_w = self.eq_c.winfo_width() / N_BANDS
        idx = int(e.x / slot_w)
        if 0 <= idx < N_BANDS:
            self._eq_dragging = idx
            self._eq_set(idx, e.y)

    def _eq_motion(self, e):
        if self._eq_dragging is not None:
            self._eq_set(self._eq_dragging, e.y)

    def _eq_set(self, idx, y):
        h = self.eq_c.winfo_height()
        self._eq_vals[idx] = max(0.0, min(1.0, 1.0 - (y / h)))
        self._draw_eq()
        self._apply_eq()

    def _apply_eq(self):
        if not self.playing: return
        total_db = sum((v - 0.5) * 48 for v in self._eq_vals)
        avg_db   = total_db / N_BANDS
        gain     = 10 ** (avg_db / 20.0)
        pygame.mixer.music.set_volume(max(0.0, min(1.0, self.volume * gain)))

    def _eq_reset(self):
        self._eq_vals = [0.5] * N_BANDS
        self._draw_eq()
        pygame.mixer.music.set_volume(self.volume)

    # ── Visualizer ────────────────────────────────────────────────────────────
    def _viz_loop(self):
        if self.playing:
            for i in range(VIZ_BARS): self._viz_t[i] = random.uniform(0.1, 0.95)
        else:
            for i in range(VIZ_BARS): self._viz_t[i] = 0.0
        for i in range(VIZ_BARS):
            self._viz_h[i] += (self._viz_t[i] - self._viz_h[i]) * 0.3
        self._draw_viz()
        self.after(55, self._viz_loop)

    def _draw_viz(self):
        try: c = self.viz_c
        except: return
        w = c.winfo_width(); h = c.winfo_height()
        if w < 2: return
        c.delete("all")
        c.configure(bg=self.T("BG"))
        bw = w / VIZ_BARS; gap = max(1, bw * 0.2)
        # parse accent color for gradient
        try:
            acc = self.T("ACCENT").lstrip("#")
            ar,ag,ab = int(acc[0:2],16), int(acc[2:4],16), int(acc[4:6],16)
        except: ar,ag,ab = 29,185,84
        for i, ht in enumerate(self._viz_h):
            x0 = i*bw+gap; x1 = (i+1)*bw-gap
            bh = max(2, ht*(h-4))
            # shade from dim to accent
            r = int(ar * ht); g = int(ag * ht); b = int(ab * ht)
            col = f"#{max(0,min(255,r)):02x}{max(0,min(255,g)):02x}{max(0,min(255,b)):02x}"
            c.create_rectangle(x0, h-bh, x1, h, fill=col, outline="")

    # ── Playlist ──────────────────────────────────────────────────────────────
    def _add_files(self):
        files = filedialog.askopenfilenames(
            title="Add files",
            filetypes=[("Audio","*.mp3 *.wav *.ogg *.flac *.aac *.m4a *.wma *.opus"),
                       ("All files","*.*")])
        for f in files:
            idx = len(self.playlist)
            self.playlist.append(f)
            self._show_icon[idx] = True
            self.listbox.insert("end", "  ♪  " + clean(f))
        if self.current_index == -1 and self.playlist:
            self._load(0)

    def _clear(self):
        pygame.mixer.music.stop()
        self.playing = False; self.current_index = -1
        self.playlist.clear(); self._show_icon.clear()
        self.listbox.delete(0,"end")
        self.title_var.set("Nothing playing"); self.artist_var.set("—")
        self._set_art(None); self.playing = False; self._update_play_btn()

    def _list_click(self, e):
        # single click just selects, icons are static
        pass

    def _list_dbl(self, e):
        sel = self.listbox.curselection()
        if sel: self._load(sel[0])

    def _refresh_list(self):
        try:
            sel = self.listbox.curselection()
            self.listbox.delete(0,"end")
            for i, path in enumerate(self.playlist):
                icon = "♪  "
                prefix = "▶  " + icon if i == self.current_index else "    " + icon
                self.listbox.insert("end", prefix + clean(path))
            if sel: self.listbox.selection_set(sel[0])
        except: pass

    def _highlight(self, idx):
        self._refresh_list()
        try:
            self.listbox.selection_set(idx)
            self.listbox.see(idx)
        except: pass

    def _remove_sel(self, e=None):
        sel = self.listbox.curselection()
        if not sel: return
        idx = sel[0]
        self.listbox.delete(idx)
        self.playlist.pop(idx)
        self._show_icon = {(k if k < idx else k-1): v
                           for k,v in self._show_icon.items() if k != idx}
        if idx == self.current_index:
            pygame.mixer.music.stop(); self.playing = False
            self.current_index = -1; self._update_play_btn()
        elif idx < self.current_index:
            self.current_index -= 1

    # ── Playback ──────────────────────────────────────────────────────────────
    def _load(self, idx):
        if idx < 0 or idx >= len(self.playlist): return
        self.current_index = idx
        path = self.playlist[idx]
        try:
            pygame.mixer.music.load(path)
            pygame.mixer.music.set_volume(self.volume)
            pygame.mixer.music.play()
            self._seek_offset = 0.0
            self.playing = True
            self._update_play_btn()
            self.duration = get_duration(path)
            self.dur_var.set(fmt(self.duration))
            self.title_var.set(clean(path))
            try:
                f = MutagenFile(path, easy=True)
                artist = f.get("artist",["—"])[0] if f else "—"
            except: artist = "—"
            self.artist_var.set(artist)
            threading.Thread(target=self._set_art, args=(path,), daemon=True).start()
            self._apply_eq()
        except Exception as ex:
            self.title_var.set(f"Error: {ex}")
        self._highlight(idx)

    def _toggle_play(self):
        if not self.playlist: self._add_files(); return
        if self.current_index == -1: self._load(0); return
        if self.playing:
            pygame.mixer.music.pause(); self.playing = False
        else:
            pygame.mixer.music.unpause(); self.playing = True
        self._update_play_btn()
        self._refresh_list()

    def _prev(self):
        if not self.playlist: return
        idx = self.current_index - 1
        if idx < 0: idx = len(self.playlist) - 1
        self._load(idx)

    def _restart(self):
        if self.current_index >= 0:
            pygame.mixer.music.play()
            self._seek_offset = 0.0

    def _next(self):
        if not self.playlist: return
        if self.repeat == "one": self._load(self.current_index); return
        idx = random.randint(0, len(self.playlist)-1) if self.shuffle else self.current_index+1
        if idx >= len(self.playlist):
            if self.repeat == "all": idx = 0
            else:
                self.playing = False
                self._update_play_btn()
                self._refresh_list(); return
        self._load(idx)

    def _toggle_repeat_one(self):
        self.repeat = "none" if self.repeat == "one" else "one"
        active = (self.repeat == "one")
        self.rep_lbl._active = active
        self.rep_lbl._redraw(active=active)

    def _seek_rel(self, s):
        if self.duration > 0:
            pos_ms = pygame.mixer.music.get_pos()
            cur = self._seek_offset + (pos_ms/1000 if pos_ms >= 0 else 0)
            new = max(0, min(self.duration, cur+s))
            try: pygame.mixer.music.set_pos(new); self._seek_offset = new
            except: pass

    def _vol_rel(self, d):
        self.volume = max(0.0, min(1.0, self.volume+d))
        pygame.mixer.music.set_volume(self.volume)
        self._draw_vol()

    # ── Poll ──────────────────────────────────────────────────────────────────
    def _poll(self):
        if self.playing and not self.seeking:
            pos_ms = pygame.mixer.music.get_pos()
            if pos_ms >= 0:
                pos = self._seek_offset + pos_ms/1000
                self.cur_var.set(fmt(pos))
                if self.duration > 0:
                    self._seek_pct = min(1.0, pos/self.duration)
                    self._draw_seek()
            if not pygame.mixer.music.get_busy() and self.playing:
                self._next()
        self.after(250, self._poll)

if __name__ == "__main__":
    app = Player()
    app.mainloop()
