# -*- coding: utf-8 -*-
"""
withcam-podcast.py — reconstruit dans DaVinci Resolve le montage fait en direct sur
https://tools.withcam.fr/podcast

Installation (une fois) : copier ce fichier dans
  ~/Library/Application Support/Blackmagic Design/DaVinci Resolve/Fusion/Scripts/Edit/
Utilisation :
  1. Dans le Media Pool, un chutier par caméra, nommé EXACTEMENT comme la caméra dans l'outil
     (« CAM 1 », « Invité »…). Le son n'est pas touché : seule l'image est posée (mix à part).
  2. Exporter l'enregistrement depuis l'outil (fichier withcam-podcast_….json dans Téléchargements).
  3. Workspace > Scripts > withcam-podcast. Le fichier .json le plus récent est utilisé.
Le résultat (et les éventuels trous) s'affiche dans Workspace > Console.

Étalonnage : chaque caméra a son groupe de couleur (créé ou réutilisé) ; on étalonne le groupe une fois.

Multicam : si un chutier « MULTICAM » contient un clip multicam, il est utilisé à la place des
chutiers par caméra. Le clip est posé coupé à chaque changement, avec un marqueur « touche N »
par morceau : il reste à choisir l'angle au pavé numérique.

Synchro : chaque plan est pris dans le rush de la caméra dont le timecode (Tentacle) couvre
l'instant. Si la caméra choisie ne tournait pas à ce moment-là, le plan est pris sur une autre
caméra qui tournait (dans l'ordre CAM 1, CAM 2…). Nécessite Resolve 18.5 ou plus récent.
"""
import glob
import json
import os
import sys


def obtenir_resolve():
    for nom in ("resolve",):
        if nom in globals():
            return globals()[nom]
    try:
        return bmd.scriptapp("Resolve")  # noqa: F821  (variable fournie par Resolve)
    except Exception:
        pass
    try:
        return app.GetResolve()  # noqa: F821
    except Exception:
        pass
    try:
        import DaVinciResolveScript as dvr
        return dvr.scriptapp("Resolve")
    except Exception:
        return None


def tc_vers_images(tc, fps):
    h, m, s, f = [int(x) for x in tc.replace(";", ":").split(":")]
    return ((h * 60 + m) * 60 + s) * fps + f


