/**
 * Transforme une date du format français (DD/MM/YYYY) au format iso (YYYY-MM-DD)
 * Exemple d'utilisation :
 * new Date(Date.fromFRA("30/06/2024")) -> new Date("2024-06-30")
 * @param {string} dateFra
 * @returns {string} dateIso
 */
Date.fromFRA = function (dateFra) {
  if (dateFra == "") return "";
  // Expression régulière pour valider le format de date ISO (YYYY-MM-DD)
  const regexDateFra = /^(0[1-9]|[12][0-9]|3[01])\/(0[1-9]|1[0-2])\/\d{4}$/;

  // Vérification du format de la date ISO
  if (!regexDateFra.test(dateFra)) {
    throw new Error(
      `La date (format français) passée en paramètre [${dateFra}] n'est pas valide.`
    );
  }

  // Conversion du format français (DD/MM/YYYY) au format ISO (YYYY-MM-DD)
  return `${dateFra.substring(6, 10)}-${dateFra.substring(
    3,
    5
  )}-${dateFra.substring(0, 2)}`;
};
/**
 * Transforme une date du format iso (YYYY-MM-DD) au format français (DD/MM/YYYY)
 * Exemple d'utilisation :
 * Date.toFRA("2024-06-30") -> "30/06/2024"
 * @param {string} dateIso
 * @returns {string} dateFra
 */
Date.toFRA = function (dateIso) {
  if (dateIso == "") return "";
  // Expression régulière pour valider le format de date ISO (YYYY-MM-DD)
  const regexDateIso = /^\d{4}-(0\d|1[0-2])-([0-2]\d|3[01])$/;

  // Vérification du format de la date ISO
  if (!regexDateIso.test(dateIso)) {
    throw new Error(
      `La date ISO passée en paramètre [${dateIso}] n'est pas valide.`
    );
  }

  // Conversion du format ISO (YYYY-MM-DD) en format français (DD/MM/YYYY)
  return `${dateIso.substring(8, 10)}/${dateIso.substring(
    5,
    7
  )}/${dateIso.substring(0, 4)}`;
};
/**
 * Vérifie si le début de l'une des périodes est avant la fin de l'autre
 * et si la fin de cette période est après le début de l'autre.
 * @param {Object} period  {deb:Object, fin:Object}
 * @param {Object} interval {deb:Object, fin:Object}
 * @returns vrai si la periode et l'interval ont une intersection commune
 */
Date.intersect = function (period, interval) {
  return period.deb <= interval.fin && period.fin >= interval.deb;
};

Date.removeUTC = function (isoString) {
  const reg = new RegExp(/[+-][\d:]+$/);
  return isoString.replace(reg, "");
};

Object.defineProperty(Date.prototype, "isValid", {
  value: function () {
    return !Number.isNaN(this.getTime());
  },
});
Object.defineProperty(Date.prototype, "startOf", {
  /**
   * Génère u la date à la plus petite valeur pour la précision donnée.
   * @param {string} precision - La précision souhaitée (year, month, day, hours, minutes, seconds, milliseconds).
   * @returns {Date} - Une nouvelle date ajustée.
   */
  value: function (precision) {
    const newDate = this.clone();
    switch (precision) {
      case "year":
        newDate.setMonth(0);
      case "month":
        newDate.setDate(1);
      case "day":
        newDate.setHours(0);
      case "hours":
        newDate.setMinutes(0);
      case "minutes":
        newDate.setSeconds(0);
      case "seconds":
        newDate.setMilliseconds(0);
        break;
      case "milliseconds":
        break;
      default:
        throw new Error(
          `Invalid precision for startOf function [${precision}]`
        );
    }
    return newDate;
  },
});

Object.defineProperty(Date.prototype, "endOf", {
  /**
   * Ajuste la date à la plus grande valeur pour la précision donnée.
   * @param {string} precision - La précision souhaitée (year, month, day, hours, minutes, seconds, milliseconds).
   * @returns {Date} - Une nouvelle date ajustée.
   */
  value: function (precision) {
    const newDate = this.clone();
    switch (precision) {
      case "year":
        newDate.setMonth(11); // Décembre est le 11ème mois (0-indexé)
      case "month":
        newDate.setDate(0); // Le jour 0 du mois suivant est le dernier jour du mois courant
        newDate.setMonth(newDate.getMonth() + 1);
      case "day":
        newDate.setHours(23); // 23 heures est la dernière heure de la journée
      case "hours":
        newDate.setMinutes(59); // 59 minutes est la dernière minute de l'heure
      case "minutes":
        newDate.setSeconds(59); // 59 secondes est la dernière seconde de la minute
      case "seconds":
        newDate.setMilliseconds(999); // 999 millisecondes est la dernière milliseconde de la seconde
        break;
      case "milliseconds":
        // Rien à faire pour les millisecondes
        break;
      default:
        throw new Error("Invalid precision for endOf function");
    }
    return newDate;
  },
});

