--[[
  LiveGotchi — script d'installation OBS

  À déposer une fois dans OBS : Outils ▸ Scripts ▸ +

  Le script n'est PAS le transport : il ne lit ni la géométrie de la scène ni
  l'état du direct, et ne lance aucun programme. Il pose ou complète les
  sources navigateur (l'overlay et les widgets) avec les bons réglages, en un
  clic :

    - l'adresse du site en ligne, jeton d'overlay compris — c'est justement le
      paramètre que les widgets oublient le plus souvent quand on les pose à la
      main, et une adresse sans jeton ne se met jamais à jour ;
    - la permission de page réglée sur « Lecture des données OBS » (READ_OBS),
      pour que la créature sache que le direct a commencé sans mot de passe.

  Il reconnaît une source par son URL, jamais par son nom : quelqu'un qui pose
  ses sources à la main en recopiant les adresses les appelle « Navigateur
  web », « Navigateur web 2 »… (les noms par défaut d'OBS), et le script doit
  les retrouver quand même — sans quoi il croirait qu'elles n'existent pas et
  en recréerait des doublons, sans jeton, à côté.

  Mettre une source à jour force aussi son rechargement (cache vidé) : une
  page déjà chargée ne relit pas forcément ses réglages sans ça. Le vrai
  responsable d'une créature restée endormie après une installation par
  ailleurs propre était côté page (`window.obsstudio` jamais retesté après
  coup, voir `src/obs/navigateur.ts`), pas ce script — mais ce rechargement
  reste une ceinture en plus des bretelles.

  Le temps d'antenne (`window.obsstudio`, voir `src/obs/navigateur.ts`) ne
  demande donc plus rien d'autre après l'installation. obs-websocket (mot de
  passe dans le studio, réglages ▸ Connexion OBS) reste utile en plus, pour que
  la créature marche sur les sources de la scène — mais ce n'est plus lui qui
  décide si elle vit.

  On a un temps envisagé un plugin natif, puis un programme compagnon à
  installer et lancer à part : les deux demandaient au streamer un geste que ce
  script rend inutile. Poser les sources à la main reste possible — ce script
  ne fait qu'éviter de le refaire à chaque nouvelle installation, ou de se
  tromper dans une adresse.

  Testé sur les versions d'OBS encore courantes aujourd'hui. La permission de
  page (« webpage_control_level ») existe depuis fin 2021 (OBS 27.x) : aucune
  version encore utilisée n'en est dépourvue.
]]

obs = obslua

--[[ obs-browser, obs-browser-source.hpp : enum class ControlLevel : int
     { None, ReadObs, ReadUser, Basic, Advanced, All } — donc 1 pour READ_OBS,
     le niveau le plus bas après aucun accès, et tout ce qu'il faut pour
     `window.obsstudio.getStatus()`. ]]
local CONTROL_LEVEL_READ_OBS = 1

local reglages = {
  api = "https://livegotchi.com",
  jeton = "",
}

--[[ ------------------------------------------------------------ les sources ]]

--[[ Une source par type de widget — nom cosmétique, taille par défaut. Le nom
     ne sert JAMAIS à reconnaître une source : quelqu'un qui pose ses sources
     à la main en recopiant les adresses depuis la page Direct les appelle
     « Navigateur web », « Navigateur web 2 »… (les noms par défaut d'OBS), et
     le script doit les reconnaître quand même. Seule l'URL fait foi : une
     source dont l'URL contient `overlay.html` est l'overlay, une dont l'URL
     contient `widget.html?type=X` est le widget de type X. ]]
local WIDGETS = {
  evolution = { nom = "LiveGotchi — évolution",  l = 420, h = 200 },
  besoin    = { nom = "LiveGotchi — besoin",     l = 320, h = 150 },
  jauges    = { nom = "LiveGotchi — jauges",     l = 340, h = 230 },
  identite  = { nom = "LiveGotchi — identité",   l = 380, h = 130 },
  compteurs = { nom = "LiveGotchi — compteurs",  l = 420, h = 130 },
}

--[[ À une toute première installation (aucun widget LiveGotchi déjà posé),
     ce sont ceux-là qui apparaissent. Les quatre valent dès la première
     minute d'antenne. Les compteurs, eux, partent à zéro pour une créature
     neuve — vides, ils ne disent rien — donc pas dans le lot de départ : qui
     les veut les ajoute à la main, comme JA l'a fait à la place de
     l'identité. Si des widgets existent déjà (n'importe lesquels), ce lot
     par défaut ne s'applique plus : on mettrait sinon un choix par-dessus un
     autre déjà fait — voir la passe 2 dans `installer()`. ]]
local TYPES_PAR_DEFAUT = { "evolution", "besoin", "jauges", "identite" }

local function chemin_widget(type_) return "widget.html?type=" .. type_ end

local function adresse(chemin)
  local base = reglages.api:gsub("/+$", "") .. "/" .. chemin
  local separateur = chemin:find("?") and "&" or "?"
  return base .. separateur .. "jeton=" .. reglages.jeton
end

local function est_overlay(url)
  return url ~= nil and url:find("overlay.html", 1, true) ~= nil
end

--[[ Le type d'un widget d'après son URL, ou nil si ce n'en est pas un —
     `find(..., true)` en recherche littérale, pas besoin d'échapper les
     points de « widget.html ». ]]
local function type_widget(url)
  if not url or not url:find("widget.html", 1, true) then return nil end
  local t = url:match("[?&]type=([%a]+)")
  if t and WIDGETS[t] then return t end
  return nil
end

--[[ Le jeton d'une adresse d'overlay, si elle en porte un. `t=` est
     l'ancienne façon de l'écrire (voir `src/core/transport.ts`) ; on lit les
     deux, on n'écrit plus que `jeton=`. ]]
local function jeton_overlay(url)
  if not est_overlay(url) then return nil end
  return url:match("[?&]jeton=([^&]+)") or url:match("[?&]t=([^&]+)")
end

--[[ Cherche un jeton déjà posé dans une source overlay existante, toutes
     scènes confondues — pour ne rien demander à qui a déjà posé son overlay
     à la main ou avec une vieille version du script. Lecture seule.

     `obs_frontend_get_scenes()` et `obs_scene_enum_items()` renvoient des
     tableaux Lua, pas un rappel comme en C (scripting.rst, « Other
     Differences From the C API ») ; on les libère respectivement avec
     `source_list_release()` et `sceneitem_list_release()`.

     Renvoie le jeton trouvé, ou nil. Un deuxième résultat à `true` signale
     une ambiguïté (plusieurs jetons différents, aucun ne se détachant depuis
     la scène courante) : mieux vaut ne rien choisir que choisir au hasard. ]]
local function jeton_depuis_les_scenes()
  local scene_active = obs.obs_frontend_get_current_scene()
  local nom_scene_active = scene_active and obs.obs_source_get_name(scene_active) or nil
  if scene_active then obs.obs_source_release(scene_active) end

  local trouves = {}          -- jeton -> true, pour dédoublonner
  local depuis_scene_active = nil

  local scenes = obs.obs_frontend_get_scenes()
  for _, scene_src in ipairs(scenes) do
    local nom_scene = obs.obs_source_get_name(scene_src)
    local items = obs.obs_scene_enum_items(obs.obs_scene_from_source(scene_src))
    for _, item in ipairs(items) do
      local src = obs.obs_sceneitem_get_source(item)
      if obs.obs_source_get_id(src) == "browser_source" then
        local s = obs.obs_source_get_settings(src)
        local j = jeton_overlay(obs.obs_data_get_string(s, "url"))
        obs.obs_data_release(s)
        if j then
          trouves[j] = true
          if nom_scene == nom_scene_active then depuis_scene_active = j end
        end
      end
    end
    obs.sceneitem_list_release(items)
  end
  obs.source_list_release(scenes)

  local distincts = {}
  for j in pairs(trouves) do distincts[#distincts + 1] = j end

  if #distincts == 0 then return nil, false end
  if #distincts == 1 then return distincts[1], false end
  if depuis_scene_active then return depuis_scene_active, false end
  return nil, true -- plusieurs jetons, la scène courante ne départage pas
end

--[[ Force un rechargement complet de la source, cache vidé — le même bouton
     que « Actualiser (sans cache) » dans ses propriétés. Ceinture et
     bretelles : `obs_source_update` a suffi chez JA (permission bien
     appliquée sans lui), mais rien ne garantit qu'une page déjà chargée
     réagisse toujours à un changement de réglages sans rechargement forcé —
     `outils/obs-setup.mjs` s'en méfiait déjà pour la même raison. Séquence
     vérifiée dans un script Lua public (rse/obs-scripts,
     refresh-browser-sources.lua) avant de l'écrire ici. ]]
local function forcer_rechargement(src)
  local props = obs.obs_source_properties(src)
  local bouton = obs.obs_properties_get(props, "refreshnocache")
  obs.obs_property_button_clicked(bouton, src)
  obs.obs_properties_destroy(props)
end

--[[ Applique l'adresse et la permission à une source déjà existante, sans
     toucher à son nom, sa taille ni sa position — un streamer qui a placé sa
     source à la main a peut-être passé du temps dessus. ]]
local function actualiser_source(src, url)
  local s = obs.obs_data_create()
  obs.obs_data_set_string(s, "url", url)
  obs.obs_data_set_int(s, "webpage_control_level", CONTROL_LEVEL_READ_OBS)
  obs.obs_source_update(src, s)
  obs.obs_data_release(s)
  forcer_rechargement(src)
end

--[[ Crée une nouvelle source à la position/taille données. ]]
local function creer_source(scene, nom, url, largeur, hauteur, x, y)
  local s = obs.obs_data_create()
  obs.obs_data_set_string(s, "url", url)
  obs.obs_data_set_int(s, "width", largeur)
  obs.obs_data_set_int(s, "height", hauteur)
  obs.obs_data_set_bool(s, "shutdown", true)
  obs.obs_data_set_bool(s, "reroute_audio", false)
  obs.obs_data_set_int(s, "webpage_control_level", CONTROL_LEVEL_READ_OBS)

  local source = obs.obs_source_create("browser_source", nom, s, nil)
  local item = obs.obs_scene_add(scene, source)
  if item then
    local pos = obs.vec2()
    pos.x = x
    pos.y = y
    obs.obs_sceneitem_set_pos(item, pos)
  end
  obs.obs_source_release(source)
  obs.obs_data_release(s)
end

--[[ Pose ou met à jour les sources, et rend un compte-rendu — affiché dans
     le panneau après le clic, pas seulement dans le journal des scripts,
     pour qu'on sache ce qui vient de se passer sans aller le chercher.

     Deux passes, dans cet ordre :
     1. reconnaître par URL ce qui existe déjà dans la scène courante (quel
        que soit son nom) et le mettre à jour EN PLACE, sans jamais changer
        le type d'un widget trouvé ;
     2. compléter ce qui manque — l'overlay toujours, les widgets du lot par
        défaut seulement si la scène n'en a encore aucun. ]]
local function installer()
  if reglages.jeton == "" then
    return "Collez d'abord la clé de ce Gotchi (page Direct ▸ OBS et Twitch) ci-dessus."
  end

  local video = obs.obs_video_info()
  local largeur, hauteur = 1920, 1080
  if obs.obs_get_video_info(video) then
    largeur, hauteur = video.base_width, video.base_height
  end

  local source_scene = obs.obs_frontend_get_current_scene()
  if not source_scene then return "Aucune scène courante — impossible d'installer." end
  local nom_scene = obs.obs_source_get_name(source_scene)
  local scene = obs.obs_scene_from_source(source_scene)

  local ajoutees, mises_a_jour = 0, 0
  local overlay_trouve = false
  local types_trouves = {}

  local items = obs.obs_scene_enum_items(scene)
  for _, item in ipairs(items) do
    local src = obs.obs_sceneitem_get_source(item)
    if obs.obs_source_get_id(src) == "browser_source" then
      local s = obs.obs_source_get_settings(src)
      local url = obs.obs_data_get_string(s, "url")
      obs.obs_data_release(s)

      if est_overlay(url) then
        overlay_trouve = true
        actualiser_source(src, adresse("overlay.html"))
        mises_a_jour = mises_a_jour + 1
      else
        local t = type_widget(url)
        if t then
          types_trouves[t] = true
          actualiser_source(src, adresse(chemin_widget(t)))
          mises_a_jour = mises_a_jour + 1
        end
      end
    end
  end
  obs.sceneitem_list_release(items)

  if not overlay_trouve then
    creer_source(scene, "LiveGotchi", adresse("overlay.html"), largeur, hauteur, 0, 0)
    ajoutees = ajoutees + 1
  end

  if next(types_trouves) == nil then
    -- Aucun widget encore posé : c'est une scène neuve, on pose le lot par
    -- défaut. Dès qu'un seul widget existe, cette branche ne joue plus — voir
    -- le commentaire sur `TYPES_PAR_DEFAUT`.
    local y = 40
    for _, t in ipairs(TYPES_PAR_DEFAUT) do
      local w = WIDGETS[t]
      creer_source(scene, w.nom, adresse(chemin_widget(t)), w.l, w.h, 40, y)
      y = y + w.h + 16
      ajoutees = ajoutees + 1
    end
  end

  obs.obs_source_release(source_scene)

  return string.format(
    "Scène « %s » : %d source%s ajoutée%s, %d mise%s à jour.",
    nom_scene, ajoutees, ajoutees ~= 1 and "s" or "", ajoutees ~= 1 and "s" or "",
    mises_a_jour, mises_a_jour ~= 1 and "s" or "")
end

--[[ --------------------------------------------------------- propriétés OBS ]]

--[[ Rempli automatiquement dès qu'un jeton a été retrouvé dans une source
     existante ; sinon reste vide, et c'est le champ à coller qui sert. ]]
local dernier_resultat = ""
local decouverte_tentee = false

function script_description()
  return [[<b>LiveGotchi</b><br/>
Clique « Installer LiveGotchi » : l'overlay et les widgets déjà présents dans
la scène courante (même posés à la main, même mal nommés) sont retrouvés par
leur adresse et mis à jour ; s'il n'y en a encore aucun, un lot de départ est
posé (évolution, besoin, jauges, identité). Reconnaît aussi la permission
« Lecture des données OBS », pour que le direct se compte sans mot de passe.
Le jeton d'overlay se reprend tout seul si une source overlay existe déjà
quelque part dans tes scènes ; sinon, colle « La clé de ce Gotchi » depuis la page
Direct ▸ OBS et Twitch.<br/><br/>
Ne montre pas ces adresses à l'écran : elles contiennent ton jeton.]]
end

function script_properties()
  local p = obs.obs_properties_create()
  obs.obs_properties_add_text(p, "api", "Adresse du site", obs.OBS_TEXT_DEFAULT)
  obs.obs_properties_add_text(p, "jeton", "Jeton d'overlay (Direct ▸ OBS et Twitch)", obs.OBS_TEXT_PASSWORD)
  obs.obs_properties_add_button(p, "installer", "Installer LiveGotchi", function()
    dernier_resultat = installer()
    obs.script_log(obs.LOG_INFO, "LiveGotchi : " .. dernier_resultat)
    return true -- redessine le panneau, pour que le texte ci-dessous apparaisse tout de suite
  end)
  if dernier_resultat ~= "" then
    obs.obs_properties_add_text(p, "resultat", dernier_resultat, obs.OBS_TEXT_INFO)
  end
  return p
end

function script_defaults(settings)
  obs.obs_data_set_default_string(settings, "api", "https://livegotchi.com")
end

function script_update(settings)
  reglages.api = obs.obs_data_get_string(settings, "api")
  reglages.jeton = obs.obs_data_get_string(settings, "jeton")

  if reglages.jeton == "" and not decouverte_tentee then
    decouverte_tentee = true
    local j, ambigu = jeton_depuis_les_scenes()
    if j then
      reglages.jeton = j
      obs.obs_data_set_string(settings, "jeton", j)
      obs.script_log(obs.LOG_INFO, "LiveGotchi : jeton repris automatiquement depuis une source overlay déjà posée.")
    elseif ambigu then
      obs.script_log(obs.LOG_WARNING,
        "LiveGotchi : plusieurs sources overlay portent des jetons différents — impossible de choisir sans risque, collez le bon vous-même.")
    end
  end
end
