// <=== CONSTANTS and GLOBALS ===>

const TIMEZONE = "Europe/Paris";
let url;

function getUrl() {
  if (!url) {
    url = SCO_URL.substring(0, SCO_URL.lastIndexOf("/"));
  }
  return url;
}

//Les valeurs par défaut de la timeline (8h -> 18h)
let currentValues = [8.0, 10.0];

//Objet stockant les étudiants et les assiduités
let etuds = {};
let assiduites = {};
let justificatifs = {};

// Variable qui définit si le processus d'action de masse est lancé
let currentMassAction = false;
let currentMassActionEtat = undefined;

/**
 * Ajout d'une fonction `capitalize` sur tous les strings
 * alice.capitalize() -> Alice
 */
Object.defineProperty(String.prototype, "capitalize", {
  value: function () {
    return this.charAt(0).toUpperCase() + this.slice(1).toLowerCase();
  },
  enumerable: false,
});
// <<== Outils ==>>
Object.defineProperty(Array.prototype, "reversed", {
  value: function () {
    return [...this].map(this.pop, this);
  },
  enumerable: false,
});

/**
 * Ajout des évents sur les boutons d'assiduité
 * @param {Document | HTMLFieldSetElement} parent par défaut le document, un field sinon
 */
function setupCheckBox(parent = document) {
  const checkboxes = Array.from(parent.querySelectorAll(".rbtn"));
  checkboxes.forEach((box) => {
    box.addEventListener("click", (event) => {
      if (!uniqueCheckBox(box)) {
        event.preventDefault();
      }
      if (!box.parentElement.classList.contains("mass")) {
        assiduiteAction(box);
      }
    });
  });
}

/**
 * Validation préalable puis désactivation des chammps :
 * - Groupe
 * - Module impl
 * - Date
 */
function validateSelectors(btn) {
  const action = () => {
    const group_ids = getGroupIds();

    etuds = {};
    group_ids.forEach((group_id) => {
      sync_get(
        getUrl() + `/api/group/${group_id}/etudiants`,
        (data, status) => {
          if (status === "success") {
            data.forEach((etud) => {
              if (!(etud.id in etuds)) {
                etuds[etud.id] = etud;
              }
            });
          }
        }
      );
    });

    if (getModuleImplId() == null && window.forceModule) {
      const HTML = `
      <p>Attention, le module doit obligatoirement être renseigné.</p>
      <p>Cela vient de la configuration du semestre ou plus largement du département.</p>
      <p>Si c'est une erreur, veuillez voir avec le ou les responsables de votre scodoc.</p>
       `;

      const content = document.createElement("div");
      content.innerHTML = HTML;

      openAlertModal("Sélection du module", content);
      return;
    }

    getAssiduitesFromEtuds(true);

    document.querySelector(".selectors").disabled = true;
    generateMassAssiduites();
    generateAllEtudRow();
    btn.remove();
    onlyAbs();
  };

  if (!verifyDateInSemester()) {
    const HTML = `
    <p>Attention, la date sélectionnée n'est pas comprise dans le semestre.</p>
    <p>Cette page permet l'affichage et la modification des assiduités uniquement pour le semestre sélectionné.</p>
    <p>Vous n'aurez donc pas accès aux assiduités.</p>
    <p>Appuyer sur "Valider" uniquement si vous souhaitez poursuivre sans modifier la date.</p>
     `;

    const content = document.createElement("div");
    content.innerHTML = HTML;

    openPromptModal("Vérification de la date", content, action);
    return;
  }

  action();
}

function onlyAbs() {
  if (getDate() > moment()) {
    document
      .querySelectorAll(".rbtn.present, .rbtn.retard")
      .forEach((el) => el.remove());
  }
}

/**
 * Limite le nombre de checkbox marquée
 * Vérifie aussi si le cliqué est fait sur des assiduités conflictuelles
 * @param {HTMLInputElement} box la checkbox utilisée
 * @returns {boolean} Faux si il y a un conflit d'assiduité, Vrai sinon
 */
function uniqueCheckBox(box) {
  const type = box.parentElement.getAttribute("type") === "conflit";
  if (!type) {
    const checkboxs = Array.from(box.parentElement.children);

    checkboxs.forEach((chbox) => {
      if (chbox.checked && chbox.value !== box.value) {
        chbox.checked = false;
      }
    });
    return true;
  }

  return false;
}

/**
 * Fait une requête GET de façon synchrone
 * @param {String} path adresse distante
 * @param {CallableFunction} success fonction à effectuer en cas de succès
 * @param {CallableFunction} errors fonction à effectuer en cas d'échec
 */
function sync_get(path, success, errors) {
  $.ajax({
    async: false,
    type: "GET",
    url: path,
    success: success,
    error: errors,
  });
}
/**
 * Fait une requête POST de façon synchrone
 * @param {String} path adresse distante
 * @param {object} data données à envoyer (objet js)
 * @param {CallableFunction} success fonction à effectuer en cas de succès
 * @param {CallableFunction} errors fonction à effectuer en cas d'échec
 */
function sync_post(path, data, success, errors) {
  $.ajax({
    async: false,
    type: "POST",
    url: path,
    data: JSON.stringify(data),
    success: success,
    error: errors,
  });
}
/**
 * Fait une requête POST de façon asynchrone
 * @param {String} path adresse distante
 * @param {object} data données à envoyer (objet js)
 * @param {CallableFunction} success fonction à effectuer en cas de succès
 * @param {CallableFunction} errors fonction à effectuer en cas d'échec
 */
