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

import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JsArray;

abstract class DOMImpl {

  static final DOMImpl impl = GWT.create(DOMImpl.class);

  /**
   * Fast helper method to convert small doubles to 32-bit int.
   *
   * <p>Note: you should be aware that this uses JavaScript rounding and thus
   * does NOT provide the same semantics as <code>int b = (int) someDouble;</code>.
   * In particular, if x is outside the range [-2^31,2^31), then toInt32(x) would return a value
   * equivalent to x modulo 2^32, whereas (int) x would evaluate to either MIN_INT or MAX_INT.
   */
  protected static native int toInt32(double val) /*-{
    return val | 0;
  }-*/;

  public native void buttonClick(ButtonElement button) /*-{
    button.click();
  }-*/;

  public native ButtonElement createButtonElement(Document doc, String type) /*-{
    var e = doc.createElement("BUTTON");
    e.type = type;
    return e;
  }-*/;

  public native InputElement createCheckInputElement(Document doc) /*-{
    var e = doc.createElement("INPUT");
    e.type = 'checkbox';
    e.value = 'on';
    return e;
  }-*/;

  public native Element createElement(Document doc, String tag) /*-{
    return doc.createElement(tag);
  }-*/;

  public abstract NativeEvent createHtmlEvent(Document doc, String type,
      boolean canBubble, boolean cancelable);

  public native InputElement createInputElement(Document doc, String type) /*-{
    var e = doc.createElement("INPUT");
    e.type = type;
    return e;
  }-*/;

  public abstract InputElement createInputRadioElement(Document doc, String name);

  public abstract NativeEvent createKeyCodeEvent(Document document,
      String type, boolean ctrlKey, boolean altKey, boolean shiftKey,
      boolean metaKey, int keyCode);

  @Deprecated
  public abstract NativeEvent createKeyEvent(Document doc, String type,
      boolean canBubble, boolean cancelable, boolean ctrlKey, boolean altKey,
      boolean shiftKey, boolean metaKey, int keyCode, int charCode);

  public abstract NativeEvent createKeyPressEvent(Document document,
      boolean ctrlKey, boolean altKey, boolean shiftKey, boolean metaKey,
      int charCode);

  public abstract NativeEvent createMouseEvent(Document doc, String type,
      boolean canBubble, boolean cancelable, int detail, int screenX,
      int screenY, int clientX, int clientY, boolean ctrlKey, boolean altKey,
      boolean shiftKey, boolean metaKey, int button, Element relatedTarget);

  public ScriptElement createScriptElement(Document doc, String source) {
    ScriptElement elem = (ScriptElement) createElement(doc, "script");
    elem.setText(source);
    return elem;
  }

  public native void cssClearOpacity(Style style) /*-{
    style.opacity = '';
  }-*/;

  public String cssFloatPropertyName() {
    return "cssFloat";
  }

  public native void cssSetOpacity(Style style, double value) /*-{
    style.opacity = value;
  }-*/;

  public abstract void dispatchEvent(Element target, NativeEvent evt);

  public native boolean eventGetAltKey(NativeEvent evt) /*-{
    return !!evt.altKey;
  }-*/;

  public native int eventGetButton(NativeEvent evt) /*-{
    return evt.button | 0;
  }-*/;

  public abstract int eventGetCharCode(NativeEvent evt);

  public int eventGetClientX(NativeEvent evt) {
    return toInt32(eventGetSubPixelClientX(evt));
  }

  public int eventGetClientY(NativeEvent evt) {
    return toInt32(eventGetSubPixelClientY(evt));
  }

  public native boolean eventGetCtrlKey(NativeEvent evt) /*-{
    return !!evt.ctrlKey;
  }-*/;

  public native EventTarget eventGetCurrentTarget(NativeEvent event) /*-{
    return event.currentTarget;
  }-*/;

  public final native int eventGetKeyCode(NativeEvent evt) /*-{
    return evt.keyCode | 0;
  }-*/;

  public native boolean eventGetMetaKey(NativeEvent evt) /*-{
    return !!evt.metaKey;
  }-*/;

  public int eventGetMouseWheelVelocityY(NativeEvent evt) {
    return (int) Math.signum(evt.getDeltaY());
  }

  public abstract EventTarget eventGetRelatedTarget(NativeEvent nativeEvent);

  public native double eventGetRotation(NativeEvent evt) /*-{
    return evt.rotation;
  }-*/;

  public native double eventGetScale(NativeEvent evt) /*-{
    return evt.scale;
  }-*/;

  public int eventGetScreenX(NativeEvent evt) {
    return toInt32(eventGetSubPixelScreenX(evt));
  }

  public int eventGetScreenY(NativeEvent evt) {
    return toInt32(eventGetSubPixelScreenY(evt));
  }

  public native boolean eventGetShiftKey(NativeEvent evt) /*-{
    return !!evt.shiftKey;
  }-*/;

  public abstract EventTarget eventGetTarget(NativeEvent evt);

  public final native String eventGetType(NativeEvent evt) /*-{
    return evt.type;
  }-*/;

  public abstract void eventPreventDefault(NativeEvent evt);

