/*
 * Copyright 2010 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.cellview.client;

import com.google.gwt.cell.client.Cell;
import com.google.gwt.cell.client.Cell.Context;
import com.google.gwt.cell.client.ValueUpdater;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.dom.client.BrowserEvents;
import com.google.gwt.dom.client.DivElement;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.EventTarget;
import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.resources.client.CssResource.ImportedWithPrefix;
import com.google.gwt.resources.client.ImageResource;
import com.google.gwt.resources.client.ImageResource.ImageOptions;
import com.google.gwt.resources.client.ImageResource.RepeatStyle;
import com.google.gwt.safehtml.client.SafeHtmlTemplates;
import com.google.gwt.safehtml.shared.SafeHtml;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
import com.google.gwt.safehtml.shared.SafeHtmlUtils;
import com.google.gwt.user.cellview.client.LoadingStateChangeEvent.LoadingState;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.AttachDetachException;
import com.google.gwt.user.client.ui.DeckPanel;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.SimplePanel;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwt.view.client.CellPreviewEvent;
import com.google.gwt.view.client.ProvidesKey;
import com.google.gwt.view.client.SelectionModel;

import java.util.Collections;
import java.util.List;
import java.util.Set;

/**
 * A single column list of cells.
 * 
 * <p>
 * <h3>Examples</h3>
 * <dl>
 * <dt>Trivial example</dt>
 * <dd>{@example com.google.gwt.examples.cellview.CellListExample}</dd>
 * <dt>Handling user input with ValueUpdater</dt>
 * <dd>{@example com.google.gwt.examples.cellview.CellListValueUpdaterExample}</dd>
 * <dt>Pushing data with List Data Provider (backed by {@link List})</dt>
 * <dd>{@example com.google.gwt.examples.view.ListDataProviderExample}</dd>
 * <dt>Pushing data asynchronously with Async Data Provider</dt>
 * <dd>{@example com.google.gwt.examples.view.AsyncDataProviderExample}</dd>
 * <dt>Writing a custom data provider</dt>
 * <dd>{@example com.google.gwt.examples.view.RangeChangeHandlerExample}</dd>
 * <dt>Using a key provider to track objects as they change</dt>
 * <dd>{@example com.google.gwt.examples.view.KeyProviderExample}</dd>
 * </dl>
 * </p>
 * 
 * @param <T> the data type of list items
 */
public class CellList<T> extends AbstractHasData<T> {

  /**
   * A ClientBundle that provides images for this widget.
   */
  public interface Resources extends ClientBundle {
    /**
     * The background used for selected items.
     */
    @ImageOptions(repeatStyle = RepeatStyle.Horizontal, flipRtl = true)
    ImageResource cellListSelectedBackground();

    /**
     * The styles used in this widget.
     */
    @Source(Style.DEFAULT_CSS)
    Style cellListStyle();
  }

  /**
   * Styles used by this widget.
   */
  @ImportedWithPrefix("gwt-CellList")
  public interface Style extends CssResource {
    /**
     * The path to the default CSS styles used by this resource.
     */
    String DEFAULT_CSS = "com/google/gwt/user/cellview/client/CellList.css";

    /**
     * Applied to even items.
     */
    String cellListEvenItem();

    /**
     * Applied to the keyboard selected item.
     */
    String cellListKeyboardSelectedItem();

    /**
     * Applied to odd items.
     */
    String cellListOddItem();

    /**
     * Applied to selected items.
     */
    String cellListSelectedItem();

    /**
     * Applied to the widget.
     */
    String cellListWidget();
  }

  interface Template extends SafeHtmlTemplates {
    @Template("<div __idx=\"{0}\" class=\"{1}\" style=\"outline:none;\" >{2}</div>")
    SafeHtml div(int idx, String classes, SafeHtml cellContents);
  }

  /**
   * The default page size.
   */
  private static final int DEFAULT_PAGE_SIZE = 25;

  private static Resources DEFAULT_RESOURCES;

  private static final Template TEMPLATE = GWT.create(Template.class);

  private static Resources getDefaultResources() {
    if (DEFAULT_RESOURCES == null) {
      DEFAULT_RESOURCES = GWT.create(Resources.class);
    }
    return DEFAULT_RESOURCES;
  }

