/*
 * 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.cell.client;

import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.EventTarget;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;

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

/**
 * <p>
 * A {@link Cell} that is composed of other {@link Cell}s.
 * </p>
 * 
 * <p>
 * When this cell is rendered, it will render each component {@link Cell} inside
 * a span. If the component {@link Cell} uses block level elements (such as a
 * Div), the component cells will stack vertically.
 * </p>
 * 
 * @param <C> the type that this Cell represents
 */
public class CompositeCell<C> extends AbstractCell<C> {

  /**
   * The events consumed by this cell.
   */
  private Set<String> consumedEvents;

  /**
   * Indicates whether or not this cell depends on selection.
   */
  private boolean dependsOnSelection;

  /**
   * Indicates whether or not this cell handles selection.
   */
  private boolean handlesSelection;

  /**
   * The cells that compose this {@link Cell}.
   * 
   * <p>
   * NOTE: Do not add add/insert/remove hasCells methods to the API. This cell
   * assumes that the index of the cellParent corresponds to the index in the
   * hasCells array.
   * </p>
   */
  private final List<HasCell<C, ?>> hasCells;

  /**
   * Construct a new {@link CompositeCell}.
   * 
   * @param hasCells the cells that makeup the composite
   */
  public CompositeCell(List<HasCell<C, ?>> hasCells) {
    // Create a new, readonly copy so cells cannot be added or removed.
    this.hasCells = Collections.unmodifiableList(new ArrayList<HasCell<C, ?>>(hasCells));

    // Get the consumed events and depends on selection.
    Set<String> theConsumedEvents = null;
    for (HasCell<C, ?> hasCell : hasCells) {
      Cell<?> cell = hasCell.getCell();
      Set<String> events = cell.getConsumedEvents();
      if (events != null) {
        if (theConsumedEvents == null) {
          theConsumedEvents = new HashSet<String>();
        }
        theConsumedEvents.addAll(events);
      }
      if (cell.dependsOnSelection()) {
        dependsOnSelection = true;
      }
      if (cell.handlesSelection()) {
        handlesSelection = true;
      }
    }
    if (theConsumedEvents != null) {
      this.consumedEvents = Collections.unmodifiableSet(theConsumedEvents);
    }
  }

  @Override
  public boolean dependsOnSelection() {
    return dependsOnSelection;
  }

  @Override
  public Set<String> getConsumedEvents() {
    return consumedEvents;
  }

  @Override
  public boolean handlesSelection() {
    return handlesSelection;
  }

  @Override
  public boolean isEditing(Context context, Element parent, C value) {
    Element curChild = getContainerElement(parent).getFirstChildElement();
    for (HasCell<C, ?> hasCell : hasCells) {
      if (isEditingImpl(context, curChild, value, hasCell)) {
        return true;
      }
      curChild = curChild.getNextSiblingElement();
    }
    return false;
  }

  @Override
  public void onBrowserEvent(Context context, Element parent, C value,
      NativeEvent event, ValueUpdater<C> valueUpdater) {
    int index = 0;
    EventTarget eventTarget = event.getEventTarget();
    if (Element.is(eventTarget)) {
      Element target = eventTarget.cast();
      Element container = getContainerElement(parent);
      Element wrapper = container.getFirstChildElement();
      while (wrapper != null) {
        if (wrapper.isOrHasChild(target)) {
          onBrowserEventImpl(context, wrapper, value, event, valueUpdater,
              hasCells.get(index));
        }

        index++;
        wrapper = wrapper.getNextSiblingElement();
      }
    }
  }

  @Override
  public void render(Context context, C value, SafeHtmlBuilder sb) {
    for (HasCell<C, ?> hasCell : hasCells) {
      render(context, value, sb, hasCell);
    }
  }

