/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */

"use strict";

class WorkerDispatcher {
  #msgId = 1;
  #worker = null;
  // Map of message ids -> promise resolution functions, for dispatching worker responses
  #pendingCalls = new Map();
  #url = "";

  constructor(url) {
    this.#url = url;
  }

  start() {
    // When running in debugger jest test, we don't have access to ChromeWorker
    if (typeof ChromeWorker == "function") {
      this.#worker = new ChromeWorker(this.#url);
    } else {
      this.#worker = new Worker(this.#url);
    }
    this.#worker.onerror = err => {
      console.error(`Error in worker ${this.#url}`, err.message);
    };
    this.#worker.addEventListener("message", this.#onMessage);
  }

  stop() {
    if (!this.#worker) {
      return;
    }

    this.#worker.removeEventListener("message", this.#onMessage);
    this.#worker.terminate();
    this.#worker = null;
    this.#pendingCalls.clear();
  }

  task(method, { queue = false } = {}) {
    const calls = [];
    const push = args => {
      return new Promise((resolve, reject) => {
        if (queue && calls.length === 0) {
          Promise.resolve().then(flush);
        }

        calls.push({ args, resolve, reject });

        if (!queue) {
          flush();
        }
      });
    };

    const flush = () => {
      const items = calls.slice();
      calls.length = 0;

      if (!this.#worker) {
        this.start();
      }

      const id = this.#msgId++;
      this.#worker.postMessage({
        id,
        method,
        calls: items.map(item => item.args),
      });

      this.#pendingCalls.set(id, items);
    };

    return (...args) => push(args);
  }

  invoke(method, ...args) {
    return this.task(method)(...args);
  }

  #onMessage = ({ data: result }) => {
    const items = this.#pendingCalls.get(result.id);
    this.#pendingCalls.delete(result.id);
    if (!items) {
      return;
    }

    if (!this.#worker) {
      return;
    }

    result.results.forEach((resultData, i) => {
      const { resolve, reject } = items[i];

      if (resultData.error) {
        const err = new Error(resultData.message);
        err.metadata = resultData.metadata;
        reject(err);
      } else {
        resolve(resultData.response);
      }
    });
  };
}

function workerHandler(publicInterface) {
  return function (msg) {
    const { id, method, calls } = msg.data;

    Promise.all(
      calls.map(args => {
        try {
          const response = publicInterface[method].apply(undefined, args);
          if (response instanceof Promise) {
            return response.then(
              val => ({ response: val }),
              err => asErrorMessage(err)
            );
          }
          return { response };
        } catch (error) {
          return asErrorMessage(error);
        }
      })
    ).then(results => {
      globalThis.postMessage({ id, results });
    });
  };
}

function asErrorMessage(error) {
  if (typeof error === "object" && error && "message" in error) {
    // Error can't be sent via postMessage, so be sure to convert to
    // string.
    return {
      error: true,
      message:
        error.message +
        (error.stack ? "\nStack in the worker:" + error.stack : ""),
      metadata: error.metadata,
    };
  }

  return {
    error: true,
    message: error == null ? error : error.toString(),
    metadata: undefined,
  };
}

// Might be loaded within a worker thread where `module` isn't available.
if (typeof module !== "undefined") {
  module.exports = {
    WorkerDispatcher,
    workerHandler,
  };
}
