/*******************************************************************************
 * Copyright (c) 2008, 2011 Broadcom 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:
 * James Blackburn (Broadcom Corp.)
 *******************************************************************************/

package org.eclipse.cdt.internal.core.settings.model.xml2;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Queue;

import org.eclipse.cdt.core.CCorePlugin;
import org.eclipse.cdt.core.settings.model.ICStorageElement;
import org.eclipse.cdt.internal.core.settings.model.CProjectDescriptionManager;
import org.eclipse.cdt.internal.core.settings.model.CProjectDescriptionStorageManager;
import org.eclipse.cdt.internal.core.settings.model.ICProjectDescriptionStorageType;
import org.eclipse.cdt.internal.core.settings.model.ICProjectDescriptionStorageType.CProjectDescriptionStorageTypeProxy;
import org.eclipse.cdt.internal.core.settings.model.xml.InternalXmlStorageElement;
import org.eclipse.cdt.internal.core.settings.model.xml.XmlProjectDescriptionStorage;
import org.eclipse.cdt.internal.core.settings.model.xml.XmlStorage;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceProxy;
import org.eclipse.core.resources.IResourceProxyVisitor;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.osgi.framework.Version;
import org.w3c.dom.Element;

/**
 * This class extends XmlProjectDescriptionStroage to provide the following
 * functionality:
 *   - Configuration Description storage modules are persisted in separate XML
 *     files stored under .csettings in the project root directory.  They
 *     are linked into the main .cproject file via "externalCElementFile" element
 *
 * It is backwards compatible with XmlProjectDescriptionStorage.  If it finds a file
 * with Version less than 5.0, it will delegate to the previous XmlProjectDescriptionStorage
 *
 * This allows users to more easily version control their CDT project descriptions
 * @see <a href="https://bugs.eclipse.org/bugs/show_bug.cgi?id=226457">Bug 226457</a>
 */
public class XmlProjectDescriptionStorage2 extends XmlProjectDescriptionStorage {
	/** Folder Name for storing Project specific configuration settings */
	static final String STORAGE_FOLDER_NAME = ".csettings"; //$NON-NLS-1$
	/** Element providing the name of the external C Element file */
	public static final String EXTERNAL_CELEMENT_KEY = "externalCElementFile"; //$NON-NLS-1$
	private static final String ID = "id"; //$NON-NLS-1$

	/** Override the description version, we support version '5.0' files */
	@SuppressWarnings("hiding")
	public static final Version STORAGE_DESCRIPTION_VERSION = new Version("5.0"); //$NON-NLS-1$
	/** The storage type id of this extended project description type */
	@SuppressWarnings("hiding")
	public static final String STORAGE_TYPE_ID = CCorePlugin.PLUGIN_ID + ".XmlProjectDescriptionStorage2"; //$NON-NLS-1$

	/** Map from storageModuleId to modification time stamp */
	Map<String, Long> modificationMap = Collections.synchronizedMap(new HashMap<String, Long>());

	public XmlProjectDescriptionStorage2(CProjectDescriptionStorageTypeProxy type, IProject project, Version version) {
		super(type, project, version);
	}

	/*
	 * Check for external modification in the module files in the .csettings directory
	 * (non-Javadoc)
	 * @see org.eclipse.cdt.internal.core.settings.model.xml.XmlProjectDescriptionStorage#checkExternalModification()
	 */
	@Override
	protected synchronized boolean checkExternalModification() {
		// Check if our parent thinks we need a reload
		if (super.checkExternalModification())
			return true;
		// If not currently loaded, then nothing to do
		if (getLoadedDescription() == null)
			return false;
		final boolean[] needReload = new boolean[] { false };
		try {
			project.getFolder(STORAGE_FOLDER_NAME).accept(new IResourceProxyVisitor() {
				@Override
				public boolean visit(IResourceProxy proxy) throws CoreException {
					if (modificationMap.containsKey(proxy.getName())) {
						long modStamp = getModificationStamp(proxy.requestResource());
						if (modificationMap.get(proxy.getName()) != modStamp) {
							// There may be old storages in here, ensure we don't infinite reload...
							modificationMap.put(proxy.getName(), modStamp);
							setCurrentDescription(null, true);
							needReload[0] = true;
						}
					}
					return true;
				}
			}, IResource.NONE);
		} catch (CoreException e) {
			// STORAGE_FOLDER_NAME doesn't exist... or something went wrong during reload
			if (project.getFolder(STORAGE_FOLDER_NAME).exists())
				CCorePlugin.log(
						"XmlProjectDescriptionStorage2: Problem checking for external modification: " + e.getMessage()); //$NON-NLS-1$
		}
		return needReload[0];
	}

