"""Bayan GlassCaption - control window (WINDOWS build).

Run via the gold "Bayan GlassCaption" shortcut (pythonw.exe, silent).
One window: pick your direction and mic, hit Start, speak. Translated
captions float over whatever you're presenting - English speech -> Arabic
captions, or Arabic speech -> English captions.

Feature-parity port of the Mac main.py: same model-load guards, same
direction-toggle behaviour (single-flight, revert on failed download,
auto-resume mid-run), same test-caption toggle, same status texts.
"""

import ctypes
import os
import queue
import threading
import tkinter as tk
from tkinter import ttk

import config
import overlay as overlay_mod
from pipeline import CaptionPipeline

TEST_AR = "هذا مثال للترجمة الفورية على الشاشة"
TEST_EN = "This is a sample on-screen caption"

# Brand palette (same as the website)
GOLD = "#C9A227"
GOLD_DEEP = "#9E7B18"
GOLD_PALE = "#EEE6CF"
PAPER = "#FCFAF4"
INK = "#15110A"
INK_SOFT = "#4A4232"


def _enable_dpi_awareness():
    """Without this, Tk is blurry and reports wrong pixel sizes on scaled
    displays (i.e. nearly every projector setup)."""
    try:
        ctypes.windll.shcore.SetProcessDpiAwareness(2)
    except Exception:
        try:
            ctypes.windll.user32.SetProcessDPIAware()
        except Exception:
            pass
    # Own taskbar identity: without this the running app groups under
    # generic "Python", separate from the pinned gold shortcut.
    try:
        ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(
            "AVLabsPro.BayanGlassCaption")
    except Exception:
        pass


