/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
 * 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/. */

import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
import { BrowserUtils } from "resource://gre/modules/BrowserUtils.sys.mjs";
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";

const APPLE_COPY_LINK = "com.apple.share.CopyLink.invite";

let lazy = {};

XPCOMUtils.defineLazyServiceGetters(lazy, {
  WindowsUIUtils: ["@mozilla.org/windows-ui-utils;1", Ci.nsIWindowsUIUtils],
});

// Use a non-caching getter so tests can swap out the service via MockRegistrar.
Object.defineProperty(lazy, "MacSharingService", {
  get() {
    return Cc["@mozilla.org/widget/macsharingservice;1"].getService(
      Ci.nsIMacSharingService
    );
  },
});

class SharingUtilsCls {
  /**
   * Updates a sharing item in a given menu, creating it if necessary.
   */
  updateShareURLMenuItem(browser, insertAfterEl) {
    if (!Services.prefs.getBoolPref("browser.menu.share_url.allow", true)) {
      return;
    }

    let shareURL = insertAfterEl.nextElementSibling;
    if (!shareURL?.matches(".share-tab-url-item")) {
      shareURL = this.#createShareURLMenuItem(insertAfterEl);
    }

    shareURL.browserToShare = Cu.getWeakReference(browser);
    if (AppConstants.platform != "macosx") {
      // On macOS, we keep the item enabled and handle enabled state
      // inside the menupopup.
      // Everywhere else, we disable the item, as there's no submenu.
      shareURL.hidden = !BrowserUtils.getShareableURL(browser.currentURI);
    }
  }

  /**
   * Creates and returns the "Share" menu item.
   */
  #createShareURLMenuItem(insertAfterEl) {
    let menu = insertAfterEl.parentNode;
    let shareURL = null;
    let document = insertAfterEl.ownerDocument;
    if (AppConstants.platform != "win" && AppConstants.platform != "macosx") {
      shareURL = this.#buildCopyLinkItem(document);
    } else {
      if (AppConstants.platform == "win") {
        shareURL = this.#buildShareURLItem(document);
      } else if (AppConstants.platform == "macosx") {
        shareURL = this.#buildShareURLMenu(document);
      }
      let l10nID =
        menu.id == "tabContextMenu"
          ? "tab-context-share-url"
          : "menu-file-share-url";
      document.l10n.setAttributes(shareURL, l10nID);
    }
    shareURL.classList.add("share-tab-url-item");