function async_post(path, data, success, errors) {
  return $.ajax({
    async: true,
    type: "POST",
    url: path,
    data: JSON.stringify(data),
    success: success,
    error: errors,
  });
}
// <<== Gestion des actions de masse ==>>
const massActionQueue = new Map();

/**
 * Cette fonction remet à zero la gestion des actions de masse
 */
function resetMassActionQueue() {
  massActionQueue.set("supprimer", []);
  massActionQueue.set("editer", []);
  massActionQueue.set("creer", []);
}

/**
 * Fonction pour alimenter la queue des actions de masse
 * @param {String} type Le type de queue ("creer", "supprimer", "editer")
 * @param {*} obj L'objet qui sera utilisé par les API
 */
function addToMassActionQueue(type, obj) {
  massActionQueue.get(type)?.push(obj);
}

/**
 * Fonction pour exécuter les actions de masse
 */
function executeMassActionQueue() {
  if (!currentMassAction) return;

  //Récupération des queues
  const toCreate = massActionQueue.get("creer");
  const toEdit = massActionQueue.get("editer");
  const toDelete = massActionQueue.get("supprimer");

  //Fonction qui créé les assidutiés de la queue "creer"
  const create = () => {
    /**
     * Création du template de l'assiduité
     *
     * {
     *   date_debut: #debut_timeline,
     *   date_fin: #fin_timeline,
     *   moduleimpl_id ?: <>
     * }
     */
    const tlTimes = getTimeLineTimes();
    const assiduite = {
      date_debut: tlTimes.deb.format(),
      date_fin: tlTimes.fin.format(),
    };
    const moduleimpl = getModuleImplId();

    if (moduleimpl !== null) {
      assiduite["moduleimpl_id"] = moduleimpl;
    }

    const createQueue = []; //liste des assiduités qui seront créées.

    /**
     * Pour chaque état de la queue 'creer' on génère une
     * assiduitée précise depuis le template
     */
    toCreate.forEach((obj) => {
      const curAssiduite = structuredClone(assiduite);
      curAssiduite.etudid = obj.etudid;
      curAssiduite.etat = obj.etat;

      createQueue.push(curAssiduite);
    });

    /**
     * On envoie les données à l'API
     */
    const path = getUrl() + `/api/assiduites/create`;
    sync_post(
      path,
      createQueue,
      (data, status) => {
        //success
      },
      (data, status) => {
        //error
        console.error(data, status);
      }
    );
    return createQueue.length;
  };

  //Fonction qui modifie les assiduités de la queue 'edition'
  const edit = () => {
    //On ajoute le moduleimpl (s'il existe) aux assiduités à modifier
    const editQueue = toEdit.map((assiduite) => {
      const moduleimpl = getModuleImplId();
      if (moduleimpl !== null) {
        assiduite["moduleimpl_id"] = moduleimpl;
      }
      return assiduite;
    });

    const path = getUrl() + `/api/assiduites/edit`;
    sync_post(
      path,
      editQueue,
      (data, status) => {
        //success
      },
      (data, status) => {
        //error
        console.error(data, status);
      }
    );
    return editQueue.length;
  };

  //Fonction qui supprime les assiduités de la queue 'supprimer'
  const supprimer = () => {
    const path = getUrl() + `/api/assiduite/delete`;
    sync_post(
      path,
      toDelete,
      (data, status) => {
        //success
      },
      (data, status) => {
        //error
        console.error(data, status);
      }
    );
    return toDelete.length;
  };

  //On exécute les fonctions de queue
  let count = 0;
  if (currentMassActionEtat == "remove") {
    count += supprimer();
    const span = document.createElement("span");
    if (count > 0) {
      span.innerHTML = `${count} assiduités ont été supprimées.`;
    } else {
      span.innerHTML = `Aucune assiduité n'a été supprimée.`;
    }
    pushToast(
      generateToast(
        span,
        getToastColorFromEtat(currentMassActionEtat.toUpperCase()),
        5
      )
    );
  } else {
    count += create();
    count += edit();
    const etat =
      currentMassActionEtat.toUpperCase() == "RETARD"
        ? "En retard"
        : currentMassActionEtat;
    const span = document.createElement("span");
    if (count > 0) {
      span.innerHTML = `${count} étudiants ont été mis <u><strong>${etat
        .capitalize()
        .trim()}</strong></u>`;
    } else {
      span.innerHTML = `Aucun étudiant n'a été mis <u><strong>${etat
        .capitalize()
        .trim()}</strong></u>`;
    }
    pushToast(
      generateToast(
        span,
        getToastColorFromEtat(currentMassActionEtat.toUpperCase()),
        5
      )
    );
  }
  //On récupère les assiduités puis on regénère les lignes d'étudiants
  getAssiduitesFromEtuds(true);
  generateAllEtudRow();
}
/**
 * Processus de peuplement des queues
 * puis d'exécution
 */