Object.defineProperty(Date.prototype, "isBefore", {
  /**
   * Retourne vrai si la date est située avant la date fournie
   * @param {Date} date
   * @returns {boolean}
   */
  value: function (date) {
    return this.valueOf() < date.valueOf();
  },
});
Object.defineProperty(Date.prototype, "isAfter", {
  /**
   * Retourne vrai si la date est située après la date fournie
   * @param {Date} date
   * @returns {boolean}
   */
  value: function (date) {
    return this.valueOf() > date.valueOf();
  },
});

Object.defineProperty(Date.prototype, "isSame", {
  /**
   * Retourne vrai si les dates sont les mêmes
   * @param {Date} date
   * @param {string} precision default : "milliseconds"
   * @returns boolean
   */
  value: function (date, precision = "milliseconds") {
    return (
      this.startOf(precision).valueOf() == date.startOf(precision).valueOf()
    );
  },
});

Object.defineProperty(Date.prototype, "isBetween", {
  /**
   * Vérifie si la date est comprise dans une période avec une précision et une inclusivité optionnelles
   * @param {Date} deb - La date de début de la période
   * @param {Date} fin - La date de fin de la période
   * @param {String} bornes - L'inclusivité/exclusivité de la comparaison ("[]", "()", "[)", "(]")
   * - bornes incluses : []
   * - bornes excluses : ()
   * - borne gauche incluse et borne droit excluse : [)
   * - borne gauche excluse et borne droit incluse : (]
   */
  value: function (deb, fin, bornes = "[]") {
    // Ajuste la date actuelle, la date de début et la date de fin à la précision spécifiée

    // Vérifie les bornes en fonction de l'inclusivité/exclusivité spécifiée dans 'bornes'
    const check_deb =
      bornes[0] === "("
        ? this.valueOf() > deb.valueOf()
        : this.valueOf() >= deb.valueOf();
    const check_fin =
      bornes[1] === ")"
        ? fin.valueOf() > this.valueOf()
        : fin.valueOf() >= this.valueOf();

    return check_deb && check_fin;
  },
});

Object.defineProperty(Date.prototype, "toIsoUtcString", {
  /**
   * @returns date au format iso utc (yyyy-mm-ddThh:MM±oo:oo:oo)
   */
  value: function () {
    // Formater la date et l'heure
    const date = this;
    var tzo = -date.getTimezoneOffset(),
      dif = tzo >= 0 ? "+" : "-",
      pad = function (num) {
        return (num < 10 ? "0" : "") + num;
      };
    return (
      this.toFakeIso() +
      dif +
      pad(Math.floor(Math.abs(tzo) / 60)) +
      ":" +
      pad(Math.abs(tzo) % 60)
    );
  },
});

Object.defineProperty(Date.prototype, "toFakeIso", {
  value: function () {
    const date = this;
    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())
    );
  },
});

Object.defineProperty(Date.prototype, "clone", {
  /**
   * @returns Retourne une copie de la date (copie non liée)
   */
  value: function () {
    return structuredClone(this);
  },
});

Object.defineProperty(Date.prototype, "format", {
  value: function (formatString) {
    let iso = this.toIsoUtcString();
    switch (formatString) {
      case "DD/MM/Y HH:mm":
        return this.toLocaleString("fr-FR", {
          day: "2-digit",
          month: "2-digit",
          year: "2-digit",
          hour: "2-digit",
          minute: "2-digit",
          hour12: false,
          timeZone: SCO_TIMEZONE,
        });
      case "DD/MM/YYYY HH:mm":
        return this.toLocaleString("fr-FR", {
          day: "2-digit",
          month: "2-digit",
          year: "numeric",
          hour: "2-digit",
          minute: "2-digit",
          hour12: false,
          timeZone: SCO_TIMEZONE,
        });

      case "YYYY-MM-DDTHH:mm":
        // slice : YYYY-MM-DDTHH
        // slice + 3 : YYYY-MM-DDTHH:mm
        return iso.slice(0, iso.indexOf(":") + 3);
      case "YYYY-MM-DD":
        return iso.slice(0, iso.indexOf("T"));
      default:
        return this.toFakeIso();
    }
  },
});

Object.defineProperty(Date.prototype, "add", {
  /**
   * Ajoute une valeur spécifiée à un élément de la date.
   * @param {number} value - La valeur à ajouter.
   * @param {string} type - Le type de la valeur (year, month, day, hours, minutes, seconds).
   */
  value: function (value, type) {
    switch (type) {
      case "years":
        this.setFullYear(this.getFullYear() + value);
        break;
      case "months":
        this.setMonth(this.getMonth() + value);
        break;
      case "days":
        this.setDate(this.getDate() + value);
        break;
      case "hours":
        this.setHours(this.getHours() + value);
        break;
      case "minutes":
        this.setMinutes(this.getMinutes() + value);
        break;
      case "seconds":
        this.setSeconds(this.getSeconds() + value);
        break;
      default:
        throw new Error(
          `Invalid type for adding to date | type : ${type} value : ${value}`
        );
    }
    return this; // Return the modified date
  },
});

