/*******************************************************************************
 * Copyright (c) 2007, 2013 Intel Corporation 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:
 *     Intel Corporation - Initial API and implementation
 *     James Blackburn (Broadcom Corp.)
 *     Baltasar Belyavsky (Texas Instruments) - bug 340219: Project metadata files are saved unnecessarily
 *     Serge Beauchamp (Freescale Semiconductor) - Bug 418184 - ConcurrentException in CProjectDescription
 *******************************************************************************/
package org.eclipse.cdt.internal.core.settings.model;

import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;

import org.eclipse.cdt.core.CCorePlugin;
import org.eclipse.cdt.core.settings.model.ICConfigurationDescription;
import org.eclipse.cdt.core.settings.model.ICProjectDescription;
import org.eclipse.cdt.core.settings.model.ICSettingBase;
import org.eclipse.cdt.core.settings.model.ICSettingContainer;
import org.eclipse.cdt.core.settings.model.ICSettingObject;
import org.eclipse.cdt.core.settings.model.ICSettingsStorage;
import org.eclipse.cdt.core.settings.model.ICStorageElement;
import org.eclipse.cdt.core.settings.model.WriteAccessException;
import org.eclipse.cdt.core.settings.model.extension.CConfigurationData;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.QualifiedName;

public class CProjectDescription implements ICProjectDescription, ICDataProxyContainer {
	private static final String ACTIVE_CFG = "activeConfiguration"; //$NON-NLS-1$
	private static final QualifiedName ACTIVE_CFG_PROPERTY = new QualifiedName(CCorePlugin.PLUGIN_ID, ACTIVE_CFG);
	private static final String SETTING_CFG = "settingConfiguration"; //$NON-NLS-1$
	private static final QualifiedName SETTING_CFG_PROPERTY = new QualifiedName(CCorePlugin.PLUGIN_ID, SETTING_CFG);

	private CfgIdPair fActiveCfgInfo;
	private CfgIdPair fSettingCfgInfo;
	private CProjectDescriptionPreferences fPrefs;
	//	private ICConfigurationDescription fActiveCfg;
	//	private String fActiveCfgId;
	//	private ICConfigurationDescription fIndexCfg;
	//	private String fIndexCfgId;
	private volatile IProject fProject;
	private final ICSettingsStorage fStorage;
	private final ICStorageElement fRootStorageElement;
	private final HashMap<String, ICConfigurationDescription> fCfgMap = new LinkedHashMap<>();
	private boolean fIsReadOnly;
	private boolean fIsModified;
	private Map<QualifiedName, Object> fPropertiesMap;
	private boolean fIsLoading;
	private boolean fIsApplying;
	private boolean fIsCreating;

	private class CfgIdPair {
		private String fId;
		private ICConfigurationDescription fCfg;
		private QualifiedName fPersistanceName;
		private boolean fNeedsPersistance;
		private boolean fIsCfgModified;

		CfgIdPair(CfgIdPair base) {
			fId = base.fId;
			fPersistanceName = base.fPersistanceName;
		}

		CfgIdPair(QualifiedName persistanceName) {
			fPersistanceName = persistanceName;
		}

		public String getId() {
			if (fId == null) {
				fId = load();
				if (fId == null) {
					fId = getFirstCfgId();
					if (fId != null) {
						fNeedsPersistance = true;
					}
				}
			}
			return fId;
		}

		public ICConfigurationDescription getConfiguration() {
			if (fCfg == null) {
				String id = getId();
				if (id != null) {
					fCfg = getConfigurationById(id);
					if (fCfg == null) {
						fId = getFirstCfgId();
						if (fId != null) {
							fCfg = getConfigurationById(fId);
							fNeedsPersistance = true;
						}
					}
				}
			}
			return fCfg;
		}

