/*
 * 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.core.client.GWT;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.dom.client.Element;
import com.google.gwt.i18n.client.LocaleInfo;
import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.ImageResource;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
import com.google.gwt.user.client.DOM;

/**
 * A panel that arranges two widgets in a single horizontal row and allows the
 * user to interactively change the proportion of the width dedicated to each of
 * the two widgets. Widgets contained within a <code>HorizontalSplitPanel</code>
 * will be automatically decorated with scrollbars when necessary.
 * 
 * <p>
 * This widget will <em>only</em> work in quirks mode. If your application is in
 * Standards Mode, use {@link SplitLayoutPanel} instead.
 * </p>
 * 
 * <p>
 * <img class='gallery' src='doc-files/HorizontalSplitPanel.png'/>
 * </p>
 * 
 * <h3>CSS Style Rules</h3>
 * <ul>
 * <li>.gwt-HorizontalSplitPanel { the panel itself }</li>
 * <li>.gwt-HorizontalSplitPanel hsplitter { the splitter }</li>
 * </ul>
 * 
 * @deprecated Use {@link SplitLayoutPanel} instead, but understand that it is
 *             not a drop in replacement for this class. It requires standards
 *             mode, and is most easily used under a {@link RootLayoutPanel} (as
 *             opposed to a {@link RootPanel}
 *
 * @see SplitLayoutPanel
 */
@Deprecated
public final class HorizontalSplitPanel extends SplitPanel {
  /**
   * The default resources used by this widget.
   */
  public interface Resources extends ClientBundle {
    /**
     * An image representing the drag thumb.
     */
    @Source("splitPanelThumb.png")
    ImageResource horizontalSplitPanelThumb();
  }

  /**
   * The standard implementation for horizontal split panels.
   */
  private static class Impl {
    private static void expandToFitParentHorizontally(Element elem) {
      addAbsolutePositoning(elem);
      final String zeroSize = "0px";
      setTop(elem, zeroSize);
      setBottom(elem, zeroSize);
    }

    protected HorizontalSplitPanel panel;

    public void init(HorizontalSplitPanel panel) {
      this.panel = panel;

      panel.getElement().getStyle().setProperty("position", "relative");

      expandToFitParentHorizontally(panel.getElement(LEFT));
      expandToFitParentHorizontally(panel.getElement(RIGHT));
      expandToFitParentHorizontally(panel.getSplitElement());

      expandToFitParentUsingCssOffsets(panel.container);

      // Right now, both panes are stacked on top of each other
      // on either the left side or the right side of the containing
      // panel. This happens because both panes have position:absolute
      // and no left/top values. The panes will be on the left side
      // if the directionality is LTR, and on the right side if the
      // directionality is RTL. In the LTR case, we need to snap the
      // right pane to the right of the container, and in the RTL case,
      // we need to snap the left pane to the left of the container.
      if (LocaleInfo.getCurrentLocale().isRTL()) {
        setLeft(panel.getElement(LEFT), "0px");
      } else {
        setRight(panel.getElement(RIGHT), "0px");
      }
    }

    public void onAttach() {
    }

    public void onDetach() {
    }

    public void onSplitResize(int px) {
      setSplitPositionUsingPixels(px);
    }

    public void setSplitPosition(String pos) {
      final Element leftElem = panel.getElement(LEFT);
      setWidth(leftElem, pos);
      setSplitPositionUsingPixels(getOffsetWidth(leftElem));
    }

    /**
     * Set the splitter's position in units of pixels.
     * 
     * px represents the splitter's position as a distance of px pixels from the
     * left edge of the container. This is true even in a bidi environment.
     * Callers of this method must be aware of this constraint.
     */
    public void setSplitPositionUsingPixels(int px) {
      final Element splitElem = panel.getSplitElement();

      final int rootElemWidth = getOffsetWidth(panel.container);
      final int splitElemWidth = getOffsetWidth(splitElem);

      // This represents an invalid state where layout is incomplete. This
      // typically happens before DOM attachment, but I leave it here as a
      // precaution because negative width/height style attributes produce
      // errors on IE.
      if (rootElemWidth < splitElemWidth) {
        return;
      }

      // Compute the new right side width.
      int newRightWidth = rootElemWidth - px - splitElemWidth;

      // Constrain the dragging to the physical size of the panel.
      if (px < 0) {
        px = 0;
        newRightWidth = rootElemWidth - splitElemWidth;
      } else if (newRightWidth < 0) {
        px = rootElemWidth - splitElemWidth;
        newRightWidth = 0;
      }

      final Element rightElem = panel.getElement(RIGHT);

      // Set the width of the left side.
      setWidth(panel.getElement(LEFT), px + "px");

      // Move the splitter to the right edge of the left element.
      setLeft(splitElem, px + "px");

      // Move the right element to the right of the splitter.
      setLeft(rightElem, (px + splitElemWidth) + "px");

      updateRightWidth(rightElem, newRightWidth);
    }