function massAction() {
  //On récupère tous les boutons d'assiduités
  const fields = Array.from(document.querySelectorAll(".btns_field.single"));
  //On récupère l'état de l'action de masse
  currentMassActionEtat = getAssiduiteValue(
    document.querySelector(".btns_field.mass")
  );

  //On remet à 0 les queues
  resetMassActionQueue();

  //on met à vrai la variable pour la suite
  currentMassAction = true;

  //On affiche le "loader" le temps du processus
  showLoader();

  //On timeout 0 pour le mettre à la fin de l'event queue de JS
  setTimeout(() => {
    const conflicts = [];
    /**
     * Pour chaque étudiant :
     * On vérifie s'il y a un conflit -> on place l'étudiant dans l'array conflicts
     * Sinon -> on fait comme si l'utilisateur cliquait sur le bouton d'assiduité
     */
    fields.forEach((field) => {
      if (field.getAttribute("type") != "conflit") {
        if (currentMassActionEtat != "remove") {
          field.querySelector(`.rbtn.${currentMassActionEtat}`).click();
        } else {
          field.querySelector(".rbtn.absent").click();
        }
      } else {
        const etudid = field.getAttribute("etudid");
        conflicts.push(etuds[parseInt(etudid)]);
      }
    });

    //on exécute les queues puis on cache le loader
    executeMassActionQueue();
    hideLoader();

    //Fin du processus, on remet à false
    currentMassAction = false;
    currentMassActionEtat = undefined;

    //On remet à zero les boutons d'assiduité de masse
    const boxes = Array.from(
      document.querySelector(".btns_field.mass").querySelectorAll(".rbtn")
    );
    boxes.forEach((box) => {
      box.checked = false;
    });

    //Si il y a des conflits d'assiduité, on affiche la liste dans une alert
    if (conflicts.length > 0) {
      const div = document.createElement("div");
      const sub = document.createElement("p");
      sub.textContent =
        "L'assiduité des étudiants suivant n'a pas pu être modifiée";
      div.appendChild(sub);
      const ul = document.createElement("ul");
      conflicts.forEach((etu) => {
        const li = document.createElement("li");
        li.textContent = `${etu.nom} ${etu.prenom.capitalize()}`;
        ul.appendChild(li);
      });
      div.appendChild(ul);
      openAlertModal("Conflits d'assiduités", div, "");
    }
  }, 0);
}

/**
 * On génère les boutons d'assiduités de masse
 * puis on ajoute les événements associés
 */
function generateMassAssiduites() {
  const content = document.getElementById("content");

  const mass = document.createElement("div");
  mass.className = "mass-selection";
  mass.innerHTML = `
    <span>Mettre tout le monde :</span>
    <fieldset class="btns_field mass">
        <input type="checkbox" value="present" name="mass_btn_assiduites" id="mass_rbtn_present"
            class="rbtn present">
        <input type="checkbox" value="retard" name="mass_btn_assiduites" id="mass_rbtn_retard" class="rbtn retard">
        <input type="checkbox" value="absent" name="mass_btn_assiduites" id="mass_rbtn_absent" class="rbtn absent">
        <input type="checkbox" value="remove" name="mass_btn_assiduites" id="mass_rbtn_aucun" class="rbtn aucun">
    </fieldset>`;

  content.insertBefore(mass, content.querySelector(".etud_holder"));

  const mass_btn = Array.from(mass.querySelectorAll(".rbtn"));
  mass_btn.forEach((btn) => {
    btn.addEventListener("click", () => {
      massAction();
    });
  });

  if (!verifyDateInSemester()) {
    content.querySelector(".btns_field.mass").setAttribute("disabled", "true");
  }
}

/**
 * Affichage du loader
 */
function showLoader() {
  document.getElementById("loaderContainer").style.display = "block";
}
/**
 * Dissimulation du loader
 */
function hideLoader() {
  document.getElementById("loaderContainer").style.display = "none";
}

// <<== Gestion du temps ==>>

/**
 * Transforme un temps numérique en string
 * 8.75 -> 08h45
 * @param {number} time Le temps (float)
 * @returns {string} le temps (string)
 */
function toTime(time) {
  let heure = Math.floor(time);
  let minutes = Math.round((time - heure) * 60);
  if (minutes < 10) {
    minutes = `0${minutes}`;
  }
  if (heure < 10) {
    heure = `0${heure}`;
  }
  return `${heure}h${minutes}`;
}
/**
 * Transforme une date iso en une date lisible:
 * new Date('2023-03-03') -> "vendredi 3 mars 2023"
 * @param {Date} date
 * @param {object} styles
 * @returns
 */
function formatDate(date, styles = { dateStyle: "full" }) {
  return new Intl.DateTimeFormat("fr-FR", styles).format(date);
}

/**
 * Met à jour la date visible sur la page en la formatant
 */
function updateDate() {
  const dateInput = document.querySelector("#tl_date");

  const date = dateInput.valueAsDate;

  if (!verifyNonWorkDays(date.getDay(), nonWorkDays)) {
    $("#datestr").text(formatDate(date).capitalize());
    dateInput.setAttribute("value", date.toISOString().split("T")[0]);
    return true;
  } else {
    const att = document.createTextNode(
      "Le jour sélectionné n'est pas un jour travaillé."
    );
    openAlertModal("Erreur", att, "", "crimson");
    dateInput.value = dateInput.getAttribute("value");
    return false;
  }
}

function verifyDateInSemester() {
  const date = new moment.tz(
    document.querySelector("#tl_date").value,
    TIMEZONE
  );

  const periodSemester = getFormSemestreDates();

  return date.isBetween(
    periodSemester.deb,
    periodSemester.fin,
    undefined,
    "[]"
  );
}

/**
 * Ajoute la possibilité d'ouvrir le calendrier
 * lorsqu'on clique sur la date
 */
function setupDate(onchange = null) {
  const datestr = document.querySelector("#datestr");
  const input = document.querySelector("#tl_date");

  datestr.addEventListener("click", () => {
    if (!input.disabled) {
      input.showPicker();
    }
  });

  if (onchange != null) {
    input.addEventListener("change", onchange);
  }
}

/**
 * GetAssiduitesOnDateChange
 * (Utilisé uniquement avec étudiant unique)
 */

