/*******************************************************************************
 * Copyright (c) 2005, 2015 BEA Systems, Inc.
 * 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:
 *    tyeung@bea.com - initial API and implementation
 *******************************************************************************/
package org.eclipse.jdt.apt.core.internal.env;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.jdt.apt.core.env.EclipseAnnotationProcessorEnvironment;
import org.eclipse.jdt.apt.core.env.Phase;
import org.eclipse.jdt.apt.core.internal.AptPlugin;
import org.eclipse.jdt.apt.core.internal.declaration.EclipseMirrorObject;
import org.eclipse.jdt.apt.core.internal.declaration.TypeDeclarationImpl;
import org.eclipse.jdt.apt.core.internal.env.MessagerImpl.Severity;
import org.eclipse.jdt.apt.core.internal.util.Factory;
import org.eclipse.jdt.apt.core.internal.util.Visitors.AnnotationVisitor;
import org.eclipse.jdt.apt.core.util.AptPreferenceConstants;
import org.eclipse.jdt.apt.core.util.EclipseMessager;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaConventions;
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.ReconcileContext;
import org.eclipse.jdt.core.dom.ASTRequestor;
import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;

import com.sun.mirror.apt.AnnotationProcessorFactory;
import com.sun.mirror.apt.AnnotationProcessorListener;
import com.sun.mirror.apt.Filer;
import com.sun.mirror.declaration.AnnotationTypeDeclaration;

