/*******************************************************************************
 * Copyright (c) 2009, 2012 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.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.cdt.core.AbstractExecutableExtensionBase;
import org.eclipse.cdt.core.settings.model.ICConfigurationDescription;
import org.eclipse.cdt.core.settings.model.ICLanguageSettingEntry;
import org.eclipse.cdt.internal.core.settings.model.SettingsModelMessages;
import org.eclipse.core.resources.IResource;

/**
 * {@code LanguageSettingsBaseProvider} is a basic implementation of {@link ILanguageSettingsProvider}
 * for the extensions defined by {@code org.eclipse.cdt.core.LanguageSettingsProvider} extension point.
 *
 * This implementation supports "static" list of entries for languages specified in
 * the extension point.
 *
 * @since 5.4
 */
public class LanguageSettingsBaseProvider extends AbstractExecutableExtensionBase implements ILanguageSettingsProvider {
	/** Language scope, i.e. list of languages the entries will be provided for. */
	protected List<String> languageScope = null;

	/** Provider-specific properties */
	protected Map<String, String> properties = new HashMap<>();

	/** List of entries defined by this provider. */
	private List<ICLanguageSettingEntry> entries = null;

	/**
	 * Default constructor.
	 */
	public LanguageSettingsBaseProvider() {
		super();
	}

	/**
	 * Constructor. Creates an "empty" non-configured provider.
	 *
	 * @param id - id of the provider.
	 * @param name - name of the provider to be presented to a user.
	 */
	public LanguageSettingsBaseProvider(String id, String name) {
		super(id, name);
	}

	/**
	 * Constructor.
	 *
	 * @param id - id of the provider.
	 * @param name - name of the provider to be presented to a user.
	 * @param languages - list of languages the {@code entries} provided for.
	 *    {@code languages} can be {@code null}, in this case the {@code entries}
	 *    are provided for any language.
	 * @param entries - the list of language settings entries this provider provides.
	 *    If {@code null} is passed, the provider creates an empty list.
	 */
	public LanguageSettingsBaseProvider(String id, String name, List<String> languages,
			List<ICLanguageSettingEntry> entries) {
		super(id, name);
		this.languageScope = languages != null ? new ArrayList<>(languages) : null;
		this.entries = getPooledList(entries);
	}

	/**
	 * Constructor.
	 *
	 * @param id - id of the provider.
	 * @param name - name of the provider to be presented to a user.
	 * @param languages - list of languages the {@code entries} provided for.
	 *    {@code languages} can be {@code null}, in this case the {@code entries}
	 *    are provided for any language.
	 * @param entries - the list of language settings entries this provider provides.
	 *    If {@code null} is passed, the provider creates an empty list.
	 * @param properties - custom properties as the means to customize providers.
	 */
	public LanguageSettingsBaseProvider(String id, String name, List<String> languages,
			List<ICLanguageSettingEntry> entries, Map<String, String> properties) {
		super(id, name);
		this.languageScope = languages != null ? new ArrayList<>(languages) : null;
		this.entries = getPooledList(entries);
		if (properties != null)
			this.properties = new HashMap<>(properties);
	}

	/**
	 * A method to configure the provider. The initialization of provider from
	 * the extension point is done in 2 steps. First, the class is created as
	 * an executable extension using the default provider. Then this method is
	 * used to configure the provider.
	 *<br><br>
	 * It is not allowed to reconfigure the provider.
	 *
	 * @param id - id of the provider.
	 * @param name - name of the provider to be presented to a user.
	 * @param languages - list of languages the {@code entries} provided for.
	 *    {@code languages} can be {@code null}, in this case the {@code entries}
	 *    are provided for any language.
	 * @param entries - the list of language settings entries this provider provides.
	 *    If {@code null} is passed, the provider creates an empty list.
	 * @param properties - custom properties as the means to customize providers.
	 *
	 * @throws UnsupportedOperationException if an attempt to reconfigure provider is made.
	 */
	public void configureProvider(String id, String name, List<String> languages, List<ICLanguageSettingEntry> entries,
			Map<String, String> properties) {
		if (this.entries != null || !this.properties.isEmpty())
			throw new UnsupportedOperationException(
					SettingsModelMessages.getString("LanguageSettingsBaseProvider.CanBeConfiguredOnlyOnce")); //$NON-NLS-1$

		setId(id);
		setName(name);
		this.languageScope = languages != null ? new ArrayList<>(languages) : null;
		this.entries = getPooledList(entries);
		if (properties != null)
			this.properties = new HashMap<>(properties);
	}