    /**
     * Implemented by subclasses.
     * 
     * @param rightElem
     * @param newRightWidth
     */
    public void updateRightWidth(Element rightElem, int newRightWidth) {
      // No need to update the width of the right side; this will be
      // recomputed automatically by CSS. This is helpful, as we do not
      // have to worry about watching for resize events and adjusting the
      // right-side width.
    }
  }

  /**
   * The Safari implementation which owes its existence entirely to a single
   * WebKit bug: http://bugs.webkit.org/show_bug.cgi?id=9137.
   */
  @SuppressWarnings("unused")
  // will be used by Safari permutation
  private static class ImplSafari extends Impl {
    @Override
    public void init(HorizontalSplitPanel panel) {
      this.panel = panel;
      final String fullSize = "100%";
      super.init(panel);
      setHeight(panel.container, fullSize);
      setHeight(panel.getElement(LEFT), fullSize);
      setHeight(panel.getElement(RIGHT), fullSize);
      setHeight(panel.getSplitElement(), fullSize);
    }
  }

  /**
   * Constant makes for readable calls to {@link #getElement(int)} and
   * {@link #getWidget(int)}.
   */
  private static final int LEFT = 0;

  /**
   * Constant makes for readable calls to {@link #getElement(int)} and
   * {@link #getWidget(int)}.
   */
  private static final int RIGHT = 1;

  // A style-free element to serve as the root container.
  private final Element container;

  private final Impl impl = GWT.create(Impl.class);

  /**
   * If the split position is set while the split panel is not attached, save it
   * here to be applied when the panel is attached to the document.
   */
  private String lastSplitPosition = "50%";

  private int initialThumbPos;

  private int initialLeftWidth;

  public HorizontalSplitPanel() {
    this(GWT.<Resources> create(Resources.class));
  }

  /**
   * Creates an empty horizontal split panel.
   * 
   * @param images ImageBundle containing an image for the splitter's drag thumb
   * @deprecated replaced by {@link #HorizontalSplitPanel(Resources)}
   */
  @Deprecated
  public HorizontalSplitPanel(HorizontalSplitPanelImages images) {
    this(images.horizontalSplitPanelThumb());
  }

  /**
   * Creates an empty horizontal split panel.
   * 
   * @param resources ClientBundle containing an image for the splitter's drag
   *          thumb
   */
  public HorizontalSplitPanel(Resources resources) {
    this(AbstractImagePrototype.create(resources.horizontalSplitPanelThumb()));
  }

  private HorizontalSplitPanel(AbstractImagePrototype thumbImage) {
    super(DOM.createDiv(), DOM.createDiv(), preventBoxStyles(DOM.createDiv()),
        preventBoxStyles(DOM.createDiv()));

    container = preventBoxStyles(DOM.createDiv());

    buildDOM(thumbImage);

    setStyleName("gwt-HorizontalSplitPanel");

    impl.init(this);

    // By default, the panel will fill its parent vertically and horizontally.
    // The horizontal case is covered by the fact that the top level div is
    // block display.
    setHeight("100%");
  }

  /**
   * Adds a widget to a pane in the HorizontalSplitPanel. The method will first
   * attempt to add the widget to the left pane. If a widget is already in that
   * position, it will attempt to add the widget to the right pane. If a widget
   * is already in that position, an exception will be thrown, as a
   * HorizontalSplitPanel can contain at most two widgets.
   * 
   * Note that this method is bidi-sensitive. In an RTL environment, this method
   * will first attempt to add the widget to the right pane, and if a widget is
   * already in that position, it will attempt to add the widget to the left
   * pane.
   * 
   * @param w the widget to be added
   * @throws IllegalStateException
   */
  @Override
  public void add(Widget w) {
    if (getStartOfLineWidget() == null) {
      setStartOfLineWidget(w);
    } else if (getEndOfLineWidget() == null) {
      setEndOfLineWidget(w);
    } else {
      throw new IllegalStateException(
          "A Splitter can only contain two Widgets.");
    }
  }

  /**
   * Gets the widget in the pane that is at the end of the line direction for
   * the layout. That is, in an RTL layout, gets the widget in the left pane,
   * and in an LTR layout, gets the widget in the right pane.
   * 
   * @return the widget, <code>null</code> if there is not one.
   */
  public Widget getEndOfLineWidget() {
    return getWidget(getEndOfLinePos());
  }

  /**
   * Gets the widget in the left side of the panel.
   * 
   * @return the widget, <code>null</code> if there is not one.
   */
  public Widget getLeftWidget() {
    return getWidget(LEFT);
  }

