/* 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/. */

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  EveryWindow: "resource:///modules/EveryWindow.sys.mjs",
  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
  clearTimeout: "resource://gre/modules/Timer.sys.mjs",
  requestIdleCallback: "resource://gre/modules/Timer.sys.mjs",
  setTimeout: "resource://gre/modules/Timer.sys.mjs",
});

let notificationsByWindow = new WeakMap();

export class _ToolbarBadgeHub {
  constructor() {
    this.id = "toolbar-badge-hub";
    this.state = {};
    this.removeAllNotifications = this.removeAllNotifications.bind(this);
    this.removeToolbarNotification = this.removeToolbarNotification.bind(this);
    this.addToolbarNotification = this.addToolbarNotification.bind(this);
    this.registerBadgeToAllWindows = this.registerBadgeToAllWindows.bind(this);
    this._sendPing = this._sendPing.bind(this);
    this.sendUserEventTelemetry = this.sendUserEventTelemetry.bind(this);

    this._handleMessageRequest = null;
    this._addImpression = null;
    this._blockMessageById = null;
    this._sendTelemetry = null;
    this._initialized = false;
  }

  async init(
    waitForInitialized,
    {
      handleMessageRequest,
      addImpression,
      blockMessageById,
      unblockMessageById,
      sendTelemetry,
    }
  ) {
    if (this._initialized) {
      return;
    }

    this._initialized = true;
    this._handleMessageRequest = handleMessageRequest;
    this._blockMessageById = blockMessageById;
    this._unblockMessageById = unblockMessageById;
    this._addImpression = addImpression;
    this._sendTelemetry = sendTelemetry;
    // Need to wait for ASRouter to initialize before trying to fetch messages
    await waitForInitialized;
    this.messageRequest({
      triggerId: "toolbarBadgeUpdate",
      template: "toolbar_badge",
    });
  }

  maybeInsertFTL(win) {
    win.MozXULElement.insertFTLIfNeeded("browser/newtab/asrouter.ftl");
  }

  _clearBadgeTimeout() {
    if (this.state.showBadgeTimeoutId) {
      lazy.clearTimeout(this.state.showBadgeTimeoutId);
    }
  }

  removeAllNotifications(event) {
    if (event) {
      // ignore right clicks
      if (
        (event.type === "mousedown" || event.type === "click") &&
        event.button !== 0
      ) {
        return;
      }
      // ignore keyboard access that is not one of the usual accessor keys
      if (
        event.type === "keypress" &&
        event.key !== " " &&
        event.key !== "Enter"
      ) {
        return;
      }

      event.target.removeEventListener(
        "mousedown",
        this.removeAllNotifications
      );
      event.target.removeEventListener("keypress", this.removeAllNotifications);
      // If we have an event it means the user interacted with the badge
      // we should send telemetry
      if (this.state.notification) {
        this.sendUserEventTelemetry("CLICK", this.state.notification);
      }
    }
    // Will call uninit on every window
    lazy.EveryWindow.unregisterCallback(this.id);
    if (this.state.notification) {
      this._blockMessageById(this.state.notification.id);
    }
    this._clearBadgeTimeout();
    this.state = {};
  }

  removeToolbarNotification(toolbarButton) {
    // Remove it from the element that displays the badge
    toolbarButton
      .querySelector(".toolbarbutton-badge")
      .classList.remove("feature-callout");
    toolbarButton.removeAttribute("badged");
    toolbarButton.removeAttribute("showing-callout");
    // Remove id used for for aria-label badge description
    const notificationDescription = toolbarButton.querySelector(
      "#toolbarbutton-notification-description"
    );
    if (notificationDescription) {
      notificationDescription.remove();
      toolbarButton.removeAttribute("aria-labelledby");
      toolbarButton.removeAttribute("aria-describedby");
    }
  }

