/*******************************************************************************
 *  Copyright (c) 2004, 2011 IBM 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:
 *    IBM - Initial API and implementation
 *    Anton Leherbauer (Wind River Systems)
 *    Markus Schorn (Wind River Systems)
 *******************************************************************************/
package org.eclipse.cdt.make.internal.core.scannerconfig2;

import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.eclipse.cdt.core.CCProjectNature;
import org.eclipse.cdt.core.CProjectNature;
import org.eclipse.cdt.core.model.CModelException;
import org.eclipse.cdt.core.model.CoreModel;
import org.eclipse.cdt.core.model.ICProject;
import org.eclipse.cdt.core.model.IPathEntry;
import org.eclipse.cdt.make.core.MakeCorePlugin;
import org.eclipse.cdt.make.core.scannerconfig.IDiscoveredPathManager.IDiscoveredPathInfo;
import org.eclipse.cdt.make.core.scannerconfig.IDiscoveredPathManager.IPerProjectDiscoveredPathInfo;
import org.eclipse.cdt.make.core.scannerconfig.IExternalScannerInfoProvider;
import org.eclipse.cdt.make.core.scannerconfig.IScannerConfigBuilderInfo2;
import org.eclipse.cdt.make.core.scannerconfig.IScannerInfoCollector;
import org.eclipse.cdt.make.core.scannerconfig.IScannerInfoCollector2;
import org.eclipse.cdt.make.core.scannerconfig.IScannerInfoCollector3;
import org.eclipse.cdt.make.core.scannerconfig.IScannerInfoCollectorCleaner;
import org.eclipse.cdt.make.core.scannerconfig.InfoContext;
import org.eclipse.cdt.make.core.scannerconfig.ScannerInfoTypes;
import org.eclipse.cdt.make.internal.core.MakeMessages;
import org.eclipse.cdt.make.internal.core.scannerconfig.DiscoveredPathContainer;
import org.eclipse.cdt.make.internal.core.scannerconfig.DiscoveredPathInfo;
import org.eclipse.cdt.make.internal.core.scannerconfig.DiscoveredScannerInfoStore;
import org.eclipse.cdt.make.internal.core.scannerconfig.ScannerConfigUtil;
import org.eclipse.cdt.make.internal.core.scannerconfig.util.CygpathTranslator;
import org.eclipse.cdt.make.internal.core.scannerconfig.util.SymbolEntry;
import org.eclipse.cdt.make.internal.core.scannerconfig.util.TraceUtil;
import org.eclipse.cdt.utils.EFSExtensionManager;
import org.eclipse.core.filesystem.EFS;
import org.eclipse.core.filesystem.IFileInfo;
import org.eclipse.core.filesystem.IFileStore;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.ISafeRunnable;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.SafeRunner;
import org.w3c.dom.Element;

/**
 * New per project scanner info collector
 *
 * @since 3.0
 * @author vhirsl
 */
public class PerProjectSICollector implements IScannerInfoCollector3, IScannerInfoCollectorCleaner {
	public static final String COLLECTOR_ID = MakeCorePlugin.getUniqueIdentifier() + ".PerProjectSICollector"; //$NON-NLS-1$

	protected IProject project;
	protected InfoContext context;
	private boolean isBuiltinConfig = false;

	protected Map<ScannerInfoTypes, List<String>> discoveredSI;
	//    private List discoveredIncludes;
	//	private List discoveredSymbols;
	//	private List discoveredTSO;	// target specific options
	// cumulative values
	protected List<String> sumDiscoveredIncludes;
	private Map<String, SymbolEntry> sumDiscoveredSymbols;
	protected boolean scPersisted = false;

	public PerProjectSICollector() {
		discoveredSI = new HashMap<>();
		//		discoveredIncludes = new ArrayList();
		//		discoveredSymbols = new ArrayList();
		//		discoveredTSO = new ArrayList();
		//
		sumDiscoveredIncludes = new ArrayList<>();
		sumDiscoveredSymbols = new LinkedHashMap<>();
	}

	/* (non-Javadoc)
	 * @see org.eclipse.cdt.make.core.scannerconfig.IScannerInfoCollector2#setProject(org.eclipse.core.resources.IProject)
	 */
	@Override
	public void setProject(IProject project) {
		this.project = project;
		this.context = new InfoContext(project);
	}

	public synchronized void contributeToScannerConfig(Object resource, @SuppressWarnings("rawtypes") Map scannerInfo,
			boolean isBuiltinConfig) {
		this.isBuiltinConfig = isBuiltinConfig;
		try {
			contributeToScannerConfig(resource, scannerInfo);
		} finally {
			this.isBuiltinConfig = false;
		}
	}

