#!/usr/bin/env python3
"""Eğitim - 01 önizleme oynatıcısı.

Orijinal dosyaları oynatır (dönüştürme yok). İki kamera yan yana, mikrofonlar ve kamera
ses kanalları tek bir zaman çizgisinde senkron. Zaman bilgisi: medya/zaman.json

Kullanım: python3 oynatici.py [Eğitim|Sohbet] [başlangıç saati, ör. 19:50]

Tuşlar:
  Boşluk        oynat / duraklat
  ← →           5 sn geri / ileri       Shift+← →  60 sn
  1..6          ses kaynağı (düğmelerdeki sıra)
  R             Log renk düzeltmesini aç / kapat
  F             tam ekran
  [ ]           hız 0.5x .. 2x
"""
import json, os, socket, subprocess, sys, tempfile, time

os.environ.setdefault("QT_QPA_PLATFORM", "xcb")      # mpv'yi pencereye gömmek için X11 (XWayland)
from PyQt6.QtCore import Qt, QTimer, QRectF
from PyQt6.QtGui import QColor, QFont, QPainter, QKeySequence, QShortcut
from PyQt6.QtWidgets import (QApplication, QButtonGroup, QHBoxLayout, QLabel, QPushButton, QSizePolicy,
                             QVBoxLayout, QWidget)

KOK = os.path.dirname(os.path.abspath(__file__))
ZAMAN = os.path.join(KOK, "medya", "zaman.json")
LUT = os.path.join(KOK, "medya", "slog3-709.cube")
KAYMA_ESIK = 0.10      # sn; bundan fazla kayan oynatıcı yeniden hizalanır


def saat(t):
    t = int(t) % 86400
    return f"{t // 3600:02d}:{t % 3600 // 60:02d}:{t % 60:02d}"


def _ebeveynle_olsun():
    """Oynatıcı kapanınca (çökse bile) mpv de kapansın: PR_SET_PDEATHSIG = SIGTERM."""
    import ctypes, signal
    ctypes.CDLL("libc.so.6").prctl(1, signal.SIGTERM)


class Mpv:
    """Ayrı bir mpv süreci; JSON IPC ile yönetilir."""

    def __init__(self, wid=None, ses_yok=False, ad="mpv"):
        self.sock = os.path.join(tempfile.gettempdir(), f"egitim-{ad}-{os.getpid()}.sock")
        arg = ["mpv", "--idle=yes", "--keep-open=yes", "--pause", "--no-config", "--really-quiet",
               f"--input-ipc-server={self.sock}", "--no-osc", "--osd-level=0", "--no-input-default-bindings",
               "--input-vo-keyboard=no", "--hr-seek=yes", "--cache=yes", "--demuxer-max-bytes=300MiB",
               "--audio-pitch-correction=yes", "--volume=100"]
        if wid is not None:
            arg += [f"--wid={int(wid)}", "--vo=gpu-next", "--hwdec=vaapi",
                    # 4:2:2 HEVC'yi sadece Intel çözebiliyor; çizimi de Intel'de yap ki kopya gerekmesin
                    # --wid sadece X11'de gömülür; Wayland'da mpv ayrı pencere açıp siyah bırakıyor
                    "--gpu-context=x11vk", "--gpu-api=vulkan","--vulkan-device=Intel(R) Graphics (RPL-S)", "--vaapi-device=/dev/dri/renderD128", "--keepaspect=yes", "--background-color=#000000",
                    # dosyada renk etiketi yok; LUT çıkışı Rec.709 olduğu için öyle işaretle
                    "--vf=format=colormatrix=bt.709:colorlevels=full:primaries=bt.709:gamma=bt.1886"]
        else:
            arg += ["--vid=no", "--force-window=no"]
        if ses_yok:
            arg += ["--mute=yes"]
        self.p = subprocess.Popen(arg, stdin=subprocess.DEVNULL, preexec_fn=_ebeveynle_olsun)
        for _ in range(100):
            try:
                self.s = socket.socket(socket.AF_UNIX); self.s.connect(self.sock); break
            except OSError:
                time.sleep(0.05)
        self.s.setblocking(True); self.s.settimeout(0.5)
        self.buf = b""; self.rid = 0
        self.dosya = None; self.klip = None

    def komut(self, *args, cevap=False):
        self.rid += 1
        rid = self.rid
        self.s.sendall((json.dumps({"command": list(args), "request_id": rid}) + "\n").encode())
        if not cevap:
            return None
        bitis = time.monotonic() + 0.4
        while time.monotonic() < bitis:
            while b"\n" in self.buf:
                satir, self.buf = self.buf.split(b"\n", 1)
                try:
                    j = json.loads(satir)
                except ValueError:
                    continue
                if j.get("request_id") == rid:
                    return j.get("data")
            try:
                self.buf += self.s.recv(65536)
            except socket.timeout:
                break
        return None

    def ayarla(self, ad, deger):
        self.komut("set_property", ad, deger)

    def oku(self, ad):
        return self.komut("get_property", ad, cevap=True)

    def yukle(self, yol, bas):
        self.komut("loadfile", yol, "replace", -1, f"start={bas:.3f}")
        self.dosya = yol

    def bosalt(self):
        if self.dosya:
            self.komut("stop"); self.dosya = None; self.klip = None

    def kapat(self):
        try:
            self.komut("quit")
        except OSError:
            pass
        try:
            self.p.wait(2)
        except subprocess.TimeoutExpired:
            self.p.kill()


