 /*******************************************************************************
 * Copyright (c) 2005, 2018 BEA Systems, Inc. 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:
 *    mkaufman@bea.com - initial API and implementation
 *    het@google.com - Bug 459601 - [clean up] Use CompilationParticipant.buildFinished() in AptCompilationParticipant 
 *******************************************************************************/

package org.eclipse.jdt.apt.core.internal;

import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jdt.apt.core.internal.util.FactoryPath;
import org.eclipse.jdt.apt.core.internal.util.TestCodeUtil;
import org.eclipse.jdt.apt.core.util.AptConfig;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.compiler.BuildContext;
import org.eclipse.jdt.core.compiler.CategorizedProblem;
import org.eclipse.jdt.core.compiler.CompilationParticipant;
import org.eclipse.jdt.core.compiler.ReconcileContext;

import com.sun.mirror.apt.AnnotationProcessorFactory;

/**
 * A singleton object, created by callback through the
 * org.eclipse.jdt.core.compilationParticipants extension point.
 */
public class AptCompilationParticipant extends CompilationParticipant
{
	/** 
	 * Batch factories that claimed some annotation in a previous round of APT processing.
	 * This currently only apply to the build case since are only generating types during build
	 * and hence cause APT rounding.
	 * The set is an order preserving. The order is determined by their first invocation.
	 */
	private Set<AnnotationProcessorFactory> _previousRoundsBatchFactories = new LinkedHashSet<>();
	private int _buildRound = 0;
	private boolean _isBatch = false; 
	private static final AptCompilationParticipant INSTANCE = new AptCompilationParticipant();
	/** 
	 * Files that has been processed by apt during the current build.
	 * Files that has been compiled may need re-compilation (from jdt's perspective) 
	 * because of newly generated types. APT only process each file once during a build and 
	 * this set will prevent unnecessary/incorrect compilation of already processed files.
	 */
	private Map<IFile, CategorizedProblem[]> _processedFiles = null;
	
	/**
	 * Files generated by Java 6 annotation processing during the current build.
	 * These must be stored here in order to communicate between the Java 6 annotation
	 * processing phase, which runs during Java compilation, and the Java 5 phase,
	 * which runs afterwards.  This member is reinitialized during aboutToBuild()
	 * and cleared in buildComplete().
	 * 
	 * Doing it this way implies that files can only be generated by Java 6 processing
	 * during a build.  That is true as of 8/07.  If that changes, then this may need
	 * to be stored as a thread local, or elsewhere entirely.
	 */
	private HashSet<IFile> _java6GeneratedFiles = null;
	
	public static AptCompilationParticipant getInstance() {
		return INSTANCE;
	}
	
	/**
	 * This class is constructed indirectly, by registering an extension to the 
	 * org.eclipse.jdt.core.compilationParticipants extension point.  Other
	 * clients should NOT construct this object.
	 */
	private AptCompilationParticipant()	{
	}
	
	@Override
	public boolean isAnnotationProcessor(){
		return true;
	}
	
	@Override
	public void buildStarting(BuildContext[] files, boolean isBatch){
		// this gets called multiple times during a build.
		// This gets called:
		// 1) after "aboutToBuild" is called.
        // 2) everytime an incremental build occur because of newly generated files
        // this gets called.
		if( _buildRound == 0 )
			_isBatch = isBatch;
	}
	
	@Override
	public void buildFinished(IJavaProject project) {
		buildComplete();
	}
	