	/* (non-Javadoc)
	 * @see org.eclipse.cdt.make.core.scannerconfig.IScannerInfoCollector#contributeToScannerConfig(java.lang.Object, java.util.Map)
	 */
	@Override
	public synchronized void contributeToScannerConfig(Object resource, @SuppressWarnings("rawtypes") Map scannerInfo) {
		// check the resource
		String errorMessage = null;
		if (resource == null) {
			errorMessage = "resource is null";//$NON-NLS-1$
		} else if (!(resource instanceof IResource)) {
			errorMessage = "resource is not an IResource";//$NON-NLS-1$
		} else if (((IResource) resource).getProject() == null) {
			errorMessage = "project is null";//$NON-NLS-1$
		} else if (!((IResource) resource).getProject().equals(project)) {
			errorMessage = "wrong project";//$NON-NLS-1$
		}
		if (errorMessage != null) {
			TraceUtil.outputError("PerProjectSICollector.contributeToScannerConfig : ", errorMessage); //$NON-NLS-1$
			return;
		}

		if (scPersisted) {
			// delete discovered scanner config
			discoveredSI.clear();
			// new collection cycle
			scPersisted = false;
		}
		try {
			if (/*project.hasNature(MakeProjectNature.NATURE_ID) && */// limits to StandardMake projects
			(project.hasNature(CProjectNature.C_NATURE_ID) || project.hasNature(CCProjectNature.CC_NATURE_ID))) {

				for (Object name : scannerInfo.keySet()) {
					ScannerInfoTypes siType = (ScannerInfoTypes) name;
					@SuppressWarnings("unchecked")
					List<String> delta = (List<String>) scannerInfo.get(siType);

					List<String> discovered = discoveredSI.get(siType);
					if (discovered == null) {
						discovered = new ArrayList<>(delta);
						discoveredSI.put(siType, discovered);
					} else {
						final boolean addSorted = !isBuiltinConfig && siType.equals(ScannerInfoTypes.INCLUDE_PATHS);
						contribute(discovered, delta, addSorted);
					}
				}
			}
		} catch (CoreException e) {
			MakeCorePlugin.log(e);
		}
	}

	/**
	 * @param discovered symbols | includes | targetSpecificOptions
	 * @param delta symbols | includes | targetSpecificOptions
	 * @param ordered - to preserve order or append at the end
	 * @return true if there is a change in discovered symbols | includes | targetSpecificOptions
	 */
	private boolean contribute(List<String> discovered, List<String> delta, boolean ordered) {
		if (delta == null || delta.isEmpty())
			return false;
		return addItemsWithOrder(discovered, delta, ordered);
	}

	/**
	 * Adds new items to the already accumulated ones preserving order
	 *
	 * @param sumIncludes - previously accumulated items
	 * @param includes - items to be added
	 * @param ordered - to preserve order or append at the end
	 * @return boolean - true if added
	 */
	protected boolean addItemsWithOrder(List<String> sumIncludes, List<String> includes, boolean ordered) {
		if (includes.isEmpty())
			return false;

		boolean addedIncludes = false;
		int insertionPoint = ordered ? 0 : sumIncludes.size();
		for (String item : includes) {
			int pos = sumIncludes.indexOf(item);
			if (pos >= 0) {
				if (ordered) {
					insertionPoint = pos + 1;
				}
			} else {
				sumIncludes.add(insertionPoint++, item);
				addedIncludes = true;
			}
		}
		return addedIncludes;
	}

	/* (non-Javadoc)
	 * @see org.eclipse.cdt.make.core.scannerconfig.IScannerInfoCollector2#updateScannerConfiguration(org.eclipse.core.resources.IProject, org.eclipse.core.runtime.IProgressMonitor)
	 */
	@Override
	public synchronized void updateScannerConfiguration(IProgressMonitor monitor) throws CoreException {
		if (monitor == null) {
			monitor = new NullProgressMonitor();
		}
		IDiscoveredPathInfo pathInfo = MakeCorePlugin.getDefault().getDiscoveryManager().getDiscoveredInfo(project,
				context);
		if (pathInfo instanceof IPerProjectDiscoveredPathInfo) {
			IPerProjectDiscoveredPathInfo projectPathInfo = (IPerProjectDiscoveredPathInfo) pathInfo;

			monitor.beginTask(MakeMessages.getString("ScannerInfoCollector.Processing"), 100); //$NON-NLS-1$
			monitor.subTask(MakeMessages.getString("ScannerInfoCollector.Processing")); //$NON-NLS-1$
			if (scannerConfigNeedsUpdate(projectPathInfo)) {
				monitor.worked(50);
				monitor.subTask(MakeMessages.getString("ScannerInfoCollector.Updating") + project.getName()); //$NON-NLS-1$
				try {
					// update scanner configuration
					List<IResource> resourceDelta = new ArrayList<>(1);
					resourceDelta.add(project);
					MakeCorePlugin.getDefault().getDiscoveryManager().updateDiscoveredInfo(context, pathInfo,
							context.isDefaultContext(), resourceDelta);
					monitor.worked(50);
				} catch (CoreException e) {
					MakeCorePlugin.log(e);
				}
			}
			monitor.done();
			scPersisted = true;
		}
	}

