/* Copyright 2022 Mozilla Foundation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

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

const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
  PdfJsTelemetry: "resource://pdf.js/PdfJsTelemetry.sys.mjs",
  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
});

class FindHandler {
  #browser;

  #callbacks = null;

  #state = null;

  constructor(aBrowser) {
    this.#browser = aBrowser;
  }

  cleanup() {
    this.#state = null;
    this.#callbacks = null;
  }

  onEvent(aEvent, aData, aCallback) {
    if (
      this.#browser.contentPrincipal.spec !==
      "resource://pdf.js/web/viewer.html"
    ) {
      return;
    }

    debug`onEvent: name=${aEvent}, data=${aData}`;

    switch (aEvent) {
      case "GeckoView:ClearMatches":
        this.cleanup();
        this.#browser.sendMessageToActor(
          "PDFJS:Child:handleEvent",
          {
            type: "findbarclose",
            detail: null,
          },
          "GeckoViewPdfjs"
        );
        break;
      case "GeckoView:DisplayMatches":
        if (!this.#state) {
          return;
        }
        this.#browser.sendMessageToActor(
          "PDFJS:Child:handleEvent",
          {
            type: "findhighlightallchange",
            detail: this.#state,
          },
          "GeckoViewPdfjs"
        );
        break;
      case "GeckoView:FindInPage": {
        const type = this.#getFindType(aData);
        this.#browser.sendMessageToActor(
          "PDFJS:Child:handleEvent",
          {
            type,
            detail: this.#state,
          },
          "GeckoViewPdfjs"
        );
        this.#callbacks.push([aCallback, this.#state]);
        break;
      }
    }
  }

  #getFindType(aData) {
    const newState = {
      query: this.#state?.query,
      caseSensitive: !!aData.matchCase,
      entireWord: !!aData.wholeWord,
      highlightAll: !!aData.highlightAll,
      findPrevious: !!aData.backwards,
      matchDiacritics: !!aData.matchDiacritics,
    };
    if (!this.#state) {
      // It's a new search.
      newState.query = aData.searchString;
      this.#state = newState;
      this.#callbacks = [];
      return "find";
    }

    if (aData.searchString && this.#state.query !== aData.searchString) {
      // The searched string has changed.
      newState.query = aData.searchString;
      this.#state = newState;
      return "find";
    }

    for (const [key, type] of [
      ["caseSensitive", "findcasesensitivitychange"],
      ["entireWord", "findentirewordchange"],
      ["matchDiacritics", "finddiacriticmatchingchange"],
    ]) {
      if (this.#state[key] !== newState[key]) {
        this.#state = newState;
        return type;
      }
    }

    this.#state = newState;
    return "findagain";
  }

  updateMatchesCount(aData, aResult) {
    if (
      (aResult !== Ci.nsITypeAheadFind.FIND_FOUND &&
        aResult !== Ci.nsITypeAheadFind.FIND_WRAPPED) ||
      !this.#state
    ) {
      return;
    }

    if (this.#callbacks.length === 0) {
      warn`There are no callbacks to use to set the matches count.`;
      return;
    }

    const [callback, state] = this.#callbacks.shift();

    aData ||= { current: 0, total: -1 };

    const response = {
      found: aData.total !== 0,
      wrapped:
        aData.total === 0 || aResult === Ci.nsITypeAheadFind.FIND_WRAPPED,
      current: aData.current,
      total: aData.total,
      searchString: state.query,
      linkURL: null,
      clientRect: null,
      flags: {
        backwards: state.findPrevious,
        matchCase: state.caseSensitive,
        wholeWord: state.entireWord,
      },
    };
    callback.onSuccess(response);
  }
}

class FileSaver {
  #browser;

  #callback = null;

  #eventDispatcher;

  constructor(aBrowser, eventDispatcher) {
    this.#browser = aBrowser;
    this.#eventDispatcher = eventDispatcher;
  }

  cleanup() {
    this.#callback = null;
  }

  onEvent(aEvent, aData, aCallback) {
    if (
      this.#browser.contentPrincipal.spec !==
      "resource://pdf.js/web/viewer.html"
    ) {
      return;
    }

    lazy.PdfJsTelemetry.onGeckoview("save_as_pdf_tapped");

    this.#callback = aCallback;
    this.#browser.sendMessageToActor(
      "PDFJS:Child:handleEvent",
      {
        type: "save",
      },
      "GeckoViewPdfjs"
    );
  }

  async save({ blobUrl, filename, originalUrl }) {
    try {
      const isPrivate = lazy.PrivateBrowsingUtils.isBrowserPrivate(
        this.#browser
      );

      if (this.#callback) {
        // "Save as PDF" from the share menu.
        this.#callback.onSuccess({
          url: blobUrl,
          filename,
          originalUrl,
          isPrivate,
        });
      } else {
        // "Download" or "Open in app" from the pdf.js toolbar.
        this.#eventDispatcher.sendRequest("GeckoView:SavePdf", {
          url: blobUrl,
          filename,
          originalUrl,
          isPrivate,
          skipConfirmation: true,
          requestExternalApp: false,
        });
      }
      lazy.PdfJsTelemetry.onGeckoview("download_succeeded");
    } catch (e) {
      lazy.PdfJsTelemetry.onGeckoview("download_failed");
      if (this.#callback) {
        this.#callback?.onError(`Cannot save the pdf: ${e}.`);
      } else {
        warn`Cannot save the pdf because of: ${e}`;
      }
    } finally {
      this.cleanup();
    }
  }
}

export class GeckoViewPdfjsParent extends GeckoViewActorParent {
  #findHandler;

  #fileSaver;

  receiveMessage(aMsg) {
    debug`receiveMessage: name=${aMsg.name}, data=${aMsg.data}`;

    switch (aMsg.name) {
      case "PDFJS:Parent:updateControlState":
        return this.#updateControlState(aMsg);
      case "PDFJS:Parent:updateMatchesCount":
        return this.#updateMatchesCount(aMsg);
      case "PDFJS:Parent:addEventListener":
        return this.#addEventListener(aMsg);
      case "PDFJS:Parent:saveURL":
        return this.#save(aMsg);
      case "PDFJS:Parent:getNimbus":
        return this.#getExperimentFeature();
      case "PDFJS:Parent:recordExposure":
        return this.#recordExposure();
      case "PDFJS:Parent:reportTelemetry":
        return this.#reportTelemetry(aMsg);
      default:
        break;
    }

    return undefined;
  }

  didDestroy() {
    debug`didDestroy`;

    if (!this.#findHandler) {
      return;
    }

    // The eventDispatcher is null when the tab is destroyed.
    if (this.eventDispatcher) {
      this.eventDispatcher.unregisterListener(this.#findHandler, [
        "GeckoView:ClearMatches",
        "GeckoView:DisplayMatches",
        "GeckoView:FindInPage",
      ]);
      this.eventDispatcher.unregisterListener(this.#fileSaver, [
        "GeckoView:PDFSave",
      ]);
    }

    this.#findHandler.cleanup();
    this.#findHandler = null;
    this.#fileSaver.cleanup();
    this.#fileSaver = null;
  }

  #addEventListener({ data: { aSupportsFind } }) {
    this.#fileSaver = new FileSaver(this.browser, this.eventDispatcher);
    this.eventDispatcher.registerListener(this.#fileSaver, [
      "GeckoView:PDFSave",
    ]);

    if (!aSupportsFind) {
      return;
    }

    if (this.#findHandler) {
      this.#findHandler.cleanup();
      return;
    }

    this.#findHandler = new FindHandler(this.browser);
    this.eventDispatcher.registerListener(this.#findHandler, [
      "GeckoView:ClearMatches",
      "GeckoView:DisplayMatches",
      "GeckoView:FindInPage",
    ]);
  }

  #updateMatchesCount({ data }) {
    this.#findHandler.updateMatchesCount(data, Ci.nsITypeAheadFind.FIND_FOUND);
  }

  #updateControlState({ data: { matchesCount, result } }) {
    this.#findHandler.updateMatchesCount(matchesCount, result);
  }

  #save({ data }) {
    this.#fileSaver.save(data);
  }

  async #getExperimentFeature() {
    let result = null;
    try {
      const experimentActor = this.window.moduleManager.getActor(
        "GeckoViewExperimentDelegate"
      );
      result = await experimentActor.getExperimentFeature("pdfjs");
    } catch (e) {
      warn`Cannot get experiment feature: ${e}`;
    }
    this.sendAsyncMessage("PDFJS:Child:getNimbus", result);
  }

  async #recordExposure() {
    try {
      const experimentActor = this.window.moduleManager.getActor(
        "GeckoViewExperimentDelegate"
      );
      await experimentActor.recordExposure("pdfjs");
    } catch (e) {
      warn`Cannot record experiment exposure: ${e}`;
    }
  }

  #reportTelemetry(aMsg) {
    lazy.PdfJsTelemetry.report(aMsg.data);
  }
}

const { debug, warn } = GeckoViewPdfjsParent.initLogging(
  "GeckoViewPdfjsParent"
);