function getAssiduitesOnDateChange() {
  if (!isSingleEtud()) return;
  actualizeEtud(etudid);
}
/**
 * Transforme une date iso en date intelligible
 * @param {String} str date iso
 * @param {String} separator le séparateur de la date intelligible (01/01/2000 {separtor} 10:00)
 * @returns {String} la date intelligible
 */
function formatDateModal(str, separator = "·") {
  return new moment.tz(str, TIMEZONE).format(`DD/MM/Y ${separator} HH:mm`);
}

/**
 * Vérifie si la date sélectionnée n'est pas un jour non travaillé
 * Renvoie Vrai si le jour est non travaillé
 */
function verifyNonWorkDays(day, nonWorkdays) {
  let d = "";
  switch (day) {
    case 0:
      d = "dim";
      break;
    case 1:
      d = "lun";
      break;
    case 2:
      d = "mar";
      break;
    case 3:
      d = "mer";
      break;
    case 4:
      d = "jeu";
      break;
    case 5:
      d = "ven";
      break;
    case 6:
      d = "sam";
      break;
  }

  return nonWorkdays.indexOf(d) != -1;
}

/**
 * Fonction qui vérifie si une période est dans un interval
 * Objet période / interval
 * {
 *      deb: moment.tz(<Date>),
 *      fin: moment.tz(<Date>),
 * }
 * @param {object} period
 * @param {object} interval
 * @returns {boolean} Vrai si la période est dans l'interval
 */
function hasTimeConflict(period, interval) {
  return period.deb.isBefore(interval.fin) && period.fin.isAfter(interval.deb);
}

/**
 * On récupère la période de la timeline
 * @returns {deb : moment.tz(), fin: moment.tz()}
 */
function getTimeLineTimes() {
  //getPeriodValues() -> retourne la position de la timeline [a,b] avec a et b des number
  let values = getPeriodValues();
  //On récupère la date
  const dateiso = document.querySelector("#tl_date").value;

  //On génère des objets temps (moment.tz)
  values = values.map((el) => {
    el = toTime(el).replace("h", ":");
    el = `${dateiso}T${el}`;
    return moment.tz(el, TIMEZONE);
  });

  return { deb: values[0], fin: values[1] };
}

/**
 * Vérification de l'égalité entre un conflit et la période de la timeline
 * @param {object} conflict
 * @returns {boolean} Renvoie Vrai si la période de la timeline est égal au conflit
 */
function isConflictSameAsPeriod(conflict, period = undefined) {
  const tlTimes = period == undefined ? getTimeLineTimes() : period;
  const clTimes = {
    deb: moment.tz(conflict.date_debut, TIMEZONE),
    fin: moment.tz(conflict.date_fin, TIMEZONE),
  };
  return tlTimes.deb.isSame(clTimes.deb) && tlTimes.fin.isSame(clTimes.fin);
}

/**
 * Retourne un objet Date de la date sélectionnée
 * @returns {Date} la date sélectionnée
 */
function getDate() {
  const date = new Date(
    document.querySelector("#tl_date").getAttribute("value")
  );
  date.setHours(0, 0, 0, 0);
  return date;
}

/**
 * Retourne un objet date représentant le jour suivant
 * @returns {Date} le jour suivant
 */
function getNextDate() {
  const date = getDate();
  const next = new Date(date.valueOf());
  next.setDate(date.getDate() + 1);
  next.setHours(0, 0, 0, 0);
  return next;
}
/**
 * Retourne un objet date représentant le jour précédent
 * @returns {Date} le jour précédent
 */
function getPrevDate() {
  const date = getDate();
  const next = new Date(date.valueOf());
  next.setDate(date.getDate() - 1);
  next.setHours(0, 0, 0, 0);
  return next;
}

/**
 * Transformation d'un objet Date en chaîne ISO
 * @param {Date} date
 * @returns {string} la date iso avec le timezone
 */
function toIsoString(date) {
  var tzo = -date.getTimezoneOffset(),
    dif = tzo >= 0 ? "+" : "-",
    pad = function (num) {
      return (num < 10 ? "0" : "") + num;
    };

  return (
    date.getFullYear() +
    "-" +
    pad(date.getMonth() + 1) +
    "-" +
    pad(date.getDate()) +
    "T" +
    pad(date.getHours()) +
    ":" +
    pad(date.getMinutes()) +
    ":" +
    pad(date.getSeconds()) +
    dif +
    pad(Math.floor(Math.abs(tzo) / 60)) +
    ":" +
    pad(Math.abs(tzo) % 60)
  );
}

/**
 * Transforme un temps numérique en une date moment.tz
 * @param {number} nb
 * @returns {moment.tz} Une date formée du temps donné et de la date courante
 */
function numberTimeToDate(nb) {
  time = toTime(nb).replace("h", ":");
  date = document.querySelector("#tl_date").value;

  datetime = `${date}T${time}`;

  return moment.tz(datetime, TIMEZONE);
}

// <<== Gestion des assiduités ==>>

/**
 * Récupère les assiduités des étudiants
 * en fonction de :
 *   - du semestre
 *   - de la date courant et du jour précédent.
 * @param {boolean} clear vidage de l'objet "assiduites" ou non
 * @returns {object} l'objets Assiduités {<etudid:str> : [<assiduite>,]}
 */
