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

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

/**
 * A panel that lays its child widgets out "docked" at its outer edges, and
 * allows its last widget to take up the remaining space in its center.
 *
 * <p>
 * This widget has limitations in standards mode that did not exist in quirks
 * mode. The child Widgets contained within a DockPanel cannot be sized using
 * percentages. Setting a child widget's height to <code>100%</code> will
 * <em>NOT</em> cause the child to fill the available height.
 * </p>
 *
 * <p>
 * If you need to work around these limitations, use {@link DockLayoutPanel}
 * 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}).
 * </p>
 *
 * <p>
 * <img class='gallery' src='doc-files/DockPanel.png'/>
 * </p>
 *
 * @see DockLayoutPanel
 */
public class DockPanel extends CellPanel implements HasAlignment {

  /**
   * DockPanel layout constant, used in
   * {@link DockPanel#add(Widget, DockPanel.DockLayoutConstant)}.
   */
  public static class DockLayoutConstant {
    private DockLayoutConstant() {
    }
  }

  /*
   * This class is package-protected for use with DockPanelTest.
   */
  static class LayoutData {
    public DockLayoutConstant direction;
    public String hAlign = ALIGN_DEFAULT.getTextAlignString();
    public String height = "";
    public Element td;
    public String vAlign = ALIGN_TOP.getVerticalAlignString();
    public String width = "";

    public LayoutData(DockLayoutConstant dir) {
      direction = dir;
    }
  }

  private static class TmpRow {
    public int center;
    public Element tr;
  }

  /**
   * Specifies that a widget be added at the center of the dock.
   */
  public static final DockLayoutConstant CENTER = new DockLayoutConstant();

  /**
   * Specifies that a widget be added at the beginning of the line direction
   * for the layout.
   */
  public static final DockLayoutConstant LINE_START = new DockLayoutConstant();

  /**
   * Specifies that a widget be added at the end of the line direction
   * for the layout.
   */
  public static final DockLayoutConstant LINE_END = new DockLayoutConstant();

  /**
   * Specifies that a widget be added at the east edge of the dock.
   */
  public static final DockLayoutConstant EAST = new DockLayoutConstant();

  /**
   * Specifies that a widget be added at the north edge of the dock.
   */
  public static final DockLayoutConstant NORTH = new DockLayoutConstant();

  /**
   * Specifies that a widget be added at the south edge of the dock.
   */
  public static final DockLayoutConstant SOUTH = new DockLayoutConstant();

  /**
   * Specifies that a widget be added at the west edge of the dock.
   */
  public static final DockLayoutConstant WEST = new DockLayoutConstant();

  /**
   * Generate a debug ID for the {@link Widget} given the direction and number
   * of occurrences of the direction.
   *
   * @param direction the direction of the widget
   * @param count the number of widgets in that direction
   */
  private static String generateDebugId(DockLayoutConstant direction, int count) {
    if (direction == NORTH) {
      return "north" + count;
    } else if (direction == SOUTH) {
      return "south" + count;
    } else if (direction == WEST) {
      return "west" + count;
    } else if (direction == EAST) {
      return "east" + count;
    } else if (direction == LINE_START) {
        return "linestart" + count;
    } else if (direction == LINE_END) {
        return "lineend" + count;
    } else {
      return "center";
    }
  }

  private HorizontalAlignmentConstant horzAlign = ALIGN_DEFAULT;
  private VerticalAlignmentConstant vertAlign = ALIGN_TOP;
  private Widget center;

  /**
   * Creates an empty dock panel.
   */
  public DockPanel() {
    getTable().setPropertyInt("cellSpacing", 0);
    getTable().setPropertyInt("cellPadding", 0);
  }

  /**
   * Adds a widget to the specified edge of the dock. If the widget is already a
   * child of this panel, this method behaves as though {@link #remove(Widget)}
   * had already been called.
   *
   * @param widget the widget to be added
   * @param direction the widget's direction in the dock
   *
   * @throws IllegalArgumentException when adding to the {@link #CENTER} and
   *           there is already a different widget there
   */
  public void add(Widget widget, DockLayoutConstant direction) {
    // Validate
    if (direction == CENTER) {
      // Early out on the case of reinserting the center at the center.
      if (widget == center) {
        return;
      } else if (center != null) {
        // Ensure a second 'center' widget is not being added.
        throw new IllegalArgumentException(
            "Only one CENTER widget may be added");
      }
    }

    // Detach new child.
    widget.removeFromParent();

    // Logical attach.
    getChildren().add(widget);
    if (direction == CENTER) {
      center = widget;
    }

    // Physical attach.
    LayoutData layout = new LayoutData(direction);
    widget.setLayoutData(layout);
    setCellHorizontalAlignment(widget, horzAlign);
    setCellVerticalAlignment(widget, vertAlign);
    realizeTable();

    // Adopt.
    adopt(widget);
  }

