/*
 * Copyright 2011 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.Context;
import com.google.gwt.cell.client.HasCell;
import com.google.gwt.dom.builder.shared.ElementBuilderBase;
import com.google.gwt.dom.builder.shared.HtmlBuilderFactory;
import com.google.gwt.dom.builder.shared.HtmlTableSectionBuilder;
import com.google.gwt.dom.builder.shared.TableRowBuilder;
import com.google.gwt.dom.builder.shared.TableSectionBuilder;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.TableRowElement;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

/**
 * Builder used to construct a CellTable.
 * 
 * @param <T> the row data type
 */
public abstract class AbstractCellTableBuilder<T> implements CellTableBuilder<T> {
  
  /**
   * The attribute used to indicate that an element contains a cell.
   */
  private static final String CELL_ATTRIBUTE = "__gwt_cell";
  
  /**
   * The attribute used to specify the logical row index.
   */
  private static final String ROW_ATTRIBUTE = "__gwt_row";

  /**
   * The attribute used to specify the subrow within a logical row value.
   */
  private static final String SUBROW_ATTRIBUTE = "__gwt_subrow";
  
  protected final AbstractCellTable<T> cellTable;
  
  /**
   * A mapping of unique cell IDs to the cell.
   */
  private final Map<String, HasCell<T, ?>> idToCellMap = new HashMap<String, HasCell<T, ?>>();
  private final Map<HasCell<T, ?>, String> cellToIdMap = new HashMap<HasCell<T, ?>, String>();

  private HtmlTableSectionBuilder tbody;  
  private int rowIndex;
  private int subrowIndex;
  private Object rowValueKey;

  /**
   * Construct a new table builder.
   * 
   * @param cellTable the table this builder will build rows for
   */
  public AbstractCellTableBuilder(AbstractCellTable<T> cellTable) {
    this.cellTable = cellTable;
  }
  
  /**
   * Build zero or more table rows for the specified row value.
   *
   * @param rowValue the value for the row to render
   * @param absRowIndex the absolute row index
   */
  @Override
  public final void buildRow(T rowValue, int absRowIndex) {
    setRowInfo(absRowIndex, rowValue);
    buildRowImpl(rowValue, absRowIndex);
  }
  
  /**
   * Create the context for a column based on the current table building state.
   * 
   * @param column the column id
   * @return the context that contains the column index, row/subrow indexes, and the row value key
   */
  public final Context createContext(int column) {
    return new Context(rowIndex, column, rowValueKey, subrowIndex);
  }
  
  /**
   * Finish the building and get the {@link TableSectionBuilder} containing the children.
   */
  @Override
  public final TableSectionBuilder finish() {
    // End dangling elements.
    while (tbody.getDepth() > 0) {
      tbody.endTBody();
    }
    return tbody;
  }
  
  /**
   * Return the column containing an element.
   *
   * @param context the context for the element
   * @param rowValue the value for the row corresponding to the element
   * @param elem the element that the column contains
   * @return the immediate column containing the element
   */
  @Override
  public final HasCell<T, ?> getColumn(Context context, T rowValue, Element elem) {
    return getColumn(elem);
  }

  /**
   * Return all the columns that this table builder has renderred.
   */
  @Override
  public final Collection<HasCell<T, ?>> getColumns() {
    return idToCellMap.values();
  }
  
  /**
   * Get the index of the row value from the associated {@link TableRowElement}.
   * 
   * @param row the row element
   * @return the row value index
   */
  @Override
  public final int getRowValueIndex(TableRowElement row) {
    try {
      return Integer.parseInt(row.getAttribute(ROW_ATTRIBUTE));
    } catch (NumberFormatException e) {
      // The attribute doesn't exist. Maybe the user is overriding
      // renderRowValues().
      return row.getSectionRowIndex() + cellTable.getPageStart();
    }
  }
  
  /**
   * Get the index of the subrow value from the associated
   * {@link TableRowElement}. The sub row value starts at 0 for the first row
   * that represents a row value.
   * 
   * @param row the row element
   * @return the subrow value index, or 0 if not found
   */
  @Override
  public final int getSubrowValueIndex(TableRowElement row) {
    try {
      return Integer.parseInt(row.getAttribute(SUBROW_ATTRIBUTE));
    } catch (NumberFormatException e) {
      // The attribute doesn't exist. Maybe the user is overriding
      // renderRowValues() in {@link AbstractCellTable}.
      return 0;
    }
  }
  
  /**
   * Return if an element contains a cell. This may be faster to execute than
   * {@link #getColumn(Element)}.
   *
   * @param elem the element of interest
   */
  @Override
  public final boolean isColumn(Element elem) {
    return getCellId(elem) != null;
  }
  