		public void setConfiguration(ICConfigurationDescription cfg) {
			if (cfg.getProjectDescription() != CProjectDescription.this)
				throw new IllegalArgumentException();

			if (cfg.getId().equals(getId()))
				return;

			fCfg = cfg;
			fId = cfg.getId();
			fIsCfgModified = true;
			fNeedsPersistance = true;
		}

		public void configurationRemoved(ICConfigurationDescription cfg) {
			if (cfg.getProjectDescription() != CProjectDescription.this)
				throw new IllegalArgumentException();

			if (!cfg.getId().equals(getId()))
				return;

			fIsCfgModified = true;
			fCfg = null;
			getConfiguration();
		}

		private String load() {
			try {
				return getProject().getPersistentProperty(fPersistanceName);
			} catch (CoreException e) {
				CCorePlugin.log(e);
			}
			return null;
		}

		private boolean store(String oldId, boolean force) {
			if (force || fIsCfgModified || fNeedsPersistance || oldId == null || !oldId.equals(fId)) {
				try {
					getProject().setPersistentProperty(fPersistanceName, fId);
					fIsCfgModified = false;
					fNeedsPersistance = false;
					return true;
				} catch (CoreException e) {
					CCorePlugin.log(e);
				}
			}
			return false;
		}

	}

	public CProjectDescription(IProject project, ICSettingsStorage storage, ICStorageElement element, boolean loading,
			boolean isCreating) throws CoreException {
		fProject = project;
		fStorage = storage;
		fRootStorageElement = element;
		fIsReadOnly = loading;
		fIsLoading = loading;
		fActiveCfgInfo = new CfgIdPair(ACTIVE_CFG_PROPERTY);
		fSettingCfgInfo = new CfgIdPair(SETTING_CFG_PROPERTY);
		fIsCreating = isCreating;
		ICStorageElement el = null;
		CProjectDescriptionManager mngr = CProjectDescriptionManager.getInstance();
		if (loading) {
			Map<String, ICStorageElement> cfgStorMap = mngr.createCfgStorages(this);

			for (ICStorageElement sel : cfgStorMap.values()) {
				CConfigurationDescriptionCache cache = new CConfigurationDescriptionCache(sel, this);
				configurationCreated(cache);
			}

			el = getStorage(CProjectDescriptionManager.MODULE_ID, false);
		}

		fPrefs = new CProjectDescriptionPreferences(el,
				(CProjectDescriptionPreferences) mngr.getProjectDescriptionWorkspacePreferences(false), false);

		fPropertiesMap = Collections.synchronizedMap(new HashMap<QualifiedName, Object>());
	}

	public void updateProject(IProject project) {
		fProject = project;
	}

	public void loadDatas() {
		if (!fIsReadOnly || !fIsLoading)
			return;

		for (Iterator<ICConfigurationDescription> iter = fCfgMap.values().iterator(); iter.hasNext();) {
			CConfigurationDescriptionCache cache = (CConfigurationDescriptionCache) iter.next();
			try {
				cache.loadData();
			} catch (CoreException e) {
				CCorePlugin.log(e);
				iter.remove();
			}
		}

		//		doneInitializing();

		//		fIsLoading = false;
	}

	public boolean applyDatas(SettingsContext context) {
		if (!fIsReadOnly || !fIsApplying)
			return false;

		boolean modified = false;
		for (Iterator<ICConfigurationDescription> iter = fCfgMap.values().iterator(); iter.hasNext();) {
			CConfigurationDescriptionCache cache = (CConfigurationDescriptionCache) iter.next();
			try {
				if (cache.applyData(context))
					modified = true;
			} catch (CoreException e) {
				CCorePlugin.log(e);
				e.printStackTrace();
				iter.remove();
			}
		}

		//		doneInitializing();

		//		fIsApplying = false;

		return modified;
	}

