package eu.dnetlib.contract.cp.comp;

import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import org.apache.log4j.Logger;
import org.contract4j5.contract.Contract;
import org.contract4j5.contract.Pre;

/**
 * Comparator modules manager.
 * @author mhorst
 *
 */
@Contract
public class ComparatorManager {
	
	protected static final Logger log = Logger.getLogger(ComparatorManager.class);
	
	/**
	 * {@link IComparatorModule} modules registry.
	 */
	Map<Class<? extends Object>, IComparatorModule<? extends Object>> modulesRegistry = 
		new HashMap<Class<? extends Object>, IComparatorModule<? extends Object>>();
	
	
	/**
	 * Check if objects are equal.
	 * If no {@link IComparatorModule} found for given class 
	 * both object will be compared using base {@link Object#equals(Object)} method of obj1.
	 * If source objects are arrays or collections their elements are being compared.
	 * In particular mixed obj1 and obj2 are maintaned (one can be Array second Collection). 
	 * @param obj1
	 * @param obj2
	 * @return true if objects are equal
	 */
	@SuppressWarnings("unchecked")
	public boolean equals(Object obj1, Object obj2) {
		if (obj1==null) {
			if (obj2==null) {
				return true;
			} else {
				return false;
			}
		} else {
			if (obj2==null) {
				return false;
			} else {
				if (obj1==obj2) {
					return true;
				} else {
					if (obj1 instanceof Object[]) {
						if (obj2 instanceof Object[]) {
							return equalArrays((Object[]) obj1, (Object[]) obj2);
						} else if (obj2 instanceof Collection) {
//							array vs collection comparsison is supported
							log.warn("converting collection to array");
							return equalArrays((Object[]) obj1, 
									((Collection)obj2).toArray(new Object[((Collection)obj2).size()]));
						} else {
							return false;
						}
					} else if (obj1 instanceof Collection) {
						if (obj2 instanceof Collection) {
							return equalCollections((Collection<?>) obj1, (Collection<?>) obj2);
						} else if (obj2 instanceof Object[]) {
//							array vs collection comparsison is supported
							log.warn("converting collection to array");
							return equalArrays(((Collection)obj1).toArray(new Object[((Collection)obj1).size()]), 
									(Object[])obj2);
						} else {
							return false;
						}
					} else {
						IComparatorModule module = findComparatorModule(obj1.getClass());
						if (module!=null) {
							if (obj2.getClass().isAssignableFrom(obj1.getClass())) {
							log.info("using IComparatorModule: " + module.getClass().getName() + 
									", for class: " + obj1.getClass().getName());
							return module.equals(obj1, obj2);
							} else {
								log.info("objects are not from the same class hierarchy");
								return false;
							}
						} else {
							log.info("no dedicated comparator found, " +
									"using standard equals() method for class: " + 
									obj1.getClass().getName());
							return obj1.equals(obj2);
						}
					}
				}
			}
		}
	}
	
	/**
	 * Finds proper comparator module for given class if any was registered.
	 * If not comparator was found for given class there is lookup performed 
	 * among all interfaces.
	 * @param clazz
	 * @return comparator module
	 */
	@SuppressWarnings("unchecked")
	protected IComparatorModule findComparatorModule(Class clazz) {
		if (clazz==null) {
			return null;
		}
		IComparatorModule result = modulesRegistry.get(clazz);
		if (result==null) {
			log.debug("no comparator found for class: " + clazz.getName()
					+ ", looking among interfaces...");
			if (clazz.getInterfaces()!=null) {
				for (Class interf : clazz.getInterfaces()) {
					IComparatorModule localResult = findComparatorModule(interf);
					if (localResult!=null) {
						return localResult;
					}
				}
			}
//			as a fallback search among super class hierarchy
			return findComparatorModule(clazz.getSuperclass());
		} else {
			log.debug("found comparator: " + result.getClass().getName() +
					", for class: " + clazz.getName());
			return result;
		}
	}
	
	
	
	/**
	 * Compares arrays.
	 * Entry parameters cannot be null.
	 * @param array1 not null array
	 * @param array2 not null array
	 * @return true if arrays are equal
	 */
	@Pre("array1 != null && array2 != null")
	public boolean equalArrays(Object[] array1, Object[] array2) {
		if (array1.length!=array2.length) {
			return false;
		} else {
			for (int i=0; i<array1.length; i++) {
				if (!equals(array1[i], array2[i])) {
					return false;
				} 
			}
			return true;
		}
	}

	/**
	 * Compares collections.
	 * Entry parameters cannot be null.
	 * @param col1 not null collection
	 * @param col2 not null collection
	 * @return true if collections are equal
	 */
	@Pre("col1 != null && col2 != null")
	public boolean equalCollections(Collection<? extends Object> col1, 
			Collection<? extends Object> col2) {
		if (col1.size()!=col2.size()) {
			return false;
		} else {
			Iterator<? extends Object> it1 = col1.iterator();
			Iterator<? extends Object> it2 = col2.iterator();
			int compCount = 0;
			while(it1.hasNext() && it2.hasNext()) {
				if (!equals(it1.next(), it2.next())) {
					return false;
				}
				compCount++;
			}
			if (compCount==col1.size()) {
				return true;
			} else {
				return false;
			}
		}
	}
	
	/**
	 * Registers {@link IComparatorModule} module(s) for maintained Class.
	 * @param module one or more modules to be registered
	 */
	public void registerModule(IComparatorModule<? extends Object>... modules) {
		if (modules!=null) {
			for (IComparatorModule<? extends Object> module : modules) {
				if (modulesRegistry.containsKey(module.getSupportedClass())) {
					log.warn("comparator module: " + 
							modulesRegistry.get(module.getSupportedClass()).getClass().getName() +
							" already registered for class: " + module.getSupportedClass().getName() +
							" , overwriting with new instance: " + module.getClass().getName());
				} 
				modulesRegistry.put(module.getSupportedClass(), module);
			}
		} else {
			log.warn("no modules provided to be registered!");
		}
	}

	/**
	 * Returns {@link IComparatorModule} modules registry.
	 * @return IComparatorModule modules registry
	 */
	public Map<Class<? extends Object>, IComparatorModule<? extends Object>> getModulesRegistry() {
		return modulesRegistry;
	}

	/**
	 * Sets {@link IComparatorModule} modules registry.
	 * @param modulesRegistry
	 */
	public void setModulesRegistry(Map<Class<? extends Object>, IComparatorModule<? extends Object>> modulesRegistry) {
		this.modulesRegistry = modulesRegistry;
	}

}
