/*! @license
 * Shaka Player
 * Copyright 2016 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

goog.provide('shaka.offline.StorageCellHandle');
goog.provide('shaka.offline.StorageCellPath');
goog.provide('shaka.offline.StorageMuxer');

goog.require('shaka.log');
goog.require('shaka.util.Error');
goog.require('shaka.util.IDestroyable');

/**
 * @typedef {{
 *  mechanism: string,
 *  cell: string
 * }}
 *
 * @property {string} mechanism
 *  The name of the mechanism that holds the cell.
 * @property {string} cell
 *  The name of the cell in the mechanism.
 */
shaka.offline.StorageCellPath;


/**
 * @typedef {{
 *   path: shaka.offline.StorageCellPath,
 *   cell: !shaka.extern.StorageCell
 * }}
 *
 * @property {shaka.offline.StorageCellPath} path
 *   The path that maps to the cell.
 * @property {shaka.extern.StorageCell} cell
 *   The storage cell that the path points to within the storage muxer.
 */
shaka.offline.StorageCellHandle;


// TODO: revisit this when Closure Compiler supports partially-exported classes.
/**
 * StorageMuxer is responsible for managing StorageMechanisms and addressing
 * cells. The primary purpose of the muxer is to give the caller the correct
 * cell for the operations they want to perform.
 *
 * |findActive| will be used when the caller wants a cell that supports
 * add-operations. This will be used when saving new content to storage.
 *
 * |findAll| will be used when the caller want to look at all the content
 * in storage.
 *
 * |resolvePath| will be used to convert a path (from |findActive| and
 * |findAll|) into a cell, which it then returns.
 *
 * @implements {shaka.util.IDestroyable}
 * @export
 */
