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

/*
 * TODO: This is based on what PdfJs was already doing, it would be
 * best to use this over there as well to reduce duplication and
 * inconsistency.
 */

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

const lazy = {};

XPCOMUtils.defineLazyServiceGetter(
  lazy,
  "HandlerService",
  "@mozilla.org/uriloader/handler-service;1",
  Ci.nsIHandlerService
);
XPCOMUtils.defineLazyServiceGetter(
  lazy,
  "MIMEService",
  "@mozilla.org/mime;1",
  Ci.nsIMIMEService
);

ChromeUtils.defineESModuleGetters(lazy, {
  Integration: "resource://gre/modules/Integration.sys.mjs",
});

const PREF_BRANCH = "browser.download.viewableInternally.";
export const PREF_ENABLED_TYPES = PREF_BRANCH + "enabledTypes";
export const PREF_BRANCH_WAS_REGISTERED = PREF_BRANCH + "typeWasRegistered.";

export const PREF_BRANCH_PREVIOUS_ACTION =
  PREF_BRANCH + "previousHandler.preferredAction.";

export const PREF_BRANCH_PREVIOUS_ASK =
  PREF_BRANCH + "previousHandler.alwaysAskBeforeHandling.";

export let DownloadsViewableInternally = {
  /**
   * Initially add/remove handlers, watch pref, register with Integration.downloads.
   */
  register() {
    // Watch the pref
    XPCOMUtils.defineLazyPreferenceGetter(
      this,
      "_enabledTypes",
      PREF_ENABLED_TYPES,
      "",
      () => this._updateAllHandlers(),
      pref => {
        let itemStr = pref.trim();
        return itemStr ? itemStr.split(",").map(s => s.trim()) : [];
      }
    );

    for (let handlerType of this._downloadTypesViewableInternally) {
      if (handlerType.initAvailable) {
        handlerType.initAvailable();
      }
    }

    // Initially update handlers
    this._updateAllHandlers();

    // Register the check for use in DownloadIntegration
    lazy.Integration.downloads.register(() => ({
      shouldViewDownloadInternally:
        this._shouldViewDownloadInternally.bind(this),
    }));
  },

  /**
   * MIME types to handle with an internal viewer, for downloaded files.
   *
   * |extension| is an extenson that will be viewable, as an alternative for
   *   the MIME type itself. It is also used more generally to identify this
   *   type: It is part of a pref name to indicate the handler was set up once,
   *   and it is the string present in |PREF_ENABLED_TYPES| to enable the type.
   *
   * |mimeTypes| are the types that will be viewable. A handler is set up for
   *   the first element in the array.
   *
   * If |managedElsewhere| is falsy, |_updateAllHandlers()| will set
   *   up or remove handlers for the type, and |_shouldViewDownloadInternally()|
   *   will check for it in |PREF_ENABLED_TYPES|.
   *
   * |available| is used to check whether this type should have
   *   handleInternally handlers set up, and if false then
   *   |_shouldViewDownloadInternally()| will also return false for this
   *   type. If |available| would change, |DownloadsViewableInternally._updateHandler()|
   *   should be called for the type.
   *
   * |initAvailable()| is an opportunity to initially set |available|, set up
   *   observers to change it when prefs change, etc.
   *
   */
  _downloadTypesViewableInternally: [
    {
      extension: "xml",
      mimeTypes: ["text/xml", "application/xml"],
      available: true,
      managedElsewhere: true,
    },
    {
      extension: "svg",
      mimeTypes: ["image/svg+xml"],

      initAvailable() {
        XPCOMUtils.defineLazyPreferenceGetter(
          this,
          "available",
          "svg.disabled",
          true,
          () => DownloadsViewableInternally._updateHandler(this),
          // transform disabled to enabled/available
          disabledPref => !disabledPref
        );
      },
      // available getter is set by initAvailable()
      managedElsewhere: true,
    },
    {
      extension: "webp",
      mimeTypes: ["image/webp"],
      available: true,
      managedElsewhere: false,
    },
    {
      extension: "avif",
      mimeTypes: ["image/avif"],
      available: true,
      managedElsewhere: false,
    },
    {
      extension: "jxl",
      mimeTypes: ["image/jxl"],
      initAvailable() {
        XPCOMUtils.defineLazyPreferenceGetter(
          this,
          "available",
          "image.jxl.enabled",
          false,
          () => DownloadsViewableInternally._updateHandler(this)
        );
      },
      // available getter is set by initAvailable()
    },
    {
      extension: "pdf",
      mimeTypes: ["application/pdf"],
      // PDF uses pdfjs.disabled rather than PREF_ENABLED_TYPES.
      // pdfjs.disabled isn't checked here because PdfJs's own _becomeHandler
      // and _unbecomeHandler manage the handler if the pref is set, and there
      // is an explicit check in nsUnknownContentTypeDialog.shouldShowInternalHandlerOption
      available: true,
      managedElsewhere: true,
    },
  ],

  /*
   * Implementation for DownloadIntegration.shouldViewDownloadInternally
   */
  _shouldViewDownloadInternally(aMimeType, aExtension) {
    if (!aMimeType) {
      return false;
    }

    return this._downloadTypesViewableInternally.some(handlerType => {
      if (
        !handlerType.managedElsewhere &&
        !this._enabledTypes.includes(handlerType.extension)
      ) {
        return false;
      }

      return (
        (handlerType.mimeTypes.includes(aMimeType) ||
          handlerType.extension == aExtension?.toLowerCase()) &&
        handlerType.available
      );
    });
  },

  _makeFakeHandler(aMimeType, aExtension) {
    // Based on PdfJs gPdfFakeHandlerInfo.
    return {
      QueryInterface: ChromeUtils.generateQI(["nsIMIMEInfo"]),
      getFileExtensions() {
        return [aExtension];
      },
      possibleApplicationHandlers: Cc["@mozilla.org/array;1"].createInstance(
        Ci.nsIMutableArray
      ),
      extensionExists(ext) {
        return ext == aExtension;
      },
      alwaysAskBeforeHandling: false,
      preferredAction: Ci.nsIHandlerInfo.handleInternally,
      type: aMimeType,
    };
  },

  _saveSettings(handlerInfo, handlerType) {
    Services.prefs.setIntPref(
      PREF_BRANCH_PREVIOUS_ACTION + handlerType.extension,
      handlerInfo.preferredAction
    );
    Services.prefs.setBoolPref(
      PREF_BRANCH_PREVIOUS_ASK + handlerType.extension,
      handlerInfo.alwaysAskBeforeHandling
    );
  },

  _restoreSettings(handlerInfo, handlerType) {
    const prevActionPref = PREF_BRANCH_PREVIOUS_ACTION + handlerType.extension;
    if (Services.prefs.prefHasUserValue(prevActionPref)) {
      handlerInfo.alwaysAskBeforeHandling = Services.prefs.getBoolPref(
        PREF_BRANCH_PREVIOUS_ASK + handlerType.extension
      );
      handlerInfo.preferredAction = Services.prefs.getIntPref(prevActionPref);
      lazy.HandlerService.store(handlerInfo);
    } else {
      // Nothing to restore, just remove the handler.
      lazy.HandlerService.remove(handlerInfo);
    }
  },

  _clearSavedSettings(extension) {
    Services.prefs.clearUserPref(PREF_BRANCH_PREVIOUS_ACTION + extension);
    Services.prefs.clearUserPref(PREF_BRANCH_PREVIOUS_ASK + extension);
  },

  _updateAllHandlers() {
    // Set up or remove handlers for each type, if not done already
    for (const handlerType of this._downloadTypesViewableInternally) {
      if (!handlerType.managedElsewhere) {
        this._updateHandler(handlerType);
      }
    }
  },

  _updateHandler(handlerType) {
    const wasRegistered = Services.prefs.getBoolPref(
      PREF_BRANCH_WAS_REGISTERED + handlerType.extension,
      false
    );

    const toBeRegistered =
      this._enabledTypes.includes(handlerType.extension) &&
      handlerType.available;

    if (toBeRegistered && !wasRegistered) {
      this._becomeHandler(handlerType);
    } else if (!toBeRegistered && wasRegistered) {
      this._unbecomeHandler(handlerType);
    }
  },

  _becomeHandler(handlerType) {
    // Set up an empty handler with only a preferred action, to avoid
    // having to ask the OS about handlers on startup.
    let fakeHandlerInfo = this._makeFakeHandler(
      handlerType.mimeTypes[0],
      handlerType.extension
    );
    if (!lazy.HandlerService.exists(fakeHandlerInfo)) {
      lazy.HandlerService.store(fakeHandlerInfo);
    } else {
      const handlerInfo = lazy.MIMEService.getFromTypeAndExtension(
        handlerType.mimeTypes[0],
        handlerType.extension
      );

      if (handlerInfo.preferredAction != Ci.nsIHandlerInfo.handleInternally) {
        // Save the previous settings of preferredAction and
        // alwaysAskBeforeHandling in case we need to revert them.
        // Even if we don't force preferredAction here, the user could
        // set handleInternally manually.
        this._saveSettings(handlerInfo, handlerType);
      } else {
        // handleInternally shouldn't already have been set, the best we
        // can do to restore is to remove the handler, so make sure
        // the settings are clear.
        this._clearSavedSettings(handlerType.extension);
      }

      // Replace the preferred action if it didn't indicate an external viewer.
      // Note: This is a point of departure from PdfJs, which always replaces
      // the preferred action.
      if (
        handlerInfo.preferredAction != Ci.nsIHandlerInfo.useHelperApp &&
        handlerInfo.preferredAction != Ci.nsIHandlerInfo.useSystemDefault
      ) {
        handlerInfo.preferredAction = Ci.nsIHandlerInfo.handleInternally;
        handlerInfo.alwaysAskBeforeHandling = false;

        lazy.HandlerService.store(handlerInfo);
      }
    }

    // Note that we set up for this type so a) we don't keep replacing the
    // handler and b) so it can be cleared later.
    Services.prefs.setBoolPref(
      PREF_BRANCH_WAS_REGISTERED + handlerType.extension,
      true
    );
  },

  _unbecomeHandler(handlerType) {
    let handlerInfo;
    try {
      handlerInfo = lazy.MIMEService.getFromTypeAndExtension(
        handlerType.mimeTypes[0],
        handlerType.extension
      );
    } catch (ex) {
      // Allow the handler lookup to fail.
    }
    // Restore preferred action if it is still handleInternally
    // (possibly just removing the handler if nothing was saved for it).
    if (handlerInfo?.preferredAction == Ci.nsIHandlerInfo.handleInternally) {
      this._restoreSettings(handlerInfo, handlerType);
    }

    // In any case we do not control this handler now.
    this._clearSavedSettings(handlerType.extension);
    Services.prefs.clearUserPref(
      PREF_BRANCH_WAS_REGISTERED + handlerType.extension
    );
  },
};
