/*******************************************************************************
 *  Copyright (c) 2004, 2011 IBM Corporation 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:
 *    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<ScannerInfoTypes, List<String>>();
//		discoveredIncludes = new ArrayList();
//		discoveredSymbols = new ArrayList();
//		discoveredTSO = new ArrayList();
//
		sumDiscoveredIncludes = new ArrayList<String>();
		sumDiscoveredSymbols = new LinkedHashMap<String, SymbolEntry>();
	}

	/* (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<String>(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<IResource>(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<String>(persistedIncludes.keySet());
			addedIncludes = addItemsWithOrder(persistedKeyList, finalSumIncludes, true);

			LinkedHashMap<String, Boolean> newPersistedIncludes;
			if (addedIncludes) {
				newPersistedIncludes = new LinkedHashMap<String, Boolean>(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<String, SymbolEntry>(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<IPathEntry>(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;
	}

}