  /**
   * Gets the widget in the right side of the panel.
   * 
   * @return the widget, <code>null</code> if there is not one.
   */
  public Widget getRightWidget() {
    return getWidget(RIGHT);
  }

  /**
   * Gets the widget in the pane that is at the start of the line direction for
   * the layout. That is, in an RTL environment, gets the widget in the right
   * pane, and in an LTR environment, gets the widget in the left pane.
   * 
   * @return the widget, <code>null</code> if there is not one.
   */
  public Widget getStartOfLineWidget() {
    return getWidget(getStartOfLinePos());
  }

  /**
   * Sets the widget in the pane that is at the end of the line direction for
   * the layout. That is, in an RTL layout, sets the widget in the left pane,
   * and in and RTL layout, sets the widget in the right pane.
   * 
   * @param w the widget
   */
  public void setEndOfLineWidget(Widget w) {
    setWidget(getEndOfLinePos(), w);
  }

  /**
   * Sets the widget in the left side of the panel.
   * 
   * @param w the widget
   */
  public void setLeftWidget(Widget w) {
    setWidget(LEFT, w);
  }

  /**
   * Sets the widget in the right side of the panel.
   * 
   * @param w the widget
   */
  public void setRightWidget(Widget w) {
    setWidget(RIGHT, w);
  }

  /**
   * Moves the position of the splitter.
   * 
   * This method is not bidi-sensitive. The size specified is always the size of
   * the left region, regardless of directionality.
   * 
   * @param pos the new size of the left region in CSS units (e.g. "10px",
   *          "1em")
   */
  @Override
  public void setSplitPosition(String pos) {
    lastSplitPosition = pos;
    impl.setSplitPosition(pos);
  }

  /**
   * Sets the widget in the pane that is at the start of the line direction for
   * the layout. That is, in an RTL layout, sets the widget in the right pane,
   * and in and RTL layout, sets the widget in the left pane.
   * 
   * @param w the widget
   */
  public void setStartOfLineWidget(Widget w) {
    setWidget(getStartOfLinePos(), w);
  }

  /**
   * <b>Affected Elements:</b>
   * <ul>
   * <li>-splitter = the container containing the splitter element.</li>
   * <li>-right = the container on the right side of the splitter.</li>
   * <li>-left = the container on the left side of the splitter.</li>
   * </ul>
   * 
   * @see UIObject#onEnsureDebugId(String)
   */
  @Override
  protected void onEnsureDebugId(String baseID) {
    super.onEnsureDebugId(baseID);
    ensureDebugId(getElement(LEFT), baseID, "left");
    ensureDebugId(getElement(RIGHT), baseID, "right");
  }

  @Override
  protected void onLoad() {
    impl.onAttach();

    /*
     * If the split position has been changed while detached, apply the change.
     * Set the position realizing that it might not work until after layout
     * runs. This first call is simply to try to avoid a jitter effect if
     * possible.
     */
    setSplitPosition(lastSplitPosition);
    Scheduler.get().scheduleDeferred(new ScheduledCommand() {
      public void execute() {
        setSplitPosition(lastSplitPosition);
      }
    });
  }

  @Override
  protected void onUnload() {
    impl.onDetach();
  }

  @Override
  void onSplitterResize(int x, int y) {
    impl.onSplitResize(initialLeftWidth + x - initialThumbPos);
  }

  @Override
  void onSplitterResizeStarted(int x, int y) {
    initialThumbPos = x;
    initialLeftWidth = getOffsetWidth(getElement(LEFT));
  }

  private void buildDOM(AbstractImagePrototype thumbImage) {
    final Element leftDiv = getElement(LEFT);
    final Element rightDiv = getElement(RIGHT);
    final Element splitDiv = getSplitElement();

    DOM.appendChild(getElement(), container);

    DOM.appendChild(container, leftDiv);
    DOM.appendChild(container, splitDiv);
    DOM.appendChild(container, rightDiv);

    /*
     * Sadly, this is the only way I've found to get vertical centering in this
     * case. The usually CSS hacks (display: table-cell, vertical-align: middle)
     * don't work in an absolute positioned DIV.
     */
    SafeHtmlBuilder sb = new SafeHtmlBuilder();
    sb.appendHtmlConstant("<table class='hsplitter' height='100%' cellpadding='0' "
        + "cellspacing='0'><tr><td align='center' valign='middle'>");
    sb.append(thumbImage.getSafeHtml());
    splitDiv.setInnerSafeHtml(sb.toSafeHtml());

    addScrolling(leftDiv);
    addScrolling(rightDiv);
  }

  private int getEndOfLinePos() {
    return (LocaleInfo.getCurrentLocale().isRTL() ? LEFT : RIGHT);
  }

  private int getStartOfLinePos() {
    return (LocaleInfo.getCurrentLocale().isRTL() ? RIGHT : LEFT);
  }
}
