/*
 * HashMapMap.java - This file is part of the Jakstab project.
 * Copyright 2007-2015 Johannes Kinder <jk@jakstab.org>
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, see <http://www.gnu.org/licenses/>.
 */
package org.jakstab.util;

import java.util.*;

import org.jakstab.util.Logger;

public class HashMapMap<K, L, V> implements MapMap<K, L, V> {

	@SuppressWarnings("unused")
	private static final Logger logger = Logger.getLogger(HashMapMap.class);
	
	public static <K, L, V> HashMapMap<K, L, V> create() {
		return new HashMapMap<K, L, V>();
	}
	
	private Map<K, Map<L,V>> map;
	
	protected Map<L, V> createSubMap() {
		return new TreeMap<L, V>();
	}
	
	protected Map<L, V> createSubMap(Map<L, V> proto) {
		return new TreeMap<L, V>(proto);
	}
	
	public HashMapMap() {
		map = new HashMap<K, Map<L, V>>();
	}
	
	public HashMapMap(MapMap<K, L, V> proto) {
		this();
		for (K key : proto.leftKeySet()) {
			map.put(key, createSubMap(proto.getSubMap(key)));
		}
	}

	public boolean containsKey(K keyLeft, L keyRight) {
		if (map.containsKey(keyLeft)) 
			return map.get(keyLeft).containsKey(keyRight);
		else return false;
	}

	public boolean containsLeftKey(K keyLeft) {
		return map.containsKey(keyLeft); 
	}

	@Override
	public V get(K keyLeft, L keyRight) {
		Map<L, V> subMap = map.get(keyLeft);
		if (subMap != null) return subMap.get(keyRight);
		else return null;
	}

	@Override
	public V put(K keyLeft, L keyRight, V value) {
		Map<L, V> subMap = map.get(keyLeft);
		if (subMap == null) {
			subMap = createSubMap();
			map.put(keyLeft, subMap);
		}
		V oldValue = subMap.put(keyRight, value);
		return oldValue;
	}

	@Override
	public void putAll(MapMap<K, L, V> other) {
		for (EntryIterator<K, L, V> it = entryIterator(); it.hasEntry(); it.next()) {
			put(it.getLeftKey(), it.getRightKey(), it.getValue());
		}
	}

	@Override
	public V remove(K keyLeft, L keyRight) {
		Map<L, V> subMap = map.get(keyLeft);
		if (subMap != null) {
			V oldValue = subMap.remove(keyRight);
			if (subMap.isEmpty()) map.remove(keyLeft);
			return oldValue;
		}
		else return null;
	}
	
	@Override
	public void remove(K keyLeft) {
		map.remove(keyLeft);
	}

	@Override
	public Map<L, V> getSubMap(K keyLeft) {
		return map.get(keyLeft);
	}

	@Override
	public void clear() {
		map.clear();
	}
	
	@Override
	public Set<K> leftKeySet() {
		return map.keySet();
	}
	
	@Override
	public EntryIterator<K, L, V> entryIterator() {
		return new DoubleIterator();
	}
	
	private final class DoubleIterator implements EntryIterator<K, L, V> {
		private Iterator<K> it1;
		private K currentLeftKey;
		private Map.Entry<L, V> currentSubMapEntry; 
		private Iterator<Map.Entry<L, V>> it2;
		
		public DoubleIterator() {
			if (map.isEmpty()) {
				it1 = null;
				it2 = null;
				currentLeftKey = null;
				currentSubMapEntry = null;
			} else {
				it1 = map.keySet().iterator();
				currentLeftKey = it1.next();
				it2 = map.get(currentLeftKey).entrySet().iterator();
				next();
			}
		}
		
		@Override
		public K getLeftKey() {
			return currentLeftKey;
		}

		@Override
		public L getRightKey() {
			return currentSubMapEntry.getKey();
		}

		@Override
		public V getValue() {
			return currentSubMapEntry.getValue();
		}

		@Override
		public boolean hasEntry() {
			return (currentLeftKey != null);
		}

		@Override
		public void next() {
			
			if (!hasEntry()) throw new NoSuchElementException();

			// If it2 has no next element, iterate it1 as long
			// as we find a map that has elements
			while (!it2.hasNext()) {
				if (!it1.hasNext()) {
					currentSubMapEntry = null;
					currentLeftKey = null;
					return;
				}
				currentLeftKey = it1.next();
				it2 = map.get(currentLeftKey).entrySet().iterator();
			}
			currentSubMapEntry = it2.next();
		}
		
	}

	@Override
	public int size() {
		int res = 0;
		for (Map.Entry<K, Map<L, V>> subEntry : map.entrySet()) {
			res += subEntry.getValue().size();
		}
		return res;
	}

	@Override
	public boolean isEmpty() {
		return size() == 0;
	}

	@Override
	public int hashCode() {
		return map.hashCode();
	}

	@SuppressWarnings("unchecked")
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		HashMapMap<K, L, V> other = (HashMapMap<K, L, V>) obj;
		if (map == null) {
			return other.map == null;
		} else {
			// MapMaps are equal even if one has empty submaps which
			// the other does not have, so we cannot simply use equals()
			if (size() != other.size()) return false;
			// Check equality of submaps only one way - if this one lacks some
			// maps of the other, sizes will be different
			for (K leftKey : leftKeySet()) {
				Map<L,V> otherSubMap = other.getSubMap(leftKey);
				if (otherSubMap == null) {
					if (!getSubMap(leftKey).isEmpty())
						return false;
					// else getSubMap is empty and otherSubMap == null, so check next submap
				} else {
					if (!otherSubMap.equals(getSubMap(leftKey)))
						return false;
				}
			}
			return true;
		}
			
	}

}
