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

cr.define('cr.ui', function() {
  /** @const */ var Event = cr.Event;
  /** @const */ var EventTarget = cr.EventTarget;

  /**
   * Creates a new selection model that is to be used with lists.
   *
   * @param {number=} opt_length The number items in the selection.
   *
   * @constructor
   * @extends {!cr.EventTarget}
   */
  function ListSelectionModel(opt_length) {
    this.length_ = opt_length || 0;
    // Even though selectedIndexes_ is really a map we use an array here to get
    // iteration in the order of the indexes.
    this.selectedIndexes_ = [];

    // True if any item could be lead or anchor. False if only selected ones.
    this.independentLeadItem_ = !cr.isMac && !cr.isChromeOS;
  }

  ListSelectionModel.prototype = {
    __proto__: EventTarget.prototype,

    /**
     * The number of items in the model.
     * @type {number}
     */
    get length() {
      return this.length_;
    },

    /**
     * The selected indexes.
     * Setter also changes lead and anchor indexes if value list is nonempty.
     * @type {!Array}
     */
    get selectedIndexes() {
      return Object.keys(this.selectedIndexes_).map(Number);
    },
    set selectedIndexes(selectedIndexes) {
      this.beginChange();
      var unselected = {};
      for (var index in this.selectedIndexes_) {
        unselected[index] = true;
      }

      for (var i = 0; i < selectedIndexes.length; i++) {
        var index = selectedIndexes[i];
        if (index in this.selectedIndexes_) {
          delete unselected[index];
        } else {
          this.selectedIndexes_[index] = true;
          this.changedIndexes_[index] = true;
        }
      }

      for (var index in unselected) {
        delete this.selectedIndexes_[index];
        this.changedIndexes_[index] = false;
      }

      if (selectedIndexes.length) {
        this.leadIndex = this.anchorIndex = selectedIndexes[0];
      } else {
        this.leadIndex = this.anchorIndex = -1;
      }
      this.endChange();
    },

    /**
     * Convenience getter which returns the first selected index.
     * Setter also changes lead and anchor indexes if value is nonnegative.
     * @type {number}
     */
    get selectedIndex() {
      for (var i in this.selectedIndexes_) {
        return Number(i);
      }
      return -1;
    },
    set selectedIndex(selectedIndex) {
      this.selectedIndexes = selectedIndex != -1 ? [selectedIndex] : [];
    },

    /**
     * Returns the last selected index or -1 if no item selected.
     * @type {number}
     */
    get lastSelectedIndex() {
      var result = -1;
      for (var i in this.selectedIndexes_) {
        result = Math.max(result, Number(i));
      }
      return result;
    },

    /**
     * Selects a range of indexes, starting with {@code start} and ends with
     * {@code end}.
     * @param {number} start The first index to select.
     * @param {number} end The last index to select.
     */
    selectRange: function(start, end) {
      // Swap if starts comes after end.
      if (start > end) {
        var tmp = start;
        start = end;
        end = tmp;
      }

      this.beginChange();

      for (var index = start; index != end; index++) {
        this.setIndexSelected(index, true);
      }
      this.setIndexSelected(end, true);

      this.endChange();
    },

    /**
     * Selects all indexes.
     */
    selectAll: function() {
      this.selectRange(0, this.length - 1);
    },

    /**
     * Clears the selection
     */
    clear: function() {
      this.beginChange();
      this.length_ = 0;
      this.anchorIndex = this.leadIndex = -1;
      this.unselectAll();
      this.endChange();
    },

    /**
     * Unselects all selected items.
     */
    unselectAll: function() {
      this.beginChange();
      for (var i in this.selectedIndexes_) {
        this.setIndexSelected(i, false);
      }
      this.endChange();
    },

    /**
     * Sets the selected state for an index.
     * @param {number} index The index to set the selected state for.
     * @param {boolean} b Whether to select the index or not.
     */
    setIndexSelected: function(index, b) {
      var oldSelected = index in this.selectedIndexes_;
      if (oldSelected == b)
        return;

      if (b)
        this.selectedIndexes_[index] = true;
      else
        delete this.selectedIndexes_[index];

      this.beginChange();

      this.changedIndexes_[index] = b;

      // End change dispatches an event which in turn may update the view.
      this.endChange();
    },

    /**
     * Whether a given index is selected or not.
     * @param {number} index The index to check.
     * @return {boolean} Whether an index is selected.
     */
    getIndexSelected: function(index) {
      return index in this.selectedIndexes_;
    },

    /**
     * This is used to begin batching changes. Call {@code endChange} when you
     * are done making changes.
     */
    beginChange: function() {
      if (!this.changeCount_) {
        this.changeCount_ = 0;
        this.changedIndexes_ = {};
        this.oldLeadIndex_ = this.leadIndex_;
        this.oldAnchorIndex_ = this.anchorIndex_;
      }
      this.changeCount_++;
    },

    /**
     * Call this after changes are done and it will dispatch a change event if
     * any changes were actually done.
     */
    endChange: function() {
      this.changeCount_--;
      if (!this.changeCount_) {
        // Calls delayed |dispatchPropertyChange|s, only when |leadIndex| or
        // |anchorIndex| has been actually changed in the batch.
        this.leadIndex_ = this.adjustIndex_(this.leadIndex_);
        if (this.leadIndex_ != this.oldLeadIndex_) {
          cr.dispatchPropertyChange(this, 'leadIndex',
                                    this.leadIndex_, this.oldLeadIndex_);
        }
        this.oldLeadIndex_ = null;

        this.anchorIndex_ = this.adjustIndex_(this.anchorIndex_);
        if (this.anchorIndex_ != this.oldAnchorIndex_) {
          cr.dispatchPropertyChange(this, 'anchorIndex',
                                    this.anchorIndex_, this.oldAnchorIndex_);
        }
        this.oldAnchorIndex_ = null;

        var indexes = Object.keys(this.changedIndexes_);
        if (indexes.length) {
          var e = new Event('change');
          e.changes = indexes.map(function(index) {
            return {
              index: Number(index),
              selected: this.changedIndexes_[index]
            };
          }, this);
          this.dispatchEvent(e);
        }
        this.changedIndexes_ = {};
      }
    },

    leadIndex_: -1,
    oldLeadIndex_: null,

    /**
     * The leadIndex is used with multiple selection and it is the index that
     * the user is moving using the arrow keys.
     * @type {number}
     */
    get leadIndex() {
      return this.leadIndex_;
    },
    set leadIndex(leadIndex) {
      var oldValue = this.leadIndex_;
      var newValue = this.adjustIndex_(leadIndex);
      this.leadIndex_ = newValue;
      // Delays the call of dispatchPropertyChange if batch is running.
      if (!this.changeCount_ && newValue != oldValue)
        cr.dispatchPropertyChange(this, 'leadIndex', newValue, oldValue);
    },

    anchorIndex_: -1,
    oldAnchorIndex_: null,

    /**
     * The anchorIndex is used with multiple selection.
     * @type {number}
     */
    get anchorIndex() {
      return this.anchorIndex_;
    },
    set anchorIndex(anchorIndex) {
      var oldValue = this.anchorIndex_;
      var newValue = this.adjustIndex_(anchorIndex);
      this.anchorIndex_ = newValue;
      // Delays the call of dispatchPropertyChange if batch is running.
      if (!this.changeCount_ && newValue != oldValue)
        cr.dispatchPropertyChange(this, 'anchorIndex', newValue, oldValue);
    },

    /**
     * Helper method that adjustes a value before assiging it to leadIndex or
     * anchorIndex.
     * @param {number} index New value for leadIndex or anchorIndex.
     * @return {number} Corrected value.
     */
    adjustIndex_: function(index) {
      index = Math.max(-1, Math.min(this.length_ - 1, index));
      // On Mac and ChromeOS lead and anchor items are forced to be among
      // selected items. This rule is not enforces until end of batch update.
      if (!this.changeCount_ && !this.independentLeadItem_ &&
          !this.getIndexSelected(index)) {
        index = this.lastSelectedIndex;
      }
      return index;
    },

    /**
     * Whether the selection model supports multiple selected items.
     * @type {boolean}
     */
    get multiple() {
      return true;
    },

    /**
     * Adjusts the selection after reordering of items in the table.
     * @param {!Array.<number>} permutation The reordering permutation.
     */
    adjustToReordering: function(permutation) {
      this.beginChange();
      var oldLeadIndex = this.leadIndex;
      var oldAnchorIndex = this.anchorIndex;
      var oldSelectedItemsCount = this.selectedIndexes.length;

      this.selectedIndexes = this.selectedIndexes.map(function(oldIndex) {
        return permutation[oldIndex];
      }).filter(function(index) {
        return index != -1;
      });

      // Will be adjusted in endChange.
      if (oldLeadIndex != -1)
        this.leadIndex = permutation[oldLeadIndex];
      if (oldAnchorIndex != -1)
        this.anchorIndex = permutation[oldAnchorIndex];

      if (oldSelectedItemsCount && !this.selectedIndexes.length &&
          this.length_) {
        // All selected items are deleted. We move selection to next item of
        // last selected item.
        this.selectedIndexes = [Math.min(oldLeadIndex, this.length_ - 1)];
      }

      this.endChange();
    },

    /**
     * Adjusts selection model length.
     * @param {number} length New selection model length.
     */
    adjustLength: function(length) {
      this.length_ = length;
    }
  };

  return {
    ListSelectionModel: ListSelectionModel
  };
});