  @Override
  public boolean resetFocus(Context context, Element parent, C value) {
    Element curChild = getContainerElement(parent).getFirstChildElement();
    for (HasCell<C, ?> hasCell : hasCells) {
      // The first child that takes focus wins. Only one child should ever be in
      // edit mode, so this is safe.
      if (resetFocusImpl(context, curChild, value, hasCell)) {
        return true;
      }
      curChild = curChild.getNextSiblingElement();
    }
    return false;
  }

  @Override
  public void setValue(Context context, Element parent, C object) {
    Element curChild = getContainerElement(parent).getFirstChildElement();
    for (HasCell<C, ?> hasCell : hasCells) {
      setValueImpl(context, curChild, object, hasCell);
      curChild = curChild.getNextSiblingElement();
    }
  }

  /**
   * Returns the readonly list of {@link HasCell}'s that make up this composite.
   */
  public List<HasCell<C, ?>> getHasCells() {
    return hasCells;
  }

  /**
   * Get the element that acts as the container for all children. If children
   * are added directly to the parent, the parent is the container. If children
   * are added in a table row, the row is the parent.
   * 
   * @param parent the parent element of the cell
   * @return the container element
   */
  protected Element getContainerElement(Element parent) {
    return parent;
  }

  /**
   * Render the composite cell as HTML into a {@link SafeHtmlBuilder}, suitable
   * for passing to {@link Element#setInnerHTML} on a container element.
   * 
   * <p>
   * Note: If your cell contains natively focusable elements, such as buttons or
   * input elements, be sure to set the tabIndex to -1 so that they do not steal
   * focus away from the containing widget.
   * </p>
   * 
   * @param context the {@link com.google.gwt.cell.client.Cell.Context Context} of the cell
   * @param value the cell value to be rendered
   * @param sb the {@link SafeHtmlBuilder} to be written to
   * @param hasCell a {@link HasCell} instance containing the cells to be
   *          rendered within this cell
   */
  protected <X> void render(Context context, C value,
      SafeHtmlBuilder sb, HasCell<C, X> hasCell) {
    Cell<X> cell = hasCell.getCell();
    sb.appendHtmlConstant("<span>");
    cell.render(context, hasCell.getValue(value), sb);
    sb.appendHtmlConstant("</span>");
  }

  private <X> boolean isEditingImpl(Context context, Element cellParent, C object,
      HasCell<C, X> hasCell) {
    return hasCell.getCell().isEditing(context, cellParent, hasCell.getValue(object));
  }  

  private <X> void onBrowserEventImpl(final Context context, Element parent,
      final C object, NativeEvent event, final ValueUpdater<C> valueUpdater,
      final HasCell<C, X> hasCell) {
    Cell<X> cell = hasCell.getCell();
    String eventType = event.getType();
    Set<String> cellConsumedEvents = cell.getConsumedEvents();
    if (cellConsumedEvents == null || !cellConsumedEvents.contains(eventType)) {
      // If this sub-cell doesn't consume this event.
      return;
    }
    ValueUpdater<X> tempUpdater = null;
    final FieldUpdater<C, X> fieldUpdater = hasCell.getFieldUpdater();
    if (fieldUpdater != null) {
      tempUpdater = new ValueUpdater<X>() {
        @Override
        public void update(X value) {
          fieldUpdater.update(context.getIndex(), object, value);
          if (valueUpdater != null) {
            valueUpdater.update(object);
          }
        }
      };
    }
    cell.onBrowserEvent(context, parent, hasCell.getValue(object), event,
        tempUpdater);
  }

  private <X> boolean resetFocusImpl(Context context, Element cellParent,
      C value, HasCell<C, X> hasCell) {
    X cellValue = hasCell.getValue(value);
    return hasCell.getCell().resetFocus(context, cellParent, cellValue);
  }

  private <X> void setValueImpl(Context context, Element cellParent, C object,
      HasCell<C, X> hasCell) {
    hasCell.getCell().setValue(context, cellParent, hasCell.getValue(object));
  }  
}