function getAssiduitesFromEtuds(clear, has_formsemestre = true, deb, fin) {
  const etudIds = Object.keys(etuds).join(",");
  const formsemestre_id = has_formsemestre
    ? `formsemestre_id=${getFormSemestreId()}&`
    : "";

  const date_debut = deb ? deb : toIsoString(getPrevDate());
  const date_fin = fin ? fin : toIsoString(getNextDate());

  if (clear) {
    assiduites = {};
  }

  const url_api =
    getUrl() +
    `/api/assiduites/group/query?date_debut=${date_debut}&${formsemestre_id}&date_fin=${date_fin}&etudids=${etudIds}`;
  sync_get(url_api, (data, status) => {
    if (status === "success") {
      const dataKeys = Object.keys(data);
      dataKeys.forEach((key) => {
        if (clear || !(key in assiduites)) {
          assiduites[key] = data[key];
        } else {
          assiduites[key] = assiduites[key].concat(data[key]);
        }
        let assi_ids = [];
        assiduites[key] = assiduites[key].reversed().filter((value) => {
          if (assi_ids.indexOf(value.assiduite_id) == -1) {
            assi_ids.push(value.assiduite_id);
            return true;
          }

          return false;
        });
      });
    }
  });
  return assiduites;
}

/**
 * Création d'une assiduité pour un étudiant
 * @param {String} etat l'état de l'étudiant
 * @param {Number | String} etudid l'identifiant de l'étudiant
 *
 * TODO : Rendre asynchrone
 */
function createAssiduite(etat, etudid) {
  const tlTimes = getTimeLineTimes();
  const assiduite = {
    date_debut: tlTimes.deb.format(),
    date_fin: tlTimes.fin.format(),
    etat: etat,
  };

  const moduleimpl = getModuleImplId();

  if (moduleimpl !== null) {
    assiduite["moduleimpl_id"] = moduleimpl;
  }

  const path = getUrl() + `/api/assiduite/${etudid}/create`;
  sync_post(
    path,
    [assiduite],
    (data, status) => {
      //success
      if (data.success.length > 0) {
        let obj = data.success["0"].assiduite_id;
      }
    },
    (data, status) => {
      //error
      console.error(data, status);
    }
  );
}

/**
 * Suppression d'une assiduité
 * @param {String | Number} assiduite_id l'identifiant de l'assiduité
 * TODO : Rendre asynchrone
 */
function deleteAssiduite(assiduite_id) {
  const path = getUrl() + `/api/assiduite/delete`;
  sync_post(
    path,
    [assiduite_id],
    (data, status) => {
      //success
      if (data.success.length > 0) {
        let obj = data.success["0"].assiduite_id;
      }
    },
    (data, status) => {
      //error
      console.error(data, status);
    }
  );
}

/**
 *
 * @param {String | Number} assiduite_id l'identifiant d'une assiduité
 * @param {String} etat l'état à modifier
 * @returns {boolean} si l'édition a fonctionné
 * TODO : Rendre asynchrone
 */
function editAssiduite(assiduite_id, etat) {
  const assiduite = {
    etat: etat,
    moduleimpl_id: getModuleImplId(),
  };
  const path = getUrl() + `/api/assiduite/${assiduite_id}/edit`;
  let bool = false;
  sync_post(
    path,
    assiduite,
    (data, status) => {
      bool = true;
    },
    (data, status) => {
      //error
      console.error(data, status);
    }
  );

  return bool;
}

/**
 * Récupération des assiduités conflictuelles avec la période de la time line
 * @param {String | Number} etudid identifiant de l'étudiant
 * @returns {Array[Assiduité]} un tableau d'assiduité
 */
function getAssiduitesConflict(etudid, periode) {
  const etudAssiduites = assiduites[etudid];
  if (!etudAssiduites) {
    return [];
  }
  if (!periode) {
    periode = getTimeLineTimes();
  }

  return etudAssiduites.filter((assi) => {
    const interval = {
      deb: moment.tz(assi.date_debut, TIMEZONE),
      fin: moment.tz(assi.date_fin, TIMEZONE),
    };
    return hasTimeConflict(periode, interval);
  });
}

/**
 * Récupération de la dernière assiduité du jour précédent
 * @param {String | Number} etudid l'identifiant de l'étudiant
 * @returns {Assiduité} la dernière assiduité du jour précédent
 */
function getLastAssiduiteOfPrevDate(etudid) {
  const etudAssiduites = assiduites[etudid];
  if (!etudAssiduites) {
    return "";
  }
  const period = {
    deb: moment.tz(getPrevDate(), TIMEZONE),
    fin: moment.tz(getDate(), TIMEZONE),
  };
  const prevAssiduites = etudAssiduites
    .filter((assi) => {
      const interval = {
        deb: moment.tz(assi.date_debut, TIMEZONE),
        fin: moment.tz(assi.date_fin, TIMEZONE),
      };

      return hasTimeConflict(period, interval);
    })
    .sort((a, b) => {
      const a_fin = moment.tz(a.date_fin, TIMEZONE);
      const b_fin = moment.tz(b.date_fin, TIMEZONE);
      return b_fin < a_fin;
    });

  if (prevAssiduites.length < 1) {
    return null;
  }

  return prevAssiduites.pop();
}

/**
 * Récupération de l'état appointé
 * @param {HTMLFieldSetElement} field le conteneur des boutons d'assiduité d'une ligne étudiant
 * @returns {String} l'état appointé : ('present','absent','retard', 'remove')
 *
 * état = 'remove' si le clic désélectionne une assiduité appointée
 */
function getAssiduiteValue(field) {
  const checkboxs = Array.from(field.children);
  let value = "remove";
  checkboxs.forEach((chbox) => {
    if (chbox.checked) {
      value = chbox.value;
    }
  });

  return value;
}

/**
 * Mise à jour des assiduités d'un étudiant
 * @param {String | Number} etudid identifiant de l'étudiant
 */
