// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
"use strict";

((window) => {
  const core = window.Deno.core;
  const webidl = window.__bootstrap.webidl;
  const { setTarget } = window.__bootstrap.event;

  const handlerSymbol = Symbol("eventHandlers");
  function makeWrappedHandler(handler) {
    function wrappedHandler(...args) {
      if (typeof wrappedHandler.handler !== "function") {
        return;
      }
      return wrappedHandler.handler.call(this, ...args);
    }
    wrappedHandler.handler = handler;
    return wrappedHandler;
  }
  // TODO(lucacasonato) reuse when we can reuse code between web crates
  function defineEventHandler(emitter, name) {
    // HTML specification section 8.1.5.1
    Object.defineProperty(emitter, `on${name}`, {
      get() {
        // TODO(bnoordhuis) The "BroadcastChannel should have an onmessage
        // event" WPT test expects that .onmessage !== undefined. Returning
        // null makes it pass but is perhaps not exactly in the spirit.
        return this[handlerSymbol]?.get(name)?.handler ?? null;
      },
      set(value) {
        if (!this[handlerSymbol]) {
          this[handlerSymbol] = new Map();
        }
        let handlerWrapper = this[handlerSymbol]?.get(name);
        if (handlerWrapper) {
          handlerWrapper.handler = value;
        } else {
          handlerWrapper = makeWrappedHandler(value);
          this.addEventListener(name, handlerWrapper);
        }
        this[handlerSymbol].set(name, handlerWrapper);
      },
      configurable: true,
      enumerable: true,
    });
  }

  const _name = Symbol("[[name]]");
  const _closed = Symbol("[[closed]]");

  const channels = [];
  let rid = null;

  async function recv() {
    while (channels.length > 0) {
      const message = await core.opAsync("op_broadcast_recv", rid);

      if (message === null) {
        break;
      }

      const [name, data] = message;
      dispatch(null, name, new Uint8Array(data));
    }

    core.close(rid);
    rid = null;
  }

  function dispatch(source, name, data) {
    for (const channel of channels) {
      if (channel === source) continue; // Don't self-send.
      if (channel[_name] !== name) continue;
      if (channel[_closed]) continue;

      const go = () => {
        if (channel[_closed]) return;
        const event = new MessageEvent("message", {
          data: core.deserialize(data), // TODO(bnoordhuis) Cache immutables.
          origin: "http://127.0.0.1",
        });
        setTarget(event, channel);
        channel.dispatchEvent(event);
      };

      defer(go);
    }
  }

  // Defer to avoid starving the event loop. Not using queueMicrotask()
  // for that reason: it lets promises make forward progress but can
  // still starve other parts of the event loop.
  function defer(go) {
    setTimeout(go, 1);
  }

  class BroadcastChannel extends EventTarget {
    [_name];
    [_closed] = false;

    get name() {
      return this[_name];
    }

    constructor(name) {
      super();

      const prefix = "Failed to construct 'BroadcastChannel'";
      webidl.requiredArguments(arguments.length, 1, { prefix });

      this[_name] = webidl.converters["DOMString"](name, {
        prefix,
        context: "Argument 1",
      });

      this[webidl.brand] = webidl.brand;

      channels.push(this);

      if (rid === null) {
        // Create the rid immediately, otherwise there is a time window (and a
        // race condition) where messages can get lost, because recv() is async.
        rid = core.opSync("op_broadcast_subscribe");
        recv();
      }
    }

    postMessage(message) {
      webidl.assertBranded(this, BroadcastChannel);

      const prefix = "Failed to execute 'postMessage' on 'BroadcastChannel'";
      webidl.requiredArguments(arguments.length, 1, { prefix });

      if (this[_closed]) {
        throw new DOMException("Already closed", "InvalidStateError");
      }

      if (typeof message === "function" || typeof message === "symbol") {
        throw new DOMException("Uncloneable value", "DataCloneError");
      }

      const data = core.serialize(message);

      // Send to other listeners in this VM.
      dispatch(this, this[_name], new Uint8Array(data));

      // Send to listeners in other VMs.
      defer(() => core.opAsync("op_broadcast_send", [rid, this[_name]], data));
    }

    close() {
      webidl.assertBranded(this, BroadcastChannel);
      this[_closed] = true;

      const index = channels.indexOf(this);
      if (index === -1) return;

      channels.splice(index, 1);
      if (channels.length === 0) core.opSync("op_broadcast_unsubscribe", rid);
    }
  }

  defineEventHandler(BroadcastChannel.prototype, "message");
  defineEventHandler(BroadcastChannel.prototype, "messageerror");

  window.__bootstrap.broadcastChannel = { BroadcastChannel };
})(this);