    menu.insertBefore(shareURL, insertAfterEl.nextSibling);
    return shareURL;
  }

  /**
   * Returns a menu item specifically for accessing Windows sharing services.
   */
  #buildShareURLItem(document) {
    let shareURLMenuItem = document.createXULElement("menuitem");
    shareURLMenuItem.addEventListener("command", this);
    return shareURLMenuItem;
  }

  /**
   * Returns a menu specifically for accessing macOSx sharing services .
   */
  #buildShareURLMenu(document) {
    let menu = document.createXULElement("menu");
    let menuPopup = document.createXULElement("menupopup");
    menuPopup.addEventListener("popupshowing", this);
    menu.appendChild(menuPopup);
    return menu;
  }

  /**
   * Return a menuitem that only copies the link. Useful for
   * OSes where we do not yet have full share support, like Linux.
   *
   * We currently also use this on macOS because for some reason Apple does not
   * provide the share service option for this.
   */
  #buildCopyLinkItem(document) {
    let shareURLMenuItem = document.createXULElement("menuitem");
    document.l10n.setAttributes(shareURLMenuItem, "menu-share-copy-link");
    shareURLMenuItem.classList.add("share-copy-link");
    shareURLMenuItem.setAttribute("data-share-name", "share_macosx_copy");

    if (AppConstants.platform == "macosx") {
      shareURLMenuItem.classList.add("menuitem-iconic");
      shareURLMenuItem.setAttribute(
        "image",
        "chrome://global/skin/icons/link.svg"
      );
    } else {
      // On macOS the command handling happens by virtue of the submenu
      // command event listener.
      shareURLMenuItem.addEventListener("command", this);
    }
    return shareURLMenuItem;
  }

  /**
   * Get the sharing data for a given DOM node.
   */
  getDataToShare(node) {
    let browser = node.browserToShare?.get();
    let urlToShare = null;
    let titleToShare = null;

    if (browser) {
      let maybeToShare = BrowserUtils.getShareableURL(browser.currentURI);
      if (maybeToShare) {
        let { gURLBar } = node.ownerGlobal;
        urlToShare = gURLBar.makeURIReadable(maybeToShare).displaySpec;

        titleToShare = browser.contentTitle;
      }
    }
    return { urlToShare, titleToShare };
  }

  /**
   * Populates the "Share" menupopup on macOSx.
   */
  initializeShareURLPopup(menuPopup) {
    if (AppConstants.platform != "macosx") {
      return;
    }

    this.populateShareMenu(menuPopup);

    menuPopup.parentNode
      .closest("menupopup")
      .addEventListener("popuphiding", this);
    menuPopup.setAttribute("data-initialized", true);
  }

  populateShareMenu(menuPopup) {
    // Clear existing
    while (menuPopup.firstChild) {
      menuPopup.firstChild.remove();
    }

    let document = menuPopup.ownerDocument;
    let node = menuPopup.parentNode;

    let { urlToShare } = this.getDataToShare(node);

    // If we can't share the current URL, we display the items disabled,
    // but enable the "more..." item at the bottom, to allow the user to
    // change sharing preferences in the system dialog.
    let shouldEnable = !!urlToShare;
    if (!urlToShare) {
      // Fake it so we can ask the sharing service for services:
      urlToShare = "https://mozilla.org/";
    }

    let services = lazy.MacSharingService.getSharingProviders(urlToShare);

    if (!menuPopup.hasAttribute("data-command-listener")) {
      menuPopup.addEventListener("command", event => {
        if (event.target.classList.contains("share-more-button")) {
          this.openMacSharePreferences();
        } else if (event.target.classList.contains("share-copy-link")) {
          this.copyLink(node);
        } else if (event.target.dataset.shareName) {
          this.shareOnMac(node, event.target.dataset.shareName);
        }
      });

      menuPopup.setAttribute("data-command-listener", "true");
    }

    // Apple seems reluctant to provide copy link as a feature. Add it at the
    // start if it's not there.
    if (!services.some(s => s.name == APPLE_COPY_LINK)) {
      let item = this.#buildCopyLinkItem(document);
      if (!shouldEnable) {
        item.setAttribute("disabled", "true");
      }
      menuPopup.appendChild(item);
    }

    // Share service items
    services.forEach(share => {
      let item = document.createXULElement("menuitem");
      item.classList.add("menuitem-iconic");
      item.setAttribute("label", share.menuItemTitle);
      item.setAttribute("data-share-name", share.name);
      item.setAttribute("image", ChromeUtils.encodeURIForSrcset(share.image));
      if (!shouldEnable) {
        item.setAttribute("disabled", "true");
      }
      menuPopup.appendChild(item);
    });
    menuPopup.appendChild(document.createXULElement("menuseparator"));

    // More item
    let moreItem = document.createXULElement("menuitem");
    document.l10n.setAttributes(moreItem, "menu-share-more");
    moreItem.classList.add("menuitem-iconic", "share-more-button");
    moreItem.setAttribute("data-share-name", "share_macosx_more");
    menuPopup.appendChild(moreItem);
  }

  onShareURLCommand(event) {
    // Only call sharing services for the "Share" menu item. These services
    // are accessed from a submenu popup for MacOS or the "Share" menu item
    // for Windows. Use .closest() as a hack to find either the item itself
    // or a parent with the right class.
    let target = event.target.closest(".share-tab-url-item");
    if (!target) {
      return;
    }

    // urlToShare/titleToShare may be null, in which case only the "more"
    // item is enabled, so handle that case first:
    if (event.target.classList.contains("share-more-button")) {
      this.openMacSharePreferences();
      return;
    }

    if (event.target.classList.contains("share-copy-link")) {
      this.copyLink(target);
    } else if (AppConstants.platform == "win") {
      this.shareOnWindows(target);
    } else {
      // On macOSX platforms
      let shareName = event.target.getAttribute("data-share-name");
      if (shareName) {
        this.shareOnMac(target, shareName);
      }
    }
  }

  onPopupHiding(event) {
    // We don't want to rebuild the contents of the "Share" menupopup if only its submenu is
    // hidden. So bail if this isn't the top menupopup in the DOM tree:
    if (event.target.parentNode.closest("menupopup")) {
      return;
    }
    // Otherwise, clear its "data-initialized" attribute.
    let menupopup = event.target.querySelector(
      ".share-tab-url-item"
    )?.menupopup;
    menupopup?.removeAttribute("data-initialized");

    event.target.removeEventListener("popuphiding", this);
  }

  onPopupShowing(event) {
    if (!event.target.hasAttribute("data-initialized")) {
      this.initializeShareURLPopup(event.target);
    }
  }

  handleEvent(aEvent) {
    switch (aEvent.type) {
      case "command":
        this.onShareURLCommand(aEvent);
        break;
      case "popuphiding":
        this.onPopupHiding(aEvent);
        break;
      case "popupshowing":
        this.onPopupShowing(aEvent);
        break;
    }
  }

  copyLink(node) {
    let { urlToShare, titleToShare } = this.getDataToShare(node);
    if (!urlToShare) {
      return;
    }

    BrowserUtils.copyLink(urlToShare, titleToShare);
  }

  shareOnWindows(node) {
    let { urlToShare, titleToShare } = this.getDataToShare(node);
    if (!urlToShare) {
      return;
    }

    lazy.WindowsUIUtils.shareUrl(urlToShare, titleToShare);
  }

  shareOnMac(node, serviceName) {
    let { urlToShare, titleToShare } = this.getDataToShare(node);
    if (!urlToShare) {
      return;
    }

    lazy.MacSharingService.shareUrl(serviceName, urlToShare, titleToShare);
  }

  openMacSharePreferences() {
    lazy.MacSharingService.openSharingPreferences();
  }

  testOnlyMockUIUtils(mock) {
    if (!Cu.isInAutomation) {
      throw new Error("Can only mock utils in automation.");
    }
    // eslint-disable-next-line mozilla/valid-lazy
    Object.defineProperty(lazy, "WindowsUIUtils", {
      get() {
        if (mock) {
          return mock;
        }
        return Cc["@mozilla.org/windows-ui-utils;1"].getService(
          Ci.nsIWindowsUIUtils
        );
      },
    });
  }
}

export let SharingUtils = new SharingUtilsCls();
