/* Any copyright is dedicated to the Public Domain.
   http://creativecommons.org/publicdomain/zero/1.0/ */

let bc = new BroadcastChannel("inter-sw-postmessage");
let myId = /\/sw-(.)+$/.exec(registration.scope)[1];
// If we are being imported by the generated script from
// `sw_always_updating_inter_sw_postmessage.sjs`, there will be a "version"
// global and it starts counting from 1.
let myVersion = "version" in globalThis ? globalThis.version : 0;
let myFullId = `${myId}#${myVersion}`;

onactivate = function () {
  bc.postMessage(`${myId}:version-activated:${myVersion}`);
};

function extractId(urlStr) {
  if (!urlStr) {
    return urlStr;
  }
  const qIndex = urlStr.indexOf("?");
  if (qIndex >= 0) {
    return urlStr.substring(qIndex + 1);
  }
  if (urlStr.endsWith("/empty_with_utils.html")) {
    return "helper";
  }
  return urlStr;
}

function describeSource(source) {
  // Note that WindowProxy is impossible here, so we don't check it.
  if (source === null) {
    return "null";
  } else if (source instanceof MessagePort) {
    return "port";
  } else if (source instanceof WindowClient) {
    return `wc-${extractId(source.url)}`;
  } else if (source instanceof Client) {
    return `c-${extractId(source.url)}`;
  } else if (source instanceof ServiceWorker) {
    return `sw-${extractId(source.scriptURL)}`;
  } else {
    return "unexpected";
  }
}

let lastPostMessageSource = null;
globalThis.onmessage = async function handle_message(evt) {
  console.log(myId, "received postMessage");
  lastPostMessageSource = evt.source;
  bc.postMessage(
    `${myId}:received-post-message-from:${describeSource(evt.source)}`
  );
};

/**
 * Map a target descriptor onto something we can postMessage.  Possible options
 * and the resulting target:
 * - `last-source`: The `.source` property of the most recent event received via
 *   `globalThis.onmessage`.
 * - `reg-sw-ID`: The active ServiceWorker found on a registration whose
 *   scriptURL ends with `?ID`.  This allows us to distinguish between multiple
 *   (non-self-updating) ServiceWorkers on the same registration because each SW
 *   can be given a distinct script path via the `?ID` suffix.  But it does not
 *   work for self-updating ServiceWorkers where the only difference is the
 *   version identifier embedded in the script itself.
 */
async function resolveTarget(descriptor) {
  if (descriptor === "last-source") {
    return lastPostMessageSource;
  } else if (descriptor.startsWith("reg-")) {
    const registrations = await navigator.serviceWorker.getRegistrations();
    let filterFunc;
    if (descriptor.startsWith("reg-sw-")) {
      const descriptorId = /^reg-sw-(.+)$/.exec(descriptor)[1];
      console.log(
        "Looking for registration with id",
        descriptorId,
        "across",
        registrations.length,
        "registrations"
      );
      filterFunc = sw => {
        if (sw) {
          console.log("checking SW", sw.scriptURL);
        }
        return extractId(sw?.scriptURL) === descriptorId;
      };
    } else {
      throw new Error(`Target selector '${descriptor}' not understood`);
    }

    for (const reg of registrations) {
      console.log("Reg scriptURL", reg.active?.scriptURL);
      if (filterFunc(reg.active)) {
        return reg.active;
      } else if (filterFunc(reg.waiting)) {
        return reg.waiting;
      } else if (filterFunc(reg.installing)) {
        return reg.installing;
      }
    }
    throw new Error("No registration matches found!");
  }
  throw new Error(`Target selector '${descriptor}' not understood`);
}

/**
 * Map a registration descriptor onto a registration.  Options:
 * - `scope-ID`: The registration with a scope ending with `/sw-ID`.
 */
async function resolveRegistration(descriptor) {
  if (descriptor.startsWith("sw-")) {
    const registrations = await navigator.serviceWorker.getRegistrations();

    const scopeSuffix = `/${descriptor}`;
    for (const reg of registrations) {
      if (reg.scope.endsWith(scopeSuffix)) {
        return reg;
      }
    }

    throw new Error("No registration matches found!");
  }
  throw new Error(`Registration selector '${descriptor}' not understood`);
}

bc.onmessage = async function handle_bc(evt) {
  // Split the message into colon-delimited commands of the form:
  // <who should do the thing>:<the command>:<the target of the command>
  if (typeof evt?.data !== "string") {
    return;
  }
  const pieces = evt?.data?.split(":");
  if (
    !pieces ||
    pieces.length < 2 ||
    (pieces[0] !== myId && pieces[0] !== myFullId)
  ) {
    return;
  }

  const cmd = pieces[1];
  try {
    if (cmd === "post-message-to") {
      const target = await resolveTarget(pieces[2]);
      target.postMessage("yo!");
    } else if (cmd === "update-reg") {
      const reg = await resolveRegistration(pieces[2]);
      reg.update();
    } else if (cmd === "install-reg") {
      const installId = pieces[2];
      const scope = `sw-${installId}`;
      const script = `sw_inter_sw_postmessage.js?${installId}`;
      await navigator.serviceWorker.register(script, {
        scope,
      });
      bc.postMessage(`${myId}:registered:${installId}`);
    } else if (cmd === "workerref-hang") {
      const topic = pieces[2];
      globalThis.WorkerTestUtils.holdStrongWorkerRefUntilMainThreadObserverNotified(
        topic
      );
      bc.postMessage(`${myId}:workerref-hung:${topic}`);
    } else if (cmd === "block") {
      const topic = pieces[2];
      globalThis.WorkerTestUtils.blockUntilMainThreadObserverNotified(
        topic,
        // This callback is invoked once the observer has been registered.
        () => {
          bc.postMessage(`${myId}:blocking:${topic}`);
        }
      );
    } else if (cmd === "notify-observer") {
      const topic = pieces[2];
      globalThis.WorkerTestUtils.notifyObserverOnMainThread(topic);
      bc.postMessage(`${myId}:notified-observer:${topic}`);
    }
  } catch (ex) {
    console.error(ex);
    bc.postMessage({
      error: ex + "",
      myId,
      processing: evt?.data,
    });
  }
};
