// Copyright (c) 1996-2002 Brian D. Carlstrom

package bdc.util;

import java.util.HashMap;
import java.util.List;
import java.util.Iterator;
import java.util.Map;

public final class MapUtil
{
    /**
        Returns whether all List elements are strings
    */
    private static boolean allElementsAreStrings (List l)
    {
        for (int i = l.size()-1; i >= 0; i--) {
            if (!(l.get(i) instanceof String)) {
                return false;
            }
        }
        return true;
    }
    
    /**
        Determine if a Map is null or empty.
        
        @param map a Map object to check
        
        @return <b>true</b> if <B>map</B> is null or empty,
        <b>false</b> otherwise
    */
    public static boolean nullOrEmptyMap (Map map)
    {
        return (map == null) || map.isEmpty();
    }
    
    /**
        Merges two Maps together. The source Map has precedence.
        
        This is too complicated to try to explain correctly -- dmitri
    */
    public static Map mergeMapIntoMap (
        Map dest, Map source)
    {
        Iterator i = source.keySet().iterator();
        
        while (i.hasNext()) {
            Object key = i.next();
            Object sourceValue = source.get(key);
            Object destValue = dest.get(key);
            if (destValue == null) {
                dest.put(key, sourceValue);
                continue;
            }
            else if ((destValue instanceof Map) &&
                     (sourceValue instanceof Map))
            {
                dest.put(key,
                         mergeMapIntoMap(
                             copyMap((Map)destValue),
                             (Map)sourceValue));
            }
            else if ((destValue instanceof Map) &&
                     (sourceValue instanceof List))
            {
                if (allElementsAreStrings((List)sourceValue)) {
                    dest.put(key,
                             mergeMapIntoMap(
                                 copyMap((Map)destValue),
                                 convertListToMap((List)sourceValue)));
                }
                else {
                    ListUtil.addIfAbsent((List)sourceValue, destValue);
                    dest.put(key, sourceValue);
                }
            }
            else if ((destValue instanceof List) &&
                     (sourceValue instanceof Map))
            {
                if (allElementsAreStrings((List)destValue)) {
                    dest.put(key,
                             mergeMapIntoMap(
                                 convertListToMap((List)destValue),
                                 (Map)sourceValue));
                }
                else {
                    ListUtil.addIfAbsent((List)destValue, sourceValue);
                }
            }
            else if ((destValue instanceof Map) &&
                     (sourceValue instanceof String))
            {
                Map destValueMap =
                    copyMap((Map)destValue);
                if (destValueMap.get(sourceValue) == null) {
                    ((Map)destValue).put(sourceValue, new HashMap());
                }
            }
            else if ((destValue instanceof String) &&
                     (sourceValue instanceof Map))
            {
                if (((Map)sourceValue).get((String)destValue) == null) {
                    ((Map)sourceValue).put(destValue, new HashMap());
                }
                dest.put(key, sourceValue);
            }
            else if ((destValue instanceof List) &&
                     (sourceValue instanceof List))
            {
                ListUtil.addIfAbsent((List)destValue, ((List)sourceValue));
            }
            else if ((destValue instanceof List) &&
                     (sourceValue instanceof String))
            {
                ListUtil.addIfAbsent((List)destValue, sourceValue);
            }
            else if ((destValue instanceof String) &&
                     (sourceValue instanceof List))
            {
                ListUtil.addIfAbsent((List)sourceValue, destValue);
                dest.put(key, sourceValue);
            }
            else if ((destValue instanceof String) &&
                     (sourceValue instanceof String))
            {
                dest.put(key, sourceValue);
            }
        }
        return dest;
    }
    
    /**
        Create a Map of Maps from a List. Creates a new Map where each
        key is an element from the List <B>keys</B>. Each element of
        the new Map is itself an empty Map.

        @param keys a List of items to become keys in created
        Map

        @return a new Map containing each element of keys.
    */
    public static Map convertListToMap (List keys)
    {
        Map map = new HashMap(keys.size());
        for (int i=0, s=keys.size(); i<s; i++) {
            map.put(keys.get(i), new HashMap());
        }
        return map;
    }