	/**
	 * Called when the read-only project description has / is being set
	 * fIsApplying => false
	 * setModified (false)
	 * set the ICSettingsStorage to readonly
	 */
	public void doneApplying() {
		doneInitializing();
		fIsApplying = false;

		try {
			getStorageBase().setReadOnly(true, false);
		} catch (CoreException e1) {
			CCorePlugin.log(e1);
		}

		setModified(false);
	}

	public void doneLoading() {
		doneInitializing();
		fIsLoading = false;
	}

	public void setLoading(boolean loading) {
		fIsLoading = loading;
	}

	private void doneInitializing() {
		for (ICConfigurationDescription cfg : fCfgMap.values()) {
			// FIXME How and why are we down casting to a CConfigurationDescriptionCache. Comments, please!
			CConfigurationDescriptionCache cache = (CConfigurationDescriptionCache) cfg;
			cache.doneInitialization();
		}

		if (fIsReadOnly)
			fPrefs.setReadOnly(true);
	}

	public boolean isLoading() {
		return fIsLoading;
	}

	public boolean isApplying() {
		return fIsApplying;
	}

	/**
	 * Create a project description based on another project description
	 *
	 * @param base
	 * @param saving
	 * @param storage
	 * @param el
	 * @param isCreating
	 */
	public CProjectDescription(CProjectDescription base, boolean saving, ICSettingsStorage storage, ICStorageElement el,
			boolean isCreating) {
		fActiveCfgInfo = new CfgIdPair(base.fActiveCfgInfo);
		fSettingCfgInfo = new CfgIdPair(base.fSettingCfgInfo);
		fProject = base.fProject;
		fStorage = storage;
		fRootStorageElement = el;
		fIsReadOnly = saving;
		fIsLoading = base.fIsLoading;
		fIsApplying = saving || base.fIsApplying;
		fIsCreating = isCreating;

		fPrefs = new CProjectDescriptionPreferences(base.fPrefs,
				(CProjectDescriptionPreferences) CProjectDescriptionManager.getInstance()
						.getProjectDescriptionWorkspacePreferences(false),
				false);

		for (Iterator<ICConfigurationDescription> iter = base.fCfgMap.values().iterator(); iter.hasNext();) {
			try {
				IInternalCCfgInfo cfgDes = (IInternalCCfgInfo) iter.next();
				if (fIsReadOnly) {
					CConfigurationData baseData = cfgDes.getConfigurationData(false);
					CConfigurationDescriptionCache baseCache = null;
					if (baseData instanceof CConfigurationDescriptionCache) {
						baseCache = (CConfigurationDescriptionCache) baseData;
						baseData = baseCache.getConfigurationData();
					}
					CConfigurationDescriptionCache cache = new CConfigurationDescriptionCache(
							(ICConfigurationDescription) cfgDes, baseData, baseCache, cfgDes.getSpecSettings(), this,
							null);
					configurationCreated(cache);
				} else {
					CConfigurationData baseData = cfgDes.getConfigurationData(false);
					CConfigurationDescription cfg = new CConfigurationDescription(baseData, this);
					configurationCreated(cfg);
				}
			} catch (CoreException e) {
				CCorePlugin.log(e);
			}
		}

		// Shallow cloning the base map
		Map<QualifiedName, Object> cloneMap = Collections.synchronizedMap(new HashMap<QualifiedName, Object>());
		synchronized (base.fPropertiesMap) {
			cloneMap.putAll(base.fPropertiesMap);
		}
		fPropertiesMap = cloneMap;
	}

	/**
	 * Convert the current CConfigurationDescriptions to cached versions
	 * This occurs during the SetCProjectDescription Operation
	 */
	void switchToCachedConfigurationDescriptions() throws CoreException {
		for (Map.Entry<String, ICConfigurationDescription> e : fCfgMap.entrySet()) {
			if (e.getValue() instanceof CConfigurationDescription) {
				CConfigurationDescription cfgDes = (CConfigurationDescription) e.getValue();
				CConfigurationData baseData = ((IInternalCCfgInfo) cfgDes).getConfigurationData(false);
				CConfigurationDescriptionCache baseCache = null;
				if (baseData instanceof CConfigurationDescriptionCache) {
					baseCache = (CConfigurationDescriptionCache) baseData;
					baseData = baseCache.getConfigurationData();
				}
				CConfigurationDescriptionCache cache = new CConfigurationDescriptionCache(cfgDes, baseData, baseCache,
						cfgDes.getSpecSettings(), this, null);
				e.setValue(cache);
			}
		}
	}