def images_vers_tc(n, fps):
    n %= 24 * 3600 * fps
    return "%02d:%02d:%02d:%02d" % (n // (3600 * fps), n // (60 * fps) % 60, n // fps % 60, n % fps)


def chutier(dossier, nom):
    """Chutier portant ce nom (sans tenir compte des majuscules), cherché dans tout le Media Pool."""
    if (dossier.GetName() or "").strip().lower() == nom.strip().lower():
        return dossier
    for sous in dossier.GetSubFolderList() or []:
        trouve = chutier(sous, nom)
        if trouve:
            return trouve
    return None


def rushes(dossier, fps, video=True):
    """Clips du chutier avec leur début et leur fin en images (timecode source)."""
    out = []
    clips, pile = [], [dossier]
    while pile:                                  # sous-chutiers compris
        d = pile.pop()
        clips.extend(d.GetClipList() or [])
        pile.extend(d.GetSubFolderList() or [])
    for c in clips:
        p = c.GetClipProperty() or {}
        typ = (p.get("Type") or "").lower()
        if video and "video" not in typ:
            continue
        if not video and "audio" not in typ:
            continue
        try:
            debut = tc_vers_images(p.get("Start TC") or "00:00:00:00", fps)
            duree = int(p.get("Frames") or 0)
        except ValueError:
            continue
        if duree > 0:
            out.append({"clip": c, "nom": p.get("File Name") or c.GetName(), "debut": debut, "fin": debut + duree})
    return sorted(out, key=lambda r: r["debut"])


COULEURS = ["Blue", "Green", "Yellow", "Red", "Purple", "Cyan", "Pink", "Sand", "Lavender"]


def creer_timeline(mp, m, suffixe):
    tl = mp.CreateEmptyTimeline("%s — %s" % (m["titre"], suffixe))
    if not tl:
        print("[podcast] Impossible de créer la timeline (le nom existe déjà ?).")
        return None
    try:
        tl.SetStartTimecode(m["debutTC"])
    except Exception:
        pass
    return tl


def clips_multicam(dossier):
    return [c for c in (dossier.GetClipList() or []) if "multicam" in ((c.GetClipProperty() or {}).get("Type") or "").lower()]


def monter_multicam(m, fps, projet, mp, racine, dossier):
    """Mode multicam : le clip multicam est posé coupé à chaque changement, avec un marqueur
    « touche N · caméra » sur chaque morceau. Il reste à choisir l'angle avec le pavé numérique
    (Timeline > Selection Follows Playhead, puis Maj+↓ pour aller au marqueur suivant)."""
    clips = clips_multicam(dossier)
    if not clips:
        print("[podcast] Le chutier « MULTICAM » ne contient pas de clip multicam.")
        return
    clip = clips[0]
    p = clip.GetClipProperty() or {}
    debut = tc_vers_images(p.get("Start TC") or "00:00:00:00", fps)
    fin_clip = debut + int(p.get("Frames") or 0)
    print("[podcast] Mode multicam : %s (%s → %s)" % (clip.GetName(), images_vers_tc(debut, fps), images_vers_tc(fin_clip, fps)))
    tl = creer_timeline(mp, m, "multicam")
    if not tl:
        return
    base = tl.GetStartFrame()
    plans, reperes = [], []
    for seg in m["plans"]:
        a, b = max(seg["de"], debut), min(seg["a"], fin_clip)
        if a >= b:
            continue
        plan = {"mediaPoolItem": clip, "startFrame": a - debut, "endFrame": b - debut - 1, "recordFrame": base + a - m["debut"], "trackIndex": 1}
        plan["mediaType"] = 1            # image seule : le son est mixé à part
        plans.append(plan)
        reperes.append((a - m["debut"], seg["cam"]))
    if not plans:
        print("[podcast] Le clip multicam ne couvre pas l'enregistrement (%s → %s)." % (m["debutTC"], images_vers_tc(m["fin"], fps)))
        return
    ok = mp.AppendToTimeline(plans)
    noms = {c["numero"]: c["nom"] for c in m["cameras"]}
    for pos, cam in reperes:
        tl.AddMarker(pos, COULEURS[(cam - 1) % len(COULEURS)], "%d · %s" % (cam, noms.get(cam, "CAM %d" % cam)), "Touche %d" % cam, 1)
    print("[podcast] %d morceaux posés sur « %s », %d marqueurs." % (len(ok or []), tl.GetName(), len(reperes)))
    print("[podcast] Ensuite : Timeline > Selection Follows Playhead, puis pour chaque marqueur Maj+↓ et la touche indiquée.")
    print("[podcast] L'ordre des angles du clip multicam doit suivre celui des caméras de l'outil (angle 1 = %s)." % noms.get(1, "CAM 1"))


def exports():
    """Fichiers exportés par l'outil, du plus récent au plus ancien."""
    return sorted(glob.glob(os.path.expanduser("~/Downloads/withcam-podcast_*.json")), key=os.path.getmtime, reverse=True)


def main(chemin=None):
    resolve = obtenir_resolve()
    if not resolve:
        print("[podcast] Lancez ce script depuis DaVinci Resolve (Workspace > Scripts).")
        return
    fichiers = [chemin] if chemin else exports()
    if not fichiers:
        print("[podcast] Aucun fichier withcam-podcast_….json dans Téléchargements : exportez l'enregistrement depuis l'outil.")
        return
    with open(fichiers[0], encoding="utf-8") as f:
        m = json.load(f)
    print("[podcast] Montage : %s (%s)" % (m["titre"], os.path.basename(fichiers[0])))

    projet = resolve.GetProjectManager().GetCurrentProject()
    mp = projet.GetMediaPool()
    racine = mp.GetRootFolder()
    fps = int(round(float(projet.GetSetting("timelineFrameRate") or m["fps"])))
    if fps != m["fps"]:
        print("[podcast] Attention : projet à %s i/s, outil réglé sur %s i/s." % (fps, m["fps"]))
    fps = m["fps"]

    multi = chutier(racine, "MULTICAM")
    if multi and clips_multicam(multi):
        return monter_multicam(m, fps, projet, mp, racine, multi)
    if multi:
        print("[podcast] Chutier « MULTICAM » vide : montage à partir des chutiers de chaque caméra.")

    cams = {}
    for c in m["cameras"]:
        d = chutier(racine, c["nom"])
        if not d:
            print("[podcast] Chutier « %s » introuvable : ses plans seront pris sur une autre caméra." % c["nom"])
            cams[c["numero"]] = {"nom": c["nom"], "dec": 0, "rushes": []}
            continue
        r = rushes(d, fps)
        print("[podcast] %s : %d rush(es)" % (c["nom"], len(r)))
        cams[c["numero"]] = {"nom": c["nom"], "dec": int(c.get("decalage") or 0), "rushes": r}

    def couvre(num, t):
        cam = cams.get(num)
        if not cam:
            return None
        s = t + cam["dec"]
        for r in cam["rushes"]:
            if r["debut"] <= s < r["fin"]:
                return r
        return None

    tl = mp.CreateEmptyTimeline("%s — montage" % m["titre"])
    if not tl:
        print("[podcast] Impossible de créer la timeline (le nom existe déjà ?).")
        return
    try:
        tl.SetStartTimecode(m["debutTC"])
    except Exception:
        pass
    base = tl.GetStartFrame()
    plans, trous, secours, cam_des_plans = [], 0, 0, []
    ordre = sorted(cams)
    for seg in m["plans"]:
        t = seg["de"]
        while t < seg["a"]:
            num, r = seg["cam"], couvre(seg["cam"], t)
            if not r:
                for autre in ordre:
                    r = couvre(autre, t)
                    if r:
                        num = autre
                        secours += 1
                        break
            if not r:
                # personne ne tournait : on saute jusqu'au prochain début de rush
                suivants = [x["debut"] - cams[n]["dec"] for n in ordre for x in cams[n]["rushes"] if t < x["debut"] - cams[n]["dec"] < seg["a"]]
                saut = min(suivants) if suivants else seg["a"]
                trous += saut - t
                t = saut
                continue
            dec = cams[num]["dec"]
            fin = min(seg["a"], r["fin"] - dec)
            if num != seg["cam"]:
                # plan de secours : on revient sur la caméra choisie dès qu'elle se remet à tourner
                reprise = [x["debut"] - cams[seg["cam"]]["dec"] for x in cams[seg["cam"]]["rushes"] if t < x["debut"] - cams[seg["cam"]]["dec"] < fin]
                fin = min([fin] + reprise)
            plans.append({"mediaPoolItem": r["clip"], "startFrame": t + dec - r["debut"], "endFrame": fin + dec - r["debut"] - 1,
                          "recordFrame": base + t - m["debut"], "trackIndex": 1, "mediaType": 1})
            cam_des_plans.append(cams[num]["nom"])
            t = fin


    if not plans:
        print("[podcast] Aucun rush ne couvre l'enregistrement (%s → %s) : vérifiez le timecode des caméras et les noms des chutiers."
              % (m["debutTC"], images_vers_tc(m["fin"], fps)))
        return
    ok = mp.AppendToTimeline(plans)
    print("[podcast] %d plans posés sur « %s »." % (len(ok or []), tl.GetName()))
    # Un groupe de couleur par caméra : l'étalonnage se fait une fois par caméra (onglet Color,
    # mode « Group Pre-Clip » ou « Group Post-Clip ») et s'applique à tous ses plans.
    try:
        groupes = {g.GetName(): g for g in (projet.GetColorGroupsList() or [])}
        for item, nom in zip(ok or [], cam_des_plans):
            if nom not in groupes:
                groupes[nom] = projet.AddColorGroup(nom)
            if groupes[nom]:
                item.AssignToColorGroup(groupes[nom])
        print("[podcast] Groupes de couleur : %s." % ", ".join(sorted(set(cam_des_plans))))
    except Exception as e:
        print("[podcast] Groupes de couleur non créés (%s) : Resolve trop ancien ?" % e)
    if secours:
        print("[podcast] %d plan(s) pris sur une autre caméra (la caméra choisie ne tournait pas)." % secours)
    if trous:
        print("[podcast] %.1f s sans aucune caméra qui tourne (trous dans la timeline)." % (trous / float(fps)))


class Journal(object):
    """Recueille ce que le script affiche, pour le montrer dans la fenêtre."""
    def __init__(self, sortie):
        self.sortie, self.lignes = sortie, []

    def write(self, t):
        self.lignes.append(t)
        try:
            self.sortie.write(t)
        except Exception:
            pass

    def flush(self):
        pass


def executer(chemin):
    """Lance le montage et renvoie son compte rendu (erreurs comprises)."""
    journal, ancien = Journal(sys.stdout), sys.stdout
    sys.stdout = journal
    try:
        main(chemin)
    except Exception:
        import traceback
        print("[podcast] ERREUR :")
        traceback.print_exc(file=journal)
    finally:
        sys.stdout = ancien
    return "".join(journal.lignes).replace("[podcast] ", "")


def resume(chemin):
    try:
        with open(chemin, encoding="utf-8") as f:
            m = json.load(f)
        duree = (m["fin"] - m["debut"]) // m["fps"]
        return "%s — %s → %s · %d min %02d s · %d plans · %d i/s\nCaméras : %s" % (
            m["titre"], m["debutTC"], images_vers_tc(m["fin"], m["fps"]), duree // 60, duree % 60, len(m["plans"]), m["fps"],
            ", ".join("%d = %s" % (c["numero"], c["nom"]) for c in m["cameras"]))
    except Exception as e:
        return "Fichier illisible : %s" % e


def fenetre():
    """Fenêtre (même mécanique que le script des retours clients). False si l'interface est indisponible."""
    fu = globals().get("fusion") or globals().get("fu")
    b = globals().get("bmd")
    if not fu or not b:
        return False
    ui = fu.UIManager
    disp = b.UIDispatcher(ui)
    win = disp.AddWindow(
        {"ID": "wcPod", "WindowTitle": "Withcam – Montage podcast", "Geometry": [300, 200, 640, 460]},
        ui.VGroup({"Spacing": 8}, [
            ui.Label({"Text": "Enregistrement exporté depuis tools.withcam.fr/podcast (dossier Téléchargements) :", "WordWrap": True, "Weight": 0}),
            ui.HGroup({"Weight": 0}, [ui.ComboBox({"ID": "fichier", "Weight": 1}), ui.Button({"ID": "actualiser", "Text": "Actualiser", "Weight": 0})]),
            ui.Label({"ID": "resume", "Text": "", "WordWrap": True, "Weight": 0}),
            ui.Label({"ID": "mode", "Text": "", "WordWrap": True, "Weight": 0}),
            ui.TextEdit({"ID": "journal", "ReadOnly": True, "Weight": 1}),
            ui.HGroup({"Weight": 0}, [ui.HGap(0, 1), ui.Button({"ID": "fermer", "Text": "Fermer"}), ui.Button({"ID": "monter", "Text": "Créer la timeline"})]),
        ]))
    itm = win.GetItems()
    liste = []

    def mode_actuel():
        try:
            pr = obtenir_resolve().GetProjectManager().GetCurrentProject()
            mc = chutier(pr.GetMediaPool().GetRootFolder(), "MULTICAM")
            if mc and clips_multicam(mc):
                return "Mode : multicam (chutier « MULTICAM » trouvé) — le clip est coupé, un marqueur « touche N » par plan."
            return "Mode : un chutier par caméra (nommé comme dans l'outil) . Pour le multicam, mettez le clip multicam dans le chutier « MULTICAM »."
        except Exception:
            return "Ouvrez d'abord un projet."

    def remplir(ev=None):
        del liste[:]
        liste.extend(exports())
        itm["fichier"].Clear()
        for c in liste:
            itm["fichier"].AddItem(os.path.basename(c).replace("withcam-podcast_", "").replace(".json", ""))
        if not liste:
            itm["resume"].Text = "Aucun export dans Téléchargements : sur /podcast, « Exporter pour DaVinci »."
        itm["mode"].Text = mode_actuel()

    def choisi(ev=None):
        i = int(itm["fichier"].CurrentIndex)
        if 0 <= i < len(liste):
            itm["resume"].Text = resume(liste[i])

    def monter(ev):
        i = int(itm["fichier"].CurrentIndex)
        if not (0 <= i < len(liste)):
            itm["journal"].PlainText = "Choisissez d'abord un enregistrement."
            return
        itm["journal"].PlainText = "Montage en cours…"
        itm["journal"].PlainText = executer(liste[i])
        itm["mode"].Text = mode_actuel()

    win.On.fichier.CurrentIndexChanged = choisi
    win.On.actualiser.Clicked = remplir
    win.On.monter.Clicked = monter
    win.On.fermer.Clicked = lambda ev: disp.ExitLoop()
    win.On.wcPod.Close = lambda ev: disp.ExitLoop()
    remplir()
    choisi()
    win.Show()
    disp.RunLoop()
    win.Hide()
    return True


if not fenetre():
    # pas d'interface Fusion : on monte le dernier export et on ouvre le compte rendu dans TextEdit
    texte = executer(None)
    chemin = os.path.expanduser("~/Downloads/withcam-podcast-compte-rendu.txt")
    try:
        with open(chemin, "w", encoding="utf-8") as f:
            f.write(texte)
        import subprocess
        subprocess.Popen(["open", "-e", chemin])
    except Exception:
        pass
