/*
 * Copyright 2008 Google Inc.
 * 
 * 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.
 */
package com.google.gwt.dom.client;

/**
 * Mozilla implementation of StandardBrowser.
 */
class DOMImplMozilla extends DOMImplStandard {

  private static int cachedGeckoVersion = -2;

  private static int getGeckoVersion() {
    if (cachedGeckoVersion == -2) {
      cachedGeckoVersion = getNativeGeckoVersion();
    }
    return cachedGeckoVersion;
  }

  private static native int getNativeGeckoVersion() /*-{
    var result = /rv:([0-9]+)\.([0-9]+)(\.([0-9]+))?.*?/.exec(navigator.userAgent.toLowerCase());
    if (result && result.length >= 3) {
      var version = (parseInt(result[1]) * 1000000) + (parseInt(result[2]) * 1000) +
        parseInt(result.length >= 5 && !isNaN(result[4]) ? result[4] : 0);
      return version;
    }
    return -1; // not gecko
  }-*/;

  /**
   * Return true if using Gecko 1.9 (Firefox 3) or later.
   *
   * @return true if using Gecko 1.9 (Firefox 3) or later
   */
  private static boolean isGecko19() {
    int geckoVersion = getGeckoVersion();
    return (geckoVersion != -1) && (geckoVersion >= 1009000);
  }

  /**
   * Return true if using Gecko 1.9.0 (Firefox 3) or earlier.
   * 
   * @return true if using Gecko 1.9.0 (Firefox 3) or earlier
   */
  private static boolean isGecko190OrBefore() {
    int geckoVersion = getGeckoVersion();
    return (geckoVersion != -1) && (geckoVersion <= 1009000);
  }

  /**
   * Return true if using Gecko 1.9.1 (Firefox 3.5) or earlier.
   * 
   * @return true if using Gecko 1.9.1 (Firefox 3.5) or earlier
   */
  private static boolean isGecko191OrBefore() {
    int geckoVersion = getGeckoVersion();
    return (geckoVersion != -1) && (geckoVersion <= 1009001);
  }

  /**
   * Return true if using Gecko 1.9.2 (Firefox 3.6) or earlier.
   * 
   * @return true if using Gecko 1.9.2 (Firefox 3.6) or earlier
   */
  private static boolean isGecko192OrBefore() {
    int geckoVersion = getGeckoVersion();
    return (geckoVersion != -1) && (geckoVersion <= 1009002);
  }

  /**
   * Return true if using Gecko 2.0.0 (Firefox 4.0) or earlier.
   * 
   * @return true if using Gecko 2.0.0 (Firefox 4.0) or earlier
   */
  private static boolean isGecko2OrBefore() {
    int geckoVersion = getGeckoVersion();
    return (geckoVersion != -1) && (geckoVersion < 2000000);
  }

  @Override
  public native void buttonClick(ButtonElement button) /*-{
    var doc = button.ownerDocument;
    if (doc != null) {
      var evt = doc.createEvent('MouseEvents');
      evt.initMouseEvent('click', true, true, null, 0, 0,
        0, 0, 0, false, false, false, false, 0, null);
      button.dispatchEvent(evt);
    }
  }-*/;

  @Override
  public NativeEvent createKeyCodeEvent(Document doc, String type,
      boolean ctrlKey, boolean altKey, boolean shiftKey, boolean metaKey,
      int keyCode) {
    return createKeyEventImpl(doc, type, true, true, ctrlKey, altKey, shiftKey,
        metaKey, keyCode, 0);
  }

  @Override
  @Deprecated
  public NativeEvent createKeyEvent(Document doc, String type,
      boolean canBubble, boolean cancelable, boolean ctrlKey, boolean altKey,
      boolean shiftKey, boolean metaKey, int keyCode, int charCode) {
    return createKeyEventImpl(doc, type, canBubble, cancelable, ctrlKey,
        altKey, shiftKey, metaKey, keyCode, charCode);
  }

  @Override
  public NativeEvent createKeyPressEvent(Document doc, boolean ctrlKey,
      boolean altKey, boolean shiftKey, boolean metaKey, int charCode) {
    return createKeyEventImpl(doc, "keypress", true, true, ctrlKey, altKey,
        shiftKey, metaKey, 0, charCode);
  }

  @Override
  public native EventTarget eventGetRelatedTarget(NativeEvent evt) /*-{
    // Hack around Mozilla bug 497780 (relatedTarget sometimes returns XUL
    // elements). Trying to access relatedTarget.nodeName will throw an
    // exception if it's a XUL element.
    var relatedTarget = evt.relatedTarget;
    if (!relatedTarget) {
      return null;
    }
    try {
      var nodeName = relatedTarget.nodeName;
      return relatedTarget;
    } catch (e) {
      return null;
    }
  }-*/;

  @Override
  public int getAbsoluteLeft(Element elem) {
    return getAbsoluteLeftImpl(elem.getOwnerDocument().getViewportElement(),
        elem);
  }

  @Override
  public int getAbsoluteTop(Element elem) {
    return getAbsoluteTopImpl(elem.getOwnerDocument().getViewportElement(),
        elem);
  }

  @Override
  public native int getBodyOffsetLeft(Document doc) /*-{
    var style = $wnd.getComputedStyle(doc.documentElement, null);
    if (style == null) {
      // Works around https://bugzilla.mozilla.org/show_bug.cgi?id=548397
      return 0;
    }
    return parseInt(style.marginLeft, 10) + parseInt(style.borderLeftWidth, 10);
  }-*/;