class App:

    def __init__(self, root):
        self.root = root
        self.cfg = config.load()
        self.running = False
        self.model_ready = False
        self.model_loading = True   # guards the direction toggle
        self.test_showing = False
        self._ui_queue = queue.Queue()

        self._build_window()
        self.overlay = overlay_mod.make_overlay(self.cfg, root)
        self.pipeline = CaptionPipeline(self.on_caption, self.cfg)

        root.after(50, self._drain_ui_queue)
        threading.Thread(target=self._load_model, daemon=True).start()

    # ---------------- UI construction ----------------

    def _build_window(self):
        r = self.root
        r.title("Bayan GlassCaption")
        r.resizable(False, False)
        r.configure(bg=PAPER)
        try:
            icon = os.path.join(config.APP_DIR, "branding", "icon.ico")
            if os.path.exists(icon):
                r.iconbitmap(icon)
        except Exception:
            pass

        # Brand styling: paper background, gold accents (matches the site).
        # Pure cosmetics on top of the native theme - nothing behavioral.
        style = ttk.Style(r)
        try:
            style.configure(".", background=PAPER, foreground=INK)
            style.configure("TFrame", background=PAPER)
            style.configure("TLabel", background=PAPER, foreground=INK)
            style.configure("TRadiobutton", background=PAPER,
                            foreground=INK)
            style.map("TRadiobutton", background=[("active", PAPER)])
            style.configure("Brand.TLabel", background=PAPER,
                            foreground=GOLD_DEEP,
                            font=("Segoe UI Semibold", 14))
            style.configure("Status.TLabel", background=PAPER,
                            foreground=INK_SOFT, font=("Segoe UI", 9))
        except Exception:
            pass

        pad = {"padx": 12, "pady": 6}
        body = ttk.Frame(r, padding=14)
        body.grid(sticky="nsew")

        head = ttk.Frame(body)
        head.grid(row=0, column=0, columnspan=3, sticky="w", pady=(0, 12))
        tk.Label(head, text="ب", font=("Segoe UI", 20, "bold"),
                 fg=GOLD, bg=PAPER).pack(side="left", padx=(2, 9))
        ttk.Label(head, text="Bayan GlassCaption — Live Captions",
                  style="Brand.TLabel").pack(side="left")

        # Direction - Latin-only labels (mixed Arabic/Latin labels get
        # visually reordered by bidi rules; bug #1 on the Mac build).
        ttk.Label(body, text="Direction:").grid(row=1, column=0, sticky="w",
                                                **pad)
        self.dir_var = tk.StringVar(value=self.cfg.get("direction", "en2ar"))
        self.dir_ar2en = ttk.Radiobutton(
            body, text="Arabic → English", value="ar2en",
            variable=self.dir_var, command=self.direction_changed,
            state="disabled")  # until the first model finishes loading
        self.dir_en2ar = ttk.Radiobutton(
            body, text="English → Arabic", value="en2ar",
            variable=self.dir_var, command=self.direction_changed,
            state="disabled")
        self.dir_ar2en.grid(row=1, column=1, sticky="w", **pad)
        self.dir_en2ar.grid(row=1, column=2, sticky="w", **pad)

        ttk.Label(body, text="Microphone:").grid(row=2, column=0, sticky="w",
                                                 **pad)
        self.mic_combo = ttk.Combobox(body, state="readonly", width=38)
        self._fill_mics()
        self.mic_combo.grid(row=2, column=1, columnspan=2, sticky="we", **pad)

        ttk.Label(body, text="Caption size:").grid(row=3, column=0,
                                                   sticky="w", **pad)
        self.slider = ttk.Scale(body, from_=24, to=80, orient="horizontal",
                                command=self.size_changed)
        self.slider.set(self.cfg["font_size"])
        self.slider.grid(row=3, column=1, columnspan=2, sticky="we", **pad)

        btns = ttk.Frame(body)
        btns.grid(row=4, column=0, columnspan=3, sticky="w", pady=(10, 4))
        # Gold primary button (plain tk: ttk buttons ignore bg on Windows)
        self.start_btn = tk.Button(btns, text="Loading model…",
                                   command=self.start_stop, state="disabled",
                                   width=18, bg=GOLD, fg="#FFFFFF",
                                   activebackground=GOLD_DEEP,
                                   activeforeground="#FFFFFF",
                                   disabledforeground=GOLD_PALE,
                                   relief="flat", bd=0, cursor="hand2",
                                   font=("Segoe UI Semibold", 10),
                                   padx=8, pady=5)
        self.start_btn.grid(row=0, column=0, padx=(0, 10))
        self.test_btn = ttk.Button(btns, text="Show test caption",
                                   command=self.test_caption, width=20)
        self.test_btn.grid(row=0, column=1)

        self.status_var = tk.StringVar(
            value="Preparing speech model (first run downloads it)…")
        ttk.Label(body, textvariable=self.status_var, wraplength=430,
                  style="Status.TLabel").grid(
            row=5, column=0, columnspan=3, sticky="w", pady=(8, 0))

        r.attributes("-topmost", True)  # never lost behind other windows;
        # (fullscreen slideshows still cover it, so it won't distract)
        r.protocol("WM_DELETE_WINDOW", self._on_close)

    def _fill_mics(self):
        """List input devices. Windows exposes every mic 3-4x (MME /
        DirectSound / WASAPI / WDM-KS); keep only the default input's host
        API so the list reads like the Mac one."""
        self.mic_devices = [None]  # index 0 = system default
        names = ["System default"]
        try:
            import sounddevice as sd
            try:
                default_in = sd.query_devices(kind="input")
                host = default_in["hostapi"]
            except Exception:
                host = None
            for i, d in enumerate(sd.query_devices()):
                if d.get("max_input_channels", 0) <= 0:
                    continue
                if host is not None and d.get("hostapi") != host:
                    continue
                names.append(d["name"])
                self.mic_devices.append(i)
        except Exception:
            pass
        self.mic_combo["values"] = names
        self.mic_combo.current(0)

    # ---------------- main-thread hop ----------------

    def _main(self, fn):
        """Run fn on the Tk thread (pipeline callbacks arrive on workers)."""
        self._ui_queue.put(fn)

    def _drain_ui_queue(self):
        try:
            while True:
                fn = self._ui_queue.get_nowait()
                try:
                    fn()
                except Exception:
                    pass
        except queue.Empty:
            pass
        self.root.after(50, self._drain_ui_queue)

    def _status(self, text):
        self._main(lambda: self.status_var.set(text))

    # ---------------- background model load ----------------

    def _other_direction(self):
        return ("ar2en" if self.cfg.get("direction", "en2ar") == "en2ar"
                else "en2ar")

    def _loading_text(self, direction):
        lang = "English" if direction == "en2ar" else "Arabic"
        if self.pipeline.model_cached(direction):
            return "Loading %s speech model…" % lang
        return ("Downloading %s speech model (one time, can take several "
                "minutes)…" % lang)

    def _load_model(self):
        try:
            self._status(self._loading_text(self.cfg.get("direction",
                                                         "en2ar")))
            self.pipeline.load_model()
            self.model_ready = True
            self.model_loading = False
            self._status("Ready. Press Start, then just speak.")
            self._main(self._enable_start)
        except Exception as e:
            self.model_loading = False
            self._status("Model failed to load: %s" % e)
            return
        # Quietly fetch the OTHER direction's model so flipping is instant.
        other = self._other_direction()
        try:
            if not self.pipeline.model_cached(other):
                self._status("Ready. Press Start, then just speak. (Also "
                             "fetching the other direction's model in the "
                             "background - flipping gets instant once done.)")
            self.pipeline.preload(other)
            if not self.running:
                self._status("Ready. Press Start, then just speak. "
                             "Both directions loaded.")
        except Exception:
            if not self.running:
                self._status("Ready. (Other direction's model didn't "
                             "download - flipping will retry.)")

    def _enable_start(self):
        self.start_btn.configure(text="Start captions", state="normal")
        self._set_direction_enabled(True)

    def _set_direction_enabled(self, on):
        state = "normal" if on else "disabled"
        self.dir_ar2en.configure(state=state)
        self.dir_en2ar.configure(state=state)

    # ---------------- actions ----------------

    def start_stop(self):
        if not self.running:
            # Update the button IMMEDIATELY so the label never lags a press
            # behind, then bring the mic up in the background.
            self.start_btn.configure(text="Starting…", state="disabled")
            self._reset_test_button()
            self.overlay.show_text("", "")  # clear any lingering test caption
            device = self.mic_devices[self.mic_combo.current()]

            def go():
                try:
                    self.pipeline.start(device=device)
                except Exception as e:
                    self._status("Mic error: %s" % e)
                    self._main(self._show_stopped)
                    return
                self.running = True
                self._main(self._show_running)
                self._status("Listening… captions appear when you pause "
                             "between phrases.")

            threading.Thread(target=go, daemon=True).start()
        else:
            self.pipeline.stop()
            self.overlay.show_text("", "")
            self.overlay.hide()
            self.running = False
            self._reset_test_button()
            self._show_stopped()
            config.save(self.cfg)

    def _reset_test_button(self):
        self.test_showing = False
        self.test_btn.configure(text="Show test caption")

    def _show_running(self):
        self.overlay.show()
        self.start_btn.configure(text="Stop captions", state="normal")

    def _show_stopped(self):
        self.start_btn.configure(text="Start captions", state="normal")

    def direction_changed(self):
        d = self.dir_var.get()
        prev = self.cfg.get("direction", "en2ar")
        if d == prev:
            return
        if self.model_loading:
            # A load is already in flight; snap the control back.
            self.dir_var.set(prev)
            self._status("Hold on - still loading a model…")
            return
        self.model_loading = True
        self.cfg["direction"] = d
        config.save(self.cfg)

        was_running = self.running
        if was_running:
            self.pipeline.stop()
            self.overlay.show_text("", "")
            self.running = False

        # Swap to the right model for this direction (instant if cached,
        # a download on the very first use), then resume.
        self.start_btn.configure(text="Loading model…", state="disabled")
        self._set_direction_enabled(False)
        self._status(self._loading_text(d))

        def go():
            try:
                self.pipeline.ensure_model(d)
            except Exception as e:
                # Revert to the direction whose model is still loaded, so a
                # failed download (e.g. no internet) can't strand the app
                # with a model/language mismatch.
                self.cfg["direction"] = prev
                config.save(self.cfg)
                self.model_loading = False

                def revert():
                    self.dir_var.set(prev)
                    self._enable_start()
                self._main(revert)
                self._status("Couldn't load that model (%s) - staying on the "
                             "current direction. Check internet and retry."
                             % e)
                return

            self.model_loading = False

            def done():
                self._enable_start()
                self._status("Ready. Press Start, then just speak.")
                if was_running:
                    self.start_stop()  # auto-resume in the new direction
            self._main(done)

        threading.Thread(target=go, daemon=True).start()

    def test_caption(self):
        if not self.test_showing:
            self.test_showing = True
            self.test_btn.configure(text="Hide test caption")
            self.overlay.show()
            if self.cfg.get("direction", "en2ar") == "en2ar":
                self.overlay.show_text(TEST_AR, TEST_EN)
            else:
                self.overlay.show_text(TEST_EN, TEST_AR)

            def report():
                self.status_var.set(
                    "Test caption shown (font: %s). Drag the size slider "
                    "to adjust." % (self.overlay.font_name or "?"))
            self.root.after(120, report)  # after the overlay's first draw
        else:
            self.test_showing = False
            self.test_btn.configure(text="Show test caption")
            self.overlay.show_text("", "")
            if not self.running:
                self.overlay.hide()
                self.status_var.set("Test caption hidden.")
            else:
                self.status_var.set("Back to live captions.")

    def size_changed(self, _value):
        self.overlay.set_font_size(float(self.slider.get()))

    # ---------------- pipeline callback ----------------

    def on_caption(self, caption, source, status):
        if caption is not None:
            self.overlay.show_text(caption, source)
        self._status(status if caption is None or not caption
                     else "%s  |  heard: %s" % (status, source[:60]))

    # ---------------- shutdown ----------------

    def _on_close(self):
        if self.running:
            try:
                self.pipeline.stop()
            except Exception:
                pass
        config.save(self.cfg)
        self.root.destroy()


def main():
    _enable_dpi_awareness()
    root = tk.Tk()
    App(root)
    root.mainloop()


if __name__ == "__main__":
    main()