  /**
   * Overloaded version for IsWidget.
   *
   * @see #add(Widget,DockLayoutConstant)
   */
  public void add(IsWidget widget, DockLayoutConstant direction) {
   this.add(widget.asWidget(), direction);
  }

  public HorizontalAlignmentConstant getHorizontalAlignment() {
    return horzAlign;
  }

  public VerticalAlignmentConstant getVerticalAlignment() {
    return vertAlign;
  }

  /**
   * Gets the layout direction of the given child widget.
   *
   * @param w the widget to be queried
   * @return the widget's layout direction, or <code>null</code> if it is not
   *         a child of this panel
   */
  public DockLayoutConstant getWidgetDirection(Widget w) {
    if (w.getParent() != this) {
      return null;
    }
    return ((LayoutData) w.getLayoutData()).direction;
  }

  @Override
  public boolean remove(Widget w) {
    boolean removed = super.remove(w);
    if (removed) {
      // Clear the center widget.
      if (w == center) {
        center = null;
      }
      realizeTable();
    }
    return removed;
  }

  @Override
  public void setCellHeight(Widget w, String height) {
    LayoutData data = (LayoutData) w.getLayoutData();
    data.height = height;
    if (data.td != null) {
      data.td.getStyle().setProperty("height", data.height);
    }
  }

  @Override
  public void setCellHorizontalAlignment(Widget w,
      HorizontalAlignmentConstant align) {
    LayoutData data = (LayoutData) w.getLayoutData();
    data.hAlign = align.getTextAlignString();
    if (data.td != null) {
      setCellHorizontalAlignment(data.td, align);
    }
  }

  @Override
  public void setCellVerticalAlignment(Widget w, VerticalAlignmentConstant align) {
    LayoutData data = (LayoutData) w.getLayoutData();
    data.vAlign = align.getVerticalAlignString();
    if (data.td != null) {
      setCellVerticalAlignment(data.td, align);
    }
  }

  @Override
  public void setCellWidth(Widget w, String width) {
    LayoutData data = (LayoutData) w.getLayoutData();
    data.width = width;
    if (data.td != null) {
      data.td.getStyle().setProperty("width", data.width);
    }
  }

  /**
   * Sets the default horizontal alignment to be used for widgets added to this
   * panel. It only applies to widgets added after this property is set.
   *
   * @see HasHorizontalAlignment#setHorizontalAlignment(HasHorizontalAlignment.HorizontalAlignmentConstant)
   */
  public void setHorizontalAlignment(HorizontalAlignmentConstant align) {
    horzAlign = align;
  }

  /**
   * Sets the default vertical alignment to be used for widgets added to this
   * panel. It only applies to widgets added after this property is set.
   *
   * @see HasVerticalAlignment#setVerticalAlignment(HasVerticalAlignment.VerticalAlignmentConstant)
   */
  public void setVerticalAlignment(VerticalAlignmentConstant align) {
    vertAlign = align;
  }

  /**
   * {@link DockPanel} supports adding more than one cell in a direction, so an
   * integer will be appended to the end of the debug id. For example, the first
   * north cell is labeled "north1", the second is "north2", and the third is
   * "north3".
   *
   * This widget recreates its structure every time a {@link Widget} is added,
   * so you must call this method after adding a new {@link Widget} or all debug
   * IDs will be lost.
   *
   * <p>
   * <b>Affected Elements:</b>
   * <ul>
   * <li>-center = the center cell.</li>
   * <li>-north# = the northern cell.</li>
   * <li>-south# = the southern cell.</li>
   * <li>-east# = the eastern cell.</li>
   * <li>-west# = the western cell.</li>
   * </ul>
   * </p>
   *
   * @see UIObject#onEnsureDebugId(String)
   */
  @Override
  protected void onEnsureDebugId(String baseID) {
    super.onEnsureDebugId(baseID);

    Map<DockLayoutConstant, Integer> dirCount = new HashMap<DockLayoutConstant, Integer>();
    Iterator<Widget> it = getChildren().iterator();
    while (it.hasNext()) {
      Widget child = it.next();
      DockLayoutConstant dir = ((LayoutData) child.getLayoutData()).direction;

      // Get a debug id
      Integer countObj = dirCount.get(dir);
      int count = countObj == null ? 1 : countObj.intValue();
      String debugID = generateDebugId(dir, count);
      ensureDebugId(DOM.getParent(child.getElement()), baseID, debugID);

      // Increment the count
      dirCount.put(dir, count + 1);
    }
  }