    /**
        Overwrites the contents of a given Map with the contents
        of another Map.
        
        @param overriding  the overriding Map
        @param overridden  the Map to be overridden
        
        @return a new Map that results from the overwriting
        process.
    */
    public static Map overwriteMap (Map overriding,
                                                Map overridden)
    {
            // first make a copy of the one to be overridden
        Map newTable = copyMap(overridden);
        overwriteMapInPlace(overriding, newTable);
        return newTable;
    }
    
    /**
        A version of overwriteMap that sideeffects the overriden
        Map directly, instead of making a copy to return.
    */
    public static void overwriteMapInPlace (Map overriding,
                                                  Map overridden)
    {
        Iterator i = overriding.keySet().iterator();
        while (i.hasNext()) {
            Object key = i.next();
            Object overridingValue = overriding.get(key);
            Object overriddenValue = overridden.get(key);
            if (overridingValue instanceof Map &&
                overriddenValue instanceof Map) {
                    // recurse
                overwriteMapInPlace((Map)overridingValue,
                                          (Map)overriddenValue);
            }
            else {
                    // overwrite the one in overridden
                overridden.put(key, overridingValue);
            }
        }
    }
    
    /**
        Applies a single deletion or list of  deletions to a Map.  This
        is used to implement the deleteProperty AML element.  deletes can be
        either a single string or a List of Objects.
        
        Returns false if it had any undeletable deletes.
    */
    public static boolean performDeletesOnMap (Object deletes, Map table)
    {
        if (deletes instanceof String) {
            return table.remove(deletes) != null;
        }
        else {
            Assert.that(deletes instanceof List,
                        "Strange type of object to try to delete: %s",
                        deletes);
            List deletesList = (List)deletes;
            boolean returnValue = true;
            for (int i = deletesList.size()-1; i >= 0; i--) {
                if (table.remove(deletesList.get(i)) == null) {
                    returnValue = false;
                }
            }
            return returnValue;
        }
    }
    
    /**
        Copy a Map.  This method will copy the source Map and output
       a destination Map.  
       For each List or Map within the source Map they will also be
       copied,all other types will be shared.  So for a Map of Lists or
       Maps, this will recurse.
       
       @param source the source Map to copy.
       @return the destination Map.
    */
    public static Map copyMap (Map source)
    {
        Map destination = new HashMap(source.size());
        Iterator i = source.keySet().iterator();
        while (i.hasNext()) {
            Object key = i.next();
            Object value = source.get(key);
            destination.put(key, ListUtil.copyValue(value));
        }
        return destination;
    }

    /**
        Determine if the two Maps are equal.
        
        @param m1 the first Map
        @param m2 the second Map
        
        @return <b>true</b> if the two Maps are equal, <b>false</b>
        otherwise
    */
    public static boolean mapEquals (Map m1, Map m2)
    {
        if (m1 == m2) {
            return true;
        }
        if (m1 == null || m2 == null || m1.size() != m2.size()) {
            return false;
        }
        Iterator i = m1.keySet().iterator();
        while (i.hasNext()) {
            Object key = i.next();
            Object val2 = m2.get(key);
            if (val2 == null) {
                return false;
            }
            else {
                Object val1 = m1.get(key);
                if ((val1 instanceof Map) &&
                    (val2 instanceof Map)) {
                    if (!mapEquals(
                        (Map)val1, (Map)val2)) {
                        return false;
                    }
                }
                else if ((val1 instanceof List) &&
                         (val2 instanceof List)) {
                    if (!ListUtil.listEquals((List)val1, (List)val2)) {
                        return false;
                    }
                }
                else {
                    if (!val1.equals(val2)) {
                        return false;
                    }
                }
            }
        }
        return true;
    }
    
    /**
        prevent people from creating this class
    */
    private MapUtil ()
    {
    }
}
