/******************************************************************************
* Copyright 2013-2016 LASIGE                                                  *
*                                                                             *
* 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.                                              *
*                                                                             *
*******************************************************************************
* A table with two variable columns and one fixed column, represented by a    *
* HashMap of HashMaps.                                                        *
*                                                                             *
* @author Daniel Faria                                                        *
******************************************************************************/
package aml.util;

import java.util.HashMap;
import java.util.Set;
import java.util.Vector;

public class Table2Map<A,B,C extends Comparable<C>>
{

//Attributes
	
	private HashMap<A,HashMap<B,C>> multimap;
	private int size;
	
//Constructors

	/**
	 * Constructs a new empty Table
	 */
	public Table2Map()
	{
		multimap = new HashMap<A,HashMap<B,C>>();
		size = 0;
	}
	
	/**
	 * Constructs a new Table that is a copy of
	 * the given Table
	 * @param m: the Table to copy
	 */
	public Table2Map(Table2Map<A,B,C> m)
	{
		multimap = new HashMap<A,HashMap<B,C>>();
		size = m.size;
		Set<A> keys = m.keySet();
		for(A a : keys)
			multimap.put(a, new HashMap<B,C>(m.get(a)));
	}

//Public Methods
	
	/**
	 * Adds the value for the given keys to the Table
	 * If there is already a value for the given keys, the
	 * value will be replaced
	 * @param keyA: the first level key to add to the Table
	 * @param keyB: the second level key to add to the Table
	 * @param valueC: the value for the pair of keys to add to the Table
	 */
	public void add(A keyA, B keyB, C valueC)
	{
		HashMap<B,C> mapsA = multimap.get(keyA);
		if(!contains(keyA,keyB))
			size++;
		if(mapsA == null)
		{
			mapsA = new HashMap<B,C>();
			mapsA.put(keyB, valueC);
			multimap.put(keyA, mapsA);
		}
		else
			mapsA.put(keyB, valueC);
	}
	
	/**
	 * Adds the value for the given keys to the Table
	 * unless there is already a value for the given keys
	 * @param keyA: the first level key to add to the Table
	 * @param keyB: the second level key to add to the Table
	 * @param valueC: the value for the pair of keys to add to the Table
	 */
	public void addIgnore(A keyA, B keyB, C valueC)
	{
		HashMap<B,C> mapsA = multimap.get(keyA);
		if(mapsA == null)
		{
			mapsA = new HashMap<B,C>();
			mapsA.put(keyB, valueC);
			multimap.put(keyA, mapsA);
			size++;
		}
		else if(!mapsA.containsKey(keyB))
		{
			mapsA.put(keyB, valueC);
			size++;
		}
	}
	
	/**
	 * Adds the value for the given keys to the Table
	 * If there is already a value for the given keys, the
	 * new value will replace the previous value only if it
	 * compares favorably as determined by the compareTo test
	 * @param keyA: the first level key to add to the Table
	 * @param keyB: the second level key to add to the Table
	 * @param valueC: the value for the pair of keys to add to the Table
	 */
	public void addUpgrade(A keyA, B keyB, C valueC)
	{
		HashMap<B,C> mapsA = multimap.get(keyA);
		if(mapsA == null)
		{
			mapsA = new HashMap<B,C>();
			mapsA.put(keyB, valueC);
			multimap.put(keyA, mapsA);
			size++;
		}
		else if(!mapsA.containsKey(keyB))
		{
			mapsA.put(keyB, valueC);
			size++;
		}
		else if(mapsA.get(keyB).compareTo(valueC) < 0)
			mapsA.put(keyB, valueC);
	}
	
	/**
	 * @param keyA: the first level key to search in the Table
	 * @return whether the Table contains the first level keyA
	 */
	public boolean contains(A keyA)
	{
		return multimap.containsKey(keyA);
	}

	/**
	 * @param keyA: the first level key to search in the Table
	 * @param keyB: the second level key to search in the Table
	 * @return whether the Table contains an entry with the two keys
	 */
	public boolean contains(A keyA, B keyB)
	{
		return multimap.containsKey(keyA) &&
			multimap.get(keyA).containsKey(keyB);
	}
	
	/**
	 * @param keyA: the first level key to search in the Table
	 * @param keyB: the second level key to search in the Table
	 * @param valueC: the value to search in the Table
	 * @return whether the Table contains an entry with the two keys
	 * and the given value
	 */
	public boolean contains(A keyA, B keyB, C valueC)
	{
		return multimap.containsKey(keyA) &&
			multimap.get(keyA).containsKey(keyB) &&
			multimap.get(keyA).get(keyB).equals(valueC);
	}