  /**
   * (Re)creates the DOM structure of the table representing the DockPanel,
   * based on the order and layout of the children.
   */
  private void realizeTable() {
    Element bodyElem = getBody();
    while (DOM.getChildCount(bodyElem) > 0) {
      bodyElem.removeChild(DOM.getChild(bodyElem, 0));
    }

    int rowCount = 1, colCount = 1;
    for (Iterator<Widget> it = getChildren().iterator(); it.hasNext();) {
      Widget child = it.next();
      DockLayoutConstant dir = ((LayoutData) child.getLayoutData()).direction;
      if ((dir == NORTH) || (dir == SOUTH)) {
        ++rowCount;
      } else if ((dir == EAST) || (dir == WEST) || (dir == LINE_START) || (dir == LINE_END)) {
        ++colCount;
      }
    }

    TmpRow[] rows = new TmpRow[rowCount];
    for (int i = 0; i < rowCount; ++i) {
      rows[i] = new TmpRow();
      rows[i].tr = DOM.createTR();
      DOM.appendChild(bodyElem, rows[i].tr);
    }

    int logicalLeftCol = 0, logicalRightCol = colCount - 1;
    int northRow = 0, southRow = rowCount - 1;
    Element centerTd = null;

    for (Iterator<Widget> it = getChildren().iterator(); it.hasNext();) {
      Widget child = it.next();
      LayoutData layout = (LayoutData) child.getLayoutData();

      Element td = DOM.createTD();
      layout.td = td;
      layout.td.setPropertyString("align", layout.hAlign);
      layout.td.getStyle().setProperty("verticalAlign", layout.vAlign);
      layout.td.setPropertyString("width", layout.width);
      layout.td.setPropertyString("height", layout.height);

      if (layout.direction == NORTH) {
        DOM.insertChild(rows[northRow].tr, td, rows[northRow].center);
        DOM.appendChild(td, child.getElement());
        td.setPropertyInt("colSpan", logicalRightCol - logicalLeftCol + 1);
        ++northRow;
      } else if (layout.direction == SOUTH) {
        DOM.insertChild(rows[southRow].tr, td, rows[southRow].center);
        DOM.appendChild(td, child.getElement());
        td.setPropertyInt("colSpan", logicalRightCol - logicalLeftCol + 1);
        --southRow;
      } else if (layout.direction == CENTER) {
        // Defer adding the center widget, so that it can be added after all
        // the others are complete.
        centerTd = td;
      } else if (shouldAddToLogicalLeftOfTable(layout.direction)) {
        TmpRow row = rows[northRow];
        DOM.insertChild(row.tr, td, row.center++);
        DOM.appendChild(td, child.getElement());
        td.setPropertyInt("rowSpan", southRow - northRow + 1);
        ++logicalLeftCol;
      } else if (shouldAddToLogicalRightOfTable(layout.direction)) {
        TmpRow row = rows[northRow];
        DOM.insertChild(row.tr, td, row.center);
        DOM.appendChild(td, child.getElement());
        td.setPropertyInt("rowSpan", southRow - northRow + 1);
        --logicalRightCol;
      }
    }

    // If there is a center widget, add it at the end (centerTd is guaranteed
    // to be initialized because it will have been set in the CENTER case in
    // the above loop).
    if (center != null) {
      TmpRow row = rows[northRow];
      DOM.insertChild(row.tr, centerTd, row.center);
      DOM.appendChild(centerTd, center.getElement());
    }
  }

  private boolean shouldAddToLogicalLeftOfTable(DockLayoutConstant widgetDirection) {

    assert (widgetDirection == LINE_START || widgetDirection == LINE_END ||
        widgetDirection == EAST || widgetDirection == WEST);

    // In a bidi-sensitive environment, adding a widget to the logical left
    // column (think DOM order) means that it will be displayed at the start
    // of the line direction for the current layout. This is because HTML
    // tables are bidi-sensitive; the column order switches depending on
    // the line direction.
    if (widgetDirection == LINE_START) {
      return true;
    }

    if (LocaleInfo.getCurrentLocale().isRTL()) {
      // In an RTL layout, the logical left columns will be displayed on the right hand
      // side. When the direction for the widget is EAST, adding the widget to the logical
      // left columns will have the desired effect of displaying the widget on the 'eastern'
      // side of the screen.
      return (widgetDirection == EAST);
    }

    // In an LTR layout, the logical left columns are displayed on the left hand
    // side. When the direction for the widget is WEST, adding the widget to the
    // logical left columns will have the desired effect of displaying the widget on the
    // 'western' side of the screen.
    return (widgetDirection == WEST);
  }

  private boolean shouldAddToLogicalRightOfTable(DockLayoutConstant widgetDirection) {

    // See comments for shouldAddToLogicalLeftOfTable for clarification

    assert (widgetDirection == LINE_START || widgetDirection == LINE_END ||
        widgetDirection == EAST || widgetDirection == WEST);

    if (widgetDirection == LINE_END) {
      return true;
    }

    if (LocaleInfo.getCurrentLocale().isRTL()) {
      return (widgetDirection == WEST);
    }

    return (widgetDirection == EAST);
  }
}
