/**
 * @typedef {Record<string, any>} WorkerTask
 * @property {number} [taskId]          An incrementing task ID used to reference task progress
 * @property {WorkerManager.WORKER_TASK_ACTIONS} action  The task action being performed, from WorkerManager.WORKER_TASK_ACTIONS
 */

/**
 * An asynchronous web Worker which can load user-defined functions and await execution using Promises.
 * @param {string} name                 The worker name to be initialized
 * @param {object} [options={}]         Worker initialization options
 * @param {boolean} [options.debug=false]           Should the worker run in debug mode?
 * @param {boolean} [options.loadPrimitives=false]  Should the worker automatically load the primitives library?
 * @param {string[]} [options.scripts]              Should the worker operates in script modes? Optional scripts.
 */
class AsyncWorker extends Worker {
  constructor(name, {debug=false, loadPrimitives=false, scripts}={}) {
    super(AsyncWorker.WORKER_HARNESS_JS);
    this.name = name;
    this.addEventListener("message", this.#onMessage.bind(this));
    this.addEventListener("error", this.#onError.bind(this));

    this.#ready = this.#dispatchTask({
      action: WorkerManager.WORKER_TASK_ACTIONS.INIT,
      workerName: name,
      debug,
      loadPrimitives,
      scripts
    });
  }

  /**
   * A path reference to the JavaScript file which provides companion worker-side functionality.
   * @type {string}
   */
  static WORKER_HARNESS_JS = "scripts/worker.js";

  /**
   * A queue of active tasks that this Worker is executing.
   * @type {Map<number, {resolve: (result: any) => void, reject: (error: Error) => void}>}
   */
  #tasks = new Map();

  /**
   * An auto-incrementing task index.
   * @type {number}
   */
  #taskIndex = 0;

  /**
   * A Promise which resolves once the Worker is ready to accept tasks
   * @type {Promise}
   */
  get ready() {
    return this.#ready;
  }

  #ready;

  /* -------------------------------------------- */
  /*  Task Management                             */
  /* -------------------------------------------- */

  /**
   * Load a function onto a given Worker.
   * The function must be a pure function with no external dependencies or requirements on global scope.
   * @param {string} functionName   The name of the function to load
   * @param {Function} functionRef  A reference to the function that should be loaded
   * @returns {Promise<unknown>}    A Promise which resolves once the Worker has loaded the function.
   */
  async loadFunction(functionName, functionRef) {
    return this.#dispatchTask({
      action: WorkerManager.WORKER_TASK_ACTIONS.LOAD,
      functionName,
      functionBody: functionRef.toString()
    });
  }

  /* -------------------------------------------- */

  /**
   * Execute a task on a specific Worker.
   * @param {string} functionName   The named function to execute on the worker. This function must first have been
   *                                loaded.
   * @param {Array<*>} [args]       An array of parameters with which to call the requested function
   * @param {Array<*>} [transfer]   An array of transferable objects which are transferred to the worker thread.
   *                                See https://developer.mozilla.org/en-US/docs/Glossary/Transferable_objects
   * @returns {Promise<unknown>}    A Promise which resolves with the returned result of the function once complete.
   */
  async executeFunction(functionName, args=[], transfer=[]) {
    const action = WorkerManager.WORKER_TASK_ACTIONS.EXECUTE;
    return this.#dispatchTask({action, functionName, args}, transfer);
  }

  /* -------------------------------------------- */

  /**
   * Dispatch a task to a named Worker, awaiting confirmation of the result.
   * @param {WorkerTask} taskData   Data to dispatch to the Worker as part of the task.
   * @param {Array<*>} transfer     An array of transferable objects which are transferred to the worker thread.
   * @returns {Promise}             A Promise which wraps the task transaction.
   */
  async #dispatchTask(taskData={}, transfer=[]) {
    const taskId = taskData.taskId = this.#taskIndex++;
    return new Promise((resolve, reject) => {
      this.#tasks.set(taskId, {resolve, reject});
      this.postMessage(taskData, transfer);
    });
  }

  /* -------------------------------------------- */

  /**
   * Handle messages emitted by the Worker thread.
   * @param {MessageEvent} event      The dispatched message event
   */
  #onMessage(event) {
    const response = event.data;
    const task = this.#tasks.get(response.taskId);
    if ( !task ) return;
    this.#tasks.delete(response.taskId);
    if ( response.error ) return task.reject(response.error);
    return task.resolve(response.result);
  }

  /* -------------------------------------------- */

  /**
   * Handle errors emitted by the Worker thread.
   * @param {ErrorEvent} error        The dispatched error event
   */
  #onError(error) {
    error.message = `An error occurred in Worker ${this.name}: ${error.message}`;
    console.error(error);
  }
}

/* -------------------------------------------- */

/**
 * A client-side class responsible for managing a set of web workers.
 * This interface is accessed as a singleton instance via game.workers.
 * @see Game#workers
 */
class WorkerManager extends Map {
  constructor() {
    if ( game.workers instanceof WorkerManager ) {
      throw new Error("The singleton WorkerManager instance has already been constructed as Game#workers");
    }
    super();
  }

  /**
   * Supported worker task actions
   * @enum {string}
   */
  static WORKER_TASK_ACTIONS = Object.freeze({
    INIT: "init",
    LOAD: "load",
    EXECUTE: "execute"
  });

  /* -------------------------------------------- */
  /*  Worker Management                           */
  /* -------------------------------------------- */

  /**
   * Create a new named Worker.
   * @param {string} name                 The named Worker to create
   * @param {object} [config={}]          Worker configuration parameters passed to the AsyncWorker constructor
   * @returns {Promise<AsyncWorker>}      The created AsyncWorker which is ready to accept tasks
   */
  async createWorker(name, config={}) {
    if (this.has(name)) {
      throw new Error(`A Worker already exists with the name "${name}"`);
    }
    const worker = new AsyncWorker(name, config);
    this.set(name, worker);
    await worker.ready;
    return worker;
  }

  /* -------------------------------------------- */

  /**
   * Retire a current Worker, terminating it immediately.
   * @see Worker#terminate
   * @param {string} name           The named worker to terminate
   */
  retireWorker(name) {
    const worker = this.get(name);
    if ( !worker ) return;
    worker.terminate();
    this.delete(name);
  }

  /* -------------------------------------------- */

  /**
   * @deprecated since 11
   * @ignore
   */
  getWorker(name) {
    foundry.utils.logCompatibilityWarning("WorkerManager#getWorker is deprecated in favor of WorkerManager#get",
      {since: 11, until: 13});
    const w = this.get(name);
    if ( !w ) throw new Error(`No worker with name ${name} currently exists!`);
    return w;
  }
}

/* -------------------------------------------- */
