// 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.

// Copyright 2008 Google Inc. All Rights Reserved.

/**
 * @fileoverview Renderer for {@link goog.ui.Palette}s.
 *
 */

goog.provide('goog.ui.PaletteRenderer');

goog.require('goog.array');
goog.require('goog.dom');
goog.require('goog.dom.NodeType');
goog.require('goog.dom.a11y');
goog.require('goog.dom.classes');
goog.require('goog.style');
goog.require('goog.ui.ControlRenderer');
goog.require('goog.userAgent');


/**
 * Default renderer for {@link goog.ui.Palette}s.  Renders the palette as an
 * HTML table wrapped in a DIV, with one palette item per cell:
 *
 *    <div class="goog-palette">
 *      <table class="goog-palette-table">
 *        <tbody class="goog-palette-body">
 *          <tr class="goog-palette-row">
 *            <td class="goog-palette-cell">...Item 0...</td>
 *            <td class="goog-palette-cell">...Item 1...</td>
 *            ...
 *          </tr>
 *          <tr class="goog-palette-row">
 *            ...
 *          </tr>
 *        </tbody>
 *      </table>
 *    </div>
 *
 * @constructor
 * @extends {goog.ui.ControlRenderer}
 */
goog.ui.PaletteRenderer = function() {
  goog.ui.ControlRenderer.call(this);
};
goog.inherits(goog.ui.PaletteRenderer, goog.ui.ControlRenderer);
goog.addSingletonGetter(goog.ui.PaletteRenderer);


/**
 * Globally unique ID sequence for cells rendered by this renderer class.
 * @type {number}
 * @private
 */
goog.ui.PaletteRenderer.cellId_ = 0;


/**
 * Default CSS class to be applied to the root element of components rendered
 * by this renderer.
 * @type {string}
 */
goog.ui.PaletteRenderer.CSS_CLASS = goog.getCssName('goog-palette');


/**
 * Returns the palette items arranged in a table wrapped in a DIV, with the
 * renderer's own CSS class and additional state-specific classes applied to
 * it.
 * @param {goog.ui.Palette} palette Palette to render.
 * @return {Element} Root element for the palette.
 */
goog.ui.PaletteRenderer.prototype.createDom = function(palette) {
  var classNames = this.getClassNames(palette);
  return palette.getDomHelper().createDom(
      'div', classNames ? classNames.join(' ') : null,
      this.createGrid(/** @type {Array.<Node>} */(palette.getContent()),
          palette.getSize(), palette.getDomHelper()));
};


/**
 * Returns the given items in a table with {@code size.width} columns and
 * {@code size.height} rows.  If the table is too big, empty cells will be
 * created as needed.  If the table is too small, the items that don't fit
 * will not be rendered.
 * @param {Array.<Node>} items Palette items.
 * @param {goog.math.Size} size Palette size (columns x rows); both dimensions
 *     must be specified as numbers.
 * @param {goog.dom.DomHelper} dom DOM helper for document interaction.
 * @return {Element} Palette table element.
 */
goog.ui.PaletteRenderer.prototype.createGrid = function(items, size, dom) {
  var rows = [];
  for (var row = 0, index = 0; row < size.height; row++) {
    var cells = [];
    for (var column = 0; column < size.width; column++) {
      var item = items && items[index++];
      cells.push(this.createCell(item, dom));
    }
    rows.push(this.createRow(cells, dom));
  }

  return this.createTable(rows, dom);
};


/**
 * Returns a table element (or equivalent) that wraps the given rows.
 * @param {Array.<Element>} rows Array of row elements.
 * @param {goog.dom.DomHelper} dom DOM helper for document interaction.
 * @return {Element} Palette table element.
 */
goog.ui.PaletteRenderer.prototype.createTable = function(rows, dom) {
  var table = dom.createDom('table',
      goog.getCssName(this.getCssClass(), 'table'),
      dom.createDom('tbody', goog.getCssName(this.getCssClass(), 'body'),
          rows));
  table.cellSpacing = 0;
  table.cellPadding = 0;
  goog.dom.a11y.setRole(table, 'grid');
  return table;
};


/**
 * Returns a table row element (or equivalent) that wraps the given cells.
 * @param {Array.<Element>} cells Array of cell elements.
 * @param {goog.dom.DomHelper} dom DOM helper for document interaction.
 * @return {Element} Row element.
 */
goog.ui.PaletteRenderer.prototype.createRow = function(cells, dom) {
  return dom.createDom('tr', goog.getCssName(this.getCssClass(), 'row'), cells);
};


/**
 * Returns a table cell element (or equivalent) that wraps the given palette
 * item (which must be a DOM node).
 * @param {Node|string} node Palette item.
 * @param {goog.dom.DomHelper} dom DOM helper for document interaction.
 * @return {Element} Cell element.
 */
goog.ui.PaletteRenderer.prototype.createCell = function(node, dom) {
  var cell = dom.createDom('td', {
    'class': goog.getCssName(this.getCssClass(), 'cell'),
    // Cells must have an ID, for accessibility, so we generate one here.
    'id': goog.getCssName(this.getCssClass(), 'cell-') +
        goog.ui.PaletteRenderer.cellId_++
  }, node);
  goog.dom.a11y.setRole(cell, 'gridcell');
  return cell;
};