function actualizeEtudAssiduite(etudid, has_formsemestre = true) {
  const formsemestre_id = has_formsemestre
    ? `formsemestre_id=${getFormSemestreId()}&`
    : "";
  const date_debut = toIsoString(getPrevDate());
  const date_fin = toIsoString(getNextDate());

  const url_api =
    getUrl() +
    `/api/assiduites/${etudid}/query?${formsemestre_id}date_debut=${date_debut}&date_fin=${date_fin}`;
  sync_get(url_api, (data, status) => {
    if (status === "success") {
      assiduites[etudid] = data;
    }
  });
}

function getAllAssiduitesFromEtud(etudid, action) {
  const url_api = getUrl() + `/api/assiduites/${etudid}`;

  $.ajax({
    async: true,
    type: "GET",
    url: url_api,
    success: (data, status) => {
      if (status === "success") {
        assiduites[etudid] = data;
        action(data);
      }
    },
    error: () => {},
  });
}

/**
 * Déclenchement d'une action après appuie sur un bouton d'assiduité
 * @param {HTMLInputElement} element Bouton d'assiduité appuyé
 */
function assiduiteAction(element) {
  const field = element.parentElement;

  const type = field.getAttribute("type");
  const etudid = parseInt(field.getAttribute("etudid"));
  const assiduite_id = parseInt(field.getAttribute("assiduite_id"));
  const etat = getAssiduiteValue(field);

  // Cas de l'action de masse -> peuplement des queues
  if (currentMassAction) {
    if (currentMassActionEtat != "remove") {
      switch (type) {
        case "création":
          addToMassActionQueue("creer", { etat: etat, etudid: etudid });
          break;
        case "édition":
          if (etat != "remove") {
            addToMassActionQueue("editer", {
              etat: etat,
              assiduite_id: assiduite_id,
            });
          }
          break;
      }
    } else if (type == "édition") {
      addToMassActionQueue("supprimer", assiduite_id);
    }
  } else {
    // Cas normal -> mise à jour en base
    switch (type) {
      case "création":
        createAssiduite(etat, etudid);
        break;
      case "édition":
        if (etat === "remove") {
          deleteAssiduite(assiduite_id);
        } else {
          editAssiduite(assiduite_id, etat);
        }
        break;
      case "conflit":
        const conflitResolver = new ConflitResolver(
          assiduites[etudid],
          getTimeLineTimes(),
          {
            deb: new moment.tz(getDate(), TIMEZONE),
            fin: new moment.tz(getNextDate(), TIMEZONE),
          }
        );
        const update = (assi) => {
          actualizeEtud(assi.etudid);
        };
        conflitResolver.callbacks = {
          delete: update,
          edit: update,
          split: update,
        };

        conflitResolver.open();
        return;
    }

    if (type != "conflit") {
      let etatAffiche;

      switch (etat.toUpperCase()) {
        case "PRESENT":
          etatAffiche =
            "%etud% a été noté(e) <u><strong>présent(e)</strong></u>";
          break;
        case "RETARD":
          etatAffiche =
            "%etud% a été noté(e) <u><strong>en retard</strong></u>";
          break;
        case "ABSENT":
          etatAffiche =
            "%etud% a été noté(e) <u><strong>absent(e)</strong></u>";
          break;
        case "REMOVE":
          etatAffiche = "L'assiduité de %etud% a été retirée.";
      }

      const nom_prenom = `${etuds[etudid].nom.toUpperCase()} ${etuds[
        etudid
      ].prenom.capitalize()}`;
      const span = document.createElement("span");
      span.innerHTML = etatAffiche.replace("%etud%", nom_prenom);

      pushToast(
        generateToast(span, getToastColorFromEtat(etat.toUpperCase()), 5)
      );
    }

    actualizeEtud(etudid, !isSingleEtud);
  }
}

// <<== Gestion de l'affichage des barres étudiant ==>>

/**
 * Génère l'HTML lié à la barre d'un étudiant
 * @param {Etudiant} etud représentation objet d'un étudiant
 * @param {Number} index l'index de l'étudiant dans la liste
 * @param {AssiduitéMod} assiduite Objet représentant l'état de l'étudiant pour la période de la timeline
 * @returns {String} l'HTML généré
 */
function generateEtudRow(
  etud,
  index,
  assiduite = {
    etatAssiduite: "",
    type: "création",
    id: -1,
    date_debut: null,
    date_fin: null,
    prevAssiduites: "",
  }
) {
  // Génération des boutons du choix de l'assiduité
  let assi = "";
  ["present", "retard", "absent"].forEach((abs) => {
    if (abs.toLowerCase() === assiduite.etatAssiduite.toLowerCase()) {
      assi += `<input checked type="checkbox" value="${abs}" name="btn_assiduites_${index}" id="rbtn_${abs}" class="rbtn ${abs}">`;
    } else {
      assi += `<input type="checkbox" value="${abs}" name="btn_assiduites_${index}" id="rbtn_${abs}" class="rbtn ${abs}">`;
    }
  });
  const conflit = assiduite.type == "conflit" ? "conflit" : "";
  const pdp_url = `${getUrl()}/api/etudiant/etudid/${etud.id}/photo?size=small`;
  const HTML = `<div class="etud_row ${conflit}" id="etud_row_${etud.id}">
  
    <div class="index">${index}</div>
    <div class="name_field">
  
        <img class="pdp" src="${pdp_url}">
  
        <div class="name_set">
  
            <h4 class="nom">${etud.nom}</h4>
            <h5 class="prenom">${etud.prenom}</h5>
  
        </div>
  
    </div>
    <div class="assiduites_bar">
        <div id="prevDateAssi" class="${assiduite.prevAssiduites?.etat?.toLowerCase()}">
        </div>
    </div>
    <fieldset class="btns_field single" etudid="${etud.id}" assiduite_id="${
    assiduite.id
  }" type="${assiduite.type}">
  
        ${assi}
  
    </fieldset>
    
  
  </div>`;

  return HTML;
}