  addToolbarNotification(win, message) {
    const document = win.browser.ownerDocument;
    let toolbarbutton = document.getElementById(message.content.target);
    if (toolbarbutton) {
      const badge = toolbarbutton.querySelector(".toolbarbutton-badge");
      badge.classList.add("feature-callout");
      toolbarbutton.setAttribute("badged", true);
      toolbarbutton.setAttribute("showing-callout", true);
      // If we have additional aria-label information for the notification
      // we add this content to the hidden `toolbarbutton-text` node.
      // We then use `aria-labelledby` to link this description to the button
      // that received the notification badge.
      if (message.content.badgeDescription) {
        // Insert strings as soon as we know we're showing them
        this.maybeInsertFTL(win);
        toolbarbutton.setAttribute(
          "aria-labelledby",
          `toolbarbutton-notification-description ${message.content.target}`
        );
        // Because tooltiptext is different to the label, it gets duplicated as
        // the description. Setting `describedby` to the same value as
        // `labelledby` will be detected by the a11y code and the description
        // will be removed.
        toolbarbutton.setAttribute(
          "aria-describedby",
          `toolbarbutton-notification-description ${message.content.target}`
        );
        const descriptionEl = document.createElement("span");
        descriptionEl.setAttribute(
          "id",
          "toolbarbutton-notification-description"
        );
        descriptionEl.hidden = true;
        document.l10n.setAttributes(
          descriptionEl,
          message.content.badgeDescription.string_id
        );
        toolbarbutton.appendChild(descriptionEl);
      }
      // `mousedown` event required because of the `onmousedown` defined on
      // the button that prevents `click` events from firing
      toolbarbutton.addEventListener("mousedown", this.removeAllNotifications);
      // `keypress` event required for keyboard accessibility
      toolbarbutton.addEventListener("keypress", this.removeAllNotifications);
      this.state = { notification: { id: message.id } };

      // Impression should be added when the badge becomes visible
      this._addImpression(message);
      // Send a telemetry ping when adding the notification badge
      this.sendUserEventTelemetry("IMPRESSION", message);

      return toolbarbutton;
    }

    return null;
  }

  registerBadgeToAllWindows(message) {
    lazy.EveryWindow.registerCallback(
      this.id,
      win => {
        if (notificationsByWindow.has(win)) {
          // nothing to do
          return;
        }
        const el = this.addToolbarNotification(win, message);
        notificationsByWindow.set(win, el);
      },
      win => {
        const el = notificationsByWindow.get(win);
        if (el) {
          this.removeToolbarNotification(el);
        }
        notificationsByWindow.delete(win);
      }
    );
  }

  registerBadgeNotificationListener(message, options = {}) {
    // We need to clear any existing notifications and only show
    // the one set by devtools
    if (options.force) {
      this.removeAllNotifications();
      // When debugging immediately show the badge
      this.registerBadgeToAllWindows(message);
      return;
    }

    if (message.content.delay) {
      this.state.showBadgeTimeoutId = lazy.setTimeout(() => {
        lazy.requestIdleCallback(() => this.registerBadgeToAllWindows(message));
      }, message.content.delay);
    } else {
      this.registerBadgeToAllWindows(message);
    }
  }

  async messageRequest({ triggerId, template }) {
    const telemetryObject = { triggerId };
    TelemetryStopwatch.start("MS_MESSAGE_REQUEST_TIME_MS", telemetryObject);
    const message = await this._handleMessageRequest({
      triggerId,
      template,
    });
    TelemetryStopwatch.finish("MS_MESSAGE_REQUEST_TIME_MS", telemetryObject);
    if (message) {
      this.registerBadgeNotificationListener(message);
    }
  }

  _sendPing(ping) {
    this._sendTelemetry({
      type: "TOOLBAR_BADGE_TELEMETRY",
      data: { action: "badge_user_event", ...ping },
    });
  }

  sendUserEventTelemetry(event, message) {
    const win = Services.wm.getMostRecentWindow("navigator:browser");
    // Only send pings for non private browsing windows
    if (
      win &&
      !lazy.PrivateBrowsingUtils.isBrowserPrivate(
        win.ownerGlobal.gBrowser.selectedBrowser
      )
    ) {
      this._sendPing({
        message_id: message.id,
        event,
      });
    }
  }

  uninit() {
    this._clearBadgeTimeout();
    this.state = {};
    this._initialized = false;
    notificationsByWindow = new WeakMap();
  }
}

/**
 * ToolbarBadgeHub - singleton instance of _ToolbarBadgeHub that can initiate
 * message requests and render messages.
 */
export const ToolbarBadgeHub = new _ToolbarBadgeHub();
