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

"use strict";

const EXPORTED_SYMBOLS = ["AboutWelcomeParent"];

const { XPCOMUtils } = ChromeUtils.importESModule(
  "resource://gre/modules/XPCOMUtils.sys.mjs"
);

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
  BuiltInThemes: "resource:///modules/BuiltInThemes.sys.mjs",
  PromiseUtils: "resource://gre/modules/PromiseUtils.sys.mjs",
  Region: "resource://gre/modules/Region.sys.mjs",
});

XPCOMUtils.defineLazyModuleGetters(lazy, {
  AddonManager: "resource://gre/modules/AddonManager.jsm",
  FxAccounts: "resource://gre/modules/FxAccounts.jsm",
  MigrationUtils: "resource:///modules/MigrationUtils.jsm",
  SpecialMessageActions:
    "resource://messaging-system/lib/SpecialMessageActions.jsm",
  AboutWelcomeTelemetry:
    "resource://activity-stream/aboutwelcome/lib/AboutWelcomeTelemetry.jsm",
  AboutWelcomeDefaults:
    "resource://activity-stream/aboutwelcome/lib/AboutWelcomeDefaults.jsm",
  ShellService: "resource:///modules/ShellService.jsm",
  LangPackMatcher: "resource://gre/modules/LangPackMatcher.jsm",
});

XPCOMUtils.defineLazyGetter(lazy, "log", () => {
  const { Logger } = ChromeUtils.import(
    "resource://messaging-system/lib/Logger.jsm"
  );
  return new Logger("AboutWelcomeParent");
});

XPCOMUtils.defineLazyGetter(
  lazy,
  "Telemetry",
  () => new lazy.AboutWelcomeTelemetry()
);

const DID_SEE_ABOUT_WELCOME_PREF = "trailhead.firstrun.didSeeAboutWelcome";
const AWTerminate = {
  WINDOW_CLOSED: "welcome-window-closed",
  TAB_CLOSED: "welcome-tab-closed",
  APP_SHUT_DOWN: "app-shut-down",
  ADDRESS_BAR_NAVIGATED: "address-bar-navigated",
};
const LIGHT_WEIGHT_THEMES = {
  AUTOMATIC: "default-theme@mozilla.org",
  DARK: "firefox-compact-dark@mozilla.org",
  LIGHT: "firefox-compact-light@mozilla.org",
  ALPENGLOW: "firefox-alpenglow@mozilla.org",
  "PLAYMAKER-SOFT": "playmaker-soft-colorway@mozilla.org",
  "PLAYMAKER-BALANCED": "playmaker-balanced-colorway@mozilla.org",
  "PLAYMAKER-BOLD": "playmaker-bold-colorway@mozilla.org",
  "EXPRESSIONIST-SOFT": "expressionist-soft-colorway@mozilla.org",
  "EXPRESSIONIST-BALANCED": "expressionist-balanced-colorway@mozilla.org",
  "EXPRESSIONIST-BOLD": "expressionist-bold-colorway@mozilla.org",
  "VISIONARY-SOFT": "visionary-soft-colorway@mozilla.org",
  "VISIONARY-BALANCED": "visionary-balanced-colorway@mozilla.org",
  "VISIONARY-BOLD": "visionary-bold-colorway@mozilla.org",
  "ACTIVIST-SOFT": "activist-soft-colorway@mozilla.org",
  "ACTIVIST-BALANCED": "activist-balanced-colorway@mozilla.org",
  "ACTIVIST-BOLD": "activist-bold-colorway@mozilla.org",
  "DREAMER-SOFT": "dreamer-soft-colorway@mozilla.org",
  "DREAMER-BALANCED": "dreamer-balanced-colorway@mozilla.org",
  "DREAMER-BOLD": "dreamer-bold-colorway@mozilla.org",
  "INNOVATOR-SOFT": "innovator-soft-colorway@mozilla.org",
  "INNOVATOR-BALANCED": "innovator-balanced-colorway@mozilla.org",
  "INNOVATOR-BOLD": "innovator-bold-colorway@mozilla.org",
};

