/*******************************************************************************
 * Copyright (c) 2007, 2011 Intel Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 * Intel Corporation - Initial API and implementation
 * Christian Walther (Indel AG) - [335344] changing language IDs
 *******************************************************************************/
package org.eclipse.cdt.internal.core.settings.model;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.eclipse.cdt.core.settings.model.CExternalSetting;
import org.eclipse.cdt.core.settings.model.ICExternalSetting;
import org.eclipse.cdt.core.settings.model.ICSettingEntry;
import org.eclipse.cdt.core.settings.model.util.CDataUtil;
import org.eclipse.cdt.core.settings.model.util.EntryContentsKey;
import org.eclipse.cdt.core.settings.model.util.EntryNameKey;
import org.eclipse.cdt.core.settings.model.util.KindBasedStore;

class CExternalSettinsDeltaCalculator {
	static private CExternalSettinsDeltaCalculator fInstance;

	private CExternalSettinsDeltaCalculator(){
	}

	public static CExternalSettinsDeltaCalculator getInstance(){
		if(fInstance == null)
			fInstance = new CExternalSettinsDeltaCalculator();
		return fInstance;
	}


	static class ExtSettingsDelta {
		CExternalSetting fSetting;
		boolean fAdded;
		KindBasedStore<ICSettingEntry[][]> fEntryChangeStore;

		ExtSettingsDelta(CExternalSetting setting){
			fSetting = setting;
			fEntryChangeStore = new KindBasedStore<ICSettingEntry[][]>(false);
		}

		ExtSettingsDelta(CExternalSetting setting, boolean added){
			fSetting = setting;
			fAdded = added;
		}

		boolean isChange(){
			return fEntryChangeStore != null;
		}

		boolean isAdded(){
			return fAdded;
		}

		CExternalSetting getSetting(){
			return fSetting;
		}

		ICSettingEntry[][] getEntriesDelta(int kind){
			if(fEntryChangeStore != null)
				return fEntryChangeStore.get(kind);
			ICSettingEntry [] entries = fSetting.getEntries(kind);
			if(entries == null || entries.length == 0)
				return null;

			ICSettingEntry[][] delta = new ICSettingEntry[2][];
			if(fAdded)
				delta[0] = entries;
			else
				delta[1] = entries;

			return delta;
		}

		ICSettingEntry[][] getEntriesDelta(){
			int kinds[] = KindBasedStore.getLanguageEntryKinds();
			List<ICSettingEntry> added = new ArrayList<ICSettingEntry>();
			List<ICSettingEntry> removed = new ArrayList<ICSettingEntry>();
			for (int kind : kinds) {
				ICSettingEntry[][] d = getEntriesDelta(kind);
				if(d == null)
					continue;

				if(d[0] != null){
					added.addAll(Arrays.asList(d[0]));
				}
				if(d[1] != null){
					removed.addAll(Arrays.asList(d[1]));
				}
			}

			ICSettingEntry[][] delta = new ICSettingEntry[2][];

			if(added.size() != 0){
				delta[0] = added.toArray(new ICSettingEntry[added.size()]);
			}
			if(removed.size() != 0){
				delta[1] = removed.toArray(new ICSettingEntry[removed.size()]);
			}

			return delta;
		}
	}

	 static class ExtSettingMapKey {
		private ICExternalSetting fSetting;
		public ExtSettingMapKey(ICExternalSetting setting){
			fSetting = setting;
		}

		@Override
		public boolean equals(Object obj) {
			if(obj == this)
				return true;

			if(!(obj instanceof ExtSettingMapKey))
				return false;

			ExtSettingMapKey other = (ExtSettingMapKey)obj;
			return settingsMatch(fSetting, other.fSetting);
		}
		@Override
		public int hashCode() {
			return code(fSetting.getCompatibleLanguageIds())
				+ code(fSetting.getCompatibleContentTypeIds())
				+ code(fSetting.getCompatibleExtensions());
		}

		private int code(String[] arr){
			if(arr == null || arr.length == 0)
				return 0;

			int code = 0;

			for (String str : arr) {
				code += str.hashCode();
			}
			return code;
		}

		public ICExternalSetting getSetting(){
			return fSetting;
		}

	}

	private static ExtSettingsDelta createDelta(CExternalSetting setting1, CExternalSetting setting2){

		int kinds[] = KindBasedStore.getAllEntryKinds();
		ExtSettingsDelta extDelta = null;
		for (int kind : kinds) {
			ICSettingEntry entries1[] = setting1.getEntries(kind);
			ICSettingEntry entries2[] = setting2.getEntries(kind);
			Map<EntryContentsKey, ICSettingEntry> map1 = CDataUtil.fillEntriesMapByContentsKey(new LinkedHashMap<EntryContentsKey, ICSettingEntry>(), entries1);
			Map<EntryContentsKey, ICSettingEntry> map2 = CDataUtil.fillEntriesMapByContentsKey(new LinkedHashMap<EntryContentsKey, ICSettingEntry>(), entries2);
			Map<EntryContentsKey, ICSettingEntry> map1Copy = new LinkedHashMap<EntryContentsKey, ICSettingEntry>(map1);
//			Set set1 = new HashSet(Arrays.asList(entries1));
//			Set set2 = new HashSet(Arrays.asList(entries2));
//			Set set1Copy = new HashSet(set1);
			map1.keySet().removeAll(map2.keySet());
			map2.keySet().removeAll(map1Copy.keySet());

			ICSettingEntry entriesAdded[] = null, entriesRemoved[] = null;

			if(map1.size() != 0)
				entriesAdded = map1.values().toArray(new ICSettingEntry[map1.size()]);

			if(map2.size() != 0)
				entriesRemoved = map2.values().toArray(new ICSettingEntry[map2.size()]);

			if(entriesAdded == null && entriesRemoved == null)
				continue;

			if(extDelta == null){
				extDelta = new ExtSettingsDelta(setting1);
			}
			extDelta.fEntryChangeStore.put(kind, new ICSettingEntry[][]{entriesAdded, entriesRemoved});
		}

		return extDelta;
	}