	/**
	 * Compare discovered include paths and symbol definitions with the ones from scanInfo.
	 */
	private boolean scannerConfigNeedsUpdate(IPerProjectDiscoveredPathInfo discPathInfo) {
		boolean addedIncludes = includePathsNeedUpdate(discPathInfo);
		boolean addedSymbols = definedSymbolsNeedUpdate(discPathInfo);

		return (addedIncludes | addedSymbols);
	}

	/**
	 * Compare include paths with already discovered.
	 */
	private boolean includePathsNeedUpdate(IPerProjectDiscoveredPathInfo discPathInfo) {
		boolean addedIncludes = false;
		List<String> discoveredIncludes = discoveredSI.get(ScannerInfoTypes.INCLUDE_PATHS);
		if (discoveredIncludes != null) {
			// Step 1. Add discovered scanner config to the existing discovered scanner config
			// add the includes from the latest discovery
			//			if (sumDiscoveredIncludes == null) {
			//				sumDiscoveredIncludes = new ArrayList(discoveredIncludes);
			//				addedIncludes = true;
			//			}
			//			else {
			//				addedIncludes = addItemsWithOrder(sumDiscoveredIncludes, discoveredIncludes, true);
			//			}
			// instead
			addedIncludes = addItemsWithOrder(sumDiscoveredIncludes, discoveredIncludes, true);

			// try to translate cygpaths to absolute paths
			List<String> finalSumIncludes = CygpathTranslator.translateIncludePaths(project, sumDiscoveredIncludes);

			// Step 2. Get project's scanner config
			LinkedHashMap<String, Boolean> persistedIncludes = discPathInfo.getIncludeMap();

			// Step 3. Merge scanner config from steps 1 and 2
			// order is important, use list to preserve it
			ArrayList<String> persistedKeyList = new ArrayList<>(persistedIncludes.keySet());
			addedIncludes = addItemsWithOrder(persistedKeyList, finalSumIncludes, true);

			LinkedHashMap<String, Boolean> newPersistedIncludes;
			if (addedIncludes) {
				newPersistedIncludes = new LinkedHashMap<>(persistedKeyList.size());
				for (String include : persistedKeyList) {
					if (persistedIncludes.containsKey(include)) {
						newPersistedIncludes.put(include, persistedIncludes.get(include));
					} else {
						// the paths may be on EFS resources, not local
						Boolean includePathExists = true;
						URI projectLocationURI = discPathInfo.getProject().getLocationURI();

						// use the project's location... create a URI that uses the same provider but that points to the include path
						URI includeURI = EFSExtensionManager.getDefault().createNewURIFromPath(projectLocationURI,
								include);

						// ask EFS if the path exists
						try {
							IFileStore fileStore = EFS.getStore(includeURI);
							IFileInfo info = fileStore.fetchInfo();
							if (!info.exists()) {
								includePathExists = false;
							}
						} catch (CoreException e) {
							MakeCorePlugin.log(e);
						}

						// if the include path doesn't exist, then we tell the scanner config system that the folder
						// has been "removed", and thus it won't show up in the UI
						newPersistedIncludes.put(include, !includePathExists);
					}
				}
			} else {
				newPersistedIncludes = persistedIncludes;
			}

			// Step 4. Set resulting scanner config
			discPathInfo.setIncludeMap(newPersistedIncludes);
		}
		return addedIncludes;
	}

