/**
 * @file middleware.js
 * @module middleware
 */
import { assign } from '../utils/obj.js' ;
import {toTitleCase} de '../utils/string-cases.js' ;

const middlewares = {} ;
const middlewareInstances = {} ;

export const TERMINATOR = {} ;

/**
 * Un objet middleware est un objet JavaScript ordinaire qui possède des méthodes qui
 * correspondent aux méthodes {@link Tech} trouvées dans les listes de méthodes autorisées
 * {@link module:middleware.allowedGetters|getters},
 * {@link module:middleware.allowedSetters|setters}, et
 * {@link module:middleware.allowedMediators|mediators}.
 *
 * @typedef {Objet} MiddlewareObject
 */

/**
 * Une fonction de fabrique d'intergiciel qui doit renvoyer un
 * {@link module:middleware~MiddlewareObject|MiddlewareObject}.
 *
 * Cette usine sera appelée pour chaque joueur en cas de besoin, avec le joueur
 * transmis en tant qu'argument.
 *
 * @callback MiddlewareFactory
 * @param {Player} player
 *        Un lecteur Video.js.
 */

/**
 * Définir un logiciel intermédiaire que le lecteur doit utiliser au moyen d'une fonction d'usine
 * qui renvoie un objet intermédiaire.
 *
 * @param {string} type
 *         Le type de MIME à rechercher ou `"*"` pour tous les types de MIME.
 *
 * @param {MiddlewareFactory} middleware
 *         Une fonction d'usine d'intergiciel qui sera exécutée pour
 *         les types de correspondance.
 */
export function use(type, middleware) {
  middlewares[type] = middlewares[type] || [] ;
  middlewares[type].push(middleware) ;
}

/**
 * Permet d'obtenir les intermédiaires par type (ou tous les intermédiaires).
 *
 * @param {string} type
 *         Le type de MIME à rechercher ou `"*"` pour tous les types de MIME.
 *
 * @return {Fonction[]|undefined}
 *         Un tableau d'intergiciels ou `undefined` s'il n'y en a pas.
 */
export function getMiddleware(type) {
  if (type) {
    return middlewares[type] ;
  }

  retourner les middlewares ;
}

/**
 * Définit de manière asynchrone une source à l'aide d'un intergiciel en parcourant tous les
 * et appeler `setSource` sur chacun d'entre eux, en leur transmettant l'élément
 * la valeur précédente renvoyée à chaque fois.
 *
 * @param {Player} player
 *         Une instance {@link Player}.
 *
 * @param {Tech~SourceObject} src
 *         Un objet source.
 *
 * @param {Fonction}
 *         Le prochain logiciel intermédiaire à exécuter.
 */
export function setSource(player, src, next) {
  player.setTimeout(() => setSourceHelper(src, middlewares[src.type], next, player), 1) ;
}

/**
 * Lorsque la technologie est définie, elle est transmise à la méthode `setTech` de chaque middleware.
 *
 * @param {Objet[]} middleware
 *        Un tableau d'instances d'intergiciels.
 *
 * @param {Tech} tech
 *        Une technologie Video.js.
 */
export function setTech(middleware, tech) {
  middleware.forEach((mw) => mw.setTech && mw.setTech(tech)) ;
}

/**
 * Appelle d'abord un getter sur le tech, à travers chaque middleware
 * de droite à gauche vers le joueur.
 *
 * @param {Objet[]} middleware
 *         Un tableau d'instances d'intergiciels.
 *
 * @param {Tech} tech
 *         La technologie actuelle.
 *
 * @param {string} method
 *         Un nom de méthode.
 *
 * @return {Mixed}
 *         La valeur finale de la technologie après que l'intergiciel l'a interceptée.
 */
export function get(middleware, tech, method) {
  return middleware.reduceRight(middlewareIterator(method), tech[method]()) ;
}

/**
 * Prend l'argument donné au lecteur et appelle la méthode setter sur chaque
 * middleware de gauche à droite jusqu'à la technologie.
 *
 * @param {Objet[]} middleware
 *         Un tableau d'instances d'intergiciels.
 *
 * @param {Tech} tech
 *         La technologie actuelle.
 *
 * @param {string} method
 *         Un nom de méthode.
 *
 * @param {Mixed} arg
 *         La valeur à définir sur le tech.
 *
 * @return {Mixed}
 *         La valeur de retour de la `méthode` de la `technique`.
 */
export function set(middleware, tech, method, arg) {
  return tech[method](middleware.reduce(middlewareIterator(method), arg)) ;
}

/**
 * Prend l'argument donné au joueur et appelle la version `call` de la fonction
 * sur chaque logiciel intermédiaire, de gauche à droite.
 *
 * Ensuite, appeler la méthode passée sur le tech et renvoyer le résultat inchangé
 * au joueur, par l'intermédiaire d'un logiciel intermédiaire, cette fois-ci de droite à gauche.
 *
 * @param {Objet[]} middleware
 *         Un tableau d'instances d'intergiciels.
 *
 * @param {Tech} tech
 *         La technologie actuelle.
 *
 * @param {string} method
 *         Un nom de méthode.
 *
 * @param {Mixed} arg
 *         La valeur à définir sur le tech.
 *
 * @return {Mixed}
 *         La valeur de retour de la `méthode` de la `tech`, quelle que soit la valeur de retour de la `tech`
 *         les valeurs de retour des logiciels intermédiaires.
 */
