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

import com.google.gwt.dom.client.BodyElement;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.i18n.client.BidiUtils;
import com.google.gwt.i18n.client.HasDirection;
import com.google.gwt.i18n.client.LocaleInfo;
import com.google.gwt.user.client.Event;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * The panel to which all other widgets must ultimately be added. RootPanels are
 * never created directly. Rather, they are accessed via {@link RootPanel#get()}
 * .
 * 
 * <p>
 * Most applications will add widgets to the default root panel in their
 * {@link com.google.gwt.core.client.EntryPoint#onModuleLoad} methods.
 * </p>
 */
public class RootPanel extends AbsolutePanel {

  /**
   * A default RootPanel implementation that wraps the body element.
   */
  private static class DefaultRootPanel extends RootPanel {

    public DefaultRootPanel() {
      super(getBodyElement());
    }

    @Override
    protected void setWidgetPositionImpl(Widget w, int left, int top) {
      // Account for the difference between absolute position and the
      // body's positioning context.
      left -= Document.get().getBodyOffsetLeft();
      top -= Document.get().getBodyOffsetTop();

      super.setWidgetPositionImpl(w, left, top);
    }
  }

  /**
   * The singleton command used to detach widgets.
   */
  private static final AttachDetachException.Command maybeDetachCommand = new AttachDetachException.Command() {
    public void execute(Widget w) {
      if (w.isAttached()) {
        w.onDetach();
      }
    }
  };

  private static Map<String, RootPanel> rootPanels = new HashMap<String, RootPanel>();
  private static Set<Widget> widgetsToDetach = new HashSet<Widget>();

  /**
   * Marks a widget as detached and removes it from the detach list.
   * 
   * <p>
   * If an element belonging to a widget originally passed to
   * {@link #detachOnWindowClose(Widget)} has been removed from the document,
   * calling this method will cause it to be marked as detached immediately.
   * Failure to do so will keep the widget from being garbage collected until
   * the page is unloaded.
   * </p>
   * 
   * <p>
   * This method may only be called per widget, and only for widgets that were
   * originally passed to {@link #detachOnWindowClose(Widget)}.
   * </p>
   *
   * @param widget the widget that no longer needs to be cleaned up when the
   *          page closes
   * @see #detachOnWindowClose(Widget)
   * @deprecated Instead, use {@link Widget#removeFromParent()}.
   */
  @Deprecated
  public static void detachNow(Widget widget) {
    assert widgetsToDetach.contains(widget) : "detachNow() called on a widget "
        + "not currently in the detach list";

    try {
      widget.onDetach();
    } finally {
      widgetsToDetach.remove(widget);
    }
  }

  /**
   * Adds a widget to the detach list. This is the list of widgets to be
   * detached when the page unloads.
   *
   * <p>
   * This method must be called for all widgets that have no parent widgets.
   * These are most commonly {@link RootPanel RootPanels}, but can also be any
   * widget used to wrap an existing element on the page. Failing to do this may
   * cause these widgets to leak memory. This method is called automatically by
   * widgets' wrap methods (e.g.
   * {@link Button#wrap(Element)}).
   * </p>
   *
   * <p>
   * This method may <em>not</em> be called on any widget whose element is
   * contained in another widget. This is to ensure that the DOM and Widget
   * hierarchies cannot get into an inconsistent state.
   * </p>
   *
   * @param widget the widget to be cleaned up when the page closes
   * @see #detachNow(Widget)
   * @deprecated While originally introduced to combat memory leaks in old browsers, this is no
   * longer necessary, and the unload event used by this method is being removed from browsers.
   * Additionally, it is unreliable as a means to ensure calls to {@link Widget#onUnload()}. See
   * <a href="https://github.com/gwtproject/gwt/issues/9908">Issue 9908</a> for more information.
   */
  @Deprecated
  public static void detachOnWindowClose(Widget widget) {
    assert !widgetsToDetach.contains(widget) : "detachOnUnload() called twice "
        + "for the same widget";
    assert !isElementChildOfWidget(widget.getElement()) : "A widget that has "
        + "an existing parent widget may not be added to the detach list";

    widgetsToDetach.add(widget);
  }

  /**
   * Gets the default root panel. This panel wraps the body of the browser's
   * document. This root panel can contain any number of widgets, which will be
   * laid out in their natural HTML ordering. Many applications, however, will
   * add a single panel to the RootPanel to provide more structure.
   * 
   * @return the default RootPanel
   */
  public static RootPanel get() {
    return get(null);
  }

  /**
   * Gets the root panel associated with a given browser element. For this to
   * work, the HTML document into which the application is loaded must have
   * specified an element with the given id.
   * 
   * @param id the id of the element to be wrapped with a root panel (
   *          <code>null</code> specifies the default instance, which wraps the
   *          &lt;body&gt; element)
   * @return the root panel, or <code>null</code> if no such element was found
   */
  public static RootPanel get(String id) {
    // See if this RootPanel is already created.
    RootPanel rp = rootPanels.get(id);

    // Find the element that this RootPanel will wrap.
    Element elem = null;
    if (id != null) {
      // Return null if the id is specified, but no element is found.
      if (null == (elem = Document.get().getElementById(id))) {
        return null;
      }
    }

    if (rp != null) {
      // If the element associated with an existing RootPanel has been replaced
      // for any reason, return a new RootPanel rather than the existing one (
      // see issue 1937).
      if ((elem == null) || (rp.getElement() == elem)) {
        // There's already an existing RootPanel for this element. Return it.
        return rp;
      }
    }

    // Note that the code in this if block only happens once -
    // on the first RootPanel.get(String) or RootPanel.get()
    // call.
    if (rootPanels.size() == 0) {
      // If we're in a RTL locale, set the RTL directionality
      // on the entire document.
      if (LocaleInfo.getCurrentLocale().isRTL()) {
        BidiUtils.setDirectionOnElement(getRootElement(),
            HasDirection.Direction.RTL);
      }
    }

    // Create the panel and put it in the map.
    if (elem == null) {
      // 'null' means use document's body element.
      rp = new DefaultRootPanel();
    } else {
      // Otherwise, wrap the existing element.
      rp = new RootPanel(elem);
    }

    rootPanels.put(id, rp);
    detachOnWindowClose(rp);
    return rp;
  }

  /**
   * Convenience method for getting the document's body element.
   * 
   * @return the document's body element
   */
  public static native com.google.gwt.user.client.Element getBodyElement() /*-{
    return $doc.body;
  }-*/;

  /**
   * Determines whether the given widget is in the detach list.
   * <p>
   * Note that modern browsers do not have the memory leaks that originally required use of this
   * feature - it is retained only to support application-specific detach events.
   * </p>
   *
   * @param widget the widget to be checked
   * @return <code>true</code> if the widget is in the detach list
   * @deprecated Use of the detach list requires the unload event, which is planned to be removed
   * in future browser updates. See notice on {@link #detachOnWindowClose(Widget)} and
   * <a href="https://github.com/gwtproject/gwt/issues/9908">Issue 9908</a> for more information.
   */
  @Deprecated
  public static boolean isInDetachList(Widget widget) {
    return widgetsToDetach.contains(widget);
  }

  /**
   * Detaches all widgets that were set to be detached on window close by a call to
   * {@link #detachOnWindowClose}. Formerly was package-protected, now can be called by projects
   * that required the old behavior and are willing to set up their own onclose handler on the
   * window.
   * <p>
   * Note that modern browsers do not have the memory leaks that originally required use of this
   * feature - it is retained only to support application-specific detach events.
   * </p>
   *
   * @deprecated See notice on {@link #detachOnWindowClose(Widget)} and
   * <a href="https://github.com/gwtproject/gwt/issues/9908">Issue 9908</a> for more information.
   */
  @Deprecated
  public static void detachWidgets() {
    // When the window is closing, detach all widgets that need to be
    // cleaned up. This will cause all of their event listeners
    // to be unhooked, which will avoid potential memory leaks.
    try {
      AttachDetachException.tryCommand(widgetsToDetach, maybeDetachCommand);
    } finally {
      widgetsToDetach.clear();

      // Clear the RootPanel cache, since we've "detached" all RootPanels at
      // this point. This would be pointless, since it only happens on unload,
      // but it is very helpful for unit tests, because it allows
      // RootPanel.get() to work properly even after a synthesized "unload".
      rootPanels.clear();
    }
  }

  /**
   * Convenience method for getting the document's root (<html>) element.
   * 
   * @return the document's root element
   */
  private static native Element getRootElement() /*-{
    return $doc;
  }-*/;

  /*
   * Checks to see whether the given element has any parent element that
   * belongs to a widget. This is not terribly efficient, and is thus only used
   * in an assertion.
   */
  private static boolean isElementChildOfWidget(Element element) {
    // Walk up the DOM hierarchy, looking for any widget with an event listener
    // set. Though it is not dependable in the general case that a widget will
    // have set its element's event listener at all times, it *is* dependable
    // if the widget is attached. Which it will be in this case.
    element = element.getParentElement();
    BodyElement body = Document.get().getBody();
    while ((element != null) && (body != element)) {
      if (Event.getEventListener(element) != null) {
        return true;
      }
      element = element.getParentElement().cast();
    }
    return false;
  }

  private RootPanel(Element elem) {
    super(elem);
    onAttach();
  }

  /**
   * Clears the rootPanel. If clearDom is true, then also remove any DOM
   * elements that are not widgets.
   *
   * <p>By default {@link #clear()} will only remove children that are GWT widgets.
   * This method also provides the option to remove all children including the
   * non-widget DOM elements that are directly added (e.g. elements added via
   * {@code getElement().appendChild(...)}.
   *
   * @param clearDom if {@code true} this method will also remove any DOM
   *  elements that are not widgets.
   *
   * @deprecated Simply removing all DOM elements can cause issues with other elements in the page.
   */
  @Deprecated
  public void clear(boolean clearDom) {
    clear();

    if (clearDom) {
      getElement().removeAllChildren();
    }
  }
}
