/*******************************************************************************
 * Copyright (c) 2009, 2013 Andrew Gvozdev 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:
 *     Andrew Gvozdev - initial API and implementation
 *******************************************************************************/
package org.eclipse.cdt.core.language.settings.providers;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.eclipse.cdt.core.CCorePlugin;
import org.eclipse.cdt.core.settings.model.ICConfigurationDescription;
import org.eclipse.cdt.core.settings.model.ICLanguageSettingEntry;
import org.eclipse.cdt.internal.core.XmlUtil;
import org.eclipse.cdt.internal.core.language.settings.providers.LanguageSettingsProvidersSerializer;
import org.eclipse.cdt.internal.core.language.settings.providers.LanguageSettingsSerializableStorage;
import org.eclipse.cdt.internal.core.settings.model.CConfigurationSpecSettings;
import org.eclipse.cdt.internal.core.settings.model.IInternalCCfgInfo;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * This class is the base class for language settings providers able to serialize
 * into XML storage.
 * Although this class has setter methods, by design its instances are not editable in UI
 * nor instances can be assigned to a configuration (cannot be global or non-shared).
 * Implement {@link ILanguageSettingsEditableProvider} interface for that. There is a generic
 * implementation of this interface available to be used, see {@link LanguageSettingsGenericProvider}.
 * 
 * For more on the suggested way of extending this class see the description of
 * {@link ILanguageSettingsProvider}.
 *
 * @since 5.4
 */
public class LanguageSettingsSerializableProvider extends LanguageSettingsBaseProvider implements ILanguageSettingsBroadcastingProvider {
	protected static final String ATTR_ID = LanguageSettingsProvidersSerializer.ATTR_ID;
	protected static final String ATTR_NAME = LanguageSettingsProvidersSerializer.ATTR_NAME;
	protected static final String ATTR_CLASS = LanguageSettingsProvidersSerializer.ATTR_CLASS;
	protected static final String ELEM_PROVIDER = LanguageSettingsProvidersSerializer.ELEM_PROVIDER;
	protected static final String ELEM_LANGUAGE_SCOPE = LanguageSettingsProvidersSerializer.ELEM_LANGUAGE_SCOPE;

	private LanguageSettingsSerializableStorage fStorage = new LanguageSettingsSerializableStorage();

	/**
	 * Default constructor. This constructor has to be always followed with setting id and name of the provider.
	 * This constructor is necessary to instantiate the class via the extension point in plugin.xml.
	 */
	public LanguageSettingsSerializableProvider() {
		super();
	}

	/**
	 * Constructor.
	 *
	 * @param id - id of the provider.
	 * @param name - name of the provider. Note that this name shows up in UI.
	 */
	public LanguageSettingsSerializableProvider(String id, String name) {
		super(id, name);
	}

	/**
	 * Constructor which allows to instantiate provider defined via XML markup.
	 *
	 * @param elementProvider
	 */
	public LanguageSettingsSerializableProvider(Element elementProvider) {
		super();
		load(elementProvider);
	}

	@Override
	public void configureProvider(String id, String name, List<String> languages, List<ICLanguageSettingEntry> entries, Map<String, String> properties) {
		// do not pass entries to super, keep them in local storage
		super.configureProvider(id, name, languages, null, properties);

		fStorage.clear();

		if (entries!=null) {
			// note that these entries are intended to be retrieved by LanguageSettingsManager.getSettingEntriesUpResourceTree()
			// when the whole resource hierarchy has been traversed up
			setSettingEntries(null, null, null, entries);
		}
	}

	/**
	 * @return {@code true} if the provider does not keep any settings yet or {@code false} if there are some.
	 */
	public boolean isEmpty() {
		return fStorage.isEmpty();
	}

	/**
	 * Sets the language scope of the provider.
	 *
	 * @param languages - the list of languages this provider provides for.
	 *    If {@code null}, the provider provides for any language.
	 *
	 * @see #getLanguageScope()
	 */
	public void setLanguageScope(List<String> languages) {
		if (languages == null) {
			this.languageScope = null;
		} else {
			this.languageScope = new ArrayList<>(languages);
		}
	}

	/**
	 * Clears all the entries for all configurations, all resources and all languages.
	 */
	public void clear() {
		fStorage.clear();
	}

	/**
	 * Sets language settings entries for the provider.
	 * Note that the entries are not persisted at that point. Use this method to set
	 * the entries for all resources one by one and after all done persist in one shot
	 * using {@link #serializeLanguageSettings(ICConfigurationDescription)}.
	 * See for example {@code AbstractBuildCommandParser} and {@code AbstractBuiltinSpecsDetector}
	 * in build plugins.
	 *
	 * @param cfgDescription - configuration description.
	 * @param rc - resource such as file or folder. 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. See {@link #getLanguageScope()}
	 * @param entries - language settings entries to set.
	 */
	public void setSettingEntries(ICConfigurationDescription cfgDescription, IResource rc, String languageId,
			List<? extends ICLanguageSettingEntry> entries) {
		String rcProjectPath = rc!=null ? rc.getProjectRelativePath().toString() : null;
		fStorage.setSettingEntries(rcProjectPath, languageId, entries);
	}

	/**
	 * {@inheritDoc}
	 * <br>
	 * Note that this list is <b>unmodifiable</b>. To modify the list copy it, change and use
	 * {@link #setSettingEntries(ICConfigurationDescription, IResource, String, List)}.
	 * <br><br>
	 * Note also that <b>you can compare these lists with simple equality operator ==</b>,
	 * as the lists themselves are backed by WeakHashSet<List<ICLanguageSettingEntry>> where
	 * identical copies (deep comparison is used) are replaced with the same one instance.
	 */
	@Override
	public List<ICLanguageSettingEntry> getSettingEntries(ICConfigurationDescription cfgDescription, IResource rc, String languageId) {
		String rcProjectPath = rc!=null ? rc.getProjectRelativePath().toString() : null;
		List<ICLanguageSettingEntry> entries = fStorage.getSettingEntries(rcProjectPath, languageId);
		if (entries == null) {
			if (languageId != null && (languageScope == null || languageScope.contains(languageId))) {
				entries = fStorage.getSettingEntries(rcProjectPath, null);
			}
		}

		return entries;
	}

	/**
	 * Serialize the provider under parent XML element.
	 * This is convenience method not intended to be overridden on purpose.
	 * Override {@link #serializeAttributes(Element)} or
	 * {@link #serializeEntries(Element)} instead.
	 *
	 * @param parentElement - element where to serialize.
	 * @return - newly created "provider" element. That element will already be
	 *    attached to the parent element.
	 */
	final public Element serialize(Element parentElement) {
		/*
		<provider id="provider.id" ...>
			<language-scope id="lang.id"/>
			<language id="lang.id">
				<resource project-relative-path="/">
					<entry flags="" kind="includePath" name="path"/>
				</resource>
			</language>
		</provider>
		 */
		Element elementProvider = serializeAttributes(parentElement);
		serializeEntries(elementProvider);
		return elementProvider;
	}

	/**
	 * Serialize the provider attributes under parent XML element. That is
	 * equivalent to serializing everything (including language scope) except entries.
	 *
	 * @param parentElement - element where to serialize.
	 * @return - newly created "provider" element. That element will already be
	 *    attached to the parent element.
	 */
	public Element serializeAttributes(Element parentElement) {
		// Keeps pairs: key, value. See JavaDoc XmlUtil.appendElement(Node, String, String[]).
		List<String> attributes = new ArrayList<String>();

		attributes.add(ATTR_ID);
		attributes.add(getId());
		attributes.add(ATTR_NAME);
		attributes.add(getName());
		attributes.add(ATTR_CLASS);
		attributes.add(getClass().getCanonicalName());
		for (Entry<String, String> entry : properties.entrySet()) {
			attributes.add(entry.getKey());
			attributes.add(entry.getValue());
		}

		Element elementProvider = XmlUtil.appendElement(parentElement, ELEM_PROVIDER, attributes.toArray(new String[0]));

		if (languageScope!=null) {
			for (String langId : languageScope) {
				XmlUtil.appendElement(elementProvider, ELEM_LANGUAGE_SCOPE, new String[] {ATTR_ID, langId});
			}
		}
		return elementProvider;
	}

	/**
	 * Serialize the provider entries under parent XML element.
	 * @param elementProvider - element where to serialize the entries.
	 */
	public void serializeEntries(Element elementProvider) {
		fStorage.serializeEntries(elementProvider);
	}

	/**
	 * Convenience method to persist language settings entries for the project or
	 * workspace as often-used operation.
	 * Note that configuration description is passed as an argument but the
	 * current implementation saves all configurations.
	 *
	 * @param cfgDescription - configuration description.
	 *    If not {@code null}, all providers of the project are serialized.
	 *    If {@code null}, global workspace providers are serialized.
	 *
	 * @return - status of operation.
	 */
	public IStatus serializeLanguageSettings(ICConfigurationDescription cfgDescription) {
		IStatus status = Status.OK_STATUS;
		try {
			if (cfgDescription != null) {
				LanguageSettingsManager.serializeLanguageSettings(cfgDescription.getProjectDescription());
			} else {
				LanguageSettingsManager.serializeLanguageSettingsWorkspace();
			}
		} catch (CoreException e) {
			status = new Status(IStatus.ERROR, CCorePlugin.PLUGIN_ID, IStatus.ERROR, "Error serializing language settings", e); //$NON-NLS-1$
			CCorePlugin.log(status);
		}
		return status;
	}

	/**
	 * Convenience method to persist language settings entries in background for the project or
	 * workspace as often-used operation.
	 * Note that configuration description is passed as an argument but the
	 * current implementation saves all configurations.
	 *
	 * @param cfgDescription - configuration description.
	 *    If not {@code null}, all providers of the project are serialized.
	 *    If {@code null}, global workspace providers are serialized.
	 */
	public void serializeLanguageSettingsInBackground(ICConfigurationDescription cfgDescription) {
		if (cfgDescription != null) {
			if (isLanguageSettingsProviderStoreChanged(cfgDescription)) {
				LanguageSettingsManager.serializeLanguageSettingsInBackground(cfgDescription.getProjectDescription());
			}
		} else {
			LanguageSettingsManager.serializeLanguageSettingsWorkspaceInBackground();
		}
	}

	/**
	 * Compare provider store with cached persistent store used to calculate delta.
	 */
	private boolean isLanguageSettingsProviderStoreChanged(ICConfigurationDescription cfgDescription) {
		if (cfgDescription instanceof IInternalCCfgInfo) {
			try {
				CConfigurationSpecSettings ss = ((IInternalCCfgInfo)cfgDescription).getSpecSettings();
				if (ss != null) {
					return ss.isLanguageSettingsProviderStoreChanged(this);
				}
			} catch (CoreException e) {
				CCorePlugin.log(e);
			}
		}

		// If something went wrong assuming it might have changed
		return true;
	}

	/**
	 * Load provider from XML provider element.
	 * This is convenience method not intended to be overridden on purpose.
	 * Override {@link #loadAttributes(Element)} or
	 * {@link #loadEntries(Element)} instead.
	 *
	 * @param providerNode - XML element "provider" to load provider from.
	 */
	final public void load(Element providerNode) {
		fStorage.clear();
		languageScope = null;

		// provider/configuration/language/resource/entry
		if (providerNode!=null) {
			loadAttributes(providerNode);
			loadEntries(providerNode);
		}
	}

	/**
	 * Determine and set language scope from given XML node.
	 */
	private void loadLanguageScopeElement(Node parentNode) {
		if (languageScope==null) {
			languageScope = new ArrayList<String>();
		}
		String id = XmlUtil.determineAttributeValue(parentNode, ATTR_ID);
		languageScope.add(id);

	}

	/**
	 * Load attributes from XML provider element.
	 * @param providerNode - XML element "provider" to load attributes from.
	 */
	public void loadAttributes(Element providerNode) {
		String providerId = XmlUtil.determineAttributeValue(providerNode, ATTR_ID);
		String providerName = XmlUtil.determineAttributeValue(providerNode, ATTR_NAME);

		properties.clear();
		NamedNodeMap attrs = providerNode.getAttributes();
		for (int i=0; i<attrs.getLength(); i++) {
			Node attr = attrs.item(i);
			if (attr.getNodeType()==Node.ATTRIBUTE_NODE) {
				String key = attr.getNodeName();
				if (!key.equals(ATTR_ID) && !key.equals(ATTR_NAME) && !key.equals(ATTR_CLASS)) {
					String value = attr.getNodeValue();
					properties.put(key, value);
				}
			}
		}

		this.setId(providerId);
		this.setName(providerName);

		NodeList nodes = providerNode.getChildNodes();
		for (int i=0;i<nodes.getLength();i++) {
			Node elementNode = nodes.item(i);
			if(elementNode.getNodeType() != Node.ELEMENT_NODE)
				continue;

			if (ELEM_LANGUAGE_SCOPE.equals(elementNode.getNodeName())) {
				loadLanguageScopeElement(elementNode);
			}
		}

	}

	/**
	 * Load provider entries from XML provider element.
	 * @param providerNode - parent XML element "provider" where entries are defined.
	 */
	public void loadEntries(Element providerNode) {
		fStorage.loadEntries(providerNode);
	}

	/**
	 * Set a custom property of the provider.
	 * <br><br>
	 * A note of caution - do not use default values for a provider which are different
	 * from empty or {@code null} value. When providers are checked for equality
	 * (during internal operations in core) the missing properties are evaluated as
	 * empty ones.
	 *
	 * @see LanguageSettingsBaseProvider#getProperty(String)
	 *
	 * @param key - name of the property.
	 * @param value - value of the property.
	 *    If value is {@code null} the property is removed from the list.
	 */
	public void setProperty(String key, String value) {
		properties.put(key, value);
	}

	/**
	 * Set a custom boolean property of the provider.
	 * <br>Please, note that default value is always {@code false}.
	 * @see LanguageSettingsBaseProvider#getProperty(String)
	 *
	 * @param key - name of the property.
	 * @param value - {@code boolean} value of the property.
	 */
	public void setPropertyBool(String key, boolean value) {
		properties.put(key, Boolean.toString(value));
	}

	/**
	 * See {@link #cloneShallow()}. This method is extracted to avoid expressing
	 * {@link #clone()} via {@link #cloneShallow()}. Do not inline to "optimize"!
	 */
	private LanguageSettingsSerializableProvider cloneShallowInternal() throws CloneNotSupportedException {
		LanguageSettingsSerializableProvider clone = (LanguageSettingsSerializableProvider)super.clone();
		if (languageScope!=null)
			clone.languageScope = new ArrayList<String>(languageScope);
		clone.properties = new HashMap<String, String>(properties);

		clone.fStorage = new LanguageSettingsSerializableStorage();
		return clone;
	}

	/**
	 * Shallow clone of the provider. "Shallow" is defined here as the exact copy except that
	 * the copy will have zero language settings entries.
	 *
	 * @return shallow copy of the provider.
	 * @throws CloneNotSupportedException in case {@link #clone()} throws the exception.
	 */
	protected LanguageSettingsSerializableProvider cloneShallow() throws CloneNotSupportedException {
		return cloneShallowInternal();
	}

	@Override
	protected LanguageSettingsSerializableProvider clone() throws CloneNotSupportedException {
		LanguageSettingsSerializableProvider clone = cloneShallowInternal();
		clone.fStorage = fStorage.clone();
		return clone;
	}

	@Override
	public LanguageSettingsStorage copyStorage() {
		try {
			return fStorage.clone();
		} catch (CloneNotSupportedException e) {
			CCorePlugin.log(e);
		}
		return null;
	}

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

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (!super.equals(obj))
			return false;
		if (getClass() != obj.getClass())
			return false;
		LanguageSettingsSerializableProvider other = (LanguageSettingsSerializableProvider) obj;

		if (fStorage == null) {
			if (other.fStorage != null)
				return false;
		} else if (!fStorage.equals(other.fStorage))
			return false;
		return true;
	}
}
