/*
 * 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.animation.client.Animation;
import com.google.gwt.dom.client.Element;
import com.google.gwt.user.client.DOM;

/**
 * A panel that displays all of its child widgets in a 'deck', where only one
 * can be visible at a time. It is used by
 * {@link com.google.gwt.user.client.ui.TabPanel}.
 * 
 * <p>
 * Once a widget has been added to a DeckPanel, its visibility, width, and
 * height attributes will be manipulated. When the widget is removed from the
 * DeckPanel, it will be visible, and its width and height attributes will be
 * cleared.
 * </p>
 */
public class DeckPanel extends ComplexPanel implements HasAnimation,
    InsertPanel.ForIsWidget {
  /**
   * An {@link Animation} used to slide in the new content.
   */
  private static class SlideAnimation extends Animation {
    /**
     * The {@link Element} holding the {@link Widget} with a lower index.
     */
    private Element container1 = null;

    /**
     * The {@link Element} holding the {@link Widget} with a higher index.
     */
    private Element container2 = null;

    /**
     * A boolean indicating whether container1 is growing or shrinking.
     */
    private boolean growing = false;

    /**
     * The fixed height of a {@link TabPanel} in pixels. If the {@link TabPanel}
     * does not have a fixed height, this will be set to -1.
     */
    private int fixedHeight = -1;

    /**
     * The old {@link Widget} that is being hidden.
     */
    private Widget oldWidget = null;

    /**
     * Switch to a new {@link Widget}.
     * 
     * @param oldWidget the {@link Widget} to hide
     * @param newWidget the {@link Widget} to show
     * @param animate true to animate, false to switch instantly
     */
    public void showWidget(Widget oldWidget, Widget newWidget, boolean animate) {
      // Immediately complete previous animation
      cancel();

      // Get the container and index of the new widget
      Element newContainer = getContainer(newWidget);
      int newIndex = DOM.getChildIndex(DOM.getParent(newContainer),
          newContainer);

      // If we aren't showing anything, don't bother with the animation
      if (oldWidget == null) {
        UIObject.setVisible(newContainer, true);
        newWidget.setVisible(true);
        return;
      }
      this.oldWidget = oldWidget;

      // Get the container and index of the old widget
      Element oldContainer = getContainer(oldWidget);
      int oldIndex = DOM.getChildIndex(DOM.getParent(oldContainer),
          oldContainer);

      // Figure out whether to grow or shrink the container
      if (newIndex > oldIndex) {
        container1 = oldContainer;
        container2 = newContainer;
        growing = false;
      } else {
        container1 = newContainer;
        container2 = oldContainer;
        growing = true;
      }

      // Start the animation
      if (animate) {
        // Figure out if the deck panel has a fixed height
        com.google.gwt.dom.client.Element deckElem = container1.getParentElement();
        int deckHeight = deckElem.getOffsetHeight();
        if (growing) {
          fixedHeight = container2.getOffsetHeight();
          container2.getStyle().setPropertyPx("height",
              Math.max(1, fixedHeight - 1));
        } else {
          fixedHeight = container1.getOffsetHeight();
          container1.getStyle().setPropertyPx("height",
              Math.max(1, fixedHeight - 1));
        }
        if (deckElem.getOffsetHeight() != deckHeight) {
          fixedHeight = -1;
        }

        // Only scope to the deck if it's fixed height, otherwise it can affect
        // the rest of the page, even if it's not visible to the user.
        run(ANIMATION_DURATION, fixedHeight == -1 ? null : deckElem);
      } else {
        onInstantaneousRun();
      }

      // We call newWidget.setVisible(true) immediately after showing the
      // widget's container so users can delay render their widget. Ultimately,
      // we should have a better way of handling this, but we need to call
      // setVisible for legacy support.
      newWidget.setVisible(true);
    }

    @Override
    protected void onComplete() {
      if (growing) {
        container1.getStyle().setProperty("height", "100%");
        UIObject.setVisible(container1, true);
        UIObject.setVisible(container2, false);
        container2.getStyle().setProperty("height", "100%");
      } else {
        UIObject.setVisible(container1, false);
        container1.getStyle().setProperty("height", "100%");
        container2.getStyle().setProperty("height", "100%");
        UIObject.setVisible(container2, true);
      }
      container1.getStyle().setProperty("overflow", "visible");
      container2.getStyle().setProperty("overflow", "visible");
      container1 = null;
      container2 = null;
      hideOldWidget();
    }

    @Override
    protected void onStart() {
      // Start the animation
      container1.getStyle().setProperty("overflow", "hidden");
      container2.getStyle().setProperty("overflow", "hidden");
      onUpdate(0.0);
      UIObject.setVisible(container1, true);
      UIObject.setVisible(container2, true);
    }

    @Override
    protected void onUpdate(double progress) {
      if (!growing) {
        progress = 1.0 - progress;
      }

      // Container1 expands (shrinks) to its target height
      int height1;
      int height2;
      if (fixedHeight == -1) {
        height1 = (int) (progress * container1.getPropertyInt("scrollHeight"));
        height2 = (int) ((1.0 - progress) * container2.getPropertyInt("scrollHeight"));
      } else {
        height1 = (int) (progress * fixedHeight);
        height2 = fixedHeight - height1;
      }

      // Issue 2339: If the height is 0px, IE7 will display the entire content
      // widget instead of hiding it completely.
      if (height1 == 0) {
        height1 = 1;
        height2 = Math.max(1, height2 - 1);
      } else if (height2 == 0) {
        height2 = 1;
        height1 = Math.max(1, height1 - 1);
      }
      container1.getStyle().setProperty("height", height1 + "px");
      container2.getStyle().setProperty("height", height2 + "px");
    }

    /**
     * Hide the old widget when the animation completes.
     */
    private void hideOldWidget() {
      // Issue 2510: Hiding the widget isn't necessary because we hide its
      // wrapper, but its in here for legacy support.
      oldWidget.setVisible(false);
      oldWidget = null;
    }

    private void onInstantaneousRun() {
      UIObject.setVisible(container1, growing);
      UIObject.setVisible(container2, !growing);
      container1 = null;
      container2 = null;
      hideOldWidget();
    }
  }

  /**
   * The duration of the animation.
   */
  private static final int ANIMATION_DURATION = 350;

  /**
   * The {@link Animation} used to slide in the new {@link Widget}.
   */
  private static SlideAnimation slideAnimation;

  /**
   * The container {@link Element} around a {@link Widget}.
   * 
   * @param w the {@link Widget}
   * @return the container {@link Element}
   */
  private static Element getContainer(Widget w) {
    return DOM.getParent(w.getElement());
  }

  private boolean isAnimationEnabled = false;

  private Widget visibleWidget;

  /**
   * Creates an empty deck panel.
   */
  public DeckPanel() {
    setElement(DOM.createDiv());
  }

  @Override
  public void add(Widget w) {
    Element container = createWidgetContainer();
    DOM.appendChild(getElement(), container);

    // The order of these methods is very important. In order to preserve
    // backward compatibility, the offsetWidth and offsetHeight of the child
    // widget should be defined (greater than zero) when w.onLoad() is called.
    // As a result, we first initialize the container with a height of 0px, then
    // we attach the child widget to the container. See Issue 2321 for more
    // details.
    super.add(w, container);

    // After w.onLoad is called, it is safe to make the container invisible and
    // set the height of the container and widget to 100%.
    finishWidgetInitialization(container, w);
  }

  /**
   * Gets the index of the currently-visible widget, if any.
   * 
   * @return the visible widget's index, or -1 if there is no such widget
   */
  public int getVisibleWidget() {
    return getWidgetIndex(visibleWidget);
  }

  public void insert(IsWidget w, int beforeIndex) {
    insert(asWidgetOrNull(w), beforeIndex);
  }

  public void insert(Widget w, int beforeIndex) {
    Element container = createWidgetContainer();
    DOM.insertChild(getElement(), container, beforeIndex);

    // See add(Widget) for important comments
    insert(w, container, beforeIndex, true);
    finishWidgetInitialization(container, w);
  }

  public boolean isAnimationEnabled() {
    return isAnimationEnabled;
  }

  @Override
  public boolean remove(Widget w) {
    Element container = getContainer(w);
    boolean removed = super.remove(w);
    if (removed) {
      resetChildWidget(w);

      getElement().removeChild(container);
      if (visibleWidget == w) {
        visibleWidget = null;
      }
    }
    return removed;
  }

  public void setAnimationEnabled(boolean enable) {
    isAnimationEnabled = enable;
  }

  /**
   * Shows the widget at the specified index. This causes the currently- visible
   * widget to be hidden.
   * 
   * @param index the index of the widget to be shown
   */
  public void showWidget(int index) {
    checkIndexBoundsForAccess(index);
    Widget oldWidget = visibleWidget;
    visibleWidget = getWidget(index);

    if (visibleWidget != oldWidget) {
      if (slideAnimation == null) {
        slideAnimation = new SlideAnimation();
      }
      slideAnimation.showWidget(oldWidget, visibleWidget, isAnimationEnabled
          && isAttached());
    }
  }

  /**
   * Setup the container around the widget.
   */
  private Element createWidgetContainer() {
    Element container = DOM.createDiv();
    container.getStyle().setProperty("width", "100%");
    container.getStyle().setProperty("height", "0px");
    container.getStyle().setProperty("padding", "0px");
    container.getStyle().setProperty("margin", "0px");
    return container;
  }

  /**
   * Setup the container around the widget.
   */
  private void finishWidgetInitialization(Element container, Widget w) {
    UIObject.setVisible(container, false);
    container.getStyle().setProperty("height", "100%");

    // Set 100% by default.
    Element element = w.getElement();
    if (element.getStyle().getProperty("width").equals("")) {
      w.setWidth("100%");
    }
    if (element.getStyle().getProperty("height").equals("")) {
      w.setHeight("100%");
    }

    // Issue 2510: Hiding the widget isn't necessary because we hide its
    // wrapper, but it's in here for legacy support.
    w.setVisible(false);
  }

  /**
   * Reset the dimensions of the widget when it is removed.
   */
  private void resetChildWidget(Widget w) {
    w.setSize("", "");
    w.setVisible(true);
  }
}
