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

const EXPORTED_SYMBOLS = [
  "BackgroundPageThumbs",
];

const DEFAULT_CAPTURE_TIMEOUT = 30000; // ms
const DESTROY_BROWSER_TIMEOUT = 60000; // ms
const FRAME_SCRIPT_URL = "chrome://global/content/backgroundPageThumbsContent.js";

const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
const HTML_NS = "http://www.w3.org/1999/xhtml";

const { classes: Cc, interfaces: Ci, utils: Cu } = Components;

Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
Cu.import("resource://gre/modules/PageThumbs.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Task.jsm");

const global = this;

// contains base64 version of a placeholder thumbnail
const BLANKTHUMB =
"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";

//@line 27 "c:\binoczilla\src\binoc-central\ux\platform\toolkit\components\thumbnails\BackgroundPageThumbs.jsm"

const BackgroundPageThumbs = {

  /**
   * Asynchronously captures a thumbnail of the given URL.
   *
   * The page is loaded anonymously, and plug-ins are disabled.
   *
   * @param url      The URL to capture.
   * @param options  An optional object that configures the capture.  Its
   *                 properties are the following, and all are optional:
   * @opt onDone     A function that will be asynchronously called when the
   *                 capture is complete or times out.  It's called as
   *                   onDone(url),
   *                 where `url` is the captured URL.
   * @opt timeout    The capture will time out after this many milliseconds have
   *                 elapsed after the capture has progressed to the head of
   *                 the queue and started.  Defaults to 30000 (30 seconds).
   */
  capture: function (url, options={}) {
    if (!PageThumbs._prefEnabled()) {
      if (options.onDone)
        schedule(() => options.onDone(url));
      return;
    }
    this._captureQueue = this._captureQueue || [];
    this._capturesByURL = this._capturesByURL || new Map();

    // We want to avoid duplicate captures for the same URL.  If there is an
    // existing one, we just add the callback to that one and we are done.
    let existing = this._capturesByURL.get(url);
    if (existing) {
      if (options.onDone)
        existing.doneCallbacks.push(options.onDone);
      // The queue is already being processed, so nothing else to do...
      return;
    }
    let cap = new Capture(url, this._onCaptureOrTimeout.bind(this), options);
    this._captureQueue.push(cap);
    this._capturesByURL.set(url, cap);
    this._processCaptureQueue();
  },

  /**
   * Asynchronously captures a thumbnail of the given URL if one does not
   * already exist.  Otherwise does nothing.
   *
   * @param url      The URL to capture.
   * @param options  An optional object that configures the capture.  See
   *                 capture() for description.
   * @return {Promise} A Promise that resolves when this task completes
   */
  captureIfMissing: Task.async(function* (url, options={}) {
    // The fileExistsForURL call is an optimization, potentially but unlikely
    // incorrect, and no big deal when it is.  After the capture is done, we
    // atomically test whether the file exists before writing it.
    let exists = yield PageThumbsStorage.fileExistsForURL(url);
    if (exists) {
      if (options.onDone) {
        options.onDone(url);
      }
      return url;
    }
    let thumbPromise = new Promise((resolve, reject) => {
      function observe(subject, topic, data) { // jshint ignore:line
        if (data === url) {
          switch (topic) {
            case "page-thumbnail:create":
              resolve();
              break;
            case "page-thumbnail:error":
              reject(new Error("page-thumbnail:error"));
              break;
          }
          Services.obs.removeObserver(observe, "page-thumbnail:create");
          Services.obs.removeObserver(observe, "page-thumbnail:error");
        }
      }
      Services.obs.addObserver(observe, "page-thumbnail:create", false);
      Services.obs.addObserver(observe, "page-thumbnail:error", false);
    });
    try {
      this.capture(url, options);
      yield thumbPromise;
    } catch (err) {
      if (options.onDone) {
        options.onDone(url);
      }
      throw err;
    }
    return url;
  }),

  /**
   * Ensures that initialization of the thumbnail browser's parent window has
   * begun.
   *
   * @return  True if the parent window is completely initialized and can be
   *          used, and false if initialization has started but not completed.
   */
  _ensureParentWindowReady: function () {
    if (this._parentWin)
      // Already fully initialized.
      return true;
    if (this._startedParentWinInit)
      // Already started initializing.
      return false;

    this._startedParentWinInit = true;

    // Create an html:iframe, stick it in the parent document, and
    // use it to host the browser.  about:blank will not have the system
    // principal, so it can't host, but a document with a chrome URI will.
    let hostWindow = Services.appShell.hiddenDOMWindow;
    let iframe = hostWindow.document.createElementNS(HTML_NS, "iframe");
    iframe.setAttribute("src", "chrome://global/content/mozilla.xhtml");
    let onLoad = function onLoadFn() {
      iframe.removeEventListener("load", onLoad, true);
      this._parentWin = iframe.contentWindow;
      this._processCaptureQueue();
    }.bind(this);
    iframe.addEventListener("load", onLoad, true);
    hostWindow.document.documentElement.appendChild(iframe);
    this._hostIframe = iframe;

    return false;
  },

  /**
   * Destroys the service.  Queued and pending captures will never complete, and
   * their consumer callbacks will never be called.
   */
  _destroy: function () {
    if (this._captureQueue)
      this._captureQueue.forEach(cap => cap.destroy());
    this._destroyBrowser();
    if (this._hostIframe)
      this._hostIframe.remove();
    delete this._captureQueue;
    delete this._hostIframe;
    delete this._startedParentWinInit;
    delete this._parentWin;
  },

  /**
   * Creates the thumbnail browser if it doesn't already exist.
   */
  _ensureBrowser: function () {
    if (this._thumbBrowser)
      return;

    let browser = this._parentWin.document.createElementNS(XUL_NS, "browser");
    browser.setAttribute("type", "content");
    browser.setAttribute("disableglobalhistory", "true");

    // Size the browser.  Make its aspect ratio the same as the canvases' that
    // the thumbnails are drawn into; the canvases' aspect ratio is the same as
    // the screen's, so use that.  Aim for a size in the ballpark of 1024x768.
    let [swidth, sheight] = [{}, {}];
    Cc["@mozilla.org/gfx/screenmanager;1"].
      getService(Ci.nsIScreenManager).
      primaryScreen.
      GetRectDisplayPix({}, {}, swidth, sheight);
    let bwidth = Math.min(1024, swidth.value);
    // Setting the width and height attributes doesn't work -- the resulting
    // thumbnails are blank and transparent -- but setting the style does.
    browser.style.width = bwidth + "px";
    browser.style.height = (bwidth * sheight.value / swidth.value) + "px";

    this._parentWin.document.documentElement.appendChild(browser);

    // an event that is sent if the remote process crashes - no need to remove
    // it as we want it to be there as long as the browser itself lives.
    browser.addEventListener("oop-browser-crashed", () => {
      Cu.reportError("BackgroundThumbnails remote process crashed - recovering");
      this._destroyBrowser();
      let curCapture = this._captureQueue.length ? this._captureQueue[0] : null;
      // we could retry the pending capture, but it's possible the crash
      // was due directly to it, so trying again might just crash again.
      // We could keep a flag to indicate if it previously crashed, but
      // "resetting" the capture requires more work - so for now, we just
      // discard it.
      if (curCapture && curCapture.pending) {
        // Continue queue processing by calling curCapture._done().  Do it after
        // this crashed listener returns, though.  A new browser will be created
        // immediately (on the same stack as the _done call stack) if there are
        // any more queued-up captures, and that seems to mess up the new
        // browser's message manager if it happens on the same stack as the
        // listener.  Trying to send a message to the manager in that case
        // throws NS_ERROR_NOT_INITIALIZED.
        Services.tm.currentThread.dispatch(() => {
          curCapture._done(null);
        }, Ci.nsIEventTarget.DISPATCH_NORMAL);
      }
      // else: we must have been idle and not currently doing a capture (eg,
      // maybe a GC or similar crashed) - so there's no need to attempt a
      // queue restart - the next capture request will set everything up.
    });

    browser.messageManager.loadFrameScript(FRAME_SCRIPT_URL, false);
    this._thumbBrowser = browser;
  },

  _destroyBrowser: function () {
    if (!this._thumbBrowser)
      return;
    this._thumbBrowser.remove();
    delete this._thumbBrowser;
  },

  /**
   * Starts the next capture if the queue is not empty and the service is fully
   * initialized.
   */
  _processCaptureQueue: function () {
    if (!this._captureQueue.length ||
        this._captureQueue[0].pending ||
        !this._ensureParentWindowReady())
      return;

    // Ready to start the first capture in the queue.
    this._ensureBrowser();
    this._captureQueue[0].start(this._thumbBrowser.messageManager);
    if (this._destroyBrowserTimer) {
      this._destroyBrowserTimer.cancel();
      delete this._destroyBrowserTimer;
    }
  },

  /**
   * Called when the current capture completes or fails (eg, times out, remote
   * process crashes.)
   */
  _onCaptureOrTimeout: function (capture) {
    // Since timeouts start as an item is being processed, only the first
    // item in the queue can be passed to this method.
    if (capture !== this._captureQueue[0])
      throw new Error("The capture should be at the head of the queue.");
    this._captureQueue.shift();
    this._capturesByURL.delete(capture.url);

    // Start the destroy-browser timer *before* processing the capture queue.
    let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
    timer.initWithCallback(this._destroyBrowser.bind(this),
                           this._destroyBrowserTimeout,
                           Ci.nsITimer.TYPE_ONE_SHOT);
    this._destroyBrowserTimer = timer;

    this._processCaptureQueue();
  },

  _destroyBrowserTimeout: DESTROY_BROWSER_TIMEOUT,
};