async function getImportableSites() {
  const sites = [];

  // Just handle these chromium-based browsers for now
  for (const browserId of ["chrome", "chromium-edge", "chromium"]) {
    // Skip if there's no profile data.
    const migrator = await lazy.MigrationUtils.getMigrator(browserId);
    if (!migrator) {
      continue;
    }

    // Check each profile for top sites
    const dataPath = await migrator.wrappedJSObject._getChromeUserDataPathIfExists();
    for (const profile of await migrator.getSourceProfiles()) {
      let path = PathUtils.join(dataPath, profile.id, "Top Sites");
      // Skip if top sites data is missing
      if (!(await IOUtils.exists(path))) {
        console.error(`Missing file at ${path}`);
        continue;
      }

      try {
        for (const row of await lazy.MigrationUtils.getRowsFromDBWithoutLocks(
          path,
          `Importable ${browserId} top sites`,
          `SELECT url
           FROM top_sites
           ORDER BY url_rank`
        )) {
          sites.push(row.getString(0));
        }
      } catch (ex) {
        console.error(
          `Failed to get importable top sites from ${browserId} ${ex}`
        );
      }
    }
  }
  return sites;
}

class AboutWelcomeObserver {
  constructor() {
    Services.obs.addObserver(this, "quit-application");

    this.win = Services.focus.activeWindow;
    if (!this.win) {
      return;
    }

    this.terminateReason = AWTerminate.ADDRESS_BAR_NAVIGATED;

    this.onWindowClose = () => {
      this.terminateReason = AWTerminate.WINDOW_CLOSED;
    };

    this.onTabClose = () => {
      this.terminateReason = AWTerminate.TAB_CLOSED;
    };

    this.win.addEventListener("TabClose", this.onTabClose, { once: true });
    this.win.addEventListener("unload", this.onWindowClose, { once: true });
  }

  observe(aSubject, aTopic, aData) {
    switch (aTopic) {
      case "quit-application":
        this.terminateReason = AWTerminate.APP_SHUT_DOWN;
        break;
    }
  }

  // Added for testing
  get AWTerminate() {
    return AWTerminate;
  }

  stop() {
    lazy.log.debug(`Terminate reason is ${this.terminateReason}`);
    Services.obs.removeObserver(this, "quit-application");
    if (!this.win) {
      return;
    }
    this.win.removeEventListener("TabClose", this.onTabClose);
    this.win.removeEventListener("unload", this.onWindowClose);
    this.win = null;
  }
}

class RegionHomeObserver {
  observe(aSubject, aTopic, aData) {
    switch (aTopic) {
      case lazy.Region.REGION_TOPIC:
        Services.obs.removeObserver(this, lazy.Region.REGION_TOPIC);
        this.regionHomeDeferred.resolve(lazy.Region.home);
        this.regionHomeDeferred = null;
        break;
    }
  }

  promiseRegionHome() {
    // Add observer and create promise that should be resolved
    // with region or rejected inside didDestroy if user exits
    // before region is available
    if (!this.regionHomeDeferred) {
      Services.obs.addObserver(this, lazy.Region.REGION_TOPIC);
      this.regionHomeDeferred = lazy.PromiseUtils.defer();
    }
    return this.regionHomeDeferred.promise;
  }

  stop() {
    if (this.regionHomeDeferred) {
      Services.obs.removeObserver(this, lazy.Region.REGION_TOPIC);
      // Reject unresolved deferred promise on exit
      this.regionHomeDeferred.reject(
        new Error("Unresolved region home promise")
      );
      this.regionHomeDeferred = null;
    }
  }
}

class AboutWelcomeParent extends JSWindowActorParent {
  constructor() {
    super();
    this.AboutWelcomeObserver = new AboutWelcomeObserver(this);
  }

  // Static methods that calls into ShellService to check
  // if Firefox is pinned or already default
  static doesAppNeedPin() {
    return lazy.ShellService.doesAppNeedPin();
  }

  static isDefaultBrowser() {
    return lazy.ShellService.isDefaultBrowser();
  }

  didDestroy() {
    if (this.AboutWelcomeObserver) {
      this.AboutWelcomeObserver.stop();
    }
    this.RegionHomeObserver?.stop();

    lazy.Telemetry.sendTelemetry({
      event: "SESSION_END",
      event_context: {
        reason: this.AboutWelcomeObserver.terminateReason,
        page: "about:welcome",
      },
      message_id: this.AWMessageId,
    });
  }

