// Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

'use strict';

lib.rtdep('lib.f');

/**
 * UI Element that controls the multi-column list in the connect dialog.
 *
 * Maybe it should be promoted to a shared lib at some point.
 */
nassh.ColumnList = function(div, items, opt_columnCount) {
  this.div_ = div || null;
  this.items_ = items;
  this.columnCount = opt_columnCount || 2;
  this.activeIndex = null;

  // List of callbacks to invoke after the next redraw().
  this.afterRedraw_ = [];

  this.document_ = null;

  if (div)
    this.decorate(div);
};

/**
 * Turn a div into a ColumnList.
 */
nassh.ColumnList.prototype.decorate = function(div) {
  this.div_ = div;
  this.document_ = div.ownerDocument;

  this.div_.style.overflowY = 'auto';
  this.div_.style.overflowX = 'hidden';
  this.div_.addEventListener('keydown', this.onKeyDown_.bind(this));

  var baseId = this.div_.getAttribute('id');
  if (!baseId) {
    baseId =  lib.f.randomInt(1, 0xffff).toString(16);
    baseId = lib.f.zpad(baseID, 4);
    baseId = 'columnlist-' + baseID;
  }

  this.baseId_ = baseId;

  this.redraw();
};

/**
 * Focus the ColumnList.
 */
nassh.ColumnList.prototype.focus = function() {
  if (!this.div_)
    throw 'Not initialized.';

  this.div_.focus();
};

/**
 * Add an event listener.
 */
nassh.ColumnList.prototype.addEventListener = function(var_args) {
  if (!this.div_)
    throw 'Not initialized.';

  this.div_.addEventListener.apply(this.div_, arguments);
};

/**
 * Have the ColumnList redraw after a brief timeout.
 *
 * Coalesces multiple invocations during the timeout period.
 */
nassh.ColumnList.prototype.scheduleRedraw = function() {
  if (this.redrawTimeout_)
    return;

  this.redrawTimeout_ = setTimeout(() => {
    this.redrawTimeout_ = null;
    this.redraw();
  }, 100);
};

/**
 * Emoty out and redraw the list.
 */
nassh.ColumnList.prototype.redraw = function() {
  var div = this.div_;

  while (div.firstChild) {
    div.removeChild(div.firstChild);
  }

  div.setAttribute('tabindex', '0');
  div.setAttribute('role', 'listbox');

  if (!this.items_.length)
    return;

  var columnWidth = (1 / this.columnCount * 100) + '%';

  var table = this.document_.createElement('table');
  table.style.tableLayout = 'fixed';
  table.style.width = '100%';
  div.appendChild(table);

  var tbody = this.document_.createElement('tbody');
  table.appendChild(tbody);

  var tr;

  for (var i = 0; i < this.items_.length; i++) {
    var row = Math.floor(i / this.columnCount);
    var column = i % this.columnCount;

    var td = this.document_.createElement('td');
    td.setAttribute('role', 'option');
    td.setAttribute('id', this.baseId_ + '-item-' + i);
    td.setAttribute('row', row);
    td.setAttribute('column', column);
    td.style.width = columnWidth;
    td.className = 'column-list-item';

    var item = this.document_.createElement('div');
    item.textContent = this.items_[i].textContent || 'no-name';
    item.addEventListener('click', this.onItemClick_.bind(this, td));
    item.addEventListener('dblclick', this.onItemClick_.bind(this, td));
    td.appendChild(item);

    if (column == 0) {
      tr = this.document_.createElement('tr');
      tbody.appendChild(tr);
    }

    tr.appendChild(td);
  }

  this.setActiveIndex(Math.min(this.activeIndex, this.items_.length - 1));

  while (this.afterRedraw_.length) {
    var callback = this.afterRedraw_.pop();
    callback();
  }
};

/**
 * Function to invoke after the next redraw() happens.
 *
 * Use this if you're doing something that will cause a redraw (like modifying a
 * preference linked to the list), and you have something to finish after the
 * redraw.
 */
nassh.ColumnList.prototype.afterNextRedraw = function(callback) {
  this.afterRedraw_.push(callback);
};

/**
 * Set the index of the item that should be considered "active".
 */
nassh.ColumnList.prototype.setActiveIndex = function(i) {
  if (isNaN(i))
    throw new Error('Index is NaN');

  var before = this.activeIndex;

  if (i != this.activeIndex) {
    var n = this.getActiveNode_();
    if (n)
      n.classList.remove('active');

    setTimeout(
        this.onActiveIndexChanged.bind(this, {before: before, now: i}), 0);
  }

  this.activeIndex = i;
  var node = this.getActiveNode_();
  node.classList.add('active');
  this.div_.setAttribute('aria-activedescendant', node.getAttribute('id'));

  setTimeout(node.scrollIntoViewIfNeeded.bind(node), 0);
};

/**
 * Return the outer DOM node for the active item.
 */
