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

let Cc = Components.classes;
let Ci = Components.interfaces;

Components.utils.import("resource:///modules/ContentUtil.jsm");

let Util = {
  /*
   * General purpose utilities
   */

  getWindowUtils: function getWindowUtils(aWindow) {
    return aWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
  },

  // Put the Mozilla networking code into a state that will kick the
  // auto-connection process.
  forceOnline: function forceOnline() {
    Services.io.offline = false;
  },

  /*
   * Timing utilties
   */

  // Executes aFunc after other events have been processed.
  executeSoon: function executeSoon(aFunc) {
    Services.tm.mainThread.dispatch({
      run: function() {
        aFunc();
      }
    }, Ci.nsIThread.DISPATCH_NORMAL);
  },

  /*
   * Console printing utilities
   */

  // Like dump, but each arg is handled and there's an automatic newline
  dumpLn: function dumpLn() {
    for (let i = 0; i < arguments.length; i++)
      dump(arguments[i] + " ");
    dump("\n");
  },

  /*
   * Element utilities
   */

  transitionElementVisibility: function(aNodes, aVisible) {
    // accept single node or a collection of nodes
    aNodes = aNodes.length ? aNodes : [aNodes];
    let defd = Promise.defer();
    let pending = 0;
    Array.forEach(aNodes, function(aNode) {
      if (aVisible) {
        aNode.hidden = false;
        aNode.removeAttribute("fade"); // trigger transition to full opacity
      } else {
        aNode.setAttribute("fade", true); // trigger transition to 0 opacity
      }
      aNode.addEventListener("transitionend", function onTransitionEnd(aEvent){
        aNode.removeEventListener("transitionend", onTransitionEnd);
        if (!aVisible) {
          aNode.hidden = true;
        }
        pending--;
        if (!pending){
          defd.resolve(true);
        }
      }, false);
      pending++;
    });
    return defd.promise;
  },

  isTextInput: function isTextInput(aElement) {
    return ((aElement instanceof Ci.nsIDOMHTMLInputElement &&
             aElement.mozIsTextField(false)) ||
            aElement instanceof Ci.nsIDOMHTMLTextAreaElement);
  },

  /**
   * Checks whether aElement's content can be edited either if it(or any of its
   * parents) has "contenteditable" attribute set to "true" or aElement's
   * ownerDocument is in design mode.
   */
  isEditableContent: function isEditableContent(aElement) {
    return !!aElement && (aElement.isContentEditable ||
                          this.isOwnerDocumentInDesignMode(aElement));

  },

  isEditable: function isEditable(aElement) {
    if (!aElement) {
      return false;
    }

    if (this.isTextInput(aElement) || this.isEditableContent(aElement)) {
      return true;
    }

    // If a body element is editable and the body is the child of an
    // iframe or div we can assume this is an advanced HTML editor
    if ((aElement instanceof Ci.nsIDOMHTMLIFrameElement ||
         aElement instanceof Ci.nsIDOMHTMLDivElement) &&
        aElement.contentDocument &&
        this.isEditableContent(aElement.contentDocument.body)) {
      return true;
    }

    return false;
  },

  /**
   * Checks whether aElement's owner document has design mode turned on.
   */
  isOwnerDocumentInDesignMode: function(aElement) {
    return !!aElement && !!aElement.ownerDocument &&
           aElement.ownerDocument.designMode == "on";
  },

  isMultilineInput: function isMultilineInput(aElement) {
    return (aElement instanceof Ci.nsIDOMHTMLTextAreaElement);
  },

  isLink: function isLink(aElement) {
    return ((aElement instanceof Ci.nsIDOMHTMLAnchorElement && aElement.href) ||
            (aElement instanceof Ci.nsIDOMHTMLAreaElement && aElement.href) ||
            aElement instanceof Ci.nsIDOMHTMLLinkElement ||
            aElement.getAttributeNS(kXLinkNamespace, "type") == "simple");
  },

  isText: function isText(aElement) {
    return (aElement instanceof Ci.nsIDOMHTMLParagraphElement ||
            aElement instanceof Ci.nsIDOMHTMLDivElement ||
            aElement instanceof Ci.nsIDOMHTMLLIElement ||
            aElement instanceof Ci.nsIDOMHTMLPreElement ||
            aElement instanceof Ci.nsIDOMHTMLHeadingElement ||
            aElement instanceof Ci.nsIDOMHTMLTableCellElement ||
            aElement instanceof Ci.nsIDOMHTMLBodyElement);
  },

  /*
   * Rect and nsIDOMRect utilities
   */

  getCleanRect: function getCleanRect() {
    return {
      left: 0, top: 0, right: 0, bottom: 0
    };
  },

  pointWithinRect: function pointWithinRect(aX, aY, aRect) {
    return (aRect.left < aX && aRect.top < aY &&
            aRect.right > aX && aRect.bottom > aY);
  },

  pointWithinDOMRect: function pointWithinDOMRect(aX, aY, aRect) {
    if (!aRect.width || !aRect.height)
      return false;
    return this.pointWithinRect(aX, aY, aRect);
  },

  isEmptyDOMRect: function isEmptyDOMRect(aRect) {
    if ((aRect.bottom - aRect.top) <= 0 &&
        (aRect.right - aRect.left) <= 0)
      return true;
    return false;
  },

  // Dumps the details of a dom rect to the console
  dumpDOMRect: function dumpDOMRect(aMsg, aRect) {
    try {
      Util.dumpLn(aMsg,
                  "left:" + Math.round(aRect.left) + ",",
                  "top:" + Math.round(aRect.top) + ",",
                  "right:" + Math.round(aRect.right) + ",",
                  "bottom:" + Math.round(aRect.bottom) + ",",
                  "width:" + Math.round(aRect.right - aRect.left) + ",",
                  "height:" + Math.round(aRect.bottom - aRect.top) );
    } catch (ex) {
      Util.dumpLn("dumpDOMRect:", ex.message);
    }
  },

  /*
   * DownloadUtils.convertByteUnits returns [size, localized-unit-string]
   * so they are joined for a single download size string.
   */
  getDownloadSize: function dv__getDownloadSize (aSize) {
    let [size, units] = DownloadUtils.convertByteUnits(aSize);
    if (aSize > 0)
      return size + units;
    else
      return Strings.browser.GetStringFromName("downloadsUnknownSize");
  },

  /*
   * URIs and schemes
   */

  makeURI: function makeURI(aURL, aOriginCharset, aBaseURI) {
    return Services.io.newURI(aURL, aOriginCharset, aBaseURI);
  },

  makeURLAbsolute: function makeURLAbsolute(base, url) {
    // Note:  makeURI() will throw if url is not a valid URI
    return this.makeURI(url, null, this.makeURI(base)).spec;
  },

  isLocalScheme: function isLocalScheme(aURL) {
    return ((aURL.indexOf("about:") == 0 &&
             aURL != "about:blank" &&
             aURL != "about:empty" &&
             aURL != "about:start") ||
            aURL.indexOf("chrome:") == 0);
  },

  // Don't display anything in the urlbar for these special URIs.
  isURLEmpty: function isURLEmpty(aURL) {
    return (!aURL ||
            aURL == "about:blank" ||
            aURL == "about:empty" ||
            aURL == "about:home" ||
            aURL == "about:newtab" ||
            aURL.startsWith("about:newtab"));
  },

  // Title to use for emptyURL tabs.
  getEmptyURLTabTitle: function getEmptyURLTabTitle() {
    let browserStrings = Services.strings.createBundle("chrome://browser/locale/browser.properties");

    return browserStrings.GetStringFromName("tabs.emptyTabTitle");
  },

  // Don't remember these pages in the session store.
  isURLMemorable: function isURLMemorable(aURL) {
    return !(aURL == "about:blank" ||
             aURL == "about:empty" ||
             aURL == "about:start");
  },

  /*
   * Math utilities
   */

  clamp: function(num, min, max) {
    return Math.max(min, Math.min(max, num));
  },

  /*
   * Screen and layout utilities
   */

   /*
    * translateToTopLevelWindow - Given an element potentially within
    * a subframe, calculate the offsets up to the top level browser.
    */
  translateToTopLevelWindow: function translateToTopLevelWindow(aElement) {
    let offsetX = 0;
    let offsetY = 0;
    let element = aElement;
    while (element &&
           element.ownerDocument &&
           element.ownerDocument.defaultView != content) {
      element = element.ownerDocument.defaultView.frameElement;
      let rect = element.getBoundingClientRect();
      offsetX += rect.left;
      offsetY += rect.top;
    }
    let win = null;
    if (element == aElement)
      win = content;
    else
      win = element.contentDocument.defaultView;
    return { targetWindow: win, offsetX: offsetX, offsetY: offsetY };
  },

  get displayDPI() {
    delete this.displayDPI;
    return this.displayDPI = this.getWindowUtils(window).displayDPI;
  },

  /*
   * aViewHeight - the height of the viewable area in the browser
   * aRect - a bounding rectangle of a selection or element.
   *
   * return - number of pixels for the browser to be shifted up by such
   * that aRect is centered vertically within aViewHeight.
   */
  centerElementInView: function centerElementInView(aViewHeight, aRect) {
    // If the bottom of the target bounds is higher than the new height,
    // there's no need to adjust. It will be above the keyboard.
    if (aRect.bottom <= aViewHeight) {
      return 0;
    }

    // height of the target element
    let targetHeight = aRect.bottom - aRect.top;
    // height of the browser view.
    let viewBottom = content.innerHeight;

    // If the target is shorter than the new content height, we can go ahead
    // and center it.
    if (targetHeight <= aViewHeight) {
      // Try to center the element vertically in the new content area, but
      // don't position such that the bottom of the browser view moves above
      // the top of the chrome. We purposely do not resize the browser window
      // by making it taller when trying to center elements that are near the
      // lower bounds. This would trigger reflow which can cause content to
      // shift around.
      let splitMargin = Math.round((aViewHeight - targetHeight) * .5);
      let distanceToPageBounds = viewBottom - aRect.bottom;
      let distanceFromChromeTop = aRect.bottom - aViewHeight;
      let distanceToCenter =
        distanceFromChromeTop + Math.min(distanceToPageBounds, splitMargin);
      return distanceToCenter;
    }
  },

  /*
   * Local system utilities
   */

  copyImageToClipboard: function Util_copyImageToClipboard(aImageLoadingContent) {
    let image = aImageLoadingContent.QueryInterface(Ci.nsIImageLoadingContent);
    if (!image) {
      Util.dumpLn("copyImageToClipboard error: image is not an nsIImageLoadingContent");
      return;
    }
    try {
      let xferable = Cc["@mozilla.org/widget/transferable;1"].createInstance(Ci.nsITransferable);
      xferable.init(null);
      let imgRequest = aImageLoadingContent.getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST);
      let mimeType = imgRequest.mimeType;
      let imgContainer = imgRequest.image;
      let imgPtr = Cc["@mozilla.org/supports-interface-pointer;1"].createInstance(Ci.nsISupportsInterfacePointer);
      imgPtr.data = imgContainer;
      xferable.setTransferData(mimeType, imgPtr, null);
      let clip = Cc["@mozilla.org/widget/clipboard;1"].getService(Ci.nsIClipboard);
      clip.setData(xferable, null, Ci.nsIClipboard.kGlobalClipboard);
    } catch (e) {
      Util.dumpLn(e.message);
    }
  },
};


