import { MelEnumerable } from '../../../../../mel_metapage/js/lib/classes/enum.js';
import { BnumEvent } from '../../../../../mel_metapage/js/lib/mel_events.js';

export { MelVideo, MelVideoManager, generate_canvas };

/**
 * Contient les classes utiles à la visualisation des vidéos
 * @module Visio/Video
 * @local Size
 * @local OnClickCallback
 * @local OnBeforeCallback
 * @local OnCreateCallback
 * @local OnDisposeCallback
 * @local MelVideo
 * @local MelVideoManager
 * @local generate_canvas
 */

/**
 * @typedef Size
 * @property {number} height
 * @property {number} width
 */

/**
 * @callback OnClickCallback
 * @param {Event} clickedEvent
 * @param {MediaDeviceInfo} device
 * @param {Size} size
 * @return {void}
 */

/**
 * @callback OnBeforeCallback
 * @param {MelVideo} caller
 * @return {Promise<void>}
 * @async
 */

/**
 * @callback OnCreateCallback
 * @param {HTMLVideoElement} video
 * @param {MediaDeviceInfo} media
 * @return {Promise<void>}
 * @async
 */

/**
 * @callback OnDisposeCallback
 * @param {MelVideo} caller
 * @return {void}
 */

/**
 * @class
 * @classdesc Représentation et affichage d'une caméra
 */
class MelVideo {
  /**
   * Initialise et assigne les variables membres
   * @param {MediaDeviceInfo} device Element qui sera affiché et gérer
   * @param {external:jQuery} $main Element parent
   * @param {number} [width=300] Largeur
   * @param {number} [height=200] Hauteur
   */
  constructor(device, $main, width = 300, height = 200) {
    /**
     * Informations du device qui doit être affiché
     * @type {MediaDeviceInfo}
     */
    this.device = device;
    /**
     * Element html qui affiche la vidéo
     * @type {HTMLVideoElement}
     */
    this.video = null;
    /**
     * Element parent
     * @type {external:jQuery}
     */
    this.$parent = $main;
    /**
     * Si la vidéo à démaré ou non
     * @type {boolean}
     */
    this.started = false;
    /**
     * Liste des devices
     * @type {MediaDeviceInfo[]}
     * @package
     */
    this._all_devices = null;
    /**
     * Taille de l'élément
     * @type {Size}
     * @readonly
     */
    this.size = {
      width,
      height,
    };

    /**
     * Est appelé lorsque l'on clicuqe sur la vidéo
     * @type {BnumEvent<OnClickCallback>}
     * @event
     * @frommodule Visio/Video {@linkto OnClickCallback}
     */
    this.onclick = new BnumEvent();
    /**
     * Liste de callback appelé avant la création de l'élément
     * @type {BnumEvent<OnBeforeCallback>}
     * @event
     * @frommodule Visio/Video {@linkto OnBeforeCallback}
     */
    this.onbeforecreate = new BnumEvent();
    /**
     * Liste de callback appelé après la création de l'élément
     * @type {BnumEvent<OnCreateCallback>}
     * @event
     * @frommodule Visio/Video {@linkto OnCreateCallback}
     */
    this.oncreate = new BnumEvent();
    /**
     * Liste de callback appelé lorsque l'on libère les élément
     * @type {BnumEvent<OnDisposeCallback>}
     * @event
     * @frommodule Visio/Video {@linkto OnDisposeCallback}
     */
    this.ondispose = new BnumEvent();
  }

  /**
   * Créer un élément vidéo qui affiche un retour caméra lié au device associé
   * @param {?MediaDeviceInfo[]} [devices=null]
   * @fires MelVideo.onbeforecreate | MelVideo.onclick
   * @returns {Promise<MelVideo>}
   * @async
   * @frommodulereturn Visio/Video {@linkto MelVideo}
   */
  async create(devices = null) {
    if (!this.video) {
      if (!this._all_devices)
        this._all_devices =
          devices || (await navigator.mediaDevices.enumerateDevices());

      await this.onbeforecreate.asyncCall(this);

      for (const d of this._all_devices) {
        if (d.kind === this.device.kind && d.label === this.device.label) {
          this.video = $('<video autoplay></video>')
            .click((event) => {
              this.onclick.call(event, d, this.size);
            })
            .css('width', `${this.size.width}px`)
            .css('height', `${this.size.height}px`)
            .appendTo(this.$parent)[0];
          this.video.srcObject = await navigator.mediaDevices.getUserMedia({
            video: {
              deviceId: d.deviceId,
            },
          });

          await this.oncreate.asyncCall(this.video, d);

          this.started = true;
          break;
        }
      }
    }

    return this;
  }

  /**
   * Met à jour la taille de la vidéo
   * @param {number} w longueur
   * @param {number} h hauteur
   * @returns {MelVideo} Chaînage
   */
  updateSize(w, h) {
    $(this.video).css('width', `${w}px`).css('height', `${h}px`);
    this.size.width = w;
    this.size.height = h;

    return this;
  }