class Duration {
  /**
   * Constructeur de la classe Duration.
   * @param {Date} start - La date de début de la période.
   * @param {Date} end - La date de fin de la période.
   */
  constructor(start, end) {
    this.start = start; // Stocke la date de début.
    this.end = end; // Stocke la date de fin.
    this.duration = end - start; // Calcule la durée en millisecondes entre les deux dates.
  }

  /**
   * Calcule le nombre d'années entre les deux dates et arrondit le résultat à quatre décimales.
   * @return {number} Le nombre d'années arrondi à quatre décimales.
   */
  get years() {
    const startYear = this.start.getFullYear(); // Obtient l'année de la date de début.
    const endYear = this.end.getFullYear(); // Obtient l'année de la date de fin.
    // Calcule la différence en années et arrondit à quatre décimales.
    return parseFloat((endYear - startYear).toFixed(4));
  }

  /**
   * Calcule le nombre de mois entre les deux dates, en tenant compte des années et des jours, et arrondit le résultat à quatre décimales.
   * @return {number} Le nombre de mois arrondi à quatre décimales.
   */
  get months() {
    const years = this.years; // Nombre d'années complètes.
    // Calcule la différence en mois, en ajoutant la différence en jours divisée par 30 pour une approximation.
    const months =
      years * 12 +
      (this.end.getMonth() - this.start.getMonth()) +
      (this.end.getDate() - this.start.getDate()) / 30;
    // Arrondit à quatre décimales.
    return parseFloat(months.toFixed(4));
  }

  /**
   * Calcule le nombre de jours entre les deux dates et arrondit le résultat à quatre décimales.
   * @return {number} Le nombre de jours arrondi à quatre décimales.
   */
  get days() {
    // Convertit la durée en millisecondes en jours et arrondit à quatre décimales.
    return parseFloat((this.duration / (24 * 60 * 60 * 1000)).toFixed(4));
  }

  /**
   * Calcule le nombre d'heures entre les deux dates et arrondit le résultat à quatre décimales.
   * @return {number} Le nombre d'heures arrondi à quatre décimales.
   */
  get hours() {
    // Convertit la durée en millisecondes en heures et arrondit à quatre décimales.
    return parseFloat((this.duration / (60 * 60 * 1000)).toFixed(4));
  }

  /**
   * Calcule le nombre de minutes entre les deux dates et arrondit le résultat à quatre décimales.
   * @return {number} Le nombre de minutes arrondi à quatre décimales.
   */
  get minutes() {
    // Convertit la durée en millisecondes en minutes et arrondit à quatre décimales.
    return parseFloat((this.duration / (60 * 1000)).toFixed(4));
  }

  /**
   * Calcule le nombre de secondes entre les deux dates et arrondit le résultat à quatre décimales.
   * @return {number} Le nombre de secondes arrondi à quatre décimales.
   */
  get seconds() {
    // Convertit la durée en millisecondes en secondes et arrondit à quatre décimales.
    return parseFloat((this.duration / 1000).toFixed(4));
  }

  /**
   * Obtient le nombre de millisecondes entre les deux dates et arrondit le résultat à quatre décimales.
   * @return {number} Le nombre de millisecondes arrondi à quatre décimales.
   */
  get milliseconds() {
    // Arrondit la durée totale en millisecondes à quatre décimales.
    return parseFloat(this.duration.toFixed(4));
  }
}

class ScoDocDateTimePicker extends HTMLElement {
  constructor() {
    super();
    // Définir si le champ est requis
    this.required = this.hasAttribute("required");

    // Initialiser le shadow DOM
    const shadow = this.attachShadow({ mode: "open" });

    // Créer l'input pour la date
    const dateInput = document.createElement("input");
    dateInput.type = "date";
    dateInput.id = "date";

    // Créer l'input pour l'heure
    const timeInput = document.createElement("input");
    timeInput.type = "time";
    timeInput.id = "time";
    timeInput.step = 60;

    // Ajouter les inputs dans le shadow DOM
    shadow.appendChild(dateInput);
    shadow.appendChild(timeInput);

    // Gestionnaires d'événements pour la mise à jour de la valeur
    dateInput.addEventListener("change", () => this.updateValue());
    timeInput.addEventListener("change", () => this.updateValue());

    // Style CSS pour les inputs
    const style = document.createElement("style");
    style.textContent = `
        input {
          display: inline-block;
        }
        input:invalid {
          border: 1px solid red;
        }
      `;

    // Ajouter le style au shadow DOM
    shadow.appendChild(style);

    //Si une value est donnée

    let value = this.getAttribute("value");
    if (value != null) {
      this.value = value;
    }
  }