export function mediate(middleware, tech, method, arg = null) {
  const callMethod = 'call' + toTitleCase(method) ;
  const middlewareValue = middleware.reduce(middlewareIterator(callMethod), arg) ;
  const terminated = middlewareValue === TERMINATOR ;
  // déprécié. La valeur de retour `null` devrait plutôt renvoyer TERMINATOR à
  // éviter toute confusion si une méthode techs renvoie en fait un résultat nul.
  const returnValue = terminated ? null : tech[method](middlewareValue) ;

  executeRight(middleware, method, returnValue, terminated) ;

  returnValue ;
}

/**
 * Enumération de getters autorisés dont les clés sont des noms de méthodes.
 *
 * @type {Objet}
 */
export const allowedGetters = {
  mis en mémoire tampon : 1,
  l'heure actuelle : 1,
  durée: 1,
  en sourdine : 1,
  a joué : 1,
  pausé : 1,
  recherchable : 1,
  volume : 1,
  terminé : 1
};

/**
 * Enumération de paramètres autorisés dont les clés sont des noms de méthodes.
 *
 * @type {Objet}
 */
export const allowedSetters = {
  setCurrentTime : 1,
  setMuted : 1,
  setVolume : 1
};

/**
 * Enumération de médiateurs autorisés dont les clés sont des noms de méthodes.
 *
 * @type {Objet}
 */
export const allowedMediators = {
  jouer : 1,
  pause : 1
};

function middlewareIterator(method) {
  return (value, mw) => {
    // si l'intergiciel précédent s'est terminé, transmettre la fin de l'intergiciel
    if (value === TERMINATOR) {
      retour TERMINATOR ;
    }

    if (mw[method]) {
      return mw[method](value) ;
    }

    valeur de retour ;
  };
}

function executeRight(mws, method, value, terminated) {
  for (let i = mws.length - 1 ; i >= 0 ; i--) {
    const mw = mws[i] ;

    if (mw[method]) {
      mw[method](terminated, value) ;
    }
  }
}

/**
 * Efface le cache de l'intergiciel d'un lecteur.
 *
 * @param {Player} player
 *         Une instance {@link Player}.
 */
export function clearCacheForPlayer(player) {
  middlewareInstances[player.id()] = null ;
}

/**
 * {
 *  [playerId] : [[mwFactory, mwInstance], ...]
 * }
 *
 * @private
 */
function getOrCreateFactory(player, mwFactory) {
  const mws = middlewareInstances[player.id()] ;
  let mw = null ;

  if (mws === undefined || mws === null) {
    mw = mwFactory(player) ;
    middlewareInstances[player.id()] = [[mwFactory, mw]] ;
    retour mw ;
  }

  for (let i = 0 ; i < mws.length ; i++) {
    const [mwf, mwi] = mws[i] ;

    if (mwf !== mwFactory) {
      continuer ;
    }

    mw = mwi ;
  }

  if (mw === null) {
    mw = mwFactory(player) ;
    mws.push([mwFactory, mw]) ;
  }

  retour mw ;
}

function setSourceHelper(src = {}, middleware = [], next, player, acc = [], lastRun = false) {
  const [mwFactory, ...mwrest] = middleware ;

  // si mwFactory est une chaîne de caractères, nous sommes à la croisée des chemins
  if (typeof mwFactory === 'string') {
    setSourceHelper(src, middlewares[mwFactory], next, player, acc, lastRun) ;

  // si nous avons une mwFactory, appelons-la avec le joueur pour obtenir le mw,
  // puis appeler la méthode setSource du mw
  else if (mwFactory) {
    const mw = getOrCreateFactory(player, mwFactory) ;

    // si setSource n'est pas présent, sélectionner implicitement cet intergiciel
    if (!mw.setSource) {
      acc.push(mw) ;
      return setSourceHelper(src, mwrest, next, player, acc, lastRun) ;
    }

    mw.setSource(assign({}, src), function(err, _src) {

      // il s'est passé quelque chose, essayer l'intergiciel suivant au niveau actuel
      // s'assurer d'utiliser l'ancien src
      if (err) {
        return setSourceHelper(src, mwrest, next, player, acc, lastRun) ;
      }

      // nous avons réussi, il faut maintenant aller plus loin
      acc.push(mw) ;

      // s'il s'agit du même type, poursuivre la chaîne en cours
      // sinon, nous voulons descendre dans la nouvelle chaîne
      setSourceHelper(
        _src,
        src.type === _src.type ? mwrest : middlewares[_src.type],
        suivant,
        joueur,
        acc,
        dernière exécution
      ) ;
    }) ;

  else if (mwrest.length) {
    setSourceHelper(src, mwrest, next, player, acc, lastRun) ;
  } else if (lastRun) {
    next(src, acc) ;
  } else {
    setSourceHelper(src, middlewares['*'], next, player, acc, true) ;
  }
}