	/**
	 * Compare symbol definitions with already discovered.
	 */
	protected boolean definedSymbolsNeedUpdate(IPerProjectDiscoveredPathInfo discPathInfo) {
		boolean addedSymbols = false;
		List<String> discoveredSymbols = discoveredSI.get(ScannerInfoTypes.SYMBOL_DEFINITIONS);
		if (discoveredSymbols != null) {
			// Step 1. Add discovered scanner config to the existing discovered scanner config
			// add the symbols from the latest discovery
			//			if (sumDiscoveredSymbols == null) {
			//				sumDiscoveredSymbols = new LinkedHashMap();
			//			}
			addedSymbols = ScannerConfigUtil.scAddSymbolsList2SymbolEntryMap(sumDiscoveredSymbols, discoveredSymbols,
					true);

			// Step 2. Get project's scanner config
			LinkedHashMap<String, SymbolEntry> persistedSymbols = discPathInfo.getSymbolMap();

			// Step 3. Merge scanner config from steps 1 and 2
			LinkedHashMap<String, SymbolEntry> candidateSymbols = new LinkedHashMap<>(persistedSymbols);
			addedSymbols |= ScannerConfigUtil.scAddSymbolEntryMap2SymbolEntryMap(candidateSymbols,
					sumDiscoveredSymbols);

			// Step 4. Set resulting scanner config
			discPathInfo.setSymbolMap(candidateSymbols);
		}
		return addedSymbols;
	}

	/* (non-Javadoc)
	 * @see org.eclipse.cdt.make.core.scannerconfig.IScannerInfoCollector#getCollectedScannerInfo(java.lang.Object, org.eclipse.cdt.make.core.scannerconfig.ScannerInfoTypes)
	 */
	@Override
	public List<String> getCollectedScannerInfo(Object resource, ScannerInfoTypes type) {
		List<String> rv = null;
		// check the resource
		String errorMessage = null;
		if (resource == null) {
			errorMessage = "resource is null";//$NON-NLS-1$
		} else if (!(resource instanceof IResource)) {
			errorMessage = "resource is not an IResource";//$NON-NLS-1$
		} else if (((IResource) resource).getProject() == null) {
			errorMessage = "project is null";//$NON-NLS-1$
		} else if (((IResource) resource).getProject() != project) {
			errorMessage = "wrong project";//$NON-NLS-1$
		}

		if (errorMessage != null) {
			TraceUtil.outputError("PerProjectSICollector.getCollectedScannerInfo : ", errorMessage); //$NON-NLS-1$
		} else if (resource != null && project.equals(((IResource) resource).getProject())) {
			rv = discoveredSI.get(type);
		}
		return rv;
	}

	/* (non-Javadoc)
	 * @see org.eclipse.cdt.make.core.scannerconfig.IScannerInfoCollector2#getDefinedSymbols()
	 */
	public Map<String, String> getDefinedSymbols() {
		Map<String, String> definedSymbols = ScannerConfigUtil.scSymbolEntryMap2Map(sumDiscoveredSymbols);
		return definedSymbols;
	}

	/* (non-Javadoc)
	 * @see org.eclipse.cdt.make.core.scannerconfig.IScannerInfoCollector2#getIncludePaths()
	 */
	public List<String> getIncludePaths() {
		return sumDiscoveredIncludes;
	}

	/* (non-Javadoc)
	 * @see org.eclipse.cdt.make.core.scannerconfig.IScannerInfoCollectorUtil#serialize(org.w3c.dom.Element)
	 */
	public void serialize(Element root) {
		// not supported in PerProject collector
	}

	/* (non-Javadoc)
	 * @see org.eclipse.cdt.make.core.scannerconfig.IScannerInfoCollectorUtil#deserialize(org.w3c.dom.Element)
	 */
	public void deserialize(Element root) {
		// not supported in PerProject collector
	}

	/* (non-Javadoc)
	 * @see org.eclipse.cdt.make.core.scannerconfig.IScannerInfoCollectorUtil#deleteAllPaths(org.eclipse.core.resources.IResource)
	 */
	@Override
	public void deleteAllPaths(IResource resource) {
		IProject project = resource.getProject();
		if (project != null && project.equals(this.project)) {
			sumDiscoveredIncludes.clear();
		}
	}

	/* (non-Javadoc)
	 * @see org.eclipse.cdt.make.core.scannerconfig.IScannerInfoCollectorUtil#deleteAllSymbols(org.eclipse.core.resources.IResource)
	 */
	@Override
	public void deleteAllSymbols(IResource resource) {
		IProject project = resource.getProject();
		if (project != null && project.equals(this.project)) {
			sumDiscoveredSymbols.clear();
		}
	}

	/* (non-Javadoc)
	 * @see org.eclipse.cdt.make.core.scannerconfig.IScannerInfoCollectorUtil#deletePath(org.eclipse.core.resources.IResource, java.lang.String)
	 */
	@Override
	public void deletePath(IResource resource, String path) {
		IProject project = resource.getProject();
		if (project != null && project.equals(this.project)) {
			sumDiscoveredIncludes.remove(path);
		}
	}