  /**
   * Handle messages from AboutWelcomeChild.jsm
   *
   * @param {string} type
   * @param {any=} data
   * @param {Browser} the xul:browser rendering the page
   */
  async onContentMessage(type, data, browser) {
    lazy.log.debug(`Received content event: ${type}`);
    switch (type) {
      case "AWPage:SET_WELCOME_MESSAGE_SEEN":
        this.AWMessageId = data;
        try {
          Services.prefs.setBoolPref(DID_SEE_ABOUT_WELCOME_PREF, true);
        } catch (e) {
          lazy.log.debug(`Fails to set ${DID_SEE_ABOUT_WELCOME_PREF}.`);
        }
        break;
      case "AWPage:SPECIAL_ACTION":
        lazy.SpecialMessageActions.handleAction(data, browser);
        break;
      case "AWPage:FXA_METRICS_FLOW_URI":
        return lazy.FxAccounts.config.promiseMetricsFlowURI("aboutwelcome");
      case "AWPage:IMPORTABLE_SITES":
        return getImportableSites();
      case "AWPage:TELEMETRY_EVENT":
        lazy.Telemetry.sendTelemetry(data);
        break;
      case "AWPage:GET_ATTRIBUTION_DATA":
        let attributionData = await lazy.AboutWelcomeDefaults.getAttributionContent();
        return attributionData;
      case "AWPage:SELECT_THEME":
        await lazy.BuiltInThemes.ensureBuiltInThemes();
        return lazy.AddonManager.getAddonByID(
          LIGHT_WEIGHT_THEMES[data]
        ).then(addon => addon.enable());
      case "AWPage:GET_SELECTED_THEME":
        let themes = await lazy.AddonManager.getAddonsByTypes(["theme"]);
        let activeTheme = themes.find(addon => addon.isActive);
        // Store the current theme ID so user can restore their previous theme.
        if (activeTheme?.id) {
          LIGHT_WEIGHT_THEMES.AUTOMATIC = activeTheme.id;
        }
        // convert this to the short form name that the front end code
        // expects
        let themeShortName = Object.keys(LIGHT_WEIGHT_THEMES).find(
          key => LIGHT_WEIGHT_THEMES[key] === activeTheme?.id
        );
        return themeShortName?.toLowerCase();
      case "AWPage:GET_REGION":
        if (lazy.Region.home !== null) {
          return lazy.Region.home;
        }
        if (!this.RegionHomeObserver) {
          this.RegionHomeObserver = new RegionHomeObserver(this);
        }
        return this.RegionHomeObserver.promiseRegionHome();
      case "AWPage:DOES_APP_NEED_PIN":
        return AboutWelcomeParent.doesAppNeedPin();
      case "AWPage:NEED_DEFAULT":
        // Only need to set default if we're supposed to check and not default.
        return (
          Services.prefs.getBoolPref("browser.shell.checkDefaultBrowser") &&
          !AboutWelcomeParent.isDefaultBrowser()
        );
      case "AWPage:WAIT_FOR_MIGRATION_CLOSE":
        return new Promise(resolve =>
          Services.ww.registerNotification(function observer(subject, topic) {
            if (
              topic === "domwindowclosed" &&
              subject.document.documentURI ===
                "chrome://browser/content/migration/migration.xhtml"
            ) {
              Services.ww.unregisterNotification(observer);
              resolve();
            }
          })
        );
      case "AWPage:GET_APP_AND_SYSTEM_LOCALE_INFO":
        return lazy.LangPackMatcher.getAppAndSystemLocaleInfo();
      case "AWPage:NEGOTIATE_LANGPACK":
        return lazy.LangPackMatcher.negotiateLangPackForLanguageMismatch(data);
      case "AWPage:ENSURE_LANG_PACK_INSTALLED":
        return lazy.LangPackMatcher.ensureLangPackInstalled(data);
      case "AWPage:SET_REQUESTED_LOCALES":
        return lazy.LangPackMatcher.setRequestedAppLocales(data);
      case "AWPage:SEND_TO_DEVICE_EMAILS_SUPPORTED": {
        return lazy.BrowserUtils.sendToDeviceEmailsSupported();
      }
      default:
        lazy.log.debug(`Unexpected event ${type} was not handled.`);
    }

    return undefined;
  }

  /**
   * @param {{name: string, data?: any}} message
   * @override
   */
  receiveMessage(message) {
    const { name, data } = message;
    let browser;

    if (this.manager.rootFrameLoader) {
      browser = this.manager.rootFrameLoader.ownerElement;
      return this.onContentMessage(name, data, browser);
    }

    lazy.log.warn(`Not handling ${name} because the browser doesn't exist.`);
    return null;
  }
}