/**
 * Overrides {@link goog.ui.ControlRenderer#canDecorate} to always return false.
 * @param {Element} element Ignored.
 * @return {boolean} False, since palettes don't support the decorate flow (for
 *     now).
 */
goog.ui.PaletteRenderer.prototype.canDecorate = function(element) {
  return false;
};


/**
 * Overrides {@link goog.ui.ControlRenderer#decorate} to be a no-op, since
 * palettes don't support the decorate flow (for now).
 * @param {goog.ui.Palette} palette Ignored.
 * @param {Element} element Ignored.
 * @return {null} Always null.
 */
goog.ui.PaletteRenderer.prototype.decorate = function(palette, element) {
  return null;
};


/**
 * Overrides {@link goog.ui.ControlRenderer#setContent} for palettes.  Locates
 * the HTML table representing the palette grid, and replaces the contents of
 * each cell with a new element from the array of nodes passed as the second
 * argument.  If the new content has too many items the table will have more
 * rows added to fit, if there are less items than the table has cells, then the
 * left over cells will be empty.
 * @param {Element} element Root element of the palette control.
 * @param {Array.<Node>} items Array of items to replace existing palette items.
 */
goog.ui.PaletteRenderer.prototype.setContent = function(element, items) {
  if (element) {
    var tbody = goog.dom.getElementsByTagNameAndClass(
        'tbody', goog.getCssName(this.getCssClass(), 'body'), element)[0];
    if (tbody) {
      var index = 0;
      goog.array.forEach(tbody.rows, function(row) {
        goog.array.forEach(row.cells, function(cell) {
          goog.dom.removeChildren(cell);
          if (items) {
            var item = items[index++];
            if (item) {
              goog.dom.appendChild(cell, item);
            }
          }
        });
      });

      // Make space for any additional items.
      if (index < items.length) {
        var cells = [];
        var dom = goog.dom.getDomHelper(element);
        var width = tbody.rows[0].cells.length;
        while (index < items.length) {
          var item = items[index++];
          cells.push(this.createCell(item, dom));
          if (cells.length == width) {
            var row = this.createRow(cells, dom);
            goog.dom.appendChild(tbody, row);
            cells.length = 0;
          }
        }
        if (cells.length > 0) {
          while (cells.length < width) {
            cells.push(this.createCell('', dom));
          }
          var row = this.createRow(cells, dom);
          goog.dom.appendChild(tbody, row);
        }
      }
    }
    // Make sure the new contents are still unselectable.
    goog.style.setUnselectable(element, true, goog.userAgent.GECKO);
  }
};


/**
 * Returns the item corresponding to the given node, or null if the node is
 * neither a palette cell nor part of a palette item.
 * @param {goog.ui.Palette} palette Palette in which to look for the item.
 * @param {Node} node Node to look for.
 * @return {Node} The corresponding palette item (null if not found).
 */
goog.ui.PaletteRenderer.prototype.getContainingItem = function(palette, node) {
  var root = palette.getElement();
  while (node && node.nodeType == goog.dom.NodeType.ELEMENT && node != root) {
    if (node.tagName == 'TD' && goog.dom.classes.has(
        /** @type {Element} */ (node),
        goog.getCssName(this.getCssClass(), 'cell'))) {
      return node.firstChild;
    }
    node = node.parentNode;
  }

  return null;
};


/**
 * Updates the highlight styling of the palette cell containing the given node
 * based on the value of the Boolean argument.
 * @param {goog.ui.Palette} palette Palette containing the item.
 * @param {Node} node Item whose cell is to be highlighted or un-highlighted.
 * @param {boolean} highlight If true, the cell is highlighted; otherwise it is
 *     un-highlighted.
 */
goog.ui.PaletteRenderer.prototype.highlightCell = function(palette,
                                                           node,
                                                           highlight) {
  if (node) {
    var cell = /** @type {Element} */ (node.parentNode);
    goog.dom.classes.enable(cell,
        goog.getCssName(this.getCssClass(), 'cell-hover'), highlight);
    // See http://www.w3.org/TR/2006/WD-aria-state-20061220/#activedescendent
    // for an explanation of the activedescendent.
    var table = /** @type {Element} */ (palette.getElement().firstChild);
    goog.dom.a11y.setState(table, 'activedescendent', cell.id);
  }
};


/**
 * Updates the selection styling of the palette cell containing the given node
 * based on the value of the Boolean argument.
 * @param {goog.ui.Palette} palette Palette containing the item.
 * @param {Node} node Item whose cell is to be selected or deselected.
 * @param {boolean} select If true, the cell is selected; otherwise it is
 *     deselected.
 */
goog.ui.PaletteRenderer.prototype.selectCell = function(palette, node, select) {
  if (node) {
    var cell = /** @type {Element} */ (node.parentNode);
    goog.dom.classes.enable(cell,
        goog.getCssName(this.getCssClass(), 'cell-selected'),
        select);
  }
};


/**
 * Returns the CSS class to be applied to the root element of components
 * rendered using this renderer.
 * @return {string} Renderer-specific CSS class.
 */
goog.ui.PaletteRenderer.prototype.getCssClass = function() {
  return goog.ui.PaletteRenderer.CSS_CLASS;
};