/**
 * Insertion de la ligne étudiant
 * @param {Etudiant} etud l'objet représentant un étudiant
 * @param {Number} index le n° de l'étudiant dans la liste des étudiants
 * @param {boolean} output ajout automatique dans la page ou non (default : Non)
 * @returns {String} HTML si output sinon rien
 */
function insertEtudRow(etud, index, output = false) {
  const etudHolder = document.querySelector(".etud_holder");
  const conflict = getAssiduitesConflict(etud.id);
  const prevAssiduite = getLastAssiduiteOfPrevDate(etud.id);
  let assiduite = {
    etatAssiduite: "",
    type: "création",
    id: -1,
    date_debut: null,
    date_fin: null,
    prevAssiduites: prevAssiduite,
  };

  if (conflict.length > 0) {
    assiduite.etatAssiduite = conflict[0].etat;

    assiduite.id = conflict[0].assiduite_id;
    assiduite.date_debut = conflict[0].date_debut;
    assiduite.date_fin = conflict[0].date_fin;
    if (isConflictSameAsPeriod(conflict[0])) {
      assiduite.type = "édition";
    } else {
      assiduite.type = "conflit";
    }
  }
  let row = generateEtudRow(etud, index, assiduite);

  if (output) {
    return row;
  }
  etudHolder.insertAdjacentHTML("beforeend", row);

  row = document.getElementById(`etud_row_${etud.id}`);
  const prev = row.querySelector("#prevDateAssi");
  setupAssiduiteBuble(prev, prevAssiduite);
  const bar = row.querySelector(".assiduites_bar");

  bar.appendChild(createMiniTimeline(assiduites[etud.id]));

  if (!verifyDateInSemester()) {
    row.querySelector(".btns_field.single").setAttribute("disabled", "true");
  }
}

/**
 * Mise à jour d'une ligne étudiant
 * @param {String | Number} etudid l'identifiant de l'étudiant
 */
function actualizeEtud(etudid) {
  actualizeEtudAssiduite(etudid, !isSingleEtud());
  //Actualize row
  const etudHolder = document.querySelector(".etud_holder");
  const ancient_row = document.getElementById(`etud_row_${etudid}`);

  let new_row = document.createElement("div");
  new_row.innerHTML = insertEtudRow(
    etuds[etudid],
    ancient_row.querySelector(".index").textContent,
    true
  );
  setupCheckBox(new_row.firstElementChild);
  const bar = new_row.firstElementChild.querySelector(".assiduites_bar");
  bar.appendChild(createMiniTimeline(assiduites[etudid]));
  const prev = new_row.firstElementChild.querySelector("#prevDateAssi");
  if (isSingleEtud()) {
    prev.classList.add("single");
  }
  setupAssiduiteBuble(prev, getLastAssiduiteOfPrevDate(etudid));
  etudHolder.replaceChild(new_row.firstElementChild, ancient_row);
}

/**
 * Génération de toutes les lignes étudiant
 */
function generateAllEtudRow() {
  if (isSingleEtud()) {
    try {
      actualizeEtud(etudid);
    } catch (ignored) {}
    return;
  }

  if (!document.querySelector(".selectors")?.disabled) {
    return;
  }

  document.querySelector(".etud_holder").innerHTML = "";
  etuds_ids = Object.keys(etuds).sort((a, b) =>
    etuds[a].nom > etuds[b].nom ? 1 : etuds[b].nom > etuds[a].nom ? -1 : 0
  );

  for (let i = 0; i < etuds_ids.length; i++) {
    const etud = etuds[etuds_ids[i]];
    insertEtudRow(etud, i + 1);
  }

  setupCheckBox();
}

// <== Gestion du modal de conflit ==>

// <<== Gestion de la récupération d'informations ==>>

/**
 * Récupération d'un nom d'utilisateur à partir d'un identifiant
 * @param {Number} id identifiant de l'utilisateur
 * @returns {String} le nom de l'utilisateur ou son pseudo ou "Non Renseigné"
 */
function getUserFromId(id) {
  if (id == "") {
    return "Non Renseigné";
  }

  let name = "Non Renseigné";

  sync_get(getUrl() + `/api/user/${id}`, (data) => {
    if (data.nom != "" && data.prenom != "") {
      name = `${data.nom} ${data.prenom}`;
    } else {
      name = data.user_name;
    }
  });

  return name;
}

/**
 * Récupération des ids des groupes
 * @returns la liste des ids des groupes
 */
function getGroupIds() {
  const btns = document.querySelector(".multiselect-container.dropdown-menu");

  const groups = Array.from(btns.querySelectorAll(".active")).map((el) => {
    return el.querySelector("input").value;
  });

  return groups;
}

/**
 * Récupération du moduleimpl_id
 * @returns {String} l'identifiant ou null si inéxistant
 */
function getModuleImplId() {
  const val = document.querySelector("#moduleimpl_select")?.value;
  return ["", undefined, null].includes(val) ? null : val;
}

/**
 * Récupération de l'id du formsemestre
 * @returns {String} l'identifiant du formsemestre
 */
function getFormSemestreId() {
  return document.querySelector(".formsemestre_id").textContent;
}