Object.defineProperty(this, "BackgroundPageThumbs", {
  value: BackgroundPageThumbs,
  enumerable: true,
  writable: false
});

/**
 * Represents a single capture request in the capture queue.
 *
 * @param url              The URL to capture.
 * @param captureCallback  A function you want called when the capture
 *                         completes.
 * @param options          The capture options.
 */
function Capture(url, captureCallback, options) {
  this.url = url;
  this.captureCallback = captureCallback;
  this.options = options;
  this.id = Capture.nextID++;
  this.creationDate = new Date();
  this.doneCallbacks = [];
  if (options.onDone)
    this.doneCallbacks.push(options.onDone);
}

Capture.prototype = {

  get pending() {
    return !!this._msgMan;
  },

  /**
   * Sends a message to the content script to start the capture.
   *
   * @param messageManager  The nsIMessageSender of the thumbnail browser.
   */
  start: function (messageManager) {
    this.startDate = new Date();

    // timeout timer
    let timeout;
    if (this.options && typeof(this.options.timeout) == "number") {
      timeout = this.options.timeout;
    } else {
      timeout = DEFAULT_CAPTURE_TIMEOUT;
    }
    this._timeoutTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
    this._timeoutTimer.initWithCallback(this, timeout,
                                        Ci.nsITimer.TYPE_ONE_SHOT);

    // didCapture registration
    this._msgMan = messageManager;
    this._msgMan.sendAsyncMessage("BackgroundPageThumbs:capture",
                                  { id: this.id, url: this.url });
    this._msgMan.addMessageListener("BackgroundPageThumbs:didCapture", this);
  },

  /**
   * The only intended external use of this method is by the service when it's
   * uninitializing and doing things like destroying the thumbnail browser.  In
   * that case the consumer's completion callback will never be called.
   */
  destroy: function () {
    // This method may be called for captures that haven't started yet, so
    // guard against not yet having _timeoutTimer, _msgMan etc properties...
    if (this._timeoutTimer) {
      this._timeoutTimer.cancel();
      delete this._timeoutTimer;
    }
    if (this._msgMan) {
      this._msgMan.removeMessageListener("BackgroundPageThumbs:didCapture",
                                         this);
      delete this._msgMan;
    }
    delete this.captureCallback;
    delete this.doneCallbacks;
    delete this.options;
  },

  // Called when the didCapture message is received.
  receiveMessage: function (msg) {
    // A different timed-out capture may have finally successfully completed, so
    // discard messages that aren't meant for this capture.
    if (msg.data.id != this.id)
      return;

    if (msg.data.failReason) {
      this._done(null);
      return;
    }

    this._done(msg.data);
  },

  // Called when the timeout timer fires.
  notify: function () {
    this._done(null);
  },

  _done: function (data) {
    // Note that _done will be called only once, by either receiveMessage or
    // notify, since it calls destroy here, which cancels the timeout timer and
    // removes the didCapture message listener.
    let { captureCallback, doneCallbacks, options } = this;
    this.destroy();

    let done = () => {
      captureCallback(this);
      for (let callback of doneCallbacks) {
        try {
          callback.call(options, this.url);
        }
        catch (err) {
          Cu.reportError(err);
        }
      }
    };

    if (!data) {
      // If this background attempt failed, cause a placeholder file to be saved, so
      // that gets loaded instead of attempting again (and again).
      PageThumbs._store(this.url, this.url, atob(BLANKTHUMB), true)
                .then(done, done);
      return;
    }

    PageThumbs._store(this.url, data.finalURL, data.imageData, true)
              .then(done, done);
  },
};

Capture.nextID = 0;

function schedule(callback) {
  Services.tm.mainThread.dispatch(callback, Ci.nsIThread.DISPATCH_NORMAL);
}
