/**
 * Copyright 2009 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 org.waveprotocol.wave.client.common.util;

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

/**
 * Collection of constants defining various browser quirky behaviours.
 *
 * Each constant should be accompanied by a detailed comment, and a "Tested:"
 * section detailing which browsers and operating systems the quirk has been
 * tested on, so that it's easy to know what's untested as new browser versions
 * come out, etc.
 *
 * Sometimes an "Untested exceptions:" field is appropriate, to note exceptions
 * to the "Tested:" field, in particular if they are concerning and represent a
 * reasonable doubt as to the correctness of the field's value.
 *
 * It is preferable to use the constants in this class than to have logic that
 * switches on explicit browser checks.
 *
 * @author danilatos@google.com (Daniel Danilatos)
 */
public final class QuirksConstants {

  /**
   * Whether we get DOM Mutation events
   *
   * Tested:
   * Safari 3-4, Firefox 3-3.5, Chrome 1-2, IE7, IE8
   *
   * Will IE9 give us mutation events? probably not.
   */
  public static final boolean PROVIDES_MUTATION_EVENTS =
      UserAgent.isFirefox() || UserAgent.isWebkit();

  /**
   * Whether the browser left normalises the caret in most cases (There are
   * exceptions, usually to do with links).
   *
   * Tested:
   * Safari 3*, Safari 4 beta, Firefox 3.0, IE7, IE8
   */
  public static final boolean USUALLY_LEFT_NORMALISES =
      UserAgent.isWebkit() || UserAgent.isWebkit();

  /**
   * Certain versions of webkit have a specific hack implemented in them, where
   * they go against the regular left-normalised behaviour at the end of an
   * anchor boundary, if it has an href. They still report the cursor as being
   * left-normalised, but if the user types, text goes to the right, outside the
   * link.
   *
   * Tested:
   * All OS: Safari 3.2.1, Safari 4 beta, Chrome 1.0, Chrome 2.0 special sauce
   */
  public static final boolean LIES_ABOUT_CARET_AT_LINK_END_BOUNDARY =
      UserAgent.isWebkit() && UserAgent.isAtLeastVersion(528, 0);

  /**
   * Similar to {@link #LIES_ABOUT_CARET_AT_LINK_END_BOUNDARY}, but does not
   * actually lie, just doesn't like reporting carets as being inside link
   * boundaries, and typing occurs outside as well.
   *
   * Tested: IE8 beta
   *
   * TODO(danilatos): check IE7
   */
  public static final boolean DOES_NOT_LEFT_NORMALISE_AT_LINK_END_BOUNDARY =
      UserAgent.isIE();

  /**
   * If the user is typing, we always get a key event before the browser changes
   * the dom.
   *
   * Tested:
   * All OS: Safari 3.2.1, 4 beta, Chrome 1, 2, Firefox 3, IE7, IE8
   *
   * Untested exceptions: Any IME on Linux!
   */
  public static final boolean ALWAYS_GIVES_KEY_EVENT_BEFORE_CHANGING_DOM =
      UserAgent.isFirefox() || UserAgent.isIE7();

  /**
   * Whether we get the magic 229 keycode for IME key events, at least for the
   * first one (sometimes we don't get key events for subsequent mutations).
   *
   * Tested:
   * All OS: Safari 3.2.1, 4 beta, Chrome 1, 2, Firefox 3.0
   *
   * Untested exceptions: Any IME on Linux!
   */
  public static final boolean CAN_TELL_WHEN_FIRST_KEY_EVENT_IS_IME =
      UserAgent.isIE() || UserAgent.isWebkit() || UserAgent.isWin();

  /**
   * Whether the old school Ctrl+Insert, Shift+Delete, Shift+Insert shortcuts
   * for Copy, Cut, Paste work.
   *
   * Tested: All OS: Firefox 3, IE 7/8, Safari 3, Chrome 2
   *
   * Untested exceptions: Safari on Windows
   */
  public static final boolean HAS_OLD_SCHOOL_CLIPBOARD_SHORTCUTS =
      UserAgent.isWin() || UserAgent.isLinux();

  // NOTE(danilatos): These selection constants, unless mentioned otherwise,
  // refer to selection boundaries (e.g. the start, or end, of a ranged
  // selection, or a collapsed selection).

  /**
   * Whether the selection is either cleared or correctly transformed by the
   * browser in at least the following scenarios:
   * - textNode.insertData adds to the cursor location, if it is after the
   *   deletion point
   * - textNode.deleteData subtracts from the cursor location, if it is after
   *   the deletion point
   *
   * Tested: Safari 3.2.1, FF 3.0, 3.5, Chrome 1, IE7, IE8
   */
  public static final boolean OK_SELECTION_ACROSS_TEXT_NODE_DATA_CHANGES =
      UserAgent.isFirefox() || UserAgent.isIE();

  /**
   * Whether the selection is either cleared or correctly transformed by the
   * browser when text nodes are deleted.
   *
   * Gecko/IE preserve, Webkit clears selection. Both OK behaviours.
   *
   * Tested: Safari 3.2.1, FF 3.0, 3.5, Chrome 1, IE7, IE8
   */
  public static final boolean OK_SELECTION_ACROSS_NODE_REMOVALS = true;

