/*******************************************************************************
 * Copyright (c) 2011, 2014 Andrew Gvozdev and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     Andrew Gvozdev - initial API and implementation
 *******************************************************************************/
package org.eclipse.cdt.core.language.settings.providers;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.eclipse.cdt.core.settings.model.ICLanguageSettingEntry;
import org.eclipse.cdt.core.settings.model.ICSettingEntry;
import org.eclipse.cdt.internal.core.WeakHashSet;
import org.eclipse.cdt.internal.core.WeakHashSetSynchronized;

/**
 * The class representing the (in-memory) storage for language settings entries {@link ICLanguageSettingEntry}.
 *
 * @since 5.4
 */
public class LanguageSettingsStorage implements Cloneable {
	/** Storage to keep settings entries. */
	protected Map<String, // languageId
			Map<String, // resource project path
					List<ICLanguageSettingEntry>>> fStorage = new HashMap<>();

	/**
	 * Pool of LSE lists implemented as WeakHashSet. That allows to gain memory savings
	 * at the expense of CPU time. WeakHashSet handles garbage collection when a list is not
	 * referenced anywhere else. See JavaDoc {@link java.lang.ref.WeakReference} about weak reference objects.
	 */
	private static WeakHashSet<List<ICLanguageSettingEntry>> listPool = new WeakHashSetSynchronized<>();

	/**
	 * Returns the list of setting entries for the given resource and language.
	 * <br> Note that this list is <b>unmodifiable</b>.
	 *
	 * @param rcProjectPath - path to the resource relative to the project.
	 * @param languageId - language id.
	 *
	 * @return the list of setting entries or {@code null} if no settings defined.
	 */
	public List<ICLanguageSettingEntry> getSettingEntries(String rcProjectPath, String languageId) {
		List<ICLanguageSettingEntry> entries = null;
		Map<String, List<ICLanguageSettingEntry>> langMap = fStorage.get(languageId);
		if (langMap != null) {
			entries = langMap.get(rcProjectPath);
		}
		return entries;
	}

	/**
	 * Some providers may collect entries in pretty much random order. For the intent of
	 * predictability, UI usability and efficient storage the entries are sorted by kinds
	 * and secondary by name for kinds where the secondary order is not significant.
	 *
	 * @param entries - list of entries to sort.
	 * @return - sorted entries.
	 */
	private List<ICLanguageSettingEntry> sortEntries(List<? extends ICLanguageSettingEntry> entries) {
		List<ICLanguageSettingEntry> sortedEntries = new ArrayList<>(entries);
		Collections.sort(sortedEntries, new Comparator<ICLanguageSettingEntry>() {
			/**
			 * This comparator sorts by kinds first and the macros are sorted additionally by name.
			 */
			@Override
			public int compare(ICLanguageSettingEntry entry0, ICLanguageSettingEntry entry1) {
				int kind0 = entry0.getKind();
				int kind1 = entry1.getKind();
				if (kind0 == ICSettingEntry.MACRO && kind1 == ICSettingEntry.MACRO) {
					return entry0.getName().compareTo(entry1.getName());
				}

				return kind0 - kind1;
			}
		});

		return sortedEntries;
	}

	/**
	 * Sets language settings entries for the resource and language.
	 *
	 * @param rcProjectPath - path to the resource relative to the project. If {@code null} the entries are
	 *    considered to be being defined as default entries for resources.
	 * @param languageId - language id. If {@code null}, then entries are considered
	 *    to be defined for the language scope.
	 * @param entries - language settings entries to set.
	 */
	public void setSettingEntries(String rcProjectPath, String languageId,
			List<? extends ICLanguageSettingEntry> entries) {
		synchronized (fStorage) {
			if (entries != null) {
				Map<String, List<ICLanguageSettingEntry>> langMap = fStorage.get(languageId);
				if (langMap == null) {
					langMap = new HashMap<>();
					fStorage.put(languageId, langMap);
				}
				List<ICLanguageSettingEntry> sortedEntries = getPooledList(sortEntries(entries), false);
				langMap.put(rcProjectPath, sortedEntries);
			} else {
				// reduct the empty maps in the tables
				Map<String, List<ICLanguageSettingEntry>> langMap = fStorage.get(languageId);
				if (langMap != null) {
					langMap.remove(rcProjectPath);
					if (langMap.isEmpty()) {
						fStorage.remove(languageId);
					}
				}
			}
		}
	}

	/**
	 * @return {@code true} if the storage is empty or {@code false} otherwise.
	 */
	public boolean isEmpty() {
		return fStorage.isEmpty();
	}

	/**
	 * Clear all the entries for all resources and all languages.
	 */
	public void clear() {
		synchronized (fStorage) {
			fStorage.clear();
		}
	}

	/**
	 * @return set of all languages associated with the entries.
	 * Note that the storage can keep default entries for the language scope
	 * of the provider, so the set can contain {@code null}.
	 */
	public Set<String> getLanguages() {
		return new HashSet<>(fStorage.keySet());
	}

	/**
	 * Returns set of paths for all resources associated with entries for given language.
	 * The paths are project relative.
	 *
	 * @param languageId - language ID.
	 * @return the set of resource paths associated with entries for the given language or empty set.
	 * Note that the storage can keep default entries for resources, so the set can contain {@code null}.
	 */
	public Set<String> getResourcePaths(String languageId) {
		Map<String, List<ICLanguageSettingEntry>> rcPathsMap = fStorage.get(languageId);
		if (rcPathsMap == null) {
			return new HashSet<>();
		}
		return new HashSet<>(rcPathsMap.keySet());
	}

	/**
	 * Find and return the equal list of entries from the pool.
	 *
	 * @param entries - list of entries to pool.
	 * @param copy - specify {@code true} to copy the list in order to prevent
	 *    back-door modification on the original list changes.
	 * @return returns the list of entries from the pool.
	 */
	private static List<ICLanguageSettingEntry> getPooledList(List<ICLanguageSettingEntry> entries, boolean copy) {
		if (entries == null)
			return null;

		List<ICLanguageSettingEntry> pooledList = listPool.get(entries);
		if (pooledList != null) {
			return pooledList;
		}

		if (entries.size() == 0) {
			return getPooledEmptyList();
		}

		if (copy) {
			entries = new ArrayList<>(entries);
		}
		pooledList = Collections.unmodifiableList(entries);
		return listPool.add(pooledList);
	}

	/**
	 * Find and return the equal list of entries from the pool to conserve the memory.
	 *
	 * @param entries - list of entries to pool.
	 * @return returns the list of entries from the pool.
	 */
	public static List<ICLanguageSettingEntry> getPooledList(List<ICLanguageSettingEntry> entries) {
		return getPooledList(entries, true);
	}

	/**
	 * @return Returns the empty immutable list which is pooled. Use this call rather than creating
	 * new empty array to ensure that faster shallow operator '==' can be used instead of equals()
	 * which goes deep on HashMaps.
	 */
	public static List<ICLanguageSettingEntry> getPooledEmptyList() {
		List<ICLanguageSettingEntry> pooledEmptyList = Collections.emptyList();
		return listPool.add(pooledEmptyList);
	}

	/**
	 * Clone storage for the entries. Copies references for lists of entries as a whole.
	 * Note that that is OK as the lists kept in storage are unmodifiable and pooled.
	 */
	@Override
	public LanguageSettingsStorage clone() throws CloneNotSupportedException {
		LanguageSettingsStorage storageClone = (LanguageSettingsStorage) super.clone();
		storageClone.fStorage = new HashMap<>();
		synchronized (fStorage) {
			Set<Entry<String, Map<String, List<ICLanguageSettingEntry>>>> entrySetLang = fStorage.entrySet();
			for (Entry<String, Map<String, List<ICLanguageSettingEntry>>> entryLang : entrySetLang) {
				String langId = entryLang.getKey();
				Map<String, List<ICLanguageSettingEntry>> mapRc = entryLang.getValue();
				Map<String, List<ICLanguageSettingEntry>> mapRcClone = new HashMap<>();
				Set<Entry<String, List<ICLanguageSettingEntry>>> entrySetRc = mapRc.entrySet();
				for (Entry<String, List<ICLanguageSettingEntry>> entryRc : entrySetRc) {
					String rcProjectPath = entryRc.getKey();
					List<ICLanguageSettingEntry> lsEntries = entryRc.getValue();
					// don't need to clone entries, they are from the LSE lists pool
					mapRcClone.put(rcProjectPath, lsEntries);
				}
				storageClone.fStorage.put(langId, mapRcClone);
			}
		}
		return storageClone;
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((fStorage == null) ? 0 : fStorage.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		LanguageSettingsStorage other = (LanguageSettingsStorage) obj;
		if (fStorage == null) {
			if (other.fStorage != null)
				return false;
		} else if (!fStorage.equals(other.fStorage))
			return false;
		return true;
	}

}
