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

/**
 * This module exports a provider that offers remote tabs.
 */

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

import {
  UrlbarProvider,
  UrlbarUtils,
} from "resource:///modules/UrlbarUtils.sys.mjs";

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
  SyncedTabs: "resource://services-sync/SyncedTabs.sys.mjs",
  UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
  UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs",
  UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.sys.mjs",
});

let _cache = null;

// By default, we add remote tabs that have been used more recently than this
// time ago. Any remaining remote tabs are added in queue if no other results
// are found.
const RECENT_REMOTE_TAB_THRESHOLD_MS = 72 * 60 * 60 * 1000; // 72 hours.

ChromeUtils.defineLazyGetter(lazy, "weaveXPCService", function () {
  try {
    return Cc["@mozilla.org/weave/service;1"].getService(Ci.nsISupports)
      .wrappedJSObject;
  } catch (ex) {
    // The app didn't build Sync.
  }
  return null;
});

XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "showRemoteIconsPref",
  "services.sync.syncedTabs.showRemoteIcons",
  true
);

XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "syncUsernamePref",
  "services.sync.username"
);

// from MDN...
function escapeRegExp(string) {
  return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

/**
 * Class used to create the provider.
 */
class ProviderRemoteTabs extends UrlbarProvider {
  constructor() {
    super();
    Services.obs.addObserver(this.observe, "weave:engine:sync:finish");
    Services.obs.addObserver(this.observe, "weave:service:start-over");
  }

  /**
   * Unique name for the provider, used by the context to filter on providers.
   *
   * @returns {string}
   */
  get name() {
    return "RemoteTabs";
  }

  /**
   * @returns {Values<typeof UrlbarUtils.PROVIDER_TYPE>}
   */
  get type() {
    return UrlbarUtils.PROVIDER_TYPE.NETWORK;
  }

  /**
   * Whether this provider should be invoked for the given context.
   * If this method returns false, the providers manager won't start a query
   * with this provider, to save on resources.
   *
   * @param {UrlbarQueryContext} queryContext The query context object
   */
  async isActive(queryContext) {
    return (
      lazy.syncUsernamePref &&
      lazy.UrlbarPrefs.get("suggest.remotetab") &&
      queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.TABS) &&
      lazy.weaveXPCService &&
      lazy.weaveXPCService.ready &&
      lazy.weaveXPCService.enabled
    );
  }

  /**
   * Starts querying. Extended classes should return a Promise resolved when the
   * provider is done searching AND returning results.
   *
   * @param {UrlbarQueryContext} queryContext The query context object
   * @param {Function} addCallback Callback invoked by the provider to add a new
   *        result. A UrlbarResult should be passed to it.
   */
  async startQuery(queryContext, addCallback) {
    let instance = this.queryInstance;

    let searchString = queryContext.tokens.map(t => t.value).join(" ");

    let re = new RegExp(escapeRegExp(searchString), "i");
    let tabsData = await this.ensureCache();
    if (instance != this.queryInstance) {
      return;
    }

    let resultsAdded = 0;
    let staleTabs = [];
    for (let { tab, client } of tabsData) {
      if (
        !searchString ||
        searchString == lazy.UrlbarTokenizer.RESTRICT.OPENPAGE ||
        re.test(tab.url) ||
        (tab.title && re.test(tab.title))
      ) {
        if (lazy.showRemoteIconsPref) {
          if (!tab.icon) {
            // It's rare that Sync supplies the icon for the page. If it does, it is a
            // string URL.
            tab.icon = UrlbarUtils.getIconForUrl(tab.url);
          } else {
            tab.icon = lazy.PlacesUtils.favicons.getFaviconLinkForIcon(
              Services.io.newURI(tab.icon)
            ).spec;
          }
        }

        let result = new lazy.UrlbarResult(
          UrlbarUtils.RESULT_TYPE.REMOTE_TAB,
          UrlbarUtils.RESULT_SOURCE.TABS,
          ...lazy.UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
            url: [tab.url, UrlbarUtils.HIGHLIGHT.TYPED],
            title: [tab.title, UrlbarUtils.HIGHLIGHT.TYPED],
            device: client.name,
            icon: lazy.showRemoteIconsPref ? tab.icon : "",
            lastUsed: (tab.lastUsed || 0) * 1000,
          })
        );

        // We want to return the most relevant remote tabs and thus the most
        // recent ones. While SyncedTabs.sys.mjs returns tabs that are sorted by
        // most recent client, then most recent tab, we can do better. For
        // example, the most recent client might have one recent tab and then
        // many very stale tabs. Those very stale tabs will push out more recent
        // tabs from staler clients. This provider first returns tabs from the
        // last 72 hours, sorted by client recency. Then, it adds remaining
        // tabs. We are not concerned about filling the remote tabs group with
        // stale tabs, because the muxer ensures remote tabs flex with other
        // results. It will only show the stale tabs if it has nothing else
        // to show.
        if (
          tab.lastUsed <=
          (Date.now() - RECENT_REMOTE_TAB_THRESHOLD_MS) / 1000
        ) {
          staleTabs.push(result);
        } else {
          addCallback(this, result);
          resultsAdded++;
        }
      }

      if (resultsAdded == queryContext.maxResults) {
        break;
      }
    }

    while (staleTabs.length && resultsAdded < queryContext.maxResults) {
      addCallback(this, staleTabs.shift());
      resultsAdded++;
    }
  }

  /**
   * Build the in-memory structure we use.
   *
   * @returns {Promise<{tab: object, client: object}[]>}
   */
  async buildItems() {
    // This is sorted by most recent client, most recent tab.
    let tabsData = [];
    // If Sync isn't initialized (either due to lag at startup or due to no user
    // being signed in), don't reach in to Weave.Service as that may initialize
    // Sync unnecessarily - we'll get an observer notification later when it
    // becomes ready and has synced a list of tabs.
    if (lazy.weaveXPCService.ready) {
      let clients = await lazy.SyncedTabs.getTabClients();
      lazy.SyncedTabs.sortTabClientsByLastUsed(clients);
      for (let client of clients) {
        for (let tab of client.tabs) {
          tabsData.push({ tab, client });
        }
      }
    }
    return tabsData;
  }

  /**
   * Ensure the cache is good.
   *
   * @returns {Promise<{tab: object, client: object}[]>}
   */
  async ensureCache() {
    if (!_cache) {
      _cache = await this.buildItems();
    }
    return _cache;
  }

  observe(subject, topic, data) {
    switch (topic) {
      case "weave:engine:sync:finish":
        if (data == "tabs") {
          // The tabs engine just finished syncing, so may have a different list
          // of tabs then we previously cached.
          _cache = null;
        }
        break;

      case "weave:service:start-over":
        // Sync is being reset due to the user disconnecting - we must invalidate
        // the cache so we don't supply tabs from a different user.
        _cache = null;
        break;

      default:
        break;
    }
  }
}

export var UrlbarProviderRemoteTabs = new ProviderRemoteTabs();
