import {FilesLoader} from '../io/filesLoader.js';
import {MemoryLoader} from '../io/memoryLoader.js';
import {UrlsLoader} from '../io/urlsLoader.js';

/**
 * Load controller.
 */
export class LoadController {

  /**
   * The default character set.
   *
   * @type {string}
   */
  #defaultCharacterSet;

  /**
   * List of current loaders.
   *
   * @type {object}
   */
  #currentLoaders = {};

  /**
   * @param {string} defaultCharacterSet The default character set.
   */
  constructor(defaultCharacterSet) {
    this.#defaultCharacterSet = defaultCharacterSet;
  }

  /**
   * Load a list of files. Can be image files or a state file.
   *
   * @param {File[]} files The list of files to load.
   * @param {string} dataId The data Id.
   */
  loadFiles(files, dataId) {
    // has been checked for emptiness.
    const ext = files[0].name.split('.').pop().toLowerCase();
    if (ext === 'json') {
      this.#loadStateFile(files[0], dataId);
    } else {
      this.#loadImageFiles(files, dataId);
    }
  }

  /**
   * Load a list of URLs. Can be image files or a state file.
   *
   * @param {string[]} urls The list of urls to load.
   * @param {string} dataId The data Id.
   * @param {object} [options] The load options:
   * - requestHeaders: an array of {name, value} to use as request headers.
   * - withCredentials: credentials flag to pass to the request.
   */

  loadURLs(urls, dataId, options) {
    // has been checked for emptiness.
    const ext = urls[0].split('.').pop().toLowerCase();
    if (ext === 'json') {
      this.#loadStateUrl(urls[0], dataId, options);
    } else {
      this.#loadImageUrls(urls, dataId, options);
    }
  }

  /**
   * Load a list of ArrayBuffers.
   *
   * @param {Array} data The list of ArrayBuffers to load
   *   in the form of [{name: '', filename: '', data: data}].
   * @param {string} dataId The data Id.
   */
  loadImageObject(data, dataId) {
    // create IO
    const memoryIO = new MemoryLoader();
    // load data
    this.#loadData(data, memoryIO, 'image', dataId);
  }

  /**
   * Get the currently loaded data ids.
   *
   * @returns {string[]} The data ids.
   */
  getLoadingDataIds() {
    return Object.keys(this.#currentLoaders);
  }

  /**
   * Abort an individual current loader.
   *
   * @param {string} dataId The data to stop loading.
   */
  abort(dataId) {
    if (typeof this.#currentLoaders[dataId] !== 'undefined') {
      this.#currentLoaders[dataId].loader.abort();
      delete this.#currentLoaders[dataId];
    }
  }

  // private ----------------------------------------------------------------

  /**
   * Load a list of image files.
   *
   * @param {File[]} files The list of image files to load.
   * @param {string} dataId The data Id.
   */
  #loadImageFiles(files, dataId) {
    // create IO
    const fileIO = new FilesLoader();
    fileIO.setDefaultCharacterSet(this.#defaultCharacterSet);
    // load data
    this.#loadData(files, fileIO, 'image', dataId);
  }

  /**
   * Load a list of image URLs.
   *
   * @param {string[]} urls The list of urls to load.
   * @param {string} [dataId] The data Id.
   * @param {object} [options] The load options:
   * - requestHeaders: an array of {name, value} to use as request headers.
   * - withCredentials: credentials flag to pass to the request.
   */
  #loadImageUrls(urls, dataId, options) {
    // create IO
    const urlIO = new UrlsLoader();
    urlIO.setDefaultCharacterSet(this.#defaultCharacterSet);
    // load data
    this.#loadData(urls, urlIO, 'image', dataId, options);
  }

  /**
   * Load a State file.
   *
   * @param {File} file The state file to load.
   * @param {string} dataId The data Id.
   */
  #loadStateFile(file, dataId) {
    // create IO
    const fileIO = new FilesLoader();
    // load data
    this.#loadData([file], fileIO, 'state', dataId);
  }