	void configurationCreated(ICConfigurationDescription des) {
		fCfgMap.put(des.getId(), des);
	}

	@Override
	public ICConfigurationDescription createConfiguration(String id, String name, ICConfigurationDescription base)
			throws CoreException {
		if (fIsReadOnly)
			throw ExceptionFactory.createIsReadOnlyException();
		CConfigurationDescription cfg = new CConfigurationDescription(id, name, base, this);
		configurationCreated(cfg);
		return cfg;
	}

	@Override
	public ICConfigurationDescription getActiveConfiguration() {
		return fActiveCfgInfo.getConfiguration();
	}

	private String getFirstCfgId() {
		if (!fCfgMap.isEmpty()) {
			return fCfgMap.keySet().iterator().next();
		}
		return null;
	}

	@Override
	public ICConfigurationDescription getConfigurationById(String id) {
		return fCfgMap.get(id);
	}

	@Override
	public ICConfigurationDescription getConfigurationByName(String name) {
		for (Iterator<ICConfigurationDescription> iter = fCfgMap.values().iterator(); iter.hasNext();) {
			ICConfigurationDescription cfg = iter.next();
			if (name.equals(cfg.getName()))
				return cfg;
		}
		return null;
	}

	@Override
	public ICConfigurationDescription[] getConfigurations() {
		return fCfgMap.values().toArray(new ICConfigurationDescription[fCfgMap.size()]);
	}

	@Override
	public void removeConfiguration(String name) throws WriteAccessException {
		if (fIsReadOnly)
			throw ExceptionFactory.createIsReadOnlyException();

		CConfigurationDescription cfgDes = (CConfigurationDescription) getConfigurationByName(name);
		if (cfgDes != null) {
			cfgDes.removeConfiguration();
		}
	}

	void configurationRemoved(CConfigurationDescription des) {
		fCfgMap.remove(des.getId());
		fIsModified = true;

		fActiveCfgInfo.configurationRemoved(des);
		fSettingCfgInfo.configurationRemoved(des);
	}

	@Override
	public void removeConfiguration(ICConfigurationDescription cfg) throws WriteAccessException {
		if (fIsReadOnly)
			throw ExceptionFactory.createIsReadOnlyException();

		((CConfigurationDescription) cfg).removeConfiguration();
	}

	@Override
	public void setActiveConfiguration(ICConfigurationDescription cfg) throws WriteAccessException {
		if (fIsReadOnly)
			throw ExceptionFactory.createIsReadOnlyException();
		if (cfg == null)
			throw new NullPointerException();

		fActiveCfgInfo.setConfiguration(cfg);

		if (getConfigurationRelations() == CONFIGS_LINK_SETTINGS_AND_ACTIVE)
			fSettingCfgInfo.setConfiguration(cfg);
	}

	@Override
	public IProject getProject() {
		return fProject;
	}

	@Override
	public ICStorageElement getStorage(String moduleId, boolean create) throws CoreException {
		return getStorageBase().getStorage(moduleId, create);
	}

	@Override
	public ICStorageElement importStorage(String id, ICStorageElement el)
			throws UnsupportedOperationException, CoreException {
		return getStorageBase().importStorage(id, el);
	}

	//	public boolean containsStorage(String id) throws CoreException {
	//		return getStorageBase().containsStorage(id);
	//	}

	@Override
	public ICSettingObject[] getChildSettings() {
		return getConfigurations();
	}