  static get observedAttributes() {
    return ["show"]; // Ajoute 'show' à la liste des attributs observés
  }

  connectedCallback() {
    // Récupérer l'attribut 'name'
    this.name = this.getAttribute("name");

    // Créer un input caché pour la valeur datetime
    this.hiddenInput = document.createElement("input");
    this.hiddenInput.type = "hidden";
    this.hiddenInput.name = this.name;
    this.appendChild(this.hiddenInput);

    // Gérer la soumission du formulaire
    this.closest("form")?.addEventListener("submit", (e) => {
      if (!this.validate()) {
        e.preventDefault(); // Empêcher la soumission si non valide
        this.dispatchEvent(
          new Event("invalid", { bubbles: true, cancelable: true })
        );
      } else {
        // Mettre à jour la valeur de l'input caché avant la soumission
        this.hiddenInput.value = this.isValid()
          ? this.valueAsDate.toFakeIso()
          : "";
      }
    });
    this.updateDisplay();
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (name === "show") {
      this.updateDisplay(); // Met à jour l'affichage si l'attribut 'show' change
    }
  }

  updateDisplay() {
    const mode = this.getAttribute("show") || "both";
    const dateInput = this.shadowRoot.querySelector("#date");
    const timeInput = this.shadowRoot.querySelector("#time");

    switch (mode) {
      case "date":
        dateInput.style.display = "inline-block";
        timeInput.style.display = "none";
        break;
      case "time":
        dateInput.style.display = "none";
        timeInput.style.display = "inline-block";
        break;
      case "both":
      default:
        dateInput.style.display = "inline-block";
        timeInput.style.display = "inline-block";
    }
  }

  // Vérifier si la valeur forme une date valide
  isValid() {
    return !Number.isNaN(this.valueAsDate.getTime());
  }

  // Valider l'élément
  validate() {
    if (this.required && !this.isValid()) {
      return false;
    }
    return true;
  }

  // Mettre à jour la valeur interne
  updateValue() {
    const dateInput = this.shadowRoot.querySelector("#date");
    const timeInput = this.shadowRoot.querySelector("#time");
    this._value = `${dateInput.value}T${timeInput.value}`;
    this.dispatchEvent(new Event("change", { bubbles: true }));

    // Appliquer le style 'invalid' si nécessaire
    dateInput.classList.toggle("invalid", this.required && !this.isValid());
    timeInput.classList.toggle("invalid", this.required && !this.isValid());
  }

  // Getter pour obtenir la valeur actuelle.
  get value() {
    return this._value;
  }

  get valueAsObject() {
    const dateInput = this.shadowRoot.querySelector("#date");
    const timeInput = this.shadowRoot.querySelector("#time");
    return {
      date: dateInput.value,
      time: timeInput.value,
    };
  }

  // Getter pour obtenir la valeur en tant qu'objet Date.
  get valueAsDate() {
    return new Date(this._value);
  }

  // Setter pour définir la valeur. Sépare la valeur en date et heure et les définit individuellement.
  set value(val) {
    let [date, time] = val.split("T");
    this.shadowRoot.querySelector("#date").value = date;

    time = time.substring(0, 5);

    this.shadowRoot.querySelector("#time").value = time;
    this._value = val;
  }

  // Setter pour définir la valeur à partir d'un objet avec les propriétés 'date' et 'time'.
  set valueAsObject(obj) {
    const dateInput = this.shadowRoot.querySelector("#date");
    const timeInput = this.shadowRoot.querySelector("#time");

    if (obj.hasOwnProperty("date")) {
      dateInput.value = obj.date || ""; // Définit la valeur de l'input de date si elle est fournie
    }

    if (obj.hasOwnProperty("time")) {
      timeInput.value = obj.time.substring(0, 5) || ""; // Définit la valeur de l'input d'heure si elle est fournie
    }

    // Met à jour la valeur interne en fonction des nouvelles valeurs des inputs
    this.updateValue();
  }

  // Setter pour définir la valeur à partir d'un objet Date.
  set valueAsDate(dateVal) {
    // Formatage de l'objet Date en string et mise à jour de la valeur.
    this.value = `${dateVal.getFullYear()}-${String(
      dateVal.getMonth() + 1
    ).padStart(2, "0")}-${String(dateVal.getDate()).padStart(2, "0")}T${String(
      dateVal.getHours()
    ).padStart(2, "0")}:${String(dateVal.getMinutes()).padStart(2, "0")}`;
  }
}

// Définition du nouvel élément personnalisé 'scodoc-datetime'.
customElements.define("scodoc-datetime", ScoDocDateTimePicker);