	/* (non-Javadoc)
	 * @see org.eclipse.cdt.make.core.scannerconfig.IScannerInfoCollectorUtil#deleteSymbol(org.eclipse.core.resources.IResource, java.lang.String)
	 */
	@Override
	public void deleteSymbol(IResource resource, String symbol) {
		IProject project = resource.getProject();
		if (project != null && project.equals(this.project)) {
			// remove it from the Map of SymbolEntries
			ScannerConfigUtil.removeSymbolEntryValue(symbol, sumDiscoveredSymbols);
		}
	}

	/* (non-Javadoc)
	 * @see org.eclipse.cdt.make.core.scannerconfig.IScannerInfoCollectorCleaner#deleteAll(org.eclipse.core.resources.IResource)
	 */
	@Override
	public void deleteAll(IResource resource) {
		deleteAllPaths(resource);
		deleteAllSymbols(resource);
	}

	/* (non-Javadoc)
	 * @see org.eclipse.cdt.make.core.scannerconfig.IScannerInfoCollector2#createPathInfoObject()
	 */
	@Override
	public IDiscoveredPathInfo createPathInfoObject() {
		DiscoveredPathInfo pathInfo = new DiscoveredPathInfo(project);
		try {
			DiscoveredScannerInfoStore.getInstance().loadDiscoveredScannerInfoFromState(project, context, pathInfo);
		} catch (CoreException e) {
			MakeCorePlugin.log(e);
		}
		return pathInfo;
	}

	/**
	 * Static method to return compiler built-in scanner info.
	 * Preconditions: resource has to be contained by a project that has following natures:
	 *     C nature, CC nature (for C++ projects), Make nature and ScannerConfig nature
	 */
	public static void calculateCompilerBuiltins(final IProject project) throws CModelException {
		createDiscoveredPathContainer(project, new NullProgressMonitor());
		String scdProfileId = ScannerConfigProfileManager.PER_PROJECT_PROFILE_ID;
		SCProfileInstance profileInstance = ScannerConfigProfileManager.getInstance().getSCProfileInstance(project,
				scdProfileId);
		final IScannerConfigBuilderInfo2 buildInfo = ScannerConfigProfileManager
				.createScannerConfigBuildInfo2(MakeCorePlugin.getDefault().getPluginPreferences(), scdProfileId, true);
		final IScannerInfoCollector collector = profileInstance.getScannerInfoCollector();
		if (collector instanceof IScannerInfoCollectorCleaner) {
			((IScannerInfoCollectorCleaner) collector).deleteAll(project);
		}
		final IExternalScannerInfoProvider esiProvider = profileInstance.createExternalScannerInfoProvider("specsFile");//$NON-NLS-1$

		// Set the arguments for the provider

		ISafeRunnable runnable = new ISafeRunnable() {
			@Override
			public void run() throws CoreException {
				IProgressMonitor monitor = new NullProgressMonitor();
				esiProvider.invokeProvider(monitor, project, "specsFile", buildInfo, collector);//$NON-NLS-1$
				if (collector instanceof IScannerInfoCollector2) {
					IScannerInfoCollector2 collector2 = (IScannerInfoCollector2) collector;
					collector2.updateScannerConfiguration(monitor);
				}
			}

			@Override
			public void handleException(Throwable exception) {
				if (exception instanceof OperationCanceledException) {
					throw (OperationCanceledException) exception;
				}
			}
		};
		SafeRunner.run(runnable);
	}

	private static void createDiscoveredPathContainer(IProject project, IProgressMonitor monitor)
			throws CModelException {
		IPathEntry container = CoreModel.newContainerEntry(DiscoveredPathContainer.CONTAINER_ID);
		ICProject cProject = CoreModel.getDefault().create(project);
		if (cProject != null) {
			IPathEntry[] entries = cProject.getRawPathEntries();
			List<IPathEntry> newEntries = new ArrayList<>(Arrays.asList(entries));
			if (!newEntries.contains(container)) {
				newEntries.add(container);
				cProject.setRawPathEntries(newEntries.toArray(new IPathEntry[newEntries.size()]), monitor);
			}
		}
		// create a new discovered scanner config store
		MakeCorePlugin.getDefault().getDiscoveryManager().removeDiscoveredInfo(project);
	}

	@Override
	public void setInfoContext(InfoContext context) {
		this.context = context;
		this.project = context.getProject();
	}

	public InfoContext getContext() {
		return this.context;
	}

}