/*
 * Timeout
 *
 * Helper class to nsITimer that adds a little more pizazz.  Callback can be an
 * object with a notify method or a function.
 */
Util.Timeout = function(aCallback) {
  this._callback = aCallback;
  this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
  this._type = null;
};

Util.Timeout.prototype = {
  // Timer callback. Don't call this manually.
  notify: function notify() {
    if (this._type == this._timer.TYPE_ONE_SHOT)
      this._type = null;

    if (this._callback.notify)
      this._callback.notify();
    else
      this._callback.apply(null);
  },

  // Helper function for once and interval.
  _start: function _start(aDelay, aType, aCallback) {
    if (aCallback)
      this._callback = aCallback;
    this.clear();
    this._timer.initWithCallback(this, aDelay, aType);
    this._type = aType;
    return this;
  },

  // Do the callback once.  Cancels other timeouts on this object.
  once: function once(aDelay, aCallback) {
    return this._start(aDelay, this._timer.TYPE_ONE_SHOT, aCallback);
  },

  // Do the callback every aDelay msecs. Cancels other timeouts on this object.
  interval: function interval(aDelay, aCallback) {
    return this._start(aDelay, this._timer.TYPE_REPEATING_SLACK, aCallback);
  },

  // Clear any pending timeouts.
  clear: function clear() {
    if (this.isPending()) {
      this._timer.cancel();
      this._type = null;
    }
    return this;
  },

  // If there is a pending timeout, call it and cancel the timeout.
  flush: function flush() {
    if (this.isPending()) {
      this.notify();
      this.clear();
    }
    return this;
  },

  // Return true if we are waiting for a callback.
  isPending: function isPending() {
    return this._type !== null;
  }
};

// Mixin the ContentUtil module exports
{
  for (let name in ContentUtil) {
    let copy = ContentUtil[name];
    if (copy !== undefined)
      Util[name] = copy;
  }
}

this.Util = Util;
