/*
 * 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.user.client.ui.impl;

import com.google.gwt.core.client.JavaScriptException;
import com.google.gwt.dom.client.Element;
import com.google.gwt.safehtml.shared.annotations.IsSafeHtml;
import com.google.gwt.safehtml.shared.annotations.SuppressIsSafeHtmlCastCheck;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.ui.RichTextArea;
import com.google.gwt.user.client.ui.RichTextArea.FontSize;
import com.google.gwt.user.client.ui.RichTextArea.Justification;

/**
 * Basic rich text platform implementation.
 *
 * <p>Deprecated, may be flattened into RichTextAreaImpl as it has no other
 * direct subclasses.</p>
 */
@Deprecated
public abstract class RichTextAreaImplStandard extends RichTextAreaImpl implements
    RichTextArea.Formatter {

  /**
   * The message displayed when the formatter is used before the RichTextArea
   * is initialized.
   */
  private static final String INACTIVE_MESSAGE = "RichTextArea formatters " +
      "cannot be used until the RichTextArea is attached and focused.";

  /**
   * Holds a cached copy of any user setHTML/setText/setEnabled actions until
   * the real text area is fully initialized.  Becomes <code>null</code> after
   * init.
   */
  private Element beforeInitPlaceholder = DOM.createDiv();

  /**
   * Set to true when the {@link RichTextArea} is attached to the page and
   * {@link #initElement()} is called.  If the {@link RichTextArea} is detached
   * before {@link #onElementInitialized()} is called, this will be set to
   * false.  See issue 1897 for details.
   */
  protected boolean initializing;

  /**
   * Indicates that the text area should be focused as soon as it is loaded.
   */
  private boolean isPendingFocus;

  /**
   * True when the element has been attached.
   */
  private boolean isReady;

  @Override
  public native Element createElement() /*-{
    return $doc.createElement('iframe');
  }-*/;

  public void createLink(String url) {
    execCommand("CreateLink", url);
  }

  public String getBackColor() {
    return queryCommandValue("BackColor");
  }

  public String getForeColor() {
    return queryCommandValue("ForeColor");
  }

  @Override
  public final String getHTML() {
    return beforeInitPlaceholder == null ? getHTMLImpl() : beforeInitPlaceholder.getInnerHTML();
  }

  @Override
  public final String getText() {
    return beforeInitPlaceholder == null ? getTextImpl() : beforeInitPlaceholder.getInnerText();
  }

  @Override
  @SuppressWarnings("deprecation")
  public native void initElement()  /*-{
    // Most browsers don't like setting designMode until slightly _after_
    // the iframe becomes attached to the DOM. Any non-zero timeout will do
    // just fine.
    var _this = this;
    _this.@com.google.gwt.user.client.ui.impl.RichTextAreaImplStandard::onElementInitializing()();
    setTimeout($entry(function() {
      // We need to check to see if the content window still is there. It might not be if the RTA
      // first was attached to the DOM and then quickly was removed before the timeout fired.
      if (_this.@com.google.gwt.user.client.ui.impl.RichTextAreaImpl::elem.contentWindow != null) {
        // Turn on design mode.
        _this.@com.google.gwt.user.client.ui.impl.RichTextAreaImpl::elem.contentWindow.document.designMode = 'On';

        // Send notification that the iframe has reached design mode.
        _this.@com.google.gwt.user.client.ui.impl.RichTextAreaImplStandard::onElementInitialized()();
      }
    }), 1);
  }-*/;

  public void insertHorizontalRule() {
    execCommand("InsertHorizontalRule", null);
  }

  public void insertHTML(@IsSafeHtml String html) {
    execCommand("InsertHTML", html);
  }

  public void insertImage(String url) {
    execCommand("InsertImage", url);
  }

  public void insertOrderedList() {
    execCommand("InsertOrderedList", null);
  }

  public void insertUnorderedList() {
    execCommand("InsertUnorderedList", null);
  }

  public boolean isBold() {
    return queryCommandState("Bold");
  }

  @Override
  public boolean isEnabled() {
    return beforeInitPlaceholder == null ? isEnabledImpl()
        : !beforeInitPlaceholder.getPropertyBoolean("disabled");
  }

  public boolean isItalic() {
    return queryCommandState("Italic");
  }

  public boolean isStrikethrough() {
    return queryCommandState("Strikethrough");
  }

  public boolean isSubscript() {
    return queryCommandState("Subscript");
  }

  public boolean isSuperscript() {
    return queryCommandState("Superscript");
  }

  public boolean isUnderlined() {
    return queryCommandState("Underline");
  }

  public void leftIndent() {
    execCommand("Outdent", null);
  }

  public void redo() {
    execCommand("Redo", "false");
  }

  public void removeFormat() {
    execCommand("RemoveFormat", null);
  }

  public void removeLink() {
    execCommand("Unlink", "false");
  }

  public void rightIndent() {
    execCommand("Indent", null);
  }

  public void selectAll() {
    execCommand("SelectAll", null);
  }

  public void setBackColor(String color) {
    execCommand("BackColor", color);
  }

  @Override
  public void setEnabled(boolean enabled) {
    if (beforeInitPlaceholder == null) {
      setEnabledImpl(enabled);
    } else {
      beforeInitPlaceholder.setPropertyBoolean("disabled", !enabled);
    }
  }

  @Override
  public void setFocus(boolean focused) {
    if (initializing) {
      // Issue 3503: if we focus before the iframe is in design mode, the text
      // caret will not appear.
      isPendingFocus = focused;
    } else {
      setFocusImpl(focused);
    }
  }

  public void setFontName(String name) {
    execCommand("FontName", name);
  }

  public void setFontSize(FontSize fontSize) {
    execCommand("FontSize", Integer.toString(fontSize.getNumber()));
  }

  public void setForeColor(String color) {
    execCommand("ForeColor", color);
  }

  @Override
  public final void setHTML(@IsSafeHtml String html) {
    if (beforeInitPlaceholder == null) {
      setHTMLImpl(html);
    } else {
      beforeInitPlaceholder.setInnerHTML(html);
    }
  }

  public void setJustification(Justification justification) {
    if (justification == Justification.CENTER) {
      execCommand("JustifyCenter", null);
    } else if (justification == Justification.FULL) {
      execCommand("JustifyFull", null);
    } else if (justification == Justification.LEFT) {
      execCommand("JustifyLeft", null);
    } else if (justification == Justification.RIGHT) {
      execCommand("JustifyRight", null);
    }
  }

  @Override
  public final void setText(String text) {
    if (beforeInitPlaceholder == null) {
      setTextImpl(text);
    } else {
      beforeInitPlaceholder.setInnerText(text);
    }
  }

  public void toggleBold() {
    execCommand("Bold", "false");
  }

  public void toggleItalic() {
    execCommand("Italic", "false");
  }

  public void toggleStrikethrough() {
    execCommand("Strikethrough", "false");
  }

  public void toggleSubscript() {
    execCommand("Subscript", "false");
  }

  public void toggleSuperscript() {
    execCommand("Superscript", "false");
  }

  public void toggleUnderline() {
    execCommand("Underline", "False");
  }

  public void undo() {
    execCommand("Undo", "false");
  }

  @Override
  @SuppressIsSafeHtmlCastCheck
  public void uninitElement() {
    isReady = false;

    // Issue 1897: initElement uses a timeout, so its possible to call this
    // method after calling initElement, but before the event system is in
    // place.
    if (initializing) {
      initializing = false;
      return;
    }

    // Unhook all custom event handlers when the element is detached.
    unhookEvents();

    // Recreate the placeholder element and store the iframe's contents and the
    // enabled status in it. This is necessary because some browsers will wipe
    // the iframe's contents when it is removed from the DOM.
    @IsSafeHtml String html = getHTML(); // TODO: mXSS
    boolean enabled = isEnabled();
    beforeInitPlaceholder = DOM.createDiv();
    beforeInitPlaceholder.setInnerHTML(html);
    setEnabled(enabled);
  }

  protected native String getHTMLImpl() /*-{
    return this.@com.google.gwt.user.client.ui.impl.RichTextAreaImpl::elem.contentWindow.document.body.innerHTML;
  }-*/;

  protected native String getTextImpl() /*-{
    return this.@com.google.gwt.user.client.ui.impl.RichTextAreaImpl::elem.contentWindow.document.body.textContent;
  }-*/;

  @Override
  protected native void hookEvents() /*-{
    var elem = this.@com.google.gwt.user.client.ui.impl.RichTextAreaImpl::elem;
    var wnd = elem.contentWindow;

    elem.__gwt_handler = $entry(function(evt) {
      @com.google.gwt.user.client.DOM::dispatchEvent(Lcom/google/gwt/user/client/Event;Lcom/google/gwt/dom/client/Element;)(evt, elem);
    });

    elem.__gwt_focusHandler = function(evt) {
      if (elem.__gwt_isFocused) {
        return;
      }

      elem.__gwt_isFocused = true;
      elem.__gwt_handler(evt);
    };

    elem.__gwt_blurHandler = function(evt) {
      if (!elem.__gwt_isFocused) {
        return;
      }

      elem.__gwt_isFocused = false;
      elem.__gwt_handler(evt);
    };

    wnd.addEventListener('keydown', elem.__gwt_handler, true);
    wnd.addEventListener('keyup', elem.__gwt_handler, true);
    wnd.addEventListener('keypress', elem.__gwt_handler, true);
    wnd.addEventListener('mousedown', elem.__gwt_handler, true);
    wnd.addEventListener('mouseup', elem.__gwt_handler, true);
    wnd.addEventListener('mousemove', elem.__gwt_handler, true);
    wnd.addEventListener('mouseover', elem.__gwt_handler, true);
    wnd.addEventListener('mouseout', elem.__gwt_handler, true);
    wnd.addEventListener('click', elem.__gwt_handler, true);

    wnd.addEventListener('focus', elem.__gwt_focusHandler, true);
    wnd.addEventListener('blur', elem.__gwt_blurHandler, true);
  }-*/;

  protected native boolean isEnabledImpl() /*-{
    var elem = this.@com.google.gwt.user.client.ui.impl.RichTextAreaImpl::elem;
    return elem.contentWindow.document.designMode.toUpperCase() == 'ON';
  }-*/;

  @Override @SuppressIsSafeHtmlCastCheck
  protected void onElementInitialized() {
    // Issue 1897: This method is called after a timeout, during which time the
    // element might by detached.
    if (!initializing) {
      return;
    }
    initializing = false;
    isReady = true;

    // When the iframe is ready, ensure cached content is set.
    if (beforeInitPlaceholder != null) {
      setHTMLImpl(beforeInitPlaceholder.getInnerHTML());
      setEnabledImpl(isEnabled());
      beforeInitPlaceholder = null;
    }

    super.onElementInitialized();

    // Focus on the element now that it is initialized
    if (isPendingFocus) {
      isPendingFocus = false;
      setFocus(true);
    }
  }

  protected void onElementInitializing() {
    initializing = true;
    isPendingFocus = false;
  }

  protected native void setEnabledImpl(boolean enabled) /*-{
    var elem = this.@com.google.gwt.user.client.ui.impl.RichTextAreaImpl::elem;
    elem.contentWindow.document.designMode = enabled ? 'On' : 'Off';
  }-*/;

  protected native void setFocusImpl(boolean focused) /*-{
   if (focused) {
      this.@com.google.gwt.user.client.ui.impl.RichTextAreaImpl::elem.contentWindow.focus();
    } else {
      this.@com.google.gwt.user.client.ui.impl.RichTextAreaImpl::elem.contentWindow.blur();
    }
  }-*/;

  protected native void setHTMLImpl(@IsSafeHtml String html) /*-{
    this.@com.google.gwt.user.client.ui.impl.RichTextAreaImpl::elem.contentWindow.document.body.innerHTML = html;
  }-*/;

  protected native void setTextImpl(String text) /*-{
    this.@com.google.gwt.user.client.ui.impl.RichTextAreaImpl::elem.contentWindow.document.body.textContent = text;
  }-*/;

  protected native void unhookEvents() /*-{
    var elem = this.@com.google.gwt.user.client.ui.impl.RichTextAreaImpl::elem;
    var wnd = elem.contentWindow;

    wnd.removeEventListener('keydown', elem.__gwt_handler, true);
    wnd.removeEventListener('keyup', elem.__gwt_handler, true);
    wnd.removeEventListener('keypress', elem.__gwt_handler, true);
    wnd.removeEventListener('mousedown', elem.__gwt_handler, true);
    wnd.removeEventListener('mouseup', elem.__gwt_handler, true);
    wnd.removeEventListener('mousemove', elem.__gwt_handler, true);
    wnd.removeEventListener('mouseover', elem.__gwt_handler, true);
    wnd.removeEventListener('mouseout', elem.__gwt_handler, true);
    wnd.removeEventListener('click', elem.__gwt_handler, true);

    wnd.removeEventListener('focus', elem.__gwt_focusHandler, true);
    wnd.removeEventListener('blur', elem.__gwt_blurHandler, true);

    elem.__gwt_handler = null;
    elem.__gwt_focusHandler = null;
    elem.__gwt_blurHandler = null;
  }-*/;

  void execCommand(String cmd, String param) {
    assert isReady : INACTIVE_MESSAGE;
    if (isReady) {
      // When executing a command, focus the iframe first, since some commands
      // don't take properly when it's not focused.
      setFocus(true);
      try {
        execCommandAssumingFocus(cmd, param);
      } catch (JavaScriptException e) {
        // In mozilla, editing throws a JS exception if the iframe is
        // *hidden, but attached*.
      }
    }
  }

  native void execCommandAssumingFocus(String cmd, String param) /*-{
    this.@com.google.gwt.user.client.ui.impl.RichTextAreaImpl::elem.contentWindow.document.execCommand(cmd, false, param);
  }-*/;

  boolean queryCommandState(String cmd) {
    if (isReady) {
      // When executing a command, focus the iframe first, since some commands
      // don't take properly when it's not focused.
      setFocus(true);
      try {
        return queryCommandStateAssumingFocus(cmd);
      } catch (JavaScriptException e) { 
        return false;
      }
    }
    return false;
  }

  native boolean queryCommandStateAssumingFocus(String cmd) /*-{
    return !!this.@com.google.gwt.user.client.ui.impl.RichTextAreaImpl::elem.contentWindow.document.queryCommandState(cmd);
  }-*/;

  String queryCommandValue(String cmd) {
    if (isReady) {
      // When executing a command, focus the iframe first, since some commands
      // don't take properly when it's not focused.
      setFocus(true);
      try {
        return queryCommandValueAssumingFocus(cmd);
      } catch (JavaScriptException e) {
        return "";
      }
    }
    return "";
  }

  native String queryCommandValueAssumingFocus(String cmd) /*-{
    return this.@com.google.gwt.user.client.ui.impl.RichTextAreaImpl::elem.contentWindow.document.queryCommandValue(cmd);
  }-*/;
}
