// Copyright 2012 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview A Map that associates multiple values with a single key.
 *
 * @author chrishenry@google.com (Chris Henry)
 */

goog.provide('goog.labs.structs.Multimap');

goog.require('goog.array');
goog.require('goog.object');



/**
 * Creates a new multimap.
 * @constructor
 * @struct
 * @final
 * @template K, V
 */
goog.labs.structs.Multimap = function() {
  this.clear();
};


/**
 * The backing map.
 * @private {!Map<K, !Array<V>>}
 */
goog.labs.structs.Multimap.prototype.map_;


/**
 * @private {number}
 */
goog.labs.structs.Multimap.prototype.count_ = 0;


/**
 * Clears the multimap.
 */
goog.labs.structs.Multimap.prototype.clear = function() {
  this.count_ = 0;
  this.map_ = new Map();
};


/**
 * Clones this multimap.
 * @return {!goog.labs.structs.Multimap<K, V>} A multimap that contains all
 *     the mapping this multimap has.
 */
goog.labs.structs.Multimap.prototype.clone = function() {
  var map = new goog.labs.structs.Multimap();
  map.addAllFromMultimap(this);
  return map;
};


/**
 * Adds the given (key, value) pair to the map. The (key, value) pair
 * is guaranteed to be added.
 * @param {K} key The key to add.
 * @param {V} value The value to add.
 */
goog.labs.structs.Multimap.prototype.add = function(key, value) {
  var values = this.map_.get(key);
  if (!values) {
    this.map_.set(key, (values = []));
  }

  values.push(value);
  this.count_++;
};


/**
 * Stores a collection of values to the given key. Does not replace
 * existing (key, value) pairs.
 * @param {K} key The key to add.
 * @param {!Array<V>} values The values to add.
 */
goog.labs.structs.Multimap.prototype.addAllValues = function(key, values) {
  goog.array.forEach(values, function(v) { this.add(key, v); }, this);
};


/**
 * Adds the contents of the given map/multimap to this multimap.
 * @param {!goog.labs.structs.Multimap<K, V>} map The
 *     map to add.
 */
goog.labs.structs.Multimap.prototype.addAllFromMultimap = function(map) {
  goog.array.forEach(map.getEntries(), function(entry) {
    this.add(entry[0], entry[1]);
  }, this);
};


/**
 * Replaces all the values for the given key with the given values.
 * @param {K} key The key whose values are to be replaced.
 * @param {!Array<V>} values The new values. If empty, this is
 *     equivalent to `removeAll(key)`.
 */
goog.labs.structs.Multimap.prototype.replaceValues = function(key, values) {
  this.removeAll(key);
  this.addAllValues(key, values);
};


/**
 * Gets the values correspond to the given key.
 * @param {K} key The key to retrieve.
 * @return {!Array<V>} An array of values corresponding to the given
 *     key. May be empty. Note that the ordering of values are not
 *     guaranteed to be consistent.
 */
goog.labs.structs.Multimap.prototype.get = function(key) {
  var values = this.map_.get(key);
  return values ? goog.array.clone(values) : [];
};


/**
 * Removes a single occurrence of (key, value) pair.
 * @param {K} key The key to remove.
 * @param {V} value The value to remove.
 * @return {boolean} Whether any matching (key, value) pair is removed.
 */
goog.labs.structs.Multimap.prototype.remove = function(key, value) {
  var values = this.map_.get(key);
  if (!values) {
    return false;
  }

  var removed = goog.array.removeIf(
      values, function(v) { return goog.object.is(value, v); });

  if (removed) {
    this.count_--;
    if (values.length == 0) {
      this.map_.delete(key);
    }
  }
  return removed;
};


/**
 * Removes all values corresponding to the given key.
 * @param {K} key The key whose values are to be removed.
 * @return {boolean} Whether any value is removed.
 */
goog.labs.structs.Multimap.prototype.removeAll = function(key) {
  // We have to first retrieve the values from the backing map because
  // we need to keep track of count (and correctly calculates the
  // return value). values may be undefined.
  var values = this.map_.get(key);
  if (this.map_.delete(key)) {
    this.count_ -= values.length;
    return true;
  }

  return false;
};


/**
 * @return {boolean} Whether the multimap is empty.
 */
goog.labs.structs.Multimap.prototype.isEmpty = function() {
  return !this.count_;
};


/**
 * @return {number} The count of (key, value) pairs in the map.
 */
goog.labs.structs.Multimap.prototype.getCount = function() {
  return this.count_;
};


/**
 * @param {K} key The key to check.
 * @param {V} value The value to check.
 * @return {boolean} Whether the (key, value) pair exists in the multimap.
 */
goog.labs.structs.Multimap.prototype.containsEntry = function(key, value) {
  var values = this.map_.get(key);
  if (!values) {
    return false;
  }

  var index = goog.array.findIndex(
      values, function(v) { return goog.object.is(v, value); });
  return index >= 0;
};


/**
 * @param {K} key The key to check.
 * @return {boolean} Whether the multimap contains at least one (key,
 *     value) pair with the given key.
 */
goog.labs.structs.Multimap.prototype.containsKey = function(key) {
  return this.getKeys().includes(key);
};


/**
 * @param {V} value The value to check.
 * @return {boolean} Whether the multimap contains at least one (key,
 *     value) pair with the given value.
 */
goog.labs.structs.Multimap.prototype.containsValue = function(value) {
  return this.getValues().includes(value);
};


/**
 * @return {!Array<K>} An array of unique keys.
 */
goog.labs.structs.Multimap.prototype.getKeys = function() {
  return [...this.map_.keys()];
};


/**
 * @return {!Array<V>} An array of values. There may be duplicates.
 */
goog.labs.structs.Multimap.prototype.getValues = function() {
  return goog.array.flatten([...this.map_.values()]);
};


/**
 * @return {!Array<!Array<K|V>>} An array of entries. Each entry is of the
 *     form [key, value].
 */
goog.labs.structs.Multimap.prototype.getEntries = function() {
  var keys = this.getKeys();
  var entries = [];
  for (var i = 0; i < keys.length; i++) {
    var key = keys[i];
    var values = this.get(key);
    for (var j = 0; j < values.length; j++) {
      entries.push([key, values[j]]);
    }
  }
  return entries;
};
