//@ts-check

import { createTrackedResponse } from "./createTrackedResponse.mjs";
import { decodeContentLength } from "./decodeContentLength.mjs";
import { retryPromise } from "./retryPromise.mjs";
import { makeFileSizeFetcher } from "./makeFileSizeFetcher.mjs";
import { makeSignal } from "./makeSignal.mjs";

/**
 * @typedef {{
 *  total?: number,
 *  onProgress?:(data:ProgressData)=>void,
 *  signal?: AbortSignal
 * }} FileLoaderOptions
 */

/**
 * @typedef {ReturnType<typeof makeFileLoader>} FileLoader
 */

/**
 * Creates an object that downloads files and allows you to track progress.
 * You can optionally provide an initial file size; if not provided, the size
 * will be attempt to be acquired through reading the headers when download starts.
 *
 * Downloads can be aborted with an AbortSignal, and the response can be obtained on
 * `response`.
 *
 * `response` can be awaited even before the download starts.
 *
 * @typedef {{received: number, total:number}} ProgressData
 * @param {string} path
 * @param {FileLoaderOptions} options
 */
export const makeFileLoader = (
  path,
  { total = 0, onProgress, signal } = {}
) => {
  const progress = makeSignal({ received: 0, total: 0 });

  if (onProgress) {
    progress.connect(onProgress, { signal });
  }

  const createResponse = () =>
    createTrackedResponse(
      (received) => progress.emit({ received, total }),
      signal
    );

  let responsePromise = createResponse();

  /**
   * Retrieves the file size if `total` was not provided.
   */
  const fetchSize = makeFileSizeFetcher(path, total);

  /**
   * Fetches a file, dispatching progress events while doing so.
   */
  const start = () =>
    fetch(path, { signal }).then((initialResponse) => {
      if (!initialResponse.body || !initialResponse.ok) {
        throw new Error(`Failed loading file '${path}'`);
      }
      total = total || decodeContentLength(initialResponse.headers);

      const reader = initialResponse.body.getReader();
      // this triggers the deferred promise and starts the loading process
      responsePromise.start(reader);
      return responsePromise.response;
    });

  /**
   * Loads a file, _if it isn't already loaded_.
   * If it is loaded, or if the loading has started, then this results in a
   * no-op.
   * It's therefore safe to use multiple times.
   * This function returns as soon as the download is triggered. To await the
   * download, await the `response` property and/or listen to the success event.
   *
   * @param {number} retries amount of time to try again in case of failure.
   *                         Defaults to 4.
   * @returns {Promise<Response>}
   */
  const load = (retries = 4) =>
    new Promise((ok, no) => {
      if (responsePromise.isStarted) {
        return ok(responsePromise.response);
      }
      if (responsePromise.isFailed) {
        return no();
      }
      return retryPromise(start, retries).then(ok).catch(no);
    });

  /**
   * clears loaded file from memory (if there are no more references to it)
   */
  const unload = () => {
    responsePromise = createResponse();
  };

  return {
    fetchSize,
    load,
    unload,
    progress,
    get path() {
      return path;
    },
    get total() {
      return total;
    },
    get received() {
      return responsePromise.received;
    },
    get isComplete() {
      return total > 0 && responsePromise.received === total;
    },
    get response() {
      return responsePromise.response;
    },
  };
};

/**
 * Allows to track a group of file loaders
 * @param {FileLoader[]} files
 */
export const makeFileLoadersTracker = (files) => {
  const done = makeSignal();
  const progress = makeSignal({ received: 0, total: 0 });
  let total = 0;
  let received = 0;
  let amount = files.length;

  const decrease = () => {
    if (--amount === 0) {
      done.emit();
    }
  };

  /**
   * Called every time any file's status changes.
   * Updates the total and received amounts.
   */
  const update = () => {
    let _total = 0;
    let _received = 0;

    files.forEach((thing) => {
      _total += thing.total;
      _received += thing.received;
      console.log(thing.path, thing.received, thing.total);
    });
    if (total != _total || received != _received) {
      total = _total;
      received = _received;
      progress.emit({ total, received });
    }
  };

  files.forEach((loader) => {
    loader.then(decrease);
    loader.progress.connect(update);
  });

  const ensureSize = () =>
    Promise.all(files.map(({ fetchSize }) => fetchSize()));

  const loadAll = () => Promise.all(files.map(({ load }) => load()));

  return { done, progress, ensureSize, loadAll };
};

export default makeFileLoader;