  public native void eventSetKeyCode(NativeEvent evt, char key) /*-{
    evt.keyCode = key;
  }-*/;

  public native void eventStopPropagation(NativeEvent evt) /*-{
    evt.stopPropagation();
  }-*/;

  public abstract String eventToString(NativeEvent evt);

  public int getAbsoluteLeft(Element elem) {
    return toInt32(getSubPixelAbsoluteLeft(elem));
  }

  public int getAbsoluteTop(Element elem) {
    return toInt32(getSubPixelAbsoluteTop(elem));
  }

  public native String getAttribute(Element elem, String name) /*-{
    return elem.getAttribute(name) || '';
  }-*/;

  public native int getBodyOffsetLeft(Document doc) /*-{
    return 0;
  }-*/;

  public native int getBodyOffsetTop(Document doc) /*-{
    return 0;
  }-*/;

  public native JsArray<Touch> getChangedTouches(NativeEvent evt) /*-{
    return evt.changedTouches;
  }-*/;

  public native Element getFirstChildElement(Element elem) /*-{
    var child = elem.firstChild;
    while (child && child.nodeType != 1)
      child = child.nextSibling;
    return child;
  }-*/;

  public native String getInnerHTML(Element elem) /*-{
    return elem.innerHTML;
  }-*/;

  public native String getInnerText(Element node) /*-{
    // To mimic IE's 'innerText' property in the W3C DOM, we need to recursively
    // concatenate all child text nodes (depth first).
    var text = '', child = node.firstChild;
    while (child) {
      // 1 == Element node
      if (child.nodeType == 1) {
        text += this.@com.google.gwt.dom.client.DOMImpl::getInnerText(Lcom/google/gwt/dom/client/Element;)(child);
      } else if (child.nodeValue) {
        text += child.nodeValue;
      }
      child = child.nextSibling;
    }
    return text;
  }-*/;

  public native Element getNextSiblingElement(Element elem) /*-{
    var sib = elem.nextSibling;
    while (sib && sib.nodeType != 1)
      sib = sib.nextSibling;
    return sib;
  }-*/;

  public native int getNodeType(Node node) /*-{
    return node.nodeType;
  }-*/;

  /**
   * Returns a numeric style property (such as zIndex) that may need to be
   * coerced to a string.
   */
  public String getNumericStyleProperty(Style style, String name) {
    return getStyleProperty(style, name);
  }

  public native Element getParentElement(Node node) /*-{
    var parent = node.parentNode;
    if (!parent || parent.nodeType != 1) {
      parent = null;
    }
    return parent;
  }-*/;

  public native Element getPreviousSiblingElement(Element elem) /*-{
    var sib = elem.previousSibling;
    while (sib && sib.nodeType != 1)
      sib = sib.previousSibling;
    return sib;
  }-*/;

  public int getScrollLeft(Document doc) {
    return ensureDocumentScrollingElement(doc).getScrollLeft();
  }

  public int getScrollLeft(Element elem) {
    return toInt32(getSubPixelScrollLeft(elem));
  }

  public int getScrollTop(Document doc) {
    return ensureDocumentScrollingElement(doc).getScrollTop();
  }

  public native String getStyleProperty(Style style, String name) /*-{
    return style[name];
  }-*/;

  public native int getTabIndex(Element elem) /*-{
    return elem.tabIndex;
  }-*/;

  public native String getTagName(Element elem) /*-{
    return elem.tagName;
  }-*/;

  public native JsArray<Touch> getTargetTouches(NativeEvent evt) /*-{
    return evt.targetTouches;
  }-*/;

  public native JsArray<Touch> getTouches(NativeEvent evt) /*-{
    return evt.touches;
  }-*/;

  public native boolean hasAttribute(Element elem, String name) /*-{
    return elem.hasAttribute(name);
  }-*/;

  public abstract boolean isOrHasChild(Node parent, Node child);

  public native void scrollIntoView(Element elem) /*-{
    var left = elem.offsetLeft, top = elem.offsetTop;
    var width = elem.offsetWidth, height = elem.offsetHeight;

    if (elem.parentNode != elem.offsetParent) {
      left -= elem.parentNode.offsetLeft;
      top -= elem.parentNode.offsetTop;
    }

    var cur = elem.parentNode;
    while (cur && (cur.nodeType == 1)) {
      if (left < cur.scrollLeft) {
        cur.scrollLeft = left;
      }
      if (left + width > cur.scrollLeft + cur.clientWidth) {
        cur.scrollLeft = (left + width) - cur.clientWidth;
      }
      if (top < cur.scrollTop) {
        cur.scrollTop = top;
      }
      if (top + height > cur.scrollTop + cur.clientHeight) {
        cur.scrollTop = (top + height) - cur.clientHeight;
      }

      var offsetLeft = cur.offsetLeft, offsetTop = cur.offsetTop;
      if (cur.parentNode != cur.offsetParent) {
        offsetLeft -= cur.parentNode.offsetLeft;
        offsetTop -= cur.parentNode.offsetTop;
      }

      left += offsetLeft - cur.scrollLeft;
      top += offsetTop - cur.scrollTop;
      cur = cur.parentNode;
    }
  }-*/;