  /**
   * Met à jour la taille de la vidéo sans la mettre en mémoire
   * @param {number} w longueur
   * @param {number} h hauteur
   * @returns {MelVideo} Chaînage
   */
  updateSizePerfect(w, h) {
    $(this.video).css('width', w).css('height', h);

    return this;
  }

  /**
   * Libère les données en mémoire
   * @fires MelVideo.ondispose
   */
  dispose() {
    if (!this.disposed) {
      this.disposed = true;

      if (this.video) {
        const tracks = this.video.srcObject.getTracks();

        for (const iterator of tracks) {
          iterator.stop();
        }

        $(this.video).remove();
      }

      this.ondispose.call(this);

      this.video = null;
      this.device = null;
      this.$parent = null;
      this.started = false;
      this._all_devices = null;
      this.size = null;
      this.onclick = null;
      this.onbeforecreate = null;
      this.oncreate = null;
      this.ondispose = null;
    }
  }
}

/**
 * @class
 * @classdesc Gère les différentes vidéos
 */
class MelVideoManager {
  /**
   * Initialise et assigne les variables membres
   */
  constructor() {
    /**
     * Liste des devices
     * @package
     * @type {?MediaDeviceInfo[]}
     */
    this._devices = null;
    /**
     * Liste des vidéos
     * @type {Object<string, MelVideo>}
     * @package
     * @frommodule Visio/Video {@linkto MelVideo}
     */
    this._videos = {};
    /**
     * Nombre de vidéos
     * @type {number}
     * @private
     */
    this._size = 0;
  }

  /**
   * Ajoute un device et l'affiche
   * @param {external:jQuery} $main DIV Parente
   * @param {MediaDeviceInfo} device Caméra à afficher
   * @param {boolean} [create=true] Si on affiche le retour vidéo ou non
   * @param {?MediaDeviceInfo[]} [devices=null] Device déjà chargés
   * @return {Promise<MelVideoManager>}
   * @async
   * @frommodulereturn Visio/Video {@linkto MelVideoManager}
   */
  async addVideo($main, device, create = true, devices = null) {
    if (!this._devices)
      this._devices =
        devices || (await navigator.mediaDevices.enumerateDevices());

    this._videos[device.deviceId] = new MelVideo(device, $main);

    if (create) await this._videos[device.deviceId].create(this._devices);

    ++this._size;

    return this;
  }

  /**
   * Affiche les différentes caméras
   * @return {Promise<MelVideoManager>}
   * @async
   * @frommodulereturn Visio/Video {@linkto MelVideoManager}
   */
  async create() {
    await Promise.allSettled(
      MelEnumerable.from(this._videos)
        .select((x) => x.value.create(this._devices))
        .toArray(),
    );
    return this;
  }

  /**
   * Met à jour la taille des images
   * @param {number} w Longueur
   * @param {number} h Hauteur
   * @returns {MelVideoManager}
   */
  updateSize(w, h) {
    for (const key in this._videos) {
      if (Object.hasOwnProperty.call(this._videos, key)) {
        this._videos[key].updateSize(w, h);
      }
    }

    return this;
  }

  /**
   * Met à jour la taille des images sans les sauvegarder en mémoire
   * @param {number} w Longueur
   * @param {number} h Hauteur
   * @returns {MelVideoManager}
   */
  updateSizePerfect(w, h) {
    for (const key in this._videos) {
      if (Object.hasOwnProperty.call(this._videos, key)) {
        this._videos[key].updateSizePerfect(w, h);
      }
    }

    return this;
  }

  /**
   * Ajoute un callback lors de la création à toute les vidéos
   * @param {OnCreateCallback} callback
   */
  oncreate(callback) {
    for (const key in this._videos) {
      if (Object.hasOwnProperty.call(this._videos, key)) {
        this._videos[key].oncreate.push(callback);
      }
    }

    return this;
  }

  /**
   * Ajoute un callback lors du click à toute les vidéos
   * @param {OnClickCallback} callback
   */
  click(callback) {
    for (const key in this._videos) {
      if (Object.hasOwnProperty.call(this._videos, key)) {
        this._videos[key].onclick.push(callback);
      }
    }
  }

  /**
   * Nombre de vidéos
   * @returns {number}
   */
  count() {
    return this._size;
  }

  /**
   * Libère les données en mémoire
   */
  dispose() {
    if (!this.disposed) {
      this.disposed = true;

      this._devices = null;

      for (const key in this._videos) {
        if (Object.hasOwnProperty.call(this._videos, key)) {
          this._videos[key].dispose();
        }
      }

      this._videos = null;
    }
  }
}

/**
 * Génère le canvas qui affiche les vidéos
 * @param {external:jQuery} $main DIV Parente
 * @param {MediaDeviceInfo[]} devices Devices
 * @return {Promise<void>}
 * @async
 */
async function generate_canvas($main, devices) {
  for (const d of devices) {
    if (d.kind === 'videoinput') {
      navigator.mediaDevices
        .getUserMedia({
          video: {
            deviceId: d.deviceId,
          },
        })
        .then((stream) => {
          $('<video autoplay></video>')
            .css('width', '300px')
            .css('height', '200px')
            .appendTo($main)[0].srcObject = stream;
        });
    }
  }
}