  /**
   * Whether the browser moves the selection into the neighbouring text node
   * after a text node split before the selection point, or at least clears the
   * selection.
   *
   * Tested: Safari 3.2.1, FF 3.0, 3.5, Chrome 1, IE7, IE8
   */
  public static final boolean OK_SELECTION_ACROSS_TEXT_NODE_SPLITS =
      UserAgent.isIE();

  /**
   * In this case, only clearing occurs by Webkit. Other two browsers move the
   * selection to the point where the moved node was. Which is BAD for wrapping!
   *
   * Tested: Safari 3.2.1, FF 3.0, 3.5, Chrome 1, IE7, IE8
   *
   * @see #PRESERVES_SEMANTIC_SELECTION_ACROSS_MUTATIONS_OR_CLEARS_IT
   */
  public static final boolean OK_SELECTION_ACROSS_MOVES =
      UserAgent.isWebkit();

  /**
   * Preserves changes to text nodes made by calling methods on the text nodes
   * directly (i.e. not moving or deleting the text nodes).
   */
  public static final boolean PRESERVES_SEMANTIC_SELECTION_ACROSS_INTRINSIC_TEXT_NODE_CHANGES =
    OK_SELECTION_ACROSS_TEXT_NODE_DATA_CHANGES &&
    OK_SELECTION_ACROSS_TEXT_NODE_SPLITS;

  /**
   * Whether the selection preservation is completely reliable across mutations
   * in terms of correctness. It might get cleared in some circumstances, but
   * that's OK, we can just check if the selection is gone and restore it. We
   * don't need to transform it for correctness.
   *
   * The biggest problem here is wrap. Currently implemented with insertBefore,
   * it breaks selections in all browsers, even IE, AND IE doesn't clear the
   * selection, just moves it. Damn! Anyway, there might be a smarter way to
   * implement wrap - perhaps using an exec command to apply a styling to a
   * region and using the resulting wrapper nodes... but that's a long way off.
   *
   * Tested: Safari 3.2.1, FF 3.0, 3.5, Chrome 1, IE7, IE8
   */
  public static final boolean PRESERVES_SEMANTIC_SELECTION_ACROSS_MUTATIONS_OR_CLEARS_IT =
    OK_SELECTION_ACROSS_TEXT_NODE_DATA_CHANGES &&
    OK_SELECTION_ACROSS_TEXT_NODE_SPLITS &&
    OK_SELECTION_ACROSS_MOVES;

  /**
   * Whether changing stuff in the middle of a ranged selection (that doesn't
   * affect the selection end points in any way), such as splitting some
   * entirely contained text node, affects the ranged selection. With firefox,
   * it appears that the selection is still "correct", but visually new text
   * nodes inserted don't get highlighted as selected, which is bad.
   *
   * Tested: Safari 3.2.1, FF 3.0, IE8
   */
  public static final boolean RANGED_SELECTION_AFFECTED_BY_INTERNAL_CHANGED =
    UserAgent.isFirefox();

  /**
   * True if IME input in not totally munged by adjacent text node mutations
   *
   * Tested:
   * Safari 3.2.1, FF 3.0, 3.5, Chrome 1, IE7, IE8
   */
  public static final boolean PRESERVES_IME_STATE_ACROSS_ADJACENT_CHANGES =
      UserAgent.isIE();

  /**
   * True if we can do the __proto__ override trick to remove defaults method
   * in a JSO.
   * WARNING(dnailatos/reuben) Should be kept as static constant for
   * speed reasons in string map implementation.
   *
   * Tested: Safari 4, FF 3.0, 3.5, Chrome 3-4, IE8
   */
  public static final boolean DOES_NOT_SUPPORT_JSO_PROTO_FIELD = UserAgent.isIE();

  /**
   * It appears that in some browsers, if you stopPropagation() an IME
   * composition or contextmenu event, the default action for the event is not
   * executed (as if you had cancelled it)
   *
   * TODO(danilatos): File a bug
   *
   * Tested:
   * FF 3.0
   *
   * Untested:
   * Everything else (at time of writing, nothing else has composition
   * events...)
   */
  public static final boolean CANCEL_BUBBLING_CANCELS_IME_COMPOSITION_AND_CONTEXTMENU =
      UserAgent.isFirefox();

  /**
   * True if mouse events have rangeParent and rangeOffset fields.
   *
   * Tested:
   * FF 3.0, 3.6
   * Safari 4
   * Chrome 5
   */
  public static final boolean SUPPORTS_EVENT_GET_RANGE_METHODS =
      UserAgent.isFirefox();

  /**
   * True if preventDefault stops a native context menu from being shown.  In
   * firefox this is not the case when dom.event.contextmenu.enabled is set.
   *
   * Tested:
   * FF 3.0, 3.6
   */
  public static final boolean PREVENT_DEFAULT_STOPS_CONTEXTMENT =
      !UserAgent.isFirefox();

