package basics.collections;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;
import sun.reflect.generics.reflectiveObjects.NotImplementedException;
import basics.testing.Units;
import basics.utl.EmptyIterator;

/**
 * A map of maps where the outer map's key is a string,
 * the inner map's key is a string, and the inner map's
 * value is a generic type <T>.
 * @author kst
 */
public class MapMap<T>
{
private Map<String, Map<String, T>> outer = new HashMap<String, Map<String, T>>();

/**
 * Store val:T with key pair k1, k2.
 * This will create a new map with key k1 if k1
 * is not in the outer map.
 * If there is a k2 key in the map of key k1,
 * then the value associated with k2 will be overwritten.
 * @param k1
 * @param k2
 * @param val
 */
public void put(String k1, String k2, T val)
{
   Map<String, T> i = contains(k1) ? get(k1) : new HashMap<String, T>();
   i.put(k2, val);
   outer.put(k1, i);
}

/**
 * Same a put(k1,k2,val) except that k1 must exist.
 * If it does not exist an exception is thrown and
 * catched that does create the map for k1 and store
 * k2 with value. This function is slightly faster
 * when then k1 key does exist.
 * @param k1 outer key must exist
 * @param k2 inner key 
 * @param val
 */
private void putDirect(String k1, String k2, T val)
{
   try
   {
      Map<String, T> i = (Map<String, T>)outer.get(k1);
      i.put(k2, val);
      outer.put(k1, i);
   }
   catch(NullPointerException e)
   {
      Map<String, T> i = new HashMap<String, T>();
      i.put(k2, val);
      outer.put(k1, i);
   }
}

/**
 * Store value val:T with k1,k2 or with k2,k1 depending if k1 or k2
 * exist in the outer map. Using this putter avoids having duplicate
 * k1,k2 (resp. k2,k1) pairs in the complete set of stored key.
 * k1/k2 and k2/k1 are unique and have the same meaning val:T.
 * @param k1
 * @param k2
 * @param val
 */
public void putMutualExclusive(String k1, String k2, T val)
{
   if(contains(k1, k2))
      putDirect(k1, k2, val);
   else
      if(contains(k2, k1))
         putDirect(k2, k1, val);
      else
      {
         Map<String, T> i = null;
         if(contains(k1))
         {
            i = get(k1);
            i.put(k2, val);
            outer.put(k1, i);
         }
         else
            if(contains(k2))
            {
               i = get(k2);
               i.put(k1, val);
               outer.put(k2, i);
            }
            else
            {
               i = new HashMap<String, T>();
               i.put(k2, val);
               outer.put(k1, i);
            }
      }
}

public int size()
{
   return outer.size();
}

public int size(String outerKey)
{
   return outer.containsKey(outerKey) ? outer.get(outerKey).size() : 0;
}

public void clear()
{
   outer.clear();
}

public Iterator<String> iterateOuterKeys()
{
   return outer.keySet().iterator();
}

public Iterator<String> iterateInnerKeys(final String outerKey)
{
   return new Iterator<String>()
   {
      Iterator<String> innerIterator = null;

      public boolean hasNext()
      {
         if(innerIterator == null)
         {
            if(!outer.containsKey(outerKey))
            {
               innerIterator = new EmptyIterator<String>();
               return false;
            }
            else
               innerIterator = get(outerKey).keySet().iterator();
         }
         return innerIterator.hasNext();
      }

      public String next()
      {
         return innerIterator.next();
      }

      public void remove()
      {
         throw new NotImplementedException();
      }
   };
}

public Map<String, T> get(String key)
{
   return (Map<String, T>)outer.get(key);
}

/**
 * return the subset map {@link key} in the outer map.
 * @param key
 * @return a set of entries, never null.
 */
public Set<Entry<String, T>> getSubSet(String key)
{
   Map<String, T> map = outer.get(key);
   if(map == null)
      return new HashSet<Entry<String, T>>();
   else
      return map.entrySet();
}

public boolean contains(String key)
{
   return outer.containsKey(key);
}

public void remove(String key)
{
   if(outer.containsKey(key))
      outer.remove(key);
}

public boolean contains(String k1, String k2)
{
   return outer.containsKey(k1) && ((Map<String, T>)outer.get(k1)).containsKey(k2);
}

public void remove(String k1, String k2)
{
   if(outer.containsKey(k1))
   {
      Map<String, T> inner = outer.get(k1);
      if(inner.containsKey(k2))
      {
         inner.remove(k2);
         outer.put(k1, inner);
      }
   }
}

/**
 * Get the value for k1/k2 or return null;
 * @param k1
 * @param k2
 * @return
 */
public T get(String k1, String k2)
{
   return outer.containsKey(k1) ? get(k1).get(k2) : null;
}

/**
 * Get the value for k1/k2 or k2/k1. If none of these exists, 
 * return null; 
 * @param k1
 * @param k2
 * @return
 */
public T getMutual(String k1, String k2)
{
   if(outer.containsKey(k1))
   {
      Map<String, T> m = outer.get(k1);
      if(m.containsKey(k2))
         return m.get(k2);
   }
   if(outer.containsKey(k2))
   {
      Map<String, T> m = outer.get(k2);
      if(m.containsKey(k1))
         return m.get(k1);
   }
   return null;
}

public String toString()
{
   StringBuilder s = new StringBuilder();
   s.append("[");
   for(Iterator<String> oi = iterateOuterKeys(); oi.hasNext();)
   {
      final String ok = oi.next();
      for(Iterator<String> ii = iterateInnerKeys(ok); ii.hasNext();)
      {
         final String ik = ii.next();
         final Object v = get(ok, ik);
         s.append(ok);
         s.append(":");
         s.append(ik);
         s.append("=" + v);
         if(ii.hasNext() || oi.hasNext())
            s.append("; ");
      }
   }
   s.append("]");
   return s.toString();
}

public Map<String, Map<String, T>> getOuter()
{
   return outer;
}

public void setOuter(Map<String, Map<String, T>> outer)
{
   this.outer = outer;
}

public static void unittest()
{
   MapMap<Integer> mm = new MapMap<Integer>();
   mm.put("a", "b", 1);
   mm.put("a", "b", 2);
   Units.assertTrue(mm.size() == 1, "Size should be 1");
   Units.assertTrue(mm.get("a", "b") == 2, "a,b should be overwritten with 2");
   mm.put("x", "y", 3);
   mm.put("x", "z", 31);
   Units.assertTrue(mm.get("x", "y") == 3, "x,y should be 3");
   mm.putDirect("c", "b", 22);
   Units.assertTrue(mm.size() == 3, "Size should be 2");
   Units.assertTrue(mm.get("c", "b") == 22, "c,b should be 22");
   mm.remove("a");
   Units.assertFalse(mm.contains("a"), "a should be deleted");
   mm.clear();
   Units.assertTrue(mm.size() == 0, "Size should be 0");
   mm.putMutualExclusive("x", "y", 3);
   Units.assertTrue(mm.get("x", "y") == 3, "x,y should be 3");
   mm.putMutualExclusive("y", "x", 2);
   mm.putMutualExclusive("x", "z", 4);
   mm.putMutualExclusive("y", "z", 4);
   Units.assertTrue(mm.get("x", "y") == 2, "x,y should be 2");
   Units.assertTrue(mm.getMutual("y", "x") == 2, "mutual x,y should be 2");
   Iterator<String> I = mm.iterateOuterKeys();
   while(I.hasNext())
   {
      String ko = I.next();
      Iterator<String> II = mm.iterateInnerKeys(ko);
      while(II.hasNext())
      {
         String ki = II.next();
         @SuppressWarnings("unused")
         final int x = mm.get(ko, ki);
      }
   }
   //   UnitTest.printout(mm.toString());
}
}