  /**
   * Load a State url.
   *
   * @param {string} url The state url to load.
   * @param {string} [dataId] The data Id.
   * @param {object} [options] The load options:
   * - requestHeaders: an array of {name, value} to use as request headers.
   * - withCredentials: credentials flag to pass to the request.
   */
  #loadStateUrl(url, dataId, options) {
    // create IO
    const urlIO = new UrlsLoader();
    // load data
    this.#loadData([url], urlIO, 'state', dataId, options);
  }

  /**
   * Load a list of data.
   *
   * @param {string[]|File[]|Array} data Array of data to load.
   * @param {object} loader The data loader.
   * @param {string} loadType The data load type: 'image' or 'state'.
   * @param {string} dataId The data id.
   * @param {object} [options] Options passed to the final loader.
   */
  #loadData(data, loader, loadType, dataId, options) {
    const eventInfo = {
      loadtype: loadType,
      dataid: dataId
    };

    // set callbacks
    loader.onloadstart = (event) => {
      // store loader to allow abort
      this.#currentLoaders[dataId] = {
        loader: loader,
        isFirstItem: true
      };
      // callback
      this.#augmentCallbackEvent(this.onloadstart, eventInfo)(event);
    };
    loader.onprogress = this.#augmentCallbackEvent(this.onprogress, eventInfo);
    loader.onloaditem = (event) => {
      const eventInfoItem = {
        loadtype: loadType,
        dataid: dataId
      };
      if (typeof this.#currentLoaders[dataId] !== 'undefined') {
        eventInfoItem.isfirstitem = this.#currentLoaders[dataId].isFirstItem;
      }
      // callback
      this.#augmentCallbackEvent(this.onloaditem, eventInfoItem)(event);
      // update loader
      if (typeof this.#currentLoaders[dataId] !== 'undefined' &&
        this.#currentLoaders[dataId].isFirstItem) {
        this.#currentLoaders[dataId].isFirstItem = false;
      }
    };
    loader.onload = this.#augmentCallbackEvent(this.onload, eventInfo);
    loader.onloadend = (event) => {
      // reset current loader
      delete this.#currentLoaders[dataId];
      // callback
      this.#augmentCallbackEvent(this.onloadend, eventInfo)(event);
    };
    loader.onerror = this.#augmentCallbackEvent(this.onerror, eventInfo);
    loader.onabort = this.#augmentCallbackEvent(this.onabort, eventInfo);
    if (typeof loader.ontimeout !== 'undefined') {
      loader.ontimeout = this.#augmentCallbackEvent(this.ontimeout, eventInfo);
    }
    // launch load
    try {
      loader.load(data, options);
    } catch (error) {
      this.onerror({
        error: error,
        dataid: dataId
      });
      this.onloadend({
        dataid: dataId
      });
      return;
    }
  }

  /**
   * Augment a callback event: adds loadtype to the event
   *  passed to a callback.
   *
   * @param {object} callback The callback to update.
   * @param {object} info Info object to append to the event.
   * @returns {object} A function representing the modified callback.
   */
  #augmentCallbackEvent(callback, info) {
    return function (event) {
      const keys = Object.keys(info);
      for (let i = 0; i < keys.length; ++i) {
        const key = keys[i];
        event[key] = info[key];
      }
      callback(event);
    };
  }

  /**
   * Handle a load start event.
   * Default does nothing.
   *
   * @param {object} _event The load start event.
   */
  onloadstart(_event) {}

  /**
   * Handle a load progress event.
   * Default does nothing.
   *
   * @param {object} _event The progress event.
   */
  onprogress(_event) {}

  /**
   * Handle a load event.
   * Default does nothing.
   *
   * @param {object} _event The load event fired
   *   when a file has been loaded successfully.
   */
  onload(_event) {}

  /**
   * Handle a load item event.
   * Default does nothing.
   *
   * @param {object} _event The load event fired
   *   when an item has been loaded successfully.
   */
  onloaditem(_event) {}

  /**
   * Handle a load end event.
   * Default does nothing.
   *
   * @param {object} _event The load end event fired
   *  when a file load has completed, successfully or not.
   */
  onloadend(_event) {}

  /**
   * Handle an error event.
   * Default does nothing.
   *
   * @param {object} _event The error event.
   */
  onerror(_event) {}

  /**
   * Handle a timeout event.
   * Default does nothing.
   *
   * @param {object} _event The timeout event.
   */
  ontimeout(_event) {}

  /**
   * Handle an abort event.
   * Default does nothing.
   *
   * @param {object} _event The abort event.
   */
  onabort(_event) {}

} // class LoadController