	@Override
	protected final InternalXmlStorageElement createStorage(IContainer container, String fileName, boolean reCreate,
			boolean createEmptyIfNotFound, boolean readOnly) throws CoreException {
		InternalXmlStorageElement el = super.createStorage(container, fileName, reCreate, createEmptyIfNotFound,
				readOnly);

		Queue<ICStorageElement> nodesToCheck = new LinkedList<>();
		nodesToCheck.addAll(Arrays.asList(el.getChildren()));
		while (!nodesToCheck.isEmpty()) {
			ICStorageElement currEl = nodesToCheck.remove();

			// If not a storageModule element or doesn't have EXTERNAL_CELEMENT_KEY, continue
			if (!XmlStorage.MODULE_ELEMENT_NAME.equals(currEl.getName())
					|| currEl.getAttribute(EXTERNAL_CELEMENT_KEY) == null) {
				nodesToCheck.addAll(Arrays.asList(currEl.getChildren()));
				continue;
			}

			try {
				// Found -- load the Document pointed to (allows multiple layers of indirection if need be)
				InternalXmlStorageElement el2 = createStorage(project.getFolder(STORAGE_FOLDER_NAME),
						currEl.getAttribute(EXTERNAL_CELEMENT_KEY), reCreate, createEmptyIfNotFound, readOnly);
				// Update the modification stamp
				modificationMap.put(currEl.getAttribute(EXTERNAL_CELEMENT_KEY), getModificationStamp(
						project.getFolder(STORAGE_FOLDER_NAME).getFile(currEl.getAttribute(EXTERNAL_CELEMENT_KEY))));

				ICStorageElement currParent = currEl.getParent();
				// Get the storageModule element in the new Document
				ICStorageElement[] childStorages = el2.getChildrenByName(XmlStorage.MODULE_ELEMENT_NAME);
				Assert.isTrue(childStorages.length == 1, "More than one storageModule in external Document!"); //$NON-NLS-1$

				// Import the loaded element
				currParent.importChild(childStorages[0]);
				// Remove the placeholder
				currParent.removeChild(currEl);
			} catch (CoreException e) {
				// If we're here there was a problem loading csettings when there is a .cproject file.
				CCorePlugin.log(e);
				throw e;
			}
		}
		return el;
	}

	/*
	 * Serialize the Project Description:
	 *    - The .cproject file contains the root XML Elements.
	 *    - We serialize the storageModule children of the CConfiguration elements (in the org.eclipse.cdt.settings module)
	 *      to separate files in the .csettings  directory to prevent unmanageably large XML deltas
	 * Return the modification stamp of the main .cproject file as our super method does
	 * (non-Javadoc)
	 * @see org.eclipse.cdt.internal.core.settings.model.xml.XmlProjectDescriptionStorage#serialize(org.eclipse.core.resources.IContainer, java.lang.String, org.eclipse.cdt.core.settings.model.ICStorageElement)
	 */
	@Override
	protected long serialize(IContainer container, String file, ICStorageElement element) throws CoreException {
		// If the current loaded version is less than our version, then delegate to parent to handle persisting
		// i.e. don't auto-upgrade
		if (version.compareTo(type.version) < 0)
			return super.serialize(container, file, element);

		// Copy the original passed in element, as we're going to re-write the children
		InternalXmlStorageElement copy = copyElement(element, false);
		// Map containing external CConfiguration elements to be serialized
		Map<String, InternalXmlStorageElement> externalStorageElements = new HashMap<>();

		// Iterate through the initial children
		for (ICStorageElement el : copy.getChildren()) {
			// Only interested in root level org.eclipse.cdt.settings storageModules
			if (!CProjectDescriptionManager.MODULE_ID.equals(el.getAttribute(XmlStorage.MODULE_ID_ATTRIBUTE)))
				continue;

			Queue<ICStorageElement> configStorages = new LinkedList<>();
			configStorages.addAll(Arrays.asList(el.getChildren()));
			while (!configStorages.isEmpty()) {
				InternalXmlStorageElement iEl = (InternalXmlStorageElement) configStorages.remove();

				// If not a stroageModule element, the just add children ...
				// e.g. configuration element...
				if (iEl.getAttribute(XmlStorage.MODULE_ID_ATTRIBUTE) == null) {
					configStorages.addAll(Arrays.asList(iEl.getChildren()));
					continue;
				}

				// Persist this storage Element fileName = configurationID_moduleID
				String storageId = ((Element) iEl.fElement.getParentNode()).getAttribute(ID) + "_" //$NON-NLS-1$
						+ iEl.getAttribute(XmlStorage.MODULE_ID_ATTRIBUTE);

				// create a dummy storage for the element <cproject> held
				InternalXmlStorageElement newEl = createStorage(project.getFolder(STORAGE_FOLDER_NAME), storageId,
						false, true, false);
				newEl.importChild(copyElement(iEl, false));
				externalStorageElements.put(storageId, newEl);

				// Clear other attributes and children
				iEl.clear();
				// Set the external c element key
				iEl.fElement.setAttribute(EXTERNAL_CELEMENT_KEY, storageId);
			}
		}

		// Serialize the Root XML document Element (.cproject)
		long cproject_mod_time = super.serialize(project, ICProjectDescriptionStorageType.STORAGE_FILE_NAME, copy);

		// Check that the .csettings directory exists and is writable
		IFolder csettings = project.getFolder(STORAGE_FOLDER_NAME);
		try {
			CProjectDescriptionStorageManager.ensureWritable(csettings);
			if (!csettings.exists())
				csettings.create(true, true, new NullProgressMonitor());
		} catch (CoreException e) {
			if (!csettings.exists())
				throw e;
		}

		// Serialize all the external elements
		for (Map.Entry<String, InternalXmlStorageElement> e : externalStorageElements.entrySet())
			modificationMap.put(e.getKey(), super.serialize(csettings, e.getKey(), e.getValue()));

		return cproject_mod_time;
	}

	@Override
	protected Version getVersion() {
		return STORAGE_DESCRIPTION_VERSION;
	}

	@Override
	protected String getStorageTypeId() {
		return STORAGE_TYPE_ID;
	}

}