  /**
   * True if displaying a context menu updates the current selection.  Safari selects
   * the word clicked unless you click on the current selection, Firefox does not
   * change the selection and Chrome and IE clears the selection unless you click
   * on the current selection.
   *
   * The selection that is active on mousedown will, in all browsers, be the
   * original selection and the selection on the contextmenu event will be the new
   * one.
   *
   * Tested:
   * FF 3.0, 3.5
   * Chrome 5
   * Safari 4
   * IE 8
   */
  public static final boolean CONTEXTMENU_SETS_SELECTION =
      !UserAgent.isFirefox();

  /**
   * True if the browser has the setBaseAndExtent JS method to allow better setting of
   * the selection within the browser.
   *
   * So far, only webkit browsers have this in their API, and documentation is scarce. See:
   * http://developer.apple.com/DOCUMENTATION/AppleApplications/Reference/WebKitDOMRef
   *       /DOMSelection_idl/Classes/DOMSelection/index.html#//apple_ref/idl/instm
   *       /DOMSelection/setBaseAndExtent/void/(inNode,inlong,inNode,inlong)
   */
  public static final boolean HAS_BASE_AND_EXTENT =
      UserAgent.isWebkit();

  /**
   * Chrome on Mac generates doesn't keypress for command combos, only keydown.
   *
   * In general, it makes sense to only fire the keypress event if the combo
   * generates content. https://bugs.webkit.org/show_bug.cgi?id=30397
   *
   * However since it is the odd one out here, it is listed as a
   * quirk.
   */
  public static final boolean COMMAND_COMBO_DOESNT_GIVE_KEYPRESS =
      UserAgent.isMac() && UserAgent.isChrome();

  /**
   * True if the browser has native support for getElementsByClassName.
   *
   * Tested:
   * Chrome, Safari 3.1, Firefox 3.0
   */
  public static final boolean SUPPORTS_GET_ELEMENTS_BY_CLASSNAME =
      GWT.isScript() && checkGetElementsByClassNameSupport();

  /**
   * True if the browser supports composition events.
   *
   * (This does not differentiate between the per-browser composition event quirks,
   * such as whether they provide a text vs a compositionupdate event, or other
   * glitches).
   *
   * Tested:
   * Chrome 3.0, 4.0; Safari 4; FF 3.0, 3.5; IE 7,8
   */
  public static final boolean SUPPORTS_COMPOSITION_EVENTS =
      UserAgent.isFirefox() || (UserAgent.isWebkit()
          && UserAgent.isAtLeastVersion(532, 5));

  /**
   * True if the browser does an extra modification of the DOM after the
   * compositionend event, and also fires a text input event if the composition
   * was not cancelled.
   *
   * Tested: Chrome 4.0; FF 3.0, 3.5;
   */
  public static final boolean MODIFIES_DOM_AND_FIRES_TEXTINPUT_AFTER_COMPOSITION =
      UserAgent.isWebkit(); // Put an upper bound on the version here when it's fixed...


  /**
   * True if the browser keeps the selection in an empty span after the
   * app has programmatically set it there.
   *
   * Tested:
   * Chrome 3.0, 4.0; Safari 3, 4; FF 3.0, 3.5; IE 7,8
   */
  public static final boolean SUPPORTS_CARET_IN_EMPTY_SPAN =
      UserAgent.isFirefox();

  /**
   * True if the browser automatically scrolls a contentEditable element
   * into view when we set focus on the element
   *
   * Tested:
   * Chrome 5.0.307.11 beta / linux, Safari 4.0.4 / mac, Firefox 3.0.7 + 3.6 / linux
   */
  public static final boolean ADJUSTS_SCROLL_TOP_WHEN_FOCUSING =
      UserAgent.isWebkit();

  /**
   * True if the browser does not emit a paste event for plaintext paste.
   * This was a bug on Webkit and has been fixed and pushed to Chrome 4+
   *
   * Tested:
   * Chrome 4.0.302.3; Safari 4.05 Mac
   */
  public static final boolean PLAINTEXT_PASTE_DOES_NOT_EMIT_PASTE_EVENT =
    UserAgent.isSafari();

  /**
   * True if the browser supports input type 'search'.
   *
   * Tested:
   * Chrome 9.0, Chrome 4.0
   */
  public static final boolean SUPPORTS_SEARCH_INPUT =
    UserAgent.isWebkit();

  /**
   * True if the browser sanitizes pasted content to contenteditable to
   * prevent script execution.
   *
   * Tested:
   * Chrome 9.0, Safari 5, FF 3.5, FF 4.0
   */
  public static final boolean SANITIZES_PASTED_CONTENT =
      (UserAgent.isWebkit() && UserAgent.isAtLeastVersion(533, 16)) ||
      (UserAgent.isFirefox() && UserAgent.isAtLeastVersion(4, 0));

  private static native boolean checkGetElementsByClassNameSupport() /*-{
    return !!document.body.getElementsByClassName;
  }-*/;

  private QuirksConstants(){}
}