  private final Cell<T> cell;
  private boolean cellIsEditing;
  private final Element childContainer;
  private SafeHtml emptyListMessage = SafeHtmlUtils.fromSafeConstant("");
  private final SimplePanel emptyListWidgetContainer = new SimplePanel();
  private final SimplePanel loadingIndicatorContainer = new SimplePanel();

  /**
   * A {@link DeckPanel} to hold widgets associated with various loading states.
   */
  private final DeckPanel messagesPanel = new DeckPanel();

  private final Style style;

  private ValueUpdater<T> valueUpdater;

  /**
   * Construct a new {@link CellList}.
   * 
   * @param cell the cell used to render each item
   */
  public CellList(final Cell<T> cell) {
    this(cell, getDefaultResources(), null);
  }

  /**
   * Construct a new {@link CellList} with the specified {@link Resources}.
   * 
   * @param cell the cell used to render each item
   * @param resources the resources used for this widget
   */
  public CellList(final Cell<T> cell, Resources resources) {
    this(cell, resources, null);
  }

  /**
   * Construct a new {@link CellList} with the specified {@link ProvidesKey key
   * provider}.
   * 
   * @param cell the cell used to render each item
   * @param keyProvider an instance of ProvidesKey<T>, or null if the record
   *          object should act as its own key
   */
  public CellList(final Cell<T> cell, ProvidesKey<T> keyProvider) {
    this(cell, getDefaultResources(), keyProvider);
  }

  /**
   * Construct a new {@link CellList} with the specified {@link Resources} and
   * {@link ProvidesKey key provider}.
   * 
   * @param cell the cell used to render each item
   * @param resources the resources used for this widget
   * @param keyProvider an instance of ProvidesKey<T>, or null if the record
   *          object should act as its own key
   */
  public CellList(final Cell<T> cell, Resources resources, ProvidesKey<T> keyProvider) {
    super(Document.get().createDivElement(), DEFAULT_PAGE_SIZE, keyProvider);
    this.cell = cell;
    this.style = resources.cellListStyle();
    this.style.ensureInjected();

    String widgetStyle = this.style.cellListWidget();
    if (widgetStyle != null) {
      // The widget style is null when used in CellBrowser.
      addStyleName(widgetStyle);
    }

    // Add the child container.
    childContainer = Document.get().createDivElement();
    DivElement outerDiv = getElement().cast();
    outerDiv.appendChild(childContainer);

    // Attach the message panel.
    outerDiv.appendChild(messagesPanel.getElement());
    adopt(messagesPanel);
    messagesPanel.add(emptyListWidgetContainer);
    messagesPanel.add(loadingIndicatorContainer);

    // Sink events that the cell consumes.
    CellBasedWidgetImpl.get().sinkEvents(this, cell.getConsumedEvents());
  }

  /**
   * Get the message that is displayed when there is no data.
   * 
   * @return the empty message
   * @see #setEmptyListMessage(SafeHtml)
   * @deprecated as of GWT 2.3, use {@link #getEmptyListWidget()} instead
   */
  @Deprecated
  public SafeHtml getEmptyListMessage() {
    return emptyListMessage;
  }

  /**
   * Get the widget displayed when the list has no rows.
   * 
   * @return the empty list widget
   */
  public Widget getEmptyListWidget() {
    return emptyListWidgetContainer.getWidget();
  }

  /**
   * Get the widget displayed when the data is loading.
   * 
   * @return the loading indicator
   */
  public Widget getLoadingIndicator() {
    return loadingIndicatorContainer.getWidget();
  }

  /**
   * Get the {@link Element} for the specified index. If the element has not
   * been created, null is returned.
   * 
   * @param indexOnPage the index on the page
   * @return the element, or null if it doesn't exists
   * @throws IndexOutOfBoundsException if the index is outside of the current
   *           page
   */
  public Element getRowElement(int indexOnPage) {
    getPresenter().flush();
    checkRowBounds(indexOnPage);
    if (childContainer.getChildCount() > indexOnPage) {
      return childContainer.getChild(indexOnPage).cast();
    }
    return null;
  }

