-- texecole-prefetch.lua — balayage précoce : soumet les calculs SymPy du
-- document avant le chargement des paquets LaTeX (calcul pendant que TeX
-- charge fontspec/tcolorbox/pgfplots) et détecte les paquets réellement
-- utilisés. Optimisation seule : tout échec retombe sur le comportement
-- normal (chargement complet, calcul à la demande).

local M = {}

local function trim(s) return (s:gsub("^%s+", ""):gsub("%s+$", "")) end

-- <fn f(x) = expr [attr:{...} ...]> : l'expression s'arrête au premier
-- attribut, comme dans sl.fn_parse.
local function extraire_fonctions(texte)
  local fns = {}
  for nom, var, def in texte:gmatch(
      "<fn%s+([%a_][%w_]*)%s*%(%s*([%a_][%w_]*)%s*%)%s*=%s*([^>]*)>") do
    local expr = def
    local p = expr:find("%f[%S][%a_][%w_]*:")
    if p then expr = expr:sub(1, p - 1) end
    fns[nom] = { var = var, expr = trim(expr) }
  end
  return fns
end

-- Chaque gestionnaire reproduit à l'octet près les arguments que le
-- bloc réel passera à CALC_SYMPY, pour toucher le même cache.
local function soumettre_calculs(texte, S)
  local fns = extraire_fonctions(texte)

  for nom in texte:gmatch("<vartab%s+([%a_][%w_]*)%s*>") do
    local d = fns[nom]
    if d then pcall(S.variations, d.expr, d.var) end
  end

  for nom in texte:gmatch("<signtab%s+([%a_][%w_]*)%s*>") do
    local d = fns[nom]
    if d then
      local okA, A = pcall(require, "texecole-affine")
      local affine = okA and A and A.parse_product
                     and (A.parse_product(d.expr, d.var)) or nil
      if not affine then pcall(S.signes, d.expr, d.var) end
    end
  end

  local blocs = {
    sympyderiv = function(m, c)
      local name, order, var = m:match("^(%S+)%s+(%d+)%s+(%S+)$")
      if not name then name, var = m:match("^(%S+)%s+(%S+)$"); order = "1" end
      S.derivative(c, var or "x", tonumber(order) or 1)
    end,
    sympyzeros = function(m, c)
      S.zeros(c, m:match("^%S+%s+(%S+)$") or "x")
    end,
    sympyprim = function(m, c)
      S.primitive(c, m:match("^%S+%s+(%S+)$") or "x")
    end,
    sympyexpand = function(m, c) S.expand(c) end,
    sympyapart = function(m, c) S.apart(c, "x") end,
    sympycanon = function(m, c) S.canonical(c, "x") end,
    sympyfactor = function(m, c) S.factor(c) end,
    sympysimplify = function(m, c) S.simplify_expr(c) end,
    sympylimit = function(m, c)
      local _, var, pt, dir = m:match("^(%S+)%s+(%S+)%s+(%S+)%s*(%S*)$")
      if var then S.limite(c, var, pt, dir ~= "" and dir or nil) end
    end,
    sympydl = function(m, c)
      local _, var, a, n = m:match("^(%S+)%s+(%S+)%s+(%S+)%s+(%d+)$")
      if var then S.dl(c, var, a, tonumber(n)) end
    end,
    sympynint = function(m, c)
      local _, var, a, b = m:match("^(%S+)%s+(%S+)%s+(%S+)%s+(%S+)$")
      if var then S.nintegrate(c, var, a, b) end
    end,
    sympysolveeq = function(m, c)
      local lhs, rhs = c:match("^(.-)=(.+)$")
      if lhs then S.solve_eq(lhs, rhs, "x", m:match("^(%S+)")) end
    end,
    sympysum = function(m, c)
      local var, a, b = m:match("^(%S+)%s+(%S+)%s+(%S+)$")
      if var then S.closed_sum("sum", c, var, a, b) end
    end,
    sympyprod = function(m, c)
      local var, a, b = m:match("^(%S+)%s+(%S+)%s+(%S+)$")
      if var then S.closed_sum("prod", c, var, a, b) end
    end,
  }

  for tag, mots, contenu in texte:gmatch("<(sympy%a+)%s*([^>]*)>%s*{(.-)}") do
    local h = blocs[tag]
    if h then pcall(h, trim(mots), trim(contenu)) end
  end
end

-- "1" (charger) est la valeur sûre ; "0" seulement si le source est lu,
-- traduit, et sans trace de la fonctionnalité.
local function detecter_besoins(source, texte)
  local besoins = { plots = "1", tab = "1", poster = "1", tableau = "1" }
  if not texte then return besoins end
  besoins.plots = (texte:find("<plot", 1, true) or texte:find("<stats", 1, true)
    or source:find("pgfplots", 1, true) or source:find("begin{axis}", 1, true))
    and "1" or "0"
  besoins.tab = (texte:find("<vartab", 1, true) or texte:find("<signtab", 1, true)
    or source:find("tkzTab", 1, true)) and "1" or "0"
  besoins.poster = (texte:find("<grid", 1, true)
    or source:find("tcbposter", 1, true)) and "1" or "0"
  -- « <table » suivi d'un espace ou de « > » : le tag des tableaux, sans
  -- confondre « <tableofcontents> » (table des matières, pas de tblr).
  besoins.tableau = (texte:find("<table[%s>]")
    or source:find("begin{tblr}", 1, true)) and "1" or "0"
  return besoins
end

-- Sans deuxième cœur, soumettre tôt ne fait que disputer le processeur.
local function machine_multicoeur()
  local f = io.open("/proc/cpuinfo", "r")
  if not f then return true end
  local n = 0
  for ligne in f:lines() do
    if ligne:match("^processor%s*:") then n = n + 1 end
  end
  f:close()
  return n ~= 1
end

-- Point d'entrée, appelé depuis texecole.cls. Ne lève jamais d'erreur.
function M.lancer()
  local source, texte
  pcall(function()
    local f = io.open(tex.jobname .. ".tex", "r")
    if not f then return end
    source = f:read("*a")
    f:close()
    local corps = source:match("\\begin%s*{document}(.*)\\end%s*{document}")
    if not corps then source = nil; return end
    local L = require("texecole-lecteur")
    texte = L.traduire(corps)
  end)

  if source then
    local besoins = detecter_besoins(source, texte)
    pcall(token.set_macro, "texecole@besoinplots", besoins.plots)
    pcall(token.set_macro, "texecole@besointab", besoins.tab)
    pcall(token.set_macro, "texecole@besoinposter", besoins.poster)
    pcall(token.set_macro, "texecole@besointableau", besoins.tableau)
  end

  if texte and machine_multicoeur()
     and (texte:find("<sympy", 1, true) or texte:find("<vartab", 1, true)
          or texte:find("<signtab", 1, true)) then
    pcall(function()
      local fabrique = require("texecole-sympy")
      local bidon = { register_tag = function() end,
                      register_block = function() end }
      local S = fabrique(bidon)
      if S.prefetch_debut then
        S.prefetch_debut()
        pcall(soumettre_calculs, texte, S)
        S.prefetch_fin()
      end
    end)
  end
end

return M