  @Override
  public native int getBodyOffsetTop(Document doc) /*-{
    var style = $wnd.getComputedStyle(doc.documentElement, null);
    if (style == null) {
      // Works around https://bugzilla.mozilla.org/show_bug.cgi?id=548397
      return 0;
    }
    return parseInt(style.marginTop, 10) + parseInt(style.borderTopWidth, 10);
  }-*/;

  @Override
  public native int getNodeType(Node node) /*-{
    try {
      return node.nodeType;
    } catch (e) {
      // Give up on 'Permission denied to get property HTMLDivElement.nodeType'
      // '0' is not a valid node type, which is appropriate in this case, since
      // the node in question is completely inaccessible.
      //
      // See https://bugzilla.mozilla.org/show_bug.cgi?id=208427
      // and http://code.google.com/p/google-web-toolkit/issues/detail?id=1909
      return 0;
    }
  }-*/;

  @Override
  public int getScrollLeft(Element elem) {
    if (!isGecko19() && isRTL(elem)) {
      return super.getScrollLeft(elem)
          - (elem.getScrollWidth() - elem.getClientWidth());
    }
    return super.getScrollLeft(elem);
  }

  @Override
  public native boolean isOrHasChild(Node parent, Node child) /*-{
    // For more information about compareDocumentPosition, see:
    // http://www.quirksmode.org/blog/archives/2006/01/contains_for_mo.html
    return (parent === child) || !!(parent.compareDocumentPosition(child) & 16);
  }-*/;

  @Override
  public void setScrollLeft(Element elem, int left) {
    if (!isGecko19() && isRTL(elem)) {
      left += elem.getScrollWidth() - elem.getClientWidth();
    }
    super.setScrollLeft(elem, left);
  }

  @Override
  public native String toString(Element elem) /*-{
    // Basic idea is to use the innerHTML property by copying the node into a
    // div and getting the innerHTML
    var doc = elem.ownerDocument;
    var temp = elem.cloneNode(true);
    var tempDiv = doc.createElement("DIV");
    tempDiv.appendChild(temp);
    outer = tempDiv.innerHTML;
    temp.innerHTML = "";
    return outer;
  }-*/;

  private native NativeEvent createKeyEventImpl(Document doc, String type,
      boolean canBubble, boolean cancelable, boolean ctrlKey, boolean altKey,
      boolean shiftKey, boolean metaKey, int keyCode, int charCode) /*-{
    if (!!window.KeyboardEvent) {
      return new KeyboardEvent(type, {
        ctrlKey: ctrlKey,
        altKey: altKey,
        shiftKey: shiftKey,
        metaKey: metaKey,
        keyCode: keyCode,
        charCode: charCode,
        bubbles: canBubble,
        cancelable: cancelable
      });
    }
    var evt = doc.createEvent('KeyboardEvent');
    if (evt.initKeyEvent) {
      // Gecko
      evt.initKeyEvent(type, canBubble, cancelable, null, ctrlKey, altKey,
        shiftKey, metaKey, keyCode, charCode);
    } else {
      // This happens to be IE11+ as of today
      if ($wnd.console) {
        $wnd.console.error("Synthetic keyboard events are not supported in this browser");
      }
    }
    return evt;
  }-*/;

  private native int getAbsoluteLeftImpl(Element viewport, Element elem) /*-{
    // Firefox 3 is actively throwing errors when getBoxObjectFor() is called,
    // so we use getBoundingClientRect() whenever possible (but it's not
    // supported on older versions). If changing this code, make sure to check
    // the museum entry for issue 1932.
    // (x) | 0 is used to coerce the value to an integer
    if (Element.prototype.getBoundingClientRect) {
      return (elem.getBoundingClientRect().left + viewport.scrollLeft) | 0;
    } else {
      // We cannot use DOMImpl here because offsetLeft/Top return erroneous
      // values when overflow is not visible.  We have to difference screenX
      // here due to a change in getBoxObjectFor which causes inconsistencies
      // on whether the calculations are inside or outside of the element's
      // border.
      // If the element is in a scrollable div, getBoxObjectFor(elem) can return
      // a value that varies by 1 pixel.
      var doc = elem.ownerDocument;
      return doc.getBoxObjectFor(elem).screenX -
        doc.getBoxObjectFor(doc.documentElement).screenX;
    }
  }-*/;

  private native int getAbsoluteTopImpl(Element viewport, Element elem) /*-{
    // Firefox 3 is actively throwing errors when getBoxObjectFor() is called,
    // so we use getBoundingClientRect() whenever possible (but it's not
    // supported on older versions). If changing this code, make sure to check
    // the museum entry for issue 1932.
    // (x) | 0 is used to coerce the value to an integer
    if (Element.prototype.getBoundingClientRect) {
      return (elem.getBoundingClientRect().top + viewport.scrollTop) | 0;
    } else {
      // We cannot use DOMImpl here because offsetLeft/Top return erroneous
      // values when overflow is not visible.  We have to difference screenX
      // here due to a change in getBoxObjectFor which causes inconsistencies
      // on whether the calculations are inside or outside of the element's
      // border.
      var doc = elem.ownerDocument;
      return doc.getBoxObjectFor(elem).screenY -
        doc.getBoxObjectFor(doc.documentElement).screenY;
    }
  }-*/;

  private native boolean isRTL(Element elem) /*-{
    var style = elem.ownerDocument.defaultView.getComputedStyle(elem, null);
    return style.direction == 'rtl';
  }-*/;
}

