#!/usr/bin/env python3
"""
fc_float_window.py — Malin FC FLOATING (transparent) window.

Reuses the ExpressionPlayer from fc_player.py, but renders the MATTED (RGBA)
library in a per-pixel-alpha translucent window so she FLOATS on the desktop
(no grey box), with an auto soft drop-shadow + subtle rim derived from the alpha
(separates her from same-colored backgrounds). Keyboard NUMBERS drive expressions
(same hotkeys as fc_player --gui).

Run on the 5090 against the MATTED RGBA library:
  pip install PySide6 opencv-python numpy
  python fc_float_window.py --library "C:\\malin\\avatar_assets\\v1\\expr_library\\_fc_player_library_rgba"

Focus note: click the avatar once so the window has keyboard focus, then the
number keys (1-4) drive her. Esc/q closes. Click-drag moves her.
"""
import argparse, sys
import numpy as np
import cv2
from fc_player import Library, ExpressionPlayer


def compose_float(frame):
    """BGRA (or BGR) frame -> PREMULTIPLIED BGRA sprite with a soft drop-shadow +
    subtle rim baked in, ready to paint in a per-pixel-alpha window."""
    if frame.shape[2] == 3:
        bgr = frame.astype(np.float32)
        a = np.ones(frame.shape[:2], np.float32)
    else:
        bgr = frame[:, :, :3].astype(np.float32)
        a = frame[:, :, 3].astype(np.float32) / 255.0
    H, W = a.shape
    # soft drop-shadow: blurred + offset alpha, dark, semi-opaque
    sh = cv2.GaussianBlur(a, (0, 0), 11)
    sh = cv2.warpAffine(sh, np.float32([[1, 0, 7], [0, 1, 12]]), (W, H)) * 0.5
    # subtle rim at her silhouette edge (helps separate on light desktops)
    edge = cv2.morphologyEx((a > 0.4).astype(np.uint8), cv2.MORPH_GRADIENT, np.ones((3, 3), np.uint8))
    edge = cv2.GaussianBlur(edge.astype(np.float32), (0, 0), 1.2)
    a_out = np.clip(np.maximum(a, sh) + edge * 0.2, 0, 1)          # her + shadow + rim alpha
    premult = bgr * a[..., None]                                  # her premultiplied (shadow = black, rgb 0)
    premult = premult * (1 - (edge * 0.30)[..., None])            # darken edge slightly = the rim
    bgra = np.dstack([premult, a_out * 255]).astype(np.uint8)
    return np.ascontiguousarray(bgra)


def run(libpath, bg_paths):
    from PySide6 import QtCore, QtGui, QtWidgets
    lib = Library(libpath)
    pl = ExpressionPlayer(lib)
    h0, w0 = lib.frame(pl.neutral, pl.idx).shape[:2]
    # swappable backgrounds, resized to the avatar frame (empty = transparent float)
    bgs = []
    for p in bg_paths:
        im = cv2.imread(p)
        if im is not None:
            bgs.append(cv2.resize(im, (w0, h0)))

    class Avatar(QtWidgets.QWidget):
        def __init__(self):
            super().__init__()
            self.setWindowFlags(QtCore.Qt.FramelessWindowHint
                                | QtCore.Qt.WindowStaysOnTopHint
                                | QtCore.Qt.Tool)
            self.setAttribute(QtCore.Qt.WA_TranslucentBackground, True)
            self.setFocusPolicy(QtCore.Qt.StrongFocus)
            self.resize(w0, h0)
            self._pix = None
            self._drag = None
            self.bgi = 0
            timer = QtCore.QTimer(self)
            timer.timeout.connect(self.tick)
            timer.start(int(1000 / lib.fps))

        def tick(self):
            spr = compose_float(pl.next_frame())                  # premult BGRA (her + shadow/rim)
            if bgs:                                               # composite over a swappable background
                bg = bgs[self.bgi % len(bgs)].astype(np.float32)
                a = spr[:, :, 3:4].astype(np.float32) / 255.0
                out = (bg * (1 - a) + spr[:, :, :3].astype(np.float32)).clip(0, 255).astype(np.uint8)
                buf = np.ascontiguousarray(np.dstack([out, np.full(out.shape[:2], 255, np.uint8)]))
                fmt = QtGui.QImage.Format_ARGB32
            else:                                                # transparent float (no bg)
                buf = spr
                fmt = QtGui.QImage.Format_ARGB32_Premultiplied
            qimg = QtGui.QImage(buf.data, buf.shape[1], buf.shape[0], buf.shape[1] * 4, fmt)
            self._pix = QtGui.QPixmap.fromImage(qimg.copy())
            self.update()

        def paintEvent(self, _):
            if self._pix is not None:
                QtGui.QPainter(self).drawPixmap(0, 0, self._pix)

        def keyPressEvent(self, e):
            if e.key() == QtCore.Qt.Key_Escape or e.text() == "q":
                self.close(); return
            if e.text() == "b" and bgs:                          # cycle background
                self.bgi += 1; return
            emo = lib.hotkeys.get(e.text())
            if emo:
                pl.trigger(emo)

        def mousePressEvent(self, e):
            self._drag = e.globalPosition().toPoint() - self.frameGeometry().topLeft()
            self.activateWindow(); self.setFocus()

        def mouseMoveEvent(self, e):
            if self._drag is not None:
                self.move(e.globalPosition().toPoint() - self._drag)

        def mouseReleaseEvent(self, _):
            self._drag = None

    app = QtWidgets.QApplication(sys.argv)
    w = Avatar()
    w.show(); w.activateWindow(); w.setFocus()
    app.exec()


if __name__ == "__main__":
    import os, glob
    ap = argparse.ArgumentParser()
    ap.add_argument("--library", required=True)
    ap.add_argument("--background", default=None,
                    help="image file OR a folder of images; the 'b' key cycles them. Omit = transparent float.")
    a = ap.parse_args()
    bgp = []
    if a.background:
        if os.path.isdir(a.background):
            for ext in ("*.png", "*.jpg", "*.jpeg", "*.bmp", "*.webp"):
                bgp += sorted(glob.glob(os.path.join(a.background, ext)))
        else:
            bgp = [a.background]
    run(a.library, bgp)