	@Override
	public ICConfigurationDescription getConfiguration() {
		return null;
	}

	@Override
	public String getId() {
		//TODO:
		return null;
	}

	@Override
	public final int getType() {
		return ICSettingBase.SETTING_PROJECT;
	}

	@Override
	public String getName() {
		return fProject.getName();
	}

	@Override
	public ICSettingContainer getParent() {
		return null;
	}

	@Override
	public boolean isValid() {
		return /*fProject.exists() &&*/ fCfgMap.size() > 0;
	}

	@Override
	public void updateChild(CDataProxy child, boolean write) {
		if (write) {
			try {
				String oldId = child.getId();
				CConfigurationDescription cfgDes = ((CConfigurationDescription) child);
				cfgDes.doWritable();
				updateMap(cfgDes, oldId);
			} catch (CoreException e) {
				CCorePlugin.log(e);
			}
		}
	}

	void updateMap(CConfigurationDescription des, String oldId) {
		if (!oldId.equals(des.getId())) {
			fCfgMap.remove(oldId);
			fCfgMap.put(des.getId(), des);
		}
	}

	public ICStorageElement getRootStorageElement() throws CoreException {
		if (fRootStorageElement == null)
			throw ExceptionFactory.createCoreException("CProjectDescription ICStorageElement == null"); //$NON-NLS-1$

		//		if (fRootStorageElement == null) {
		//			fRootStorageElement = CProjectDescriptionManager.getInstance().createStorage(fProject, true, true, isReadOnly());
		//		}
		return fRootStorageElement;
	}

	//	ICStorageElement doGetCachedRootStorageElement() {
	//		return fRootStorageElement;
	//	}

	ICSettingsStorage getStorageBase() throws CoreException {
		if (fStorage == null)
			//			fStorage = new CStorage((InternalXmlStorageElement) getRootStorageElement());
			throw ExceptionFactory.createCoreException("CProjectDescription ICSettingsStorage == null"); //$NON-NLS-1$
		return fStorage;
	}

	@Override
	public ICConfigurationDescription createConfiguration(String buildSystemId, CConfigurationData data)
			throws CoreException {
		if (fIsReadOnly)
			throw ExceptionFactory.createIsReadOnlyException();
		CConfigurationDescription cfg = new CConfigurationDescription(data, buildSystemId, this);
		configurationCreated(cfg);
		return cfg;
	}

	public CConfigurationDescription createConvertedConfiguration(String id, String name, ICStorageElement el)
			throws CoreException {
		if (fIsReadOnly)
			throw ExceptionFactory.createIsReadOnlyException();
		CConfigurationDescription cfg = new CConfigurationDescription(id, name, el, this);
		configurationCreated(cfg);
		return cfg;
	}

	boolean needsDescriptionPersistence() {
		if (fIsModified)
			return true;

		if (fPrefs.isModified())
			return true;

		if (fStorage.isModified())
			return true;

		for (ICConfigurationDescription cfgDes : fCfgMap.values()) {
			if (cfgDes.isModified())
				return true;
		}

		return false;
	}

	@Override
	public boolean isModified() {
		if (needsDescriptionPersistence())
			return true;

		if (needsActiveCfgPersistence())
			return true;

		if (needsSettingCfgPersistence())
			return true;

		return false;
	}

	private void setModified(boolean modified) {
		fIsModified = modified;

		if (!modified) {
			fActiveCfgInfo.fIsCfgModified = false;

			fSettingCfgInfo.fIsCfgModified = false;

			fPrefs.setModified(false);

			// no need to do that for config cache since they always maintain the "isModified == false"
		}
	}

	@Override
	public boolean isReadOnly() {
		return fIsReadOnly && !(fIsLoading || fIsApplying);
	}

	@Override
	public void setReadOnly(boolean readOnly, boolean keepModify) {
		fIsReadOnly = readOnly;
		try {
			getStorageBase().setReadOnly(readOnly, keepModify);
		} catch (CoreException e) {
			CCorePlugin.log(e);
		}
	}