	@Override
	public void processAnnotations(BuildContext[] allfiles) {	
		// This should not happen. There should always be file that that needs 
		// building when 
		final int total = allfiles == null ? 0 : allfiles.length;
		if( total == 0 )
			return;

		final IProject project = allfiles[0].getFile().getProject();
		final IJavaProject javaProject = JavaCore.create(project);
		// Don't dispatch on pre-1.5 project. They cannot legally have annotations
		String javaVersion = javaProject.getOption("org.eclipse.jdt.core.compiler.source", true); //$NON-NLS-1$		
		// Check for 1.3 or 1.4, as we don't want this to break in the future when 1.6
		// is a possibility
		if ("1.3".equals(javaVersion) || "1.4".equals(javaVersion)) { //$NON-NLS-1$ //$NON-NLS-2$
			return;
		}
		
		if ( _isBatch && _buildRound == 0 ) {
			AnnotationProcessorFactoryLoader.getLoader().resetBatchProcessors(javaProject);
			_previousRoundsBatchFactories.clear();
		}
		
		try {
		
			// split up the list of files with annotations from those that don't
			// also exclude files that has already been processed.
			int annoFileCount = 0;
			int noAnnoFileCount = 0;
			boolean test = false;
			for( int i=0; i<total; i++ ){
				BuildContext bc = allfiles[i];
				if(bc.isTestCode()) {
					test = true;
				}
				if( _buildRound > 0 && _processedFiles.containsKey( bc.getFile() )){
					// We've already processed this file; we'll skip reprocessing it, on
					// the assumption that nothing would change, but we need to re-report
					// any problems we reported earlier because JDT will have cleared them.
					CategorizedProblem[] problems = _processedFiles.get(bc.getFile());
					if (null != problems && problems.length > 0) {
						bc.recordNewProblems(problems);
					}
					continue;
				}
				if( bc.hasAnnotations() )
					annoFileCount ++;
				else
					noAnnoFileCount ++;
			}
			// apt has already processed all files
			// files that are reported at this point is triggered by
			// dependencies introduced by type creation. 
			if( annoFileCount == 0 && noAnnoFileCount == 0 )
				return;
			
			BuildContext[] withAnnotation = null;
			BuildContext[] withoutAnnotation = null;
			
			if( annoFileCount != 0 )
				withAnnotation = new BuildContext[annoFileCount];
			if(noAnnoFileCount != 0 )
				withoutAnnotation = new BuildContext[noAnnoFileCount];
			int wIndex = 0; // index for 'withAnnotation' array
			int woIndex = 0; // index of 'withoutAnnotation' array
			for( int i=0; i<total; i++ ){		
				if( _processedFiles.containsKey( allfiles[i].getFile() ) )
					continue;
				if( allfiles[i].hasAnnotations() )
					withAnnotation[wIndex ++] = allfiles[i];
				else
					withoutAnnotation[woIndex ++] = allfiles[i];
			}
			
			for( BuildContext file : allfiles )
				_processedFiles.put(file.getFile(), null);
		
			Map<AnnotationProcessorFactory, FactoryPath.Attributes> factories =
				AnnotationProcessorFactoryLoader.getLoader().getJava5FactoriesAndAttributesForProject(javaProject);
			
			AptProject aptProject = AptPlugin.getAptProject(javaProject);			
			Set<AnnotationProcessorFactory> dispatchedBatchFactories = 
				APTDispatchRunnable.runAPTDuringBuild(
						withAnnotation, 
						withoutAnnotation,
						_processedFiles,
						aptProject, 
						factories, 
						_previousRoundsBatchFactories, 
						_isBatch,
						test);
			_previousRoundsBatchFactories.addAll(dispatchedBatchFactories);
		}
		finally {			
			_buildRound ++;
		}
	}
	
	@Override
	public void reconcile(ReconcileContext context){
		final ICompilationUnit workingCopy = context.getWorkingCopy();
		if( workingCopy == null ) 
			return;
		IJavaProject javaProject = workingCopy.getJavaProject();			
		if( javaProject == null )
			return;
		if (!AptConfig.shouldProcessDuringReconcile(javaProject)) {
			AptPlugin.trace("Reconcile-time processing is disabled for project: " + javaProject.getElementName()); //$NON-NLS-1$
			return;
		}
		AptProject aptProject = AptPlugin.getAptProject(javaProject);
		
		Map<AnnotationProcessorFactory, FactoryPath.Attributes> factories = 
			AnnotationProcessorFactoryLoader.getLoader().getJava5FactoriesAndAttributesForProject( javaProject );
		APTDispatchRunnable.runAPTDuringReconcile(context, aptProject, factories, TestCodeUtil.isTestCode(workingCopy));
	}
	
	@Override
	public void cleanStarting(IJavaProject javaProject){
		IProject p = javaProject.getProject();
		
		AptPlugin.getAptProject(javaProject).projectClean( true, true, true );
		try{
			// clear out all markers during a clean.
			IMarker[] markers = p.findMarkers(AptPlugin.APT_BATCH_PROCESSOR_PROBLEM_MARKER, true, IResource.DEPTH_INFINITE);
			if( markers != null ){
				for( IMarker marker : markers )
					marker.delete();
			}
		}
		catch(CoreException e){
			AptPlugin.log(e, "Unable to delete batch annotation processor markers"); //$NON-NLS-1$
		}
	}
	
	/**
	 * Does APT have anything to do for this project?
	 * Even if there are no processors on the factory path, apt may still
	 * be involved during a clean.
	 */
	@Override
	public boolean isActive(IJavaProject project){
		return AptConfig.isEnabled(project);
	}
	
	@Override
	public int aboutToBuild(IJavaProject project) {
		if (AptConfig.isEnabled(project)) {
			// setup the classpath and make sure the generated source folder is on disk.
			AptPlugin.getAptProject(project).compilationStarted();
		}		
		_buildRound = 0; // reset
		// Note that for each project build, we blow away the last project's processed files.
		_processedFiles = new HashMap<>();
		_java6GeneratedFiles = new HashSet<>();
		// TODO: (wharley) if the factory path is different we need a full build
		return CompilationParticipant.READY_FOR_BUILD;
	}
	
	/**
	 * Called during Java 6 annotation processing phase to register newly-generated files.
	 * This information is then used in the Java 5 (post-compilation) phase when 
	 * determining no-longer-generated files to delete.  The list of files is discarded
	 * at the end of each build.
	 */
	public void addJava6GeneratedFile(IFile file) {
		_java6GeneratedFiles.add(file);
	}
	
	/**
	 * Get the files generated during this build by Java 6 processors.
	 * This is only meaningful in the context of a build, not a reconcile.
	 * @return an unmodifiable, non-null but possibly empty, set of IFiles.
	 */
	public Set<IFile> getJava6GeneratedFiles() {
		if (null == _java6GeneratedFiles) {
			return Collections.emptySet();
		}
		return Collections.unmodifiableSet(_java6GeneratedFiles);
	}
	
	private void buildComplete() {
		_processedFiles = null;
		_java6GeneratedFiles = null;
	}
}