	static boolean settingsMatch(ICExternalSetting setting1, ICExternalSetting setting2) {
		if(setting1.equals(setting2))
			return true;

		return settingsMatch(setting1,
				setting2.getCompatibleLanguageIds(),
				setting2.getCompatibleContentTypeIds(),
				setting2.getCompatibleExtensions());
	}

	static boolean settingsMatch(ICExternalSetting setting,
			String languageIDs[], String contentTypeIDs[], String extensions[]){
		if(!Arrays.equals(setting.getCompatibleLanguageIds(), languageIDs))
			return false;
		if(!Arrays.equals(setting.getCompatibleContentTypeIds(), contentTypeIDs))
			return false;
		if(!Arrays.equals(setting.getCompatibleExtensions(), extensions))
			return false;

		return true;
	}

	private static Map<ExtSettingMapKey, ICExternalSetting> toSettingsKeyMap(ICExternalSetting[] settings){
		Map<ExtSettingMapKey, ICExternalSetting> map = new HashMap<ExtSettingMapKey, ICExternalSetting>();
		for (ICExternalSetting setting : settings) {
			if(map.put(new ExtSettingMapKey(setting), setting) != null)
				throw new IllegalArgumentException();
		}
		return map;
	}

	ExtSettingsDelta[] getSettingChange(CExternalSetting newSettings[],
			CExternalSetting oldSettings[]){


		if(newSettings == null || newSettings.length == 0)
			return createDeltas(oldSettings, false);
		if(oldSettings == null || oldSettings.length == 0)
			return createDeltas(newSettings, true);

		LinkedList<ExtSettingsDelta> deltaList = new LinkedList<ExtSettingsDelta>();

		Map<ExtSettingMapKey, ICExternalSetting> newMap= toSettingsKeyMap(newSettings);
		Map<ExtSettingMapKey, ICExternalSetting> oldMap = toSettingsKeyMap(oldSettings);
		for (Entry<ExtSettingMapKey, ICExternalSetting> entry : newMap.entrySet()) {
			CExternalSetting newSetting = (CExternalSetting)entry.getValue();
			CExternalSetting oldSetting = (CExternalSetting)oldMap.remove(entry.getKey());
			if(oldSetting == null){
				deltaList.addLast(new ExtSettingsDelta(newSetting, true));
			} else {
				ExtSettingsDelta delta = createDelta(newSetting, oldSetting);
				if(delta != null)
					deltaList.addLast(delta);
			}
		}

		for (ICExternalSetting oldSettng : oldMap.values()) {
			// removals must be prepended to the list so that they are applied before additions,
			// otherwise a setting that was just added might be immediately removed again in
			// CExternalSettingsDeltaProcessor.applyDelta(ICLanguageSetting, ExtSettingsDelta[], int)
			// if the old and new setting only differ in their language sets and these overlap
			deltaList.addFirst(new ExtSettingsDelta((CExternalSetting)oldSettng, false));
		}

		if(deltaList.size() == 0)
			return null;
		return deltaList.toArray(new ExtSettingsDelta[deltaList.size()]);
	}

	private static ExtSettingsDelta[] createDeltas(CExternalSetting settings[], boolean added){
		if(settings == null || settings.length == 0)
			return null;

		ExtSettingsDelta deltas[] = new ExtSettingsDelta[settings.length];
		for(int i = 0; i < settings.length; i++){
			deltas[i] = new ExtSettingsDelta(settings[i], added);
		}

		return deltas;
	}

	Set<ICSettingEntry> calculateUpdatedEntries(ICSettingEntry current[], ICSettingEntry added[], ICSettingEntry removed[]){
	//	EntryComparator comparator = new EntryComparator();
		LinkedHashSet<ICSettingEntry> set = new LinkedHashSet<ICSettingEntry>();
		set.addAll(Arrays.asList(current));
		set.addAll(Arrays.asList(added));
		set.removeAll(Arrays.asList(removed));

		return set;
	}

	static ICSettingEntry[][] getAllEntries(ExtSettingsDelta[] deltas, int kind){
		if(deltas == null || deltas.length == 0)
			return null;

		Map<EntryNameKey, ICSettingEntry> addedMap = new LinkedHashMap<EntryNameKey, ICSettingEntry>();
		Map<EntryNameKey, ICSettingEntry> removedMap = new LinkedHashMap<EntryNameKey, ICSettingEntry>();
		for (ExtSettingsDelta delta : deltas) {
			ICSettingEntry[][] change = delta.getEntriesDelta(kind);
			if(change == null)
				continue;

			if(change[0] != null){
				CDataUtil.fillEntriesMapByNameKey(addedMap, change[0]);
			}
			if(change[1] != null){
				CDataUtil.fillEntriesMapByNameKey(removedMap, change[1]);
			}
			removedMap.keySet().removeAll(addedMap.keySet());
		}

		if(addedMap.size() == 0 && removedMap.size() == 0)
			return null;

		ICSettingEntry[][] result = new ICSettingEntry[2][];
		if(addedMap.size() != 0){
			result[0] = addedMap.values().toArray(new ICSettingEntry[addedMap.size()]);
		}
		if(removedMap.size() != 0){
			result[1] = removedMap.values().toArray(new ICSettingEntry[removedMap.size()]);
		}

		return result;
	}

}