  /**
   * Render the cell into an {@link ElementBuilderBase}.
   * 
   * @param builder the {@link ElementBuilderBase} that cell contents append to
   * @param context the context for the element
   * @param column the column containing the cell
   * @param rowValue the value for the row corresponding to the element
   */
  public final <C> void renderCell(ElementBuilderBase<?> builder, Context context,
      HasCell<T, C> column, T rowValue) {
    // Generate a unique ID for the cell.
    String cellId = cellToIdMap.get(column);
    if (cellId == null) {
      cellId = "cell-" + Document.get().createUniqueId();
      idToCellMap.put(cellId, column);
      cellToIdMap.put(column, cellId);
    }
    builder.attribute(CELL_ATTRIBUTE, cellId);

    // Render the cell into the builder.
    SafeHtmlBuilder cellBuilder = new SafeHtmlBuilder();
    if (column instanceof Column) {
      /*
       * If the HasCell is a Column, let it render the Cell itself. This is
       * here for legacy support.
       */
      Column<T, C> theColumn = (Column<T, C>) column;
      theColumn.render(context, rowValue, cellBuilder);
    } else {
      column.getCell().render(context, column.getValue(rowValue), cellBuilder);
    }
    builder.html(cellBuilder.toSafeHtml());
  }
  
  /**
   * 
   */
  /**
   * Start building rows. Reset the internal table section builder. If the table builder is going
   * to re-build all rows, the internal the maps associating the cells and ids will be cleared.
   *
   * @param isRebuildingAllRows is this start intended for rebuilding all rows
   */
  @Override
  public final void start(boolean isRebuildingAllRows) {
    /*
     * TODO(jlabanca): Test with DomBuilder.
     * 
     * DOM manipulation is sometimes faster than String concatenation and
     * innerHTML, but not when mixing the two. Cells render as HTML strings,
     * so its faster to render the entire table as a string.
     */
    tbody = HtmlBuilderFactory.get().createTBodyBuilder();
    if (isRebuildingAllRows) {
      cellToIdMap.clear();
      idToCellMap.clear();
    }
  }

  /**
   * Start a row and return the {@link TableRowBuilder} for this row.
   */
  public final TableRowBuilder startRow() {
    return startRow(null);
  }

  /**
   * Start a row and return the {@link TableRowBuilder} for this row. The row can be initialized
   * according to its corresponding value.
   *
   * @param rowValue the value for the row corresponding to the element. Can be null.
   */
  public final TableRowBuilder startRow(T rowValue) {
    // End any dangling rows.
    while (tbody.getDepth() > 1) {
      tbody.end();
    }

    // Verify the depth.
    if (tbody.getDepth() < 1) {
      throw new IllegalStateException(
          "Cannot start a row.  Did you call TableRowBuilder.end() too many times?");
    }

    // Start the next row.
    TableRowBuilder row = tbody.startTR();
    row.attribute(ROW_ATTRIBUTE, rowIndex);
    row.attribute(SUBROW_ATTRIBUTE, subrowIndex);
    addRowAttributes(row, rowValue);
    subrowIndex++;
    return row;
  }

  /**
   * Hook for subclasses to add their own attributes to each row in the table.
   * The default does nothing.
   *
   * @param row the row element
   *
   * @see #addRowAttributes(TableRowBuilder, Object)
   */
  protected void addRowAttributes(TableRowBuilder row) {
  }

  /**
   * Hook for subclasses to add their own attributes to each row in the table.
   * The default does nothing.
   *
   * @param row the row element
   * @param rowValue the value for the row corresponding to the element. Can be null.
   */
  protected void addRowAttributes(TableRowBuilder row, T rowValue) {
    addRowAttributes(row);
  }

  /**
   * Build zero or more table rows for the specified row value.
   * 
   * @param rowValue the value for the row to render
   * @param absRowIndex the absolute row index
   */
  protected abstract void buildRowImpl(T rowValue, int absRowIndex);
  
  /**
   * Check if an element is the parent of a rendered cell.
   * 
   * @param elem the element to check
   * @return the cellId if a cell parent, null if not
   */
  private String getCellId(Element elem) {
    if (elem == null) {
      return null;
    }
    String cellId = elem.getAttribute(CELL_ATTRIBUTE);
    return (cellId == null) || (cellId.length() == 0) ? null : cellId;
  }

  /**
   * Return the column containing an element.
   *
   * @param elem the elm that the column contains
   * @return the column containing the element.
   */
  private HasCell<T, ?> getColumn(Element elem) {
    String cellId = getCellId(elem);
    return (cellId == null) ? null : idToCellMap.get(cellId);
  }

  /**
   * Set the information for the current row to build.
   *
   * @param rowIndex the index of the row
   * @param rowValue the value of this row
   */
  private void setRowInfo(int rowIndex, T rowValue) {
    this.rowIndex = rowIndex;
    this.rowValueKey = cellTable.getValueKey(rowValue);
    this.subrowIndex = 0; // Reset the subrow.
  }
}
