// Petite équipe de processus ouvriers, réutilisés d'une photo à l'autre.
// Un ouvrier qui meurt est remplacé, et la photo qui l'a tué est signalée
// comme telle plutôt que d'emporter le serveur avec elle.

import { fork } from 'node:child_process';
import path from 'node:path';
import os from 'node:os';
import { fileURLToPath } from 'node:url';

const SCRIPT = path.join(path.dirname(fileURLToPath(import.meta.url)), 'ouvrier.mjs');
const EFFECTIF = Math.max(1, Math.min(3, os.cpus().length - 1));
const DELAI_MAX = 5 * 60 * 1000;

const disponibles = [];
const attente = [];
let arretDemande = false;

function embaucher() {
  const ouvrier = fork(SCRIPT, {
    stdio: ['ignore', 'inherit', 'inherit', 'ipc'],
    // Une fois l'outil empaqueté, le programme lancé est Electron et non Node.
    // Cette variable lui demande de se comporter en simple Node : sans elle,
    // chaque ouvrier ouvrirait une nouvelle fenêtre de l'application.
    env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' },
  });
  ouvrier.tache = null;

  ouvrier.on('message', (reponse) => {
    const tache = ouvrier.tache;
    ouvrier.tache = null;
    if (!tache) return;
    clearTimeout(tache.minuteur);
    liberer(ouvrier);
    if (reponse.ok) tache.resoudre(reponse);
    else tache.rejeter(new Error(reponse.erreur));
  });

  ouvrier.on('exit', (code, signal) => {
    const tache = ouvrier.tache;
    ouvrier.tache = null;
    if (tache) {
      clearTimeout(tache.minuteur);
      tache.rejeter(new Error(
        "cette image a fait tomber le décodeur d'images "
        + `(${signal || 'code ' + code}) — elle est probablement corrompue, ou trop lourde pour la mémoire disponible`,
      ));
    }
    const rang = disponibles.indexOf(ouvrier);
    if (rang >= 0) disponibles.splice(rang, 1);
    if (!arretDemande) liberer(embaucher());
  });

  return ouvrier;
}

function liberer(ouvrier) {
  const suivante = attente.shift();
  if (suivante) confier(ouvrier, suivante);
  else disponibles.push(ouvrier);
}

function confier(ouvrier, tache) {
  ouvrier.tache = tache;
  tache.minuteur = setTimeout(() => {
    if (ouvrier.tache === tache) {
      ouvrier.tache = null;
      tache.rejeter(new Error('traitement interrompu : plus de cinq minutes sur cette image'));
      ouvrier.kill();
    }
  }, DELAI_MAX);
  ouvrier.send(tache.travail);
}

/** Confie une photo à un ouvrier libre, ou attend qu'il s'en libère un. */
export function traiterIsole(travail) {
  return new Promise((resoudre, rejeter) => {
    const tache = { travail, resoudre, rejeter, minuteur: null };
    const ouvrier = disponibles.shift();
    if (ouvrier) confier(ouvrier, tache);
    else attente.push(tache);
  });
}

export function demarrerEquipe() {
  for (let i = 0; i < EFFECTIF; i += 1) disponibles.push(embaucher());
  return EFFECTIF;
}

export function arreterEquipe() {
  arretDemande = true;
  for (const o of disponibles) o.kill();
}