/**
 * Récupère la période du semestre
 * @returns {object} période {deb,fin}
 */
function getFormSemestreDates() {
  const dateDeb = document.getElementById(
    "formsemestre_date_debut"
  ).textContent;
  const dateFin = document.getElementById("formsemestre_date_fin").textContent;

  return {
    deb: dateDeb,
    fin: dateFin,
  };
}

/**
 * Récupère un objet étudiant à partir de son id
 * @param {Number} etudid
 */
function getSingleEtud(etudid) {
  sync_get(getUrl() + `/api/etudiant/etudid/${etudid}`, (data) => {
    etuds[etudid] = data;
  });
}

function isSingleEtud() {
  return location.href.includes("SignaleAssiduiteEtud");
}

function getCurrentAssiduiteModuleImplId() {
  const currentAssiduites = getAssiduitesConflict(etudid);
  if (currentAssiduites.length > 0) {
    const mod = currentAssiduites[0].moduleimpl_id;
    return mod == null ? "" : mod;
  }
  return "";
}

function getCurrentAssiduite(etudid) {
  const field = document.querySelector(
    `fieldset.btns_field.single[etudid='${etudid}']`
  );

  if (!field) return null;

  const assiduite_id = parseInt(field.getAttribute("assiduite_id"));
  const type = field.getAttribute("type");

  if (type == "édition") {
    let assi = null;
    assiduites[etudid].forEach((a) => {
      if (a.assiduite_id === assiduite_id) {
        assi = a;
      }
    });
    return assi;
  } else {
    return null;
  }
}

// <<== Gestion de la justification ==>>

function getJustificatifFromPeriod(date, etudid, update) {
  $.ajax({
    async: true,
    type: "GET",
    url:
      getUrl() +
      `/api/justificatifs/${etudid}/query?date_debut=${date.deb
        .add(1, "s")
        .format()}&date_fin=${date.fin.subtract(1, "s").format()}`,
    success: (data) => {
      update(data);
    },
    error: () => {},
  });
}

function updateJustifyBtn() {
  if (isSingleEtud()) {
    const assi = getCurrentAssiduite(etudid);

    const just = assi ? !assi.est_just : false;
    const btn = document.getElementById("justif-rapide");
    if (!just) {
      btn.setAttribute("disabled", "true");
    } else {
      btn.removeAttribute("disabled");
    }
  }
}

function fastJustify(assiduite) {
  const period = {
    deb: new moment.tz(assiduite.date_debut, TIMEZONE),
    fin: new moment.tz(assiduite.date_fin, TIMEZONE),
  };
  const action = (justifs) => {
    if (justifs.length > 0) {
      justifyAssiduite(assiduite.assiduite_id, !assiduite.est_just);
    } else {
      console.debug("WIP");
      //créer un nouveau justificatif
      // Afficher prompt -> demander raison et état

      const success = () => {
        const raison = document.getElementById("promptText").value;
        const etat = document.getElementById("promptSelect").value;

        //créer justificatif

        const justif = {
          date_debut: new moment.tz(assiduite.date_debut, TIMEZONE)
            .add(1, "s")
            .format(),
          date_fin: new moment.tz(assiduite.date_fin, TIMEZONE)
            .subtract(1, "s")
            .format(),
          raison: raison,
          etat: etat,
        };

        createJustificatif(justif);

        // justifyAssiduite(assiduite.assiduite_id, true);
        generateAllEtudRow();
      };

      const content = document.createElement("fieldset");

      const htmlPrompt = `<legend>Entrez l'état du justificatif :</legend>
    <select name="promptSelect" id="promptSelect" required>
      <option value="valide">Valide</option>
      <option value="attente">En Attente de validation</option>
      <option value="non_valide">Non Valide</option>
      <option value="modifie">Modifié</option>
  </select>
  <legend>Raison:</legend>
  <textarea type="text" placeholder="Explication du justificatif (non obligatoire)" id="promptText" style="width:100%;"></textarea>
  `;

      content.innerHTML = htmlPrompt;

      openPromptModal(
        "Nouveau justificatif (Rapide)",
        content,
        success,
        () => {},
        "#7059FF"
      );
    }
  };
  if (assiduite.etudid) {
    getJustificatifFromPeriod(period, assiduite.etudid, action);
  }
}

function justifyAssiduite(assiduite_id, justified) {
  const assiduite = {
    est_just: justified,
  };
  const path = getUrl() + `/api/assiduite/${assiduite_id}/edit`;
  let bool = false;
  sync_post(
    path,
    assiduite,
    (data, status) => {
      bool = true;
    },
    (data, status) => {
      //error
      console.error(data, status);
    }
  );

  return bool;
}

function createJustificatif(justif) {
  const path = getUrl() + `/api/justificatif/${etudid}/create`;
  sync_post(
    path,
    [justif],
    (data, status) => {
      //success
      if (data.success.length > 0) {
        console.table(data[0]);
      }
    },
    (data, status) => {
      //error
      console.error(data, status);
    }
  );
}

function getAllJustificatifsFromEtud(etudid, action) {
  const url_api = getUrl() + `/api/justificatifs/${etudid}`;
  $.ajax({
    async: true,
    type: "GET",
    url: url_api,
    success: (data, status) => {
      if (status === "success") {
        action(data);
      }
    },
    error: () => {},
  });
}

function deleteJustificatif(justif_id) {
  const path = getUrl() + `/api/justificatif/delete`;
  sync_post(
    path,
    [justif_id],
    (data, status) => {
      //success
      if (data.success.length > 0) {
      }
    },
    (data, status) => {
      //error
      console.error(data, status);
    }
  );
}