"""Transparent, click-through caption strip for WINDOWS - floats above
everything, including full-screen PowerPoint / Canva slideshows.

How it works (Win32 replacement for the Mac NSPanel):
  * A borderless Tk Toplevel painted in a single "chroma key" colour;
    Windows' layered-window colour-key makes those pixels fully transparent.
  * ctypes adds WS_EX_TRANSPARENT (clicks pass through), WS_EX_NOACTIVATE
    (never steals focus) and WS_EX_TOOLWINDOW (no taskbar entry), and the
    window re-asserts HWND_TOPMOST every 2 s in case a slideshow grabs it.
  * Text is drawn in TWO PASSES like the Mac build: a thick white outline
    (the same glyphs stamped in a ring of offsets), then solid gold on top.
  * Tk on Windows does not shape or reorder Arabic, so lines are wrapped
    first, then run through arabic-reshaper + python-bidi per line. Tahoma /
    Arial carry the presentation-form glyphs this produces.
"""

import ctypes
import tkinter as tk
import tkinter.font as tkfont

# Any colour that will never appear in the caption itself.
_CHROMA = "#010101"
_SHADOW = "#2A2A2A"

# --- Win32 constants -------------------------------------------------------
_GWL_EXSTYLE = -20
_WS_EX_LAYERED = 0x00080000
_WS_EX_TRANSPARENT = 0x00000020
_WS_EX_NOACTIVATE = 0x08000000
_WS_EX_TOOLWINDOW = 0x00000080
_HWND_TOPMOST = -1
_SWP_NOMOVE = 0x0002
_SWP_NOSIZE = 0x0001
_SWP_NOACTIVATE = 0x0010
_GA_ROOT = 2


def _is_rtl_text(s):
    """True if the first strong-direction character is Arabic/Hebrew."""
    for ch in s or "":
        o = ord(ch)
        if 0x0590 <= o <= 0x08FF or 0xFB1D <= o <= 0xFEFC:
            return True
        if 65 <= o <= 90 or 97 <= o <= 122:
            return False
    return True


def _has_arabic(s):
    return any(0x0600 <= ord(ch) <= 0x08FF or 0xFB50 <= ord(ch) <= 0xFEFC
               for ch in s or "")


def _shape_line(line):
    """Shape + visually reorder ONE already-wrapped line for Tk rendering.
    Falls back to the raw string if the helper libs are missing."""
    if not _has_arabic(line):
        return line
    try:
        import arabic_reshaper
        from bidi.algorithm import get_display
        return get_display(arabic_reshaper.reshape(line))
    except Exception:
        return line


def _pick_font_name(candidates):
    try:
        installed = {f.lower() for f in tkfont.families()}
    except Exception:
        return candidates[0] if candidates else "Arial"
    for name in candidates:
        if name.lower() in installed:
            return name
    return "Arial"


class CaptionOverlay:
    """Create on the MAIN (Tk) thread, passing the app's root window.
    show_text()/set_font_size() may be called from any thread - they hop to
    the Tk thread via `root.after`, which is the one Tk entry point that is
    safe cross-thread in CPython's tkinter."""

    def __init__(self, cfg, root):
        self.cfg = cfg
        self.root = root
        self.font_name = None
        self._primary = ""
        self._secondary = ""
        self._build()

    # ---------------- construction ----------------

    def _geometry(self):
        sw = self.root.winfo_screenwidth()
        sh = self.root.winfo_screenheight()
        width = int(sw * self.cfg["width_pct"])
        lines = self.cfg["max_lines"] + (1 if self.cfg["show_english"] else 0)
        height = int(self.cfg["font_size"] * 1.7 * (lines + 0.3))
        x = (sw - width) // 2
        y = sh - self.cfg["bottom_margin"] - height
        return width, height, x, y

    def _build(self):
        top = tk.Toplevel(self.root)
        top.overrideredirect(True)
        top.attributes("-topmost", True)
        top.configure(bg=_CHROMA)
        try:
            top.attributes("-transparentcolor", _CHROMA)
        except tk.TclError:
            pass  # non-Windows fallback: opaque near-black strip

        w, h, x, y = self._geometry()
        top.geometry("%dx%d+%d+%d" % (w, h, x, y))

        canvas = tk.Canvas(top, bg=_CHROMA, highlightthickness=0, bd=0)
        canvas.pack(fill="both", expand=True)

        self.win = top
        self.canvas = canvas
        top.update_idletasks()
        self._make_click_through()
        top.withdraw()
        self._keep_on_top()

    def _hwnd(self):
        try:
            hwnd = ctypes.windll.user32.GetAncestor(self.win.winfo_id(),
                                                    _GA_ROOT)
            return hwnd or self.win.winfo_id()
        except Exception:
            return None

    def _make_click_through(self):
        try:
            user32 = ctypes.windll.user32
        except AttributeError:
            return  # not on Windows (dev sandbox)
        hwnd = self._hwnd()
        if not hwnd:
            return
        style = user32.GetWindowLongW(hwnd, _GWL_EXSTYLE)
        style |= (_WS_EX_LAYERED | _WS_EX_TRANSPARENT
                  | _WS_EX_NOACTIVATE | _WS_EX_TOOLWINDOW)
        user32.SetWindowLongW(hwnd, _GWL_EXSTYLE, style)

    def _keep_on_top(self):
        """Slideshows love stealing topmost; take it back every 2 s."""
        try:
            hwnd = self._hwnd()
            if hwnd:
                ctypes.windll.user32.SetWindowPos(
                    hwnd, _HWND_TOPMOST, 0, 0, 0, 0,
                    _SWP_NOMOVE | _SWP_NOSIZE | _SWP_NOACTIVATE)
        except Exception:
            try:
                self.win.attributes("-topmost", True)
            except Exception:
                pass
        self.root.after(2000, self._keep_on_top)

    # ---------------- text layout ----------------

    def _wrap(self, text, font, max_width):
        """Word-wrap LOGICAL text to the strip width. Bidi reordering happens
        per line AFTER wrapping (reordering first would scramble line
        boundaries for multi-line Arabic)."""
        lines = []
        for para in (text or "").split("\n"):
            words = para.split()
            if not words:
                continue
            cur = words[0]
            for word in words[1:]:
                trial = cur + " " + word
                if font.measure(trial) <= max_width:
                    cur = trial
                else:
                    lines.append(cur)
                    cur = word
            lines.append(cur)
        max_lines = self.cfg["max_lines"] + (1 if self.cfg["show_english"]
                                             else 0)
        return lines[:max_lines]

    def _redraw(self):
        c = self.canvas
        c.delete("all")
        primary = self._primary
        if self.cfg["show_english"] and self._secondary:
            text = self._secondary + "\n" + primary
        else:
            text = primary
        if not text.strip():
            return

        cfg = self.cfg
        rtl = _is_rtl_text(primary)
        candidates = (cfg["font_candidates"] if rtl
                      else cfg.get("font_candidates_latin")
                      or cfg["font_candidates"])
        self.font_name = _pick_font_name(candidates)
        size = int(cfg["font_size"])
        font = tkfont.Font(family=self.font_name, size=-size, weight="bold")

        w, h, _x, _y = self._geometry()
        lines = [_shape_line(l) for l in self._wrap(text, font, w - 20)]
        if not lines:
            return

        line_h = int(size * 1.55)
        total_h = line_h * len(lines)
        y0 = h - total_h - 4  # bottom-anchored, like the Mac strip

        # Outline ring: same glyphs stamped around a circle of offsets.
        radius = max(2, round(size * cfg["outline_width_pct"] / 100.0))
        ring = []
        for r in (radius, max(1, radius // 2)):
            for dx, dy in ((r, 0), (-r, 0), (0, r), (0, -r),
                           (r, r), (r, -r), (-r, r), (-r, -r)):
                ring.append((dx, dy))

        for i, line in enumerate(lines):
            cx = w // 2
            cy = y0 + i * line_h + line_h // 2
            # soft-shadow stand-in (Canvas has no blur): one dark stamp
            c.create_text(cx + 2, cy + 3, text=line, font=font,
                          fill=_SHADOW, anchor="center")
            for dx, dy in ring:
                c.create_text(cx + dx, cy + dy, text=line, font=font,
                              fill=cfg["outline_color"], anchor="center")
            c.create_text(cx, cy, text=line, font=font,
                          fill=cfg["fill_color"], anchor="center")

    # ---------------- API (thread-safe) ----------------

    def show_text(self, primary, secondary=""):
        """primary = the translated caption (AR or EN depending on direction);
        secondary = the spoken-language transcript, shown above it when
        cfg["show_english"] is on."""
        def apply():
            self._primary = primary or ""
            self._secondary = secondary or ""
            self._redraw()
        self.root.after(0, apply)

    def set_font_size(self, size):
        def apply():
            self.cfg["font_size"] = int(size)
            w, h, x, y = self._geometry()
            self.win.geometry("%dx%d+%d+%d" % (w, h, x, y))
            self._redraw()
        self.root.after(0, apply)

    def show(self):
        def apply():
            self.win.deiconify()
            self._make_click_through()  # some styles reset on deiconify
        self.root.after(0, apply)

    def hide(self):
        self.root.after(0, self.win.withdraw)


def make_overlay(cfg, root):
    return CaptionOverlay(cfg, root)