  /**
   * Set the message to display when there is no data.
   * 
   * @param html the message to display when there are no results
   * @see #getEmptyListMessage()
   * @deprecated as of GWT 2.3, use
   *             {@link #setEmptyListWidget(com.google.gwt.user.client.ui.Widget)}
   *             instead
   */
  @Deprecated
  public void setEmptyListMessage(SafeHtml html) {
    this.emptyListMessage = html;
    setEmptyListWidget(html == null ? null : new HTML(html));
  }

  /**
   * Set the widget to display when the list has no rows.
   * 
   * @param widget the empty data widget
   */
  public void setEmptyListWidget(Widget widget) {
    emptyListWidgetContainer.setWidget(widget);
  }

  /**
   * Set the widget to display when the data is loading.
   * 
   * @param widget the loading indicator
   */
  public void setLoadingIndicator(Widget widget) {
    loadingIndicatorContainer.setWidget(widget);
  }

  /**
   * Set the value updater to use when cells modify items.
   * 
   * @param valueUpdater the {@link ValueUpdater}
   */
  public void setValueUpdater(ValueUpdater<T> valueUpdater) {
    this.valueUpdater = valueUpdater;
  }

  @Override
  protected boolean dependsOnSelection() {
    return cell.dependsOnSelection();
  }

  @Override
  protected void doAttachChildren() {
    try {
      doAttach(messagesPanel);
    } catch (Throwable e) {
      throw new AttachDetachException(Collections.singleton(e));
    }
  }

  @Override
  protected void doDetachChildren() {
    try {
      doDetach(messagesPanel);
    } catch (Throwable e) {
      throw new AttachDetachException(Collections.singleton(e));
    }
  }

  /**
   * Fire an event to the cell.
   * 
   * @param context the {@link Context} of the cell
   * @param event the event that was fired
   * @param parent the parent of the cell
   * @param value the value of the cell
   */
  protected void fireEventToCell(Context context, Event event, Element parent, T value) {
    Set<String> consumedEvents = cell.getConsumedEvents();
    if (consumedEvents != null && consumedEvents.contains(event.getType())) {
      boolean cellWasEditing = cell.isEditing(context, parent, value);
      cell.onBrowserEvent(context, parent, value, event, valueUpdater);
      cellIsEditing = cell.isEditing(context, parent, value);
      if (cellWasEditing && !cellIsEditing) {
        CellBasedWidgetImpl.get().resetFocus(new Scheduler.ScheduledCommand() {
          @Override
          public void execute() {
            setFocus(true);
          }
        });
      }
    }
  }

  /**
   * Return the cell used to render each item.
   */
  protected Cell<T> getCell() {
    return cell;
  }

  /**
   * Get the parent element that wraps the cell from the list item. Override
   * this method if you add structure to the element.
   * 
   * @param item the row element that wraps the list item
   * @return the parent element of the cell
   */
  protected Element getCellParent(Element item) {
    return item;
  }

  @Override
  protected Element getChildContainer() {
    return childContainer;
  }

  @Override
  protected Element getKeyboardSelectedElement() {
    // Do not use getRowElement() because that will flush the presenter.
    int rowIndex = getKeyboardSelectedRow();
    if (rowIndex >= 0 && childContainer.getChildCount() > rowIndex) {
      return childContainer.getChild(rowIndex).cast();
    }
    return null;
  }

  @Override
  protected boolean isKeyboardNavigationSuppressed() {
    return cellIsEditing;
  }

  @SuppressWarnings("deprecation")
  @Override
  protected void onBrowserEvent2(Event event) {
    // Get the event target.
    EventTarget eventTarget = event.getEventTarget();
    if (!Element.is(eventTarget)) {
      return;
    }
    final Element target = event.getEventTarget().cast();

    // Forward the event to the cell.
    String idxString = "";
    Element cellTarget = target;
    while ((cellTarget != null) && ((idxString = cellTarget.getAttribute("__idx")).length() == 0)) {
      cellTarget = cellTarget.getParentElement();
    }
    if (idxString.length() > 0) {
      // Select the item if the cell does not consume events. Selection occurs
      // before firing the event to the cell in case the cell operates on the
      // currently selected item.
      String eventType = event.getType();
      boolean isClick = BrowserEvents.CLICK.equals(eventType);
      int idx = Integer.parseInt(idxString);
      int indexOnPage = idx - getPageStart();
      if (!isRowWithinBounds(indexOnPage)) {
        // If the event causes us to page, then the index will be out of bounds.
        return;
      }

      // Get the cell parent before doing selection in case the list is redrawn.
      boolean isSelectionHandled =
          cell.handlesSelection()
              || KeyboardSelectionPolicy.BOUND_TO_SELECTION == getKeyboardSelectionPolicy();
      Element cellParent = getCellParent(cellTarget);
      T value = getVisibleItem(indexOnPage);
      Context context = new Context(idx, 0, getValueKey(value));
      CellPreviewEvent<T> previewEvent =
          CellPreviewEvent.fire(this, event, this, context, value, cellIsEditing,
              isSelectionHandled);

      // Fire the event to the cell if the list has not been refreshed.
      if (!previewEvent.isCanceled()) {
        fireEventToCell(context, event, cellParent, value);
      }
    }
  }