nassh.ColumnList.prototype.getActiveNode_ = function() {
  return this.getNodeByIndex_(this.activeIndex);
};

/**
 * Given an index into the list, return the (row, column) location.
 */
nassh.ColumnList.prototype.getRowColByIndex_ = function(i) {
  return {
    row: parseInt(i / this.columnCount),
    column: i % this.columnCount
  };
};

/**
 * Given a 1d index into the list, return the DOM node.
 */
nassh.ColumnList.prototype.getNodeByIndex_ = function(i) {
  var rc = this.getRowColByIndex_(i);
  return this.getNodeByRowCol_(rc.row, rc.column);
};

/**
 * Given a (row, column) location, return an index into the list.
 */
nassh.ColumnList.prototype.getIndexByRowCol_ = function(
    row, column) {
  return this.columnCount * row + column;
};

/**
 * Given a (row, column) location, return a DOM node.
 */
nassh.ColumnList.prototype.getNodeByRowCol_ = function(row, column) {
  return this.div_.querySelector(
      '[row="' + row + '"][column="' + column + '"]');
};

/**
 * Someone clicked on an item in the list.
 */
nassh.ColumnList.prototype.onItemClick_ = function(srcNode, e) {
  var i = this.getIndexByRowCol_(parseInt(srcNode.getAttribute('row')),
                                 parseInt(srcNode.getAttribute('column')));
  this.setActiveIndex(i);

  e.preventDefault();
  return false;
};

/**
 * Return the height (in items) of a given, zero-based column.
 */
nassh.ColumnList.prototype.getColumnHeight_ = function(column) {
  var tallestColumn = Math.ceil(this.items_.length / this.columnCount);

  if (column + 1 <= Math.floor(this.columnCount / column + 1))
    return tallestColumn;

  return tallestColumn - 1;
};

/**
 * Clients can override this to learn when the active index changes.
 */
nassh.ColumnList.prototype.onActiveIndexChanged = function(e) { };

/**
 * Clients can override this to handle onKeyDown events.
 *
 * They can return false (literally) to block the ColumnList from also
 * handling the event.
 */
nassh.ColumnList.prototype.onKeyDown = function(e) { };

/**
 * Handle a key down event on the div.
 */
nassh.ColumnList.prototype.onKeyDown_ = function(e) {
  if (this.onKeyDown(e) === false)
    return;

  var i = this.activeIndex;
  var rc = this.getRowColByIndex_(i);
  var node = this.getActiveNode_();

  switch (e.keyCode) {
    case 38:  // UP
      if (i == 0) {
        // UP from the first item, warp to the last.
        i = this.items_.length - 1;
      } else if (rc.row == 0) {
       // UP from the first row, warp to bottom of previous column.
        i = this.getIndexByRowCol_(this.getColumnHeight_(rc.column - 1) - 1,
                                   rc.column - 1);
      } else {
        // UP from anywhere else, just move up a row.
        i = this.getIndexByRowCol_(rc.row - 1, rc.column);
      }
      break;

    case 40:  // DOWN
      if (i == this.items_.length - 1) {
        // DOWN from last item, warp to the first.
        i = 0;
      } else if (rc.row == this.getColumnHeight_(rc.column) - 1) {
        // DOWN from the bottom row, warp to top of the next.
        i = this.getIndexByRowCol_(0, rc.column + 1);
      } else {
        // DOWN from anywhere else, move down a row.
        i = this.getIndexByRowCol_(rc.row + 1, rc.column);
        // If the next row is incomplete, warp to top of the next.
        if (i > this.items_.length - 1) {
          if (rc.column >= this.columnCount - 1)
            i = 0;
          else
            i = this.getIndexByRowCol_(0, rc.column + 1);
        }
      }
      break;

    case 39:  // RIGHT
      if (i == this.items_.length - 1) {
        // RIGHT from last item, warp to the first.
        i = 0;
      } else if (rc.column >= this.columnCount - 1 ||
                 rc.row >= this.getColumnHeight_(rc.column + 1)) {
        // RIGHT from last column (of this row), warp to the first column of
        // next row.
        i = this.getIndexByRowCol_(rc.row + 1, 0);
      } else {
        // RIGHT from anywhere else, move right a column.
        i = this.getIndexByRowCol_(rc.row, rc.column + 1);
      }
      break;

    case 37:  // LEFT
      if (i == 0) {
        // LEFT from first item, warp to the last.
        i = this.items_.length - 1;
      } else if (rc.column == 0) {
        // LEFT from first column, warp to the last column of previous row.
        i = this.getIndexByRowCol_(rc.row - 1, this.columnCount - 1);
      } else {
        // LEFT from anywhere else, move left a column.
        i = this.getIndexByRowCol_(rc.row, rc.column - 1);
      }
      break;
  }

  if (i != this.activeIndex) {
    this.setActiveIndex(i);
  }
};