shaka.offline.StorageMuxer = class {
  constructor() {
    /**
     * A key in this map is the name given when registering a StorageMechanism.
     *
     * @private {!Map.<string, !shaka.extern.StorageMechanism>}
     */
    this.mechanisms_ = new Map();
  }

  // TODO: revisit this when the compiler supports partially-exported classes.
  /**
   * Free all resources used by the muxer, mechanisms, and cells. This should
   * not affect the stored content.
   *
   * @override
   * @export
   */
  destroy() {
    /** @type {!Array.<!Promise>} */
    const destroys = [];
    for (const mechanism of this.mechanisms_.values()) {
      destroys.push(mechanism.destroy());
    }

    // Empty the map so that subsequent calls will be no-ops.
    this.mechanisms_.clear();

    return Promise.all(destroys);
  }

  /**
   * Initialize the storage muxer. This must be called before any other calls.
   * This will initialize the muxer to use all mechanisms that have been
   * registered with |StorageMuxer.register|.
   *
   * @return {!Promise}
   */
  init() {
    // Add the new instance of each mechanism to the muxer.
    const registry = shaka.offline.StorageMuxer.getRegistry_();
    registry.forEach((factory, name) => {
      const mech = factory();
      if (mech) {
        this.mechanisms_.set(name, mech);
      } else {
        shaka.log.info(
            'Skipping ' + name + ' as it is not supported on this platform');
      }
    });

    /** @type {!Array.<!Promise>} */
    const initPromises = [];
    for (const mechanism of this.mechanisms_.values()) {
      initPromises.push(mechanism.init());
    }

    return Promise.all(initPromises);
  }

  /**
   * Get a promise that will resolve with a storage cell that supports
   * add-operations. If no cell can be found, the promise will be rejected.
   *
   * @return {shaka.offline.StorageCellHandle}
   */
  getActive() {
    /** @type {?shaka.offline.StorageCellHandle} */
    let handle = null;

    this.mechanisms_.forEach((mechanism, mechanismName) => {
      mechanism.getCells().forEach((cell, cellName) => {
        // If this cell is not useful to us or we already have a handle, then
        // we don't need to make a new handle.
        if (cell.hasFixedKeySpace() || handle) {
          return;
        }

        const path = {
          mechanism: mechanismName,
          cell: cellName,
        };

        handle = {
          path: path,
          cell: cell,
        };
      });
    });

    if (handle) {
      return /** @type {shaka.offline.StorageCellHandle} */(handle);
    }

    throw new shaka.util.Error(
        shaka.util.Error.Severity.CRITICAL,
        shaka.util.Error.Category.STORAGE,
        shaka.util.Error.Code.MISSING_STORAGE_CELL,
        'Could not find a cell that supports add-operations');
  }

  /**
   * @param {function(!shaka.offline.StorageCellPath,
   *                  !shaka.extern.StorageCell)} callback
   */
  forEachCell(callback) {
    this.mechanisms_.forEach((mechanism, mechanismName) => {
      mechanism.getCells().forEach((cell, cellName) => {
        const path = {
          mechanism: mechanismName,
          cell: cellName,
        };

        callback(path, cell);
      });
    });
  }

  /**
   * Get a specific storage cell. The promise will resolve with the storage
   * cell if it is found. If the storage cell is not found, the promise will
   * be rejected.
   *
   * @param {string} mechanismName
   * @param {string} cellName
   * @return {!shaka.extern.StorageCell}
   */
  getCell(mechanismName, cellName) {
    const mechanism = this.mechanisms_.get(mechanismName);
    if (!mechanism) {
      throw new shaka.util.Error(
          shaka.util.Error.Severity.CRITICAL,
          shaka.util.Error.Category.STORAGE,
          shaka.util.Error.Code.MISSING_STORAGE_CELL,
          'Could not find mechanism with name ' + mechanismName);
    }

    const cell = mechanism.getCells().get(cellName);
    if (!cell) {
      throw new shaka.util.Error(
          shaka.util.Error.Severity.CRITICAL,
          shaka.util.Error.Category.STORAGE,
          shaka.util.Error.Code.MISSING_STORAGE_CELL,
          'Could not find cell with name ' + cellName);
    }

    return cell;
  }

  /**
   * @param {function(!shaka.extern.EmeSessionStorageCell)} callback
   */
  forEachEmeSessionCell(callback) {
    this.mechanisms_.forEach((mechanism, name) => {
      callback(mechanism.getEmeSessionCell());
    });
  }

  /**
   * Gets an arbitrary EME session cell that can be used for storing new session
   * info.
   *
   * @return {!shaka.extern.EmeSessionStorageCell}
   */
  getEmeSessionCell() {
    const mechanisms = Array.from(this.mechanisms_.keys());
    if (!mechanisms.length) {
      throw new shaka.util.Error(
          shaka.util.Error.Severity.CRITICAL,
          shaka.util.Error.Category.STORAGE,
          shaka.util.Error.Code.STORAGE_NOT_SUPPORTED,
          'No supported storage mechanisms found');
    }

    return this.mechanisms_.get(mechanisms[0]).getEmeSessionCell();
  }

  /**
   * Find the cell that the path points to. A path is made up of a mount point
   * and a cell id. If a cell can be found, the cell will be returned. If no
   * cell is found, null will be returned.
   *
   * @param {shaka.offline.StorageCellPath} path
   * @return {shaka.extern.StorageCell}
   */
  resolvePath(path) {
    const mechanism = this.mechanisms_.get(path.mechanism);

    if (!mechanism) {
      return null;
    }

    return mechanism.getCells().get(path.cell);
  }

  /**
   * This will erase all previous content from storage. Using paths obtained
   * before calling |erase| is discouraged, as cells may have changed during a
   * erase.
   *
   * @return {!Promise}
   */
  async erase() {
    // If we have initialized, we will use the existing mechanism instances.
    /** @type {!Array.<!shaka.extern.StorageMechanism>} */
    const mechanisms = Array.from(this.mechanisms_.values());
    const alreadyInitialized = mechanisms.length > 0;

    // If we have not initialized, we should still be able to erase.  This is
    // critical to our ability to wipe the DB in case of a version mismatch.
    // If there are no instances, create temporary ones and destroy them later.
    if (!alreadyInitialized) {
      const registry = shaka.offline.StorageMuxer.getRegistry_();
      registry.forEach((factory, name) => {
        const mech = factory();
        if (mech) {
          mechanisms.push(mech);
        }
      });
    }

    // Erase all storage mechanisms.
    await Promise.all(mechanisms.map((m) => m.erase()));

    // If we were erasing temporary instances, destroy them, too.
    if (!alreadyInitialized) {
      await Promise.all(mechanisms.map((m) => m.destroy()));
    }
  }

  /**
   * Register a storage mechanism for use with the default storage muxer. This
   * will have no effect on any storage muxer already in main memory.
   *
   * @param {string} name
   * @param {function():shaka.extern.StorageMechanism} factory
   * @export
   */
  static register(name, factory) {
    shaka.offline.StorageMuxer.registry_.set(name, factory);
  }


  /**
   * Unregister a storage mechanism for use with the default storage muxer. This
   * will have no effect on any storage muxer already in main memory.
   *
   * @param {string} name The name that the storage mechanism was registered
   *                      under.
   * @export
   */
  static unregister(name) {
    shaka.offline.StorageMuxer.registry_.delete(name);
  }

  /**
   * Check if there is support for storage on this platform. It is assumed that
   * if there are any mechanisms registered, it means that storage is supported
   * on this platform. We do not check if the mechanisms have any cells.
   *
   * @return {boolean}
   */
  static support() {
    const registry = shaka.offline.StorageMuxer.getRegistry_();
    // Make sure that we will have SOME mechanisms created by creating a
    // mechanism and immediately destroying it.
    for (const create of registry.values()) {
      const instance = create();

      if (instance) {
        instance.destroy();
        return true;
      }
    }

    return false;
  }

  /**
   * Replace the mechanism map used by the muxer. This should only be used
   * in testing.
   *
   * @param {Map.<string, function():shaka.extern.StorageMechanism>} map
   */
  static overrideSupport(map) {
    shaka.offline.StorageMuxer.override_ = map;
  }

  /**
   * Undo a previous call to |overrideSupport|.
   */
  static clearOverride() {
    shaka.offline.StorageMuxer.override_ = null;
  }

  /**
   * Get the registry. If the support has been disabled, this will always
   * an empty registry. Reading should always be done via |getRegistry_|.
   *
   * @return {!Map.<string, function():shaka.extern.StorageMechanism>}
   * @private
   */
  static getRegistry_() {
    const override = shaka.offline.StorageMuxer.override_;
    const registry = shaka.offline.StorageMuxer.registry_;

    if (COMPILED) {
      return registry;
    } else {
      return override || registry;
    }
  }
};


/**
 * @private {Map.<string, function():shaka.extern.StorageMechanism>}
 */
shaka.offline.StorageMuxer.override_ = null;


/**
 * @private {!Map.<string, function():shaka.extern.StorageMechanism>}
 */
shaka.offline.StorageMuxer.registry_ = new Map();