class Iz:
    """Zaman çizgisinde bir iz (kamera ya da mikrofon) ve onu çalan mpv."""

    def __init__(self, ad, klipler, mpv):
        self.ad = ad; self.klipler = sorted(klipler, key=lambda k: k["bas"]); self.mpv = mpv

    def bul(self, t):
        for k in self.klipler:
            if k["bas"] <= t < k["bas"] + k["sure"]:
                return k
        return None

    def hizala(self, t, calisiyor, hiz, zorla=False):
        k = self.bul(t)
        m = self.mpv
        if k is None:
            m.bosalt(); return False
        hedef = t - k["bas"]
        if m.klip is not k:
            m.klip = k
            m.yukle(k["yol"], hedef)
            m.ayarla("pause", not calisiyor); m.ayarla("speed", hiz)
            return True
        if zorla:
            m.komut("seek", hedef, "absolute+exact")
        elif calisiyor:
            p = m.oku("time-pos")
            if isinstance(p, (int, float)) and abs(p - hedef) > KAYMA_ESIK * max(1, hiz):
                m.komut("seek", hedef, "absolute+exact")
        return True


class Cizgi(QWidget):
    """Zaman çizgisi: klip çubukları + oynatma başı; tıklayınca o ana gider."""

    def __init__(self, ana):
        super().__init__(); self.ana = ana
        self.setMinimumHeight(24 + 16 * len(ana.izler)); self.setMouseTracking(True)

    def x_t(self, x):
        b, s = self.ana.bas, self.ana.son
        return b + (s - b) * min(max(x - 90, 0), self.width() - 100) / max(self.width() - 100, 1)

    def t_x(self, t):
        b, s = self.ana.bas, self.ana.son
        return 90 + (t - b) / (s - b) * (self.width() - 100)

    def mousePressEvent(self, e):
        self.ana.git(self.x_t(e.position().x()))

    def mouseMoveEvent(self, e):
        if e.buttons() & Qt.MouseButton.LeftButton:
            self.ana.git(self.x_t(e.position().x()))
        self.setToolTip(saat(self.x_t(e.position().x())))

    def paintEvent(self, _):
        p = QPainter(self); p.fillRect(self.rect(), QColor("#161616"))
        p.setFont(QFont("Sans", 8))
        renk = ["#4f8ef7", "#3cc48a", "#e8a33d", "#d65db1"]
        for i, iz in enumerate(self.ana.izler):
            y = 6 + i * 16
            p.setPen(QColor("#bbbbbb")); p.drawText(QRectF(4, y - 1, 84, 14), Qt.AlignmentFlag.AlignVCenter, iz.ad)
            for k in iz.klipler:
                x0, x1 = self.t_x(k["bas"]), self.t_x(k["bas"] + k["sure"])
                c = QColor(renk[i % 4]); c.setAlpha(120 if k.get("tahmini") else 220)
                p.fillRect(QRectF(x0, y, max(x1 - x0, 1.5), 11), c)
        # saat işaretleri
        p.setPen(QColor("#777777"))
        t = (int(self.ana.bas) // 600 + 1) * 600
        while t < self.ana.son:
            x = self.t_x(t); p.drawLine(int(x), self.height() - 14, int(x), self.height() - 10)
            p.drawText(int(x) - 16, self.height() - 1, saat(t)[:5]); t += 600
        x = self.t_x(self.ana.konum())
        p.setPen(QColor("#ff4040")); p.drawLine(int(x), 0, int(x), self.height())


class Oynatici(QWidget):
    def __init__(self, bolum, baslangic=None):
        super().__init__()
        z = json.load(open(ZAMAN, encoding="utf-8"))
        self.bolumler = list(z["bolumler"]); b = z["bolumler"][bolum]
        self.setWindowTitle(f"Önizleme – {bolum}")
        self.bas, self.son = b["bas"], b["son"]
        self.calisiyor = False; self.hiz = 1.0; self.renk = True
        self.t0 = self.bas; self.m0 = time.monotonic()

        ust = QHBoxLayout(); ust.setSpacing(2)
        self.ekran = {}
        for kam in b["video"]:
            w = QWidget(); w.setAttribute(Qt.WidgetAttribute.WA_NativeWindow); w.setStyleSheet("background:#000")
            w.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding); w.setMinimumSize(320, 180)
            kutu = QVBoxLayout(); etiket = QLabel(f"Kamera {kam}"); etiket.setStyleSheet("color:#aaa")
            kutu.addWidget(etiket); kutu.addWidget(w, 1); ust.addLayout(kutu, 1); self.ekran[kam] = (w, etiket)
        self.show(); QApplication.processEvents()

        self.izler = []; self.video = {}
        for kam, klipler in b["video"].items():
            m = Mpv(wid=self.ekran[kam][0].winId(), ses_yok=True, ad=f"kam{kam}")
            m.ayarla("lut", LUT); m.ayarla("lut-type", "native")
            iz = Iz(f"Kamera {kam}", klipler, m); self.izler.append(iz); self.video[kam] = iz
        self.mik = {}
        for ad, klipler in b["ses"].items():
            iz = Iz(ad, klipler, Mpv(ses_yok=True, ad=ad.replace(" ", ""))); self.izler.append(iz); self.mik[ad] = iz

        # ses kaynakları: mikrofonlar + kamera kanalları
        self.kaynaklar = [(ad, "mik", ad, None) for ad in self.mik]
        for kam in self.video:
            if kam not in z.get("kanal", {}):      # kanalları ayrıca listelenmeyen kamera (ör. A: sesi ayrı izde)
                continue
            for kanal, etiket in (("L", "sol"), ("R", "sağ")):
                not_ = z.get("kanal", {}).get(kam, {}).get(kanal, "")
                self.kaynaklar.append((f"Kamera {kam} {etiket}" + (f" ({not_})" if not_ else ""), "kam", kam, kanal))

        alt = QHBoxLayout()
        self.dugme = QPushButton("▶"); self.dugme.setFixedWidth(44); self.dugme.clicked.connect(self.oynat_duraklat)
        self.saat = QLabel(); self.saat.setStyleSheet("color:#eee; font: 15px monospace"); self.saat.setMinimumWidth(230)
        alt.addWidget(self.dugme); alt.addWidget(self.saat)
        self.grup = QButtonGroup(self); self.grup.setExclusive(True)
        for i, (ad, *_r) in enumerate(self.kaynaklar):
            d = QPushButton(f"{i + 1} {ad}"); d.setCheckable(True); self.grup.addButton(d, i); alt.addWidget(d)
        self.grup.idClicked.connect(self.ses_sec)
        self.renk_d = QPushButton("Renk düzeltme: açık"); self.renk_d.clicked.connect(self.renk_degis); alt.addWidget(self.renk_d)
        alt.addStretch(1)

        self.cizgi = Cizgi(self)
        ana = QVBoxLayout(self); ana.setContentsMargins(4, 4, 4, 4)
        ana.addLayout(ust, 1); ana.addWidget(self.cizgi); ana.addLayout(alt)
        self.setStyleSheet("QWidget{background:#202020;color:#ddd} QPushButton{padding:4px 8px;background:#333;border:1px solid #444}"
                           "QPushButton:checked{background:#2d5fb5;border-color:#4f8ef7}")
        self.resize(1700, 900)

        kis = {"Space": self.oynat_duraklat, "Left": lambda: self.git(self.konum() - 5),
               "Right": lambda: self.git(self.konum() + 5), "Shift+Left": lambda: self.git(self.konum() - 60),
               "Shift+Right": lambda: self.git(self.konum() + 60), "R": self.renk_degis, "F": self.tam_ekran,
               "[": lambda: self.hiz_ayarla(-0.25), "]": lambda: self.hiz_ayarla(0.25), "Esc": self.showNormal}
        for i in range(len(self.kaynaklar)):
            kis[str(i + 1)] = (lambda j=i: (self.grup.button(j).setChecked(True), self.ses_sec(j)))
        for tus, f in kis.items():
            QShortcut(QKeySequence(tus), self, f)

        self.secili = None
        ilk = next((i for i, k in enumerate(self.kaynaklar) if k[0] == "Miks"), 1 if len(self.kaynaklar) > 1 else 0)
        self.grup.button(ilk).setChecked(True); self.ses_sec(ilk)
        self.git(baslangic if baslangic is not None else self.bas + 1)
        self.zaman = QTimer(self); self.zaman.timeout.connect(self.tik); self.zaman.start(250)

    # --- saat
    def konum(self):
        if self.calisiyor:
            return min(self.t0 + (time.monotonic() - self.m0) * self.hiz, self.son)
        return self.t0

    def git(self, t):
        self.t0 = min(max(t, self.bas), self.son - 0.1); self.m0 = time.monotonic()
        for iz in self.izler:
            iz.hizala(self.t0, self.calisiyor, self.hiz, zorla=True)
        self.guncelle()

    def oynat_duraklat(self):
        self.t0 = self.konum(); self.m0 = time.monotonic(); self.calisiyor = not self.calisiyor
        for iz in self.izler:
            iz.mpv.ayarla("pause", not self.calisiyor)
        self.git(self.t0)
        self.dugme.setText("❚❚" if self.calisiyor else "▶")

    def hiz_ayarla(self, d):
        self.t0 = self.konum(); self.m0 = time.monotonic()
        self.hiz = min(max(self.hiz + d, 0.5), 2.0)
        for iz in self.izler:
            iz.mpv.ayarla("speed", self.hiz)
        self.guncelle()

    def ses_sec(self, i):
        ad, tur, hedef, kanal = self.kaynaklar[i]
        for iz in self.mik.values():
            iz.mpv.ayarla("mute", True)
        for iz in self.video.values():
            iz.mpv.ayarla("mute", True)
        if tur == "mik":
            self.mik[hedef].mpv.ayarla("mute", False)
        else:
            m = self.video[hedef].mpv
            m.ayarla("af", f"lavfi=[pan=mono|c0={'c0' if kanal == 'L' else 'c1'}]")
            m.ayarla("mute", False)
        self.secili = i

    def renk_degis(self):
        self.renk = not self.renk
        for iz in self.video.values():
            iz.mpv.ayarla("lut", LUT if self.renk else "")
        self.renk_d.setText(f"Renk düzeltme: {'açık' if self.renk else 'kapalı (Log)'}")

    def tam_ekran(self):
        self.showNormal() if self.isFullScreen() else self.showFullScreen()

    # --- döngü
    def tik(self):
        t = self.konum()
        if t >= self.son - 0.1 and self.calisiyor:
            self.oynat_duraklat()
        for iz in self.izler:
            iz.hizala(t, self.calisiyor, self.hiz)
        self.guncelle()

    def guncelle(self):
        t = self.konum()
        bilgi = []
        for kam, iz in self.video.items():
            k = iz.bul(t)
            self.ekran[kam][1].setText(f"Kamera {kam}  " + (f"{os.path.basename(k['yol'])}  +{saat(t - k['bas'])}" if k else "— kayıt yok —"))
        ad = self.kaynaklar[self.secili][0] if self.secili is not None else ""
        _, tur, hedef, _k = self.kaynaklar[self.secili]
        var = (self.mik[hedef] if tur == "mik" else self.video[hedef]).bul(t) is not None
        self.saat.setText(f"{saat(t)}   {self.hiz:.2g}x   {'' if var else '(bu kaynakta kayıt yok)'}")
        self.cizgi.update()

    def closeEvent(self, e):
        self.zaman.stop()
        for iz in self.izler:
            iz.mpv.kapat()
        super().closeEvent(e)


if __name__ == "__main__":
    import fcntl, resource
    # Aynı anda tek oynatıcı: iki tanesi aynı Intel GPU'yu paylaşınca video süreçleri çöküyordu
    kilit = open(os.path.join(tempfile.gettempdir(), "egitim-oynatici.lock"), "w")
    try:
        fcntl.flock(kilit, fcntl.LOCK_EX | fcntl.LOCK_NB)
    except OSError:
        sys.exit("Oynatıcı zaten açık. Önce açık olanı kapatın.")
    # Açık dosya sınırını yükselt (mpv'ler de bunu devralır)
    _, sert = resource.getrlimit(resource.RLIMIT_NOFILE)
    resource.setrlimit(resource.RLIMIT_NOFILE, (min(sert, 65536), sert))
    app = QApplication(sys.argv)
    bolum = sys.argv[1] if len(sys.argv) > 1 else "Eğitim"
    bas = None
    if len(sys.argv) > 2:      # başlangıç saati: SS:DD veya SS:DD:ss
        p = [int(x) for x in sys.argv[2].split(":")] + [0, 0]
        bas = p[0] * 3600 + p[1] * 60 + p[2]
    o = Oynatici(bolum, bas)
    sys.exit(app.exec())