/** Base environment to be used during reconcile or build */ 
public abstract class AbstractCompilationEnv 
	extends BaseProcessorEnv 
	implements EclipseAnnotationProcessorEnvironment {
	
	// Bugzilla 188185: accept "-AenableTypeGenerationInEditor" as well as "enableTypeGenerationInEditor".
	private final static String RTTG_ENABLED_DASH_A_OPTION = "-A" + AptPreferenceConstants.RTTG_ENABLED_OPTION; //$NON-NLS-1$
	private final static String PROCESSING_IN_EDITOR_DISABLED_DASH_A_OPTION = "-A" + AptPreferenceConstants.PROCESSING_IN_EDITOR_DISABLED_OPTION; //$NON-NLS-1$
	
	private Set<AnnotationProcessorListener> _listeners = null;
	
	protected List<APTProblem> _problems = new ArrayList<>();
	private boolean _isClosed = false;
	
	EnvCallback _callback;

    private Set<IFile> _allGeneratedSourceFiles = new HashSet<>();
    private Set<IFile> _modifiedGeneratedSourceFiles = new HashSet<>();

    /**
	 * Currently open dom pipeline, used to request type bindings.
	 */
	protected ASTRequestor _requestor;

	/**
	 * The processor that is currently being executed, or null if processing is not underway.
	 */
	private AnnotationProcessorFactory _currentProcessorFactory = null;

	/**
	 * True if the currently active processor will be called during reconcile as well as build.
	 * Takes into account project settings, factory path, and processor options.
	 */
	private boolean _currentProcessorFactoryWillReconcile;
	
	public static interface EnvCallback {
		public void run(AbstractCompilationEnv env);
	}
	
	public static void newReconcileEnv(ReconcileContext reconcileContext,  EnvCallback callback)
	{
		assert reconcileContext != null : "reconcile context is null"; //$NON-NLS-1$
		ReconcileEnv env = ReconcileEnv.newEnv(reconcileContext);
		env._callback = callback;
		env.openPipeline();
	}
    
    public static void newBuildEnv(
    		BuildContext[] filesWithAnnotations,
    		final BuildContext[] additionalFiles,
    		IJavaProject javaProj,
    		EnvCallback callback)
    {
    	assert filesWithAnnotations != null : "missing files"; //$NON-NLS-1$    	
    
		// note, we are not reading any files.
		BuildEnv env = new BuildEnv(filesWithAnnotations, additionalFiles, javaProj);
		env._callback = callback;
		env.createASTs(filesWithAnnotations);
    }
    
    /**
     * Determine whether a processor wants to be called during reconcile. By default
     * processors are called during both build and reconcile, but a processor can choose
     * not to be called during reconcile by reporting 
     * {@link AptPreferenceConstants#PROCESSING_IN_EDITOR_DISABLED_OPTION}
     * in its supportedOptions() method.
     * @return false if the processor reports PROCESSING_IN_EDITOR_DISABLED_OPTION.
     * This does not consider project or factory path settings.
     */
    public static boolean doesFactorySupportReconcile(AnnotationProcessorFactory factory) {
    	Collection<String> options = factory.supportedOptions();
    	return options == null || 
    		(!options.contains(AptPreferenceConstants.PROCESSING_IN_EDITOR_DISABLED_OPTION) &&
    		!options.contains(PROCESSING_IN_EDITOR_DISABLED_DASH_A_OPTION));
    }
	
	AbstractCompilationEnv(
			CompilationUnit compilationUnit,
			IFile file,
			IJavaProject javaProj,
			Phase phase)
	{
		super(compilationUnit, file, javaProj, phase);
	}
	
	@Override
	protected IBinding getBindingFromKey(String key, ICompilationUnit unit) {
		return _requestor.createBindings(new String[] {key})[0];
	}

	public void addListener(AnnotationProcessorListener listener)
    {
		checkValid();
        if(_listeners == null )
			_listeners = new HashSet<>();
		_listeners.add(listener);
    }

    public void removeListener(AnnotationProcessorListener listener)
    {
		checkValid();
        if( _listeners == null ) return;
		_listeners.remove(listener);
    }

	public Set<AnnotationProcessorListener> getProcessorListeners()
	{
		if( _listeners == null )
			return Collections.emptySet();
		// Return a copy, to avoid ConcurrentModificationException if a listener
		// removes itself in response to the callback.
		return new HashSet<>(_listeners);
	}
	
	public Map<String, String> getOptions()
    {
        final HashMap<String, String> options = new HashMap<>(_options);
		options.put("phase", getPhase().toString()); //$NON-NLS-1$
		return options;
    }
	
	abstract public CompilationUnit getASTFrom(final IFile file);
	
	public CompilationUnit getAST(){
		return _astRoot;
	}
	
	public EclipseMessager getMessager()
    {
		checkValid();
		return new MessagerImpl(this);
	}
	
	abstract void addMessage(
			IFile resource, 
		    int start, 
			int end,
            Severity severity, 
            String msg, 
            int line,
            String[] arguments);
	
	public List<? extends CategorizedProblem> getProblems(){
		checkValid();
		if( !_problems.isEmpty() )
			EnvUtil.updateProblemLength(_problems, getAstCompilationUnit());
		return _problems;
	}
	
	APTProblem createProblem(
	    		IFile resource, 
			    int start, 
				int end,
	            Severity severity, 
	            String msg, 
	            int line,
	            String[] arguments)
    {	 
    	// end-1 since IProblem ending offsets are inclusive but DOM layer
    	// ending offsets are exclusive.
    	final APTProblem newProblem = 
        	new APTProblem(msg, severity, resource, start, end-1, line, arguments, !_currentProcessorFactoryWillReconcile);
    	return newProblem;
    }
	
	public abstract Filer getFiler();
	
	public void addGeneratedSourceFile( IFile f, boolean contentsChanged ) {
		if (!f.toString().endsWith(".java")) { //$NON-NLS-1$
			throw new IllegalArgumentException("Source files must be java source files, and end with .java"); //$NON-NLS-1$
		}
		
		boolean addedToAll = _allGeneratedSourceFiles.add(f);
		boolean addedToMod = false;
		if (contentsChanged)
			addedToMod = _modifiedGeneratedSourceFiles.add(f);
		if (AptPlugin.DEBUG_COMPILATION_ENV) {
			AptPlugin.trace("add generated file " + f + " to env " + this + //$NON-NLS-1$ //$NON-NLS-2$
					"; addToAll = " + addedToAll + "; addToMod = " + addedToMod + //$NON-NLS-1$ //$NON-NLS-2$
					"; contentsChanged = " + contentsChanged); //$NON-NLS-1$
		}
	}
	
	public void addGeneratedNonSourceFile(final IFile file) {
		_allGeneratedSourceFiles.add(file);
	}
	
    public Set<IFile> getAllGeneratedFiles() { 
    	return _allGeneratedSourceFiles; 
    }
    
    public Set<IFile> getModifiedGeneratedFiles() { 
    	return _modifiedGeneratedSourceFiles; 
    }

	/**
	 * @return true iff source files has been generated.
	 *         Always return false when this environment is closed.
	 */
	public boolean hasGeneratedSourceFiles(){ return !_allGeneratedSourceFiles.isEmpty();  }


	/**
	 * @return all annotation types in the current compilation unit.
	 */
	public Map<String, AnnotationTypeDeclaration> getAnnotationTypes()
    {
    	checkValid();
    	final List<Annotation> instances = new ArrayList<>();
		final Map<String, AnnotationTypeDeclaration> decls = 
			new HashMap<>();
		final AnnotationVisitor visitor = new AnnotationVisitor(instances);
		_astRoot.accept(visitor);
			
		for (int instanceIndex=0, size = instances.size(); instanceIndex < size; instanceIndex++) {
			final Annotation instance = instances.get(instanceIndex);
			final ITypeBinding annoType = instance.resolveTypeBinding();
			if (annoType == null)
				continue;
			final TypeDeclarationImpl decl = 
				Factory.createReferenceType(annoType, this);
			if (decl != null && decl.kind() == EclipseMirrorObject.MirrorKind.TYPE_ANNOTATION){
				final AnnotationTypeDeclaration annoDecl = (AnnotationTypeDeclaration)decl;
				decls.put(annoDecl.getQualifiedName(), annoDecl);
			}
		}
		
		return decls;
    }
	
	/* package */ void checkValid()
	{
		if( _isClosed )
			throw new IllegalStateException("Environment has expired"); //$NON-NLS-1$
	}
	
	// Call this after each file; cf. BuildEnv#beginFileProcessing()
	protected void completedProcessing() {
		_modifiedGeneratedSourceFiles.clear();
	}
	
	public void close(){
		if (isClosed()) 
			return; 
		if(_listeners != null)
			_listeners.clear();
		_problems = null;
		_typeCache.clear();
		_packageRootsCache = null;
		_isClosed = true;
		_callback = null;
		_requestor = null;
		_allGeneratedSourceFiles = null;
		_modifiedGeneratedSourceFiles = null;
		if (AptPlugin.DEBUG_COMPILATION_ENV) AptPlugin.trace(
				"closed env " + this); //$NON-NLS-1$
	}
	
	boolean isClosed(){ return _isClosed; }

	/**
	 * Check typeName to ensure it doesn't contain any bogus characters.
	 * @param typeName
	 * @throws CoreException 
	 */
	public void validateTypeName(String typeName) throws CoreException
	{
        Map<String, String> options = getJavaProject().getOptions(true);
        String sourceLevel = options.get(JavaCore.COMPILER_SOURCE);
        String complianceLevel = options.get(JavaCore.COMPILER_COMPLIANCE);
        IStatus status = JavaConventions.validateJavaTypeName(typeName, sourceLevel, complianceLevel);
        if (status.matches(IStatus.ERROR)) {
        	throw new CoreException(status);
        }
	}

	public AnnotationProcessorFactory getCurrentProcessorFactory() {
		return _currentProcessorFactory;
	}
	
	/**
	 * @param factory a processor factory, or null to indicate processing is over.
	 * @param willReconcile true if the processor will be called during reconcile as well as during build,
	 * taking into account project settings, factory path, and processor options.
	 */
	public void setCurrentProcessorFactory(AnnotationProcessorFactory factory, boolean willReconcile)
	{
		_currentProcessorFactory = factory;
		_currentProcessorFactoryWillReconcile = willReconcile;
	}

	public boolean currentProcessorSupportsRTTG()
	{
		AnnotationProcessorFactory factory = getCurrentProcessorFactory();
		if (null == factory) {
			return false;
		}
		Collection<String> options = factory.supportedOptions();
		if (null == options) {
			return false;
		}
		return options.contains(AptPreferenceConstants.RTTG_ENABLED_OPTION) ||
			options.contains(RTTG_ENABLED_DASH_A_OPTION);
	}

}