  /**
   * Called when the loading state changes.
   * 
   * @param state the new loading state
   */
  @Override
  protected void onLoadingStateChanged(LoadingState state) {
    Widget message = null;
    if (state == LoadingState.LOADING) {
      // Loading indicator.
      message = loadingIndicatorContainer;
    } else if (state == LoadingState.LOADED && getPresenter().isEmpty()) {
      // Empty table.
      message = emptyListWidgetContainer;
    }

    // Switch out the message to display.
    if (message != null) {
      messagesPanel.showWidget(messagesPanel.getWidgetIndex(message));
    }

    // Show the correct container.
    showOrHide(getChildContainer(), message == null);
    messagesPanel.setVisible(message != null);

    // Fire an event.
    super.onLoadingStateChanged(state);
  }

  @Override
  protected void renderRowValues(SafeHtmlBuilder sb, List<T> values, int start,
      SelectionModel<? super T> selectionModel) {
    String keyboardSelectedItem = " " + style.cellListKeyboardSelectedItem();
    String selectedItem = " " + style.cellListSelectedItem();
    String evenItem = style.cellListEvenItem();
    String oddItem = style.cellListOddItem();
    int keyboardSelectedRow = getKeyboardSelectedRow() + getPageStart();
    int length = values.size();
    int end = start + length;
    for (int i = start; i < end; i++) {
      T value = values.get(i - start);
      boolean isSelected = selectionModel == null ? false : selectionModel.isSelected(value);

      StringBuilder classesBuilder = new StringBuilder();
      classesBuilder.append(i % 2 == 0 ? evenItem : oddItem);
      if (isSelected) {
        classesBuilder.append(selectedItem);
      }
      if (i == keyboardSelectedRow) {
        classesBuilder.append(keyboardSelectedItem);
      }

      SafeHtmlBuilder cellBuilder = new SafeHtmlBuilder();
      Context context = new Context(i, 0, getValueKey(value));
      cell.render(context, value, cellBuilder);
      sb.append(TEMPLATE.div(i, classesBuilder.toString(), cellBuilder.toSafeHtml()));
    }
  }

  @Override
  protected boolean resetFocusOnCell() {
    int row = getKeyboardSelectedRow();
    if (isRowWithinBounds(row)) {
      Element rowElem = getKeyboardSelectedElement();
      Element cellParent = getCellParent(rowElem);
      T value = getVisibleItem(row);
      Context context = new Context(row + getPageStart(), 0, getValueKey(value));
      return cell.resetFocus(context, cellParent, value);
    }
    return false;
  }

  @Override
  protected void setKeyboardSelected(int index, boolean selected, boolean stealFocus) {
    if (!isRowWithinBounds(index)) {
      return;
    }

    Element elem = getRowElement(index);
    if (!selected || isFocused || stealFocus) {
      setStyleName(elem, style.cellListKeyboardSelectedItem(), selected);
    }
    setFocusable(elem, selected);
    if (selected && stealFocus && !cellIsEditing) {
      elem.focus();
      onFocus();
    }
  }

  /**
   * @deprecated this method is never called by AbstractHasData, render the
   *             selected styles in
   *             {@link #renderRowValues(SafeHtmlBuilder, List, int, SelectionModel)}
   */
  @Override
  @Deprecated
  protected void setSelected(Element elem, boolean selected) {
    setStyleName(elem, style.cellListSelectedItem(), selected);
  }
}