  public native void selectAdd(SelectElement select, OptionElement option,
      OptionElement before) /*-{
    select.add(option, before);
  }-*/;

  public native void selectClear(SelectElement select) /*-{
    select.options.length = 0;
  }-*/;

  public native int selectGetLength(SelectElement select) /*-{
    return select.options.length;
  }-*/;

  public native NodeList<OptionElement> selectGetOptions(SelectElement select) /*-{
    return select.options;
  }-*/;

  public native void selectRemoveOption(SelectElement select, int index) /*-{
    select.remove(index);
  }-*/;

  public native void setDraggable(Element elem, String draggable) /*-{
    elem.draggable = draggable;
  }-*/;

  public native void setInnerText(Element elem, String text) /*-{
    // Remove all children first.
    while (elem.firstChild) {
      elem.removeChild(elem.firstChild);
    }
    // Add a new text node.
    if (text != null) {
      elem.appendChild(elem.ownerDocument.createTextNode(text));
    }
  }-*/;

  public void setScrollLeft(Document doc, int left) {
    ensureDocumentScrollingElement(doc).setScrollLeft(left);
  }

  public native void setScrollLeft(Element elem, int left) /*-{
    elem.scrollLeft = left;
  }-*/;

  public void setScrollTop(Document doc, int top) {
    ensureDocumentScrollingElement(doc).setScrollTop(top);
  }

  public native String toString(Element elem) /*-{
    return elem.outerHTML;
  }-*/;

  private Element ensureDocumentScrollingElement(Document document) {
    // In some case (e.g SVG document and old Webkit browsers), getDocumentScrollingElement can
    // return null. In this case, default to documentElement.
    Element scrollingElement = getDocumentScrollingElement(document);
    return scrollingElement != null ? scrollingElement : document.getDocumentElement();
  }

  Element getDocumentScrollingElement(Document doc)  {
    return doc.getViewportElement();
  }

  public int touchGetClientX(Touch touch) {
    return toInt32(touchGetSubPixelClientX(touch));
  }

  public int touchGetClientY(Touch touch) {
    return toInt32(touchGetSubPixelClientY(touch));
  }

  public native int touchGetIdentifier(Touch touch) /*-{
    return touch.identifier;
  }-*/;

  public int touchGetPageX(Touch touch) {
    return toInt32(touchGetSubPixelPageX(touch));
  }

  public int touchGetPageY(Touch touch) {
    return toInt32(touchGetSubPixelPageY(touch));
  }

  public int touchGetScreenX(Touch touch) {
    return toInt32(touchGetSubPixelScreenX(touch));
  }

  public int touchGetScreenY(Touch touch) {
    return toInt32(touchGetSubPixelScreenY(touch));
  }

  public native EventTarget touchGetTarget(Touch touch) /*-{
    return touch.target;
  }-*/;

  private native double getSubPixelAbsoluteLeft(Element elem) /*-{
    var left = 0;
    var curr = elem;
    // This intentionally excludes body which has a null offsetParent.
    while (curr.offsetParent) {
      left -= curr.scrollLeft;
      curr = curr.parentNode;
    }
    while (elem) {
      left += elem.offsetLeft;
      elem = elem.offsetParent;
    }
    return left;
  }-*/;

  private native double getSubPixelAbsoluteTop(Element elem) /*-{
    var top = 0;
    var curr = elem;
    // This intentionally excludes body which has a null offsetParent.
    while (curr.offsetParent) {
      top -= curr.scrollTop;
      curr = curr.parentNode;
    }
    while (elem) {
      top += elem.offsetTop;
      elem = elem.offsetParent;
    }
    return top;
  }-*/;

  private native double eventGetSubPixelScreenX(NativeEvent evt) /*-{
    return evt.screenX || 0;
  }-*/;

  private native double eventGetSubPixelScreenY(NativeEvent evt) /*-{
    return evt.screenY || 0;
  }-*/;

  private native double getSubPixelScrollLeft(Element elem) /*-{
    return elem.scrollLeft || 0;
  }-*/;

  private native double touchGetSubPixelClientX(Touch touch) /*-{
    return touch.clientX || 0;
  }-*/;

  private native double touchGetSubPixelClientY(Touch touch) /*-{
    return touch.clientY || 0;
  }-*/;

  private native double touchGetSubPixelPageX(Touch touch) /*-{
    return touch.pageX || 0;
  }-*/;

  private native double touchGetSubPixelPageY(Touch touch) /*-{
    return touch.pageY || 0;
  }-*/;

  private native double touchGetSubPixelScreenX(Touch touch) /*-{
    return touch.screenX || 0;
  }-*/;

  private native double touchGetSubPixelScreenY(Touch touch) /*-{
    return touch.screenY || 0;
  }-*/;

  private native double eventGetSubPixelClientX(NativeEvent evt) /*-{
    return evt.clientX || 0;
  }-*/;

  private native double eventGetSubPixelClientY(NativeEvent evt) /*-{
    return evt.clientY || 0;
  }-*/;
}