	/**
	 * @param keyA: the first level key to search in the Table
	 * @return the number of entries with keyA
	 */
	public int entryCount(A keyA)
	{
		HashMap<B,C> mapsA = multimap.get(keyA);
		if(mapsA == null)
			return 0;
		return mapsA.size();
	}
	
	/**
	 * @param keyA: the first level key to search in the Table
	 * @param valueC: the value to search in the Table
	 * @return the number of entries with keyA that have valueC
	 */
	public int entryCount(A keyA, C valueC)
	{
		int count = 0;
		HashMap<B,C> mapsA = multimap.get(keyA);
		if(mapsA == null)
			return count;
		Set<B> setA = mapsA.keySet();
		for(B b : setA)
			if(mapsA.get(b).equals(valueC))
				count++;
		return count;
	}
	
	/**
	 * @param keyA: the first level key to search in the Table
	 * @return the HashMap with all entries for keyA
	 */
	public HashMap<B,C> get(A keyA)
	{
		return multimap.get(keyA);
	}
	
	/**
	 * @param keyA: the first level key to search in the Table
	 * @param keyB: the second level key to search in the Table
	 * @return the value for the entry with the two keys or null
	 * if no such entry exists
	 */	
	public C get(A keyA, B keyB)
	{
		HashMap<B,C> mapsA = multimap.get(keyA);
		if(mapsA == null || !mapsA.containsKey(keyB))
			return null;
		return mapsA.get(keyB);
	}
	
	/**
	 * @param keyA: the first level key to search in the Table
	 * @return the maximum value in entries with keyA
	 */
	public B getKeyMaximum(A keyA)
	{
		HashMap<B,C> mapsA = multimap.get(keyA);
		if(mapsA == null)
			return null;
		Vector<B> setA = new Vector<B>(mapsA.keySet());
		B max = setA.get(0);
		C maxVal = mapsA.get(max);
		for(B b : setA)
		{
			C value = mapsA.get(b);
			if(value.compareTo(maxVal) > 0)
			{
				maxVal = value;
				max = b;
			}
		}
		return max;
	}
	
	/**
	 * @param keyA: the first level key to search in the Table
	 * @param valueC: the value to search in the Table
	 * @return the list of second level keys in entries with keyA and valueC
	 */	
	public Vector<B> getMatchingKeys(A keyA, C valueC)
	{
		Vector<B> keysB = new Vector<B>(0,1);
		HashMap<B,C> mapsA = multimap.get(keyA);
		if(mapsA == null)
			return keysB;
		Set<B> setA = mapsA.keySet();
		for(B b : setA)
			if(mapsA.get(b).equals(valueC))
				keysB.add(b);
		return keysB;
	}
	
	/**
	 * @param keyA: the first level key to search in the Table
	 * @return the maximum value in entries with keyA
	 */
	public C getMaximumValue(A keyA)
	{
		HashMap<B,C> mapsA = multimap.get(keyA);
		if(mapsA == null)
			return null;
		Vector<B> setA = new Vector<B>(mapsA.keySet());
		C max = mapsA.get(setA.get(0));
		for(B b : setA)
		{
			C value = mapsA.get(b);
			if(value.compareTo(max) > 0)
				max = value;
		}
		return max;
	}
	
	/**
	 * @return the set of first level keys in the Table
	 */
	public Set<A> keySet()
	{
		return multimap.keySet();
	}
	
	/**
	 * @param keyA: the first level key to search in the Table
	 * @return the set of second level keys in all entries with keyA
	 */
	public Set<B> keySet(A keyA)
	{
		HashMap<B,C> mapsA = multimap.get(keyA);
		if(mapsA == null)
			return null;
		return mapsA.keySet();
	}
	
	/**
	 * @return the number of first level keys in the Table
	 */
	public int keyCount()
	{
		return multimap.size();
	}
	
	
	/**
	 * Removes all entries for the given first level key
	 * @param keyA: the key to remove from the Table
	 */
	public void remove(A keyA)
	{
		if(multimap.get(keyA) != null)
			size -= multimap.get(keyA).size();
		multimap.remove(keyA);
	}
	
	/**
	 * Removes the entry for the given key pair
	 * @param keyA: the first level key to search in the Table
	 * @param keyB: the second level key to remove from the Table
	 */
	public void remove(A keyA, B keyB)
	{
		HashMap<B,C> maps = multimap.get(keyA);
		if(maps != null)
		{
			maps.remove(keyB);
			size--;
		}
	}
	
	/**
	 * @return the total number of entries in the Table
	 */
	public int size()
	{
		return size;
	}
}