	/**
	 * {@code LanguageSettingsBaseProvider} keeps the list of key-value pairs
	 * so extenders of this class can customize the provider. The properties
	 * of {@code LanguageSettingsBaseProvider} come from the extension in plugin.xml
	 * although the extenders can provide their own method.
	 * <br><br>
	 * Please note that empty string value is treated as "default" value and
	 * the same as {@code null} and the same as missing property, which allows
	 * {@link #equals(Object)} evaluate the property as equal while comparing providers.
	 *
	 * @param key - property to check the value.
	 * @return value of the property. If the property is missing returns empty string.
	 */
	public String getProperty(String key) {
		String value = properties.get(key);
		if (value == null) {
			value = ""; //$NON-NLS-1$
		}
		return value;
	}

	/**
	 * Convenience method to get boolean property.
	 * @see #getProperty(String)
	 *
	 * @param key - property to check the value.
	 * @return boolean value of the property. If the property is missing or cannot be
	 *    interpreted as boolean returns {@code false}.
	 */
	public boolean getPropertyBool(String key) {
		return Boolean.parseBoolean(properties.get(key));
	}

	private List<ICLanguageSettingEntry> getPooledList(List<ICLanguageSettingEntry> entries) {
		if (entries != null) {
			return LanguageSettingsStorage.getPooledList(entries);
		}
		return null;
	}

	/**
	 * {@inheritDoc}
	 *
	 * @param languageId - language id. If {@code null}, then entries defined for
	 *    the language scope are returned. See {@link #getLanguageScope()}
	 *
	 * @return unmodifiable list of setting entries or {@code null} if no settings defined.
	 *    the list is internally pooled and guaranteed to be the same object for equal
	 *    lists.
	 */
	@Override
	public List<ICLanguageSettingEntry> getSettingEntries(ICConfigurationDescription cfgDescription, IResource rc,
			String languageId) {
		if (languageScope == null) {
			return entries;
		}
		for (String lang : languageScope) {
			if (lang.equals(languageId)) {
				return entries;
			}
		}
		return null;
	}

	/**
	 * @return the unmodifiable list of languages this provider provides for.
	 *    If {@code null}, the provider provides for any language.
	 */
	public List<String> getLanguageScope() {
		if (languageScope == null)
			return null;
		return Collections.unmodifiableList(languageScope);
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((getId() == null) ? 0 : getId().hashCode());
		result = prime * result + ((getName() == null) ? 0 : getName().hashCode());
		result = prime * result + ((entries == null) ? 0 : entries.hashCode());
		result = prime * result + ((languageScope == null) ? 0 : languageScope.hashCode());
		// exclude field "properties" because of special rules for equals()
		result = prime * result + getClass().hashCode();
		return result;
	}

	/**
	 * @return {@code true} if the objects are equal, {@code false } otherwise.
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		LanguageSettingsBaseProvider other = (LanguageSettingsBaseProvider) obj;

		String id = getId();
		String otherId = other.getId();
		if (id == null) {
			if (otherId != null)
				return false;
		} else if (!id.equals(otherId))
			return false;

		String name = getName();
		String otherName = other.getName();
		if (name == null) {
			if (otherName != null)
				return false;
		} else if (!name.equals(otherName))
			return false;

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

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

		if (properties == null) {
			if (other.properties != null)
				return false;
		} else if (other.properties == null) {
			return false;
		} else {
			// The trouble to ensure default properties are equal to missing ones.
			Set<String> keys = new HashSet<>(properties.keySet());
			keys.addAll(other.properties.keySet());
			for (String key : keys) {
				String value = properties.get(key);
				if (value == null || value.equals(Boolean.FALSE.toString()))
					value = ""; //$NON-NLS-1$
				String otherValue = other.properties.get(key);
				if (otherValue == null || otherValue.equals(Boolean.FALSE.toString()))
					otherValue = ""; //$NON-NLS-1$
				if (!value.equals(otherValue))
					return false;
			}
		}

		return true;
	}

}