	public ICSettingObject getChildSettingById(String id) {
		return getConfigurationById(id);
	}

	@Override
	public ICConfigurationDescription getDefaultSettingConfiguration() {
		if (getConfigurationRelations() == CONFIGS_LINK_SETTINGS_AND_ACTIVE)
			return getActiveConfiguration();

		return fSettingCfgInfo.getConfiguration();
	}

	@Override
	public void setDefaultSettingConfiguration(ICConfigurationDescription cfg) throws WriteAccessException {
		if (fIsReadOnly)
			throw ExceptionFactory.createIsReadOnlyException();
		if (cfg == null)
			throw new NullPointerException();

		fSettingCfgInfo.setConfiguration(cfg);

		if (getConfigurationRelations() == CONFIGS_LINK_SETTINGS_AND_ACTIVE)
			fActiveCfgInfo.setConfiguration(cfg);
	}

	@Override
	public Object getSessionProperty(QualifiedName name) {
		return fPropertiesMap.get(name);
	}

	@Override
	public void setSessionProperty(QualifiedName name, Object value) {
		if (value != null) {
			fPropertiesMap.put(name, value);
		} else {
			fPropertiesMap.remove(name);
		}

		fIsModified = true;
	}

	@Override
	public void removeStorage(String id) throws CoreException {
		getStorageBase().removeStorage(id);
	}

	void switchToCachedAppliedData(CProjectDescription appliedCache) {
		if (fIsReadOnly)
			return;

		ICConfigurationDescription[] cfgs = appliedCache.getConfigurations();
		for (int i = 0; i < cfgs.length; i++) {
			CConfigurationDescriptionCache cfgCache = (CConfigurationDescriptionCache) cfgs[i];
			CConfigurationDescription des = (CConfigurationDescription) getChildSettingById(cfgCache.getId());
			if (des != null) {
				des.setData(cfgCache);
				//				ICResourceDescription rcDes = des.getResourceDescription(new Path("dd"), false);
				//				rcDes = des.getResourceDescription(new Path("dd"), false);
				//				ICBuildSetting bs = des.getBuildSetting();
				//				ICLanguageSetting lss[] = ((ICFolderDescription) rcDes).getLanguageSettings();
			}
		}
	}

	boolean checkPersistActiveCfg(String oldId, boolean force) {
		return fActiveCfgInfo.store(oldId, force);
	}

	boolean checkPersistSettingCfg(String oldId, boolean force) {
		return fSettingCfgInfo.store(oldId, force);
	}

	boolean needsActiveCfgPersistence() {
		return fActiveCfgInfo.fIsCfgModified;
	}

	boolean needsSettingCfgPersistence() {
		return fSettingCfgInfo.fIsCfgModified;
	}

	CProjectDescriptionPreferences getPreferences() {
		return fPrefs;
	}

	@Override
	public int getConfigurationRelations() {
		return fPrefs.getConfigurationRelations();
	}

	@Override
	public boolean isDefaultConfigurationRelations() {
		return fPrefs.isDefaultConfigurationRelations();
	}

	@Override
	public void setConfigurationRelations(int status) {
		fPrefs.setConfigurationRelations(status);
	}

	@Override
	public void useDefaultConfigurationRelations() {
		fPrefs.useDefaultConfigurationRelations();
	}

	@Override
	public boolean isCdtProjectCreating() {
		return fIsCreating;
	}

	@Override
	public void setCdtProjectCreated() {
		if (!fIsCreating)
			return;

		if (fIsReadOnly)
			throw ExceptionFactory.createIsReadOnlyException();

		fIsCreating = false;
		fIsModified = true;
	}

	public void touch() throws WriteAccessException {
		if (fIsReadOnly)
			throw ExceptionFactory.createIsReadOnlyException();

		fIsModified = true;
	}
}
