/*
 * Copyright (C) 2009-2020 The Project Lombok Authors.
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package lombok.eclipse;

import static lombok.eclipse.handlers.EclipseHandlerUtil.*;

import java.lang.reflect.Field;
import java.util.Collections;
import java.util.Map;
import java.util.WeakHashMap;

import lombok.ConfigurationKeys;
import lombok.core.LombokConfiguration;
import lombok.core.debug.DebugSnapshotStore;
import lombok.core.debug.HistogramTracker;
import lombok.patcher.Symbols;
import lombok.permit.Permit;

import org.eclipse.jdt.internal.compiler.ast.AbstractMethodDeclaration;
import org.eclipse.jdt.internal.compiler.ast.Annotation;
import org.eclipse.jdt.internal.compiler.ast.Argument;
import org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration;
import org.eclipse.jdt.internal.compiler.ast.FieldDeclaration;
import org.eclipse.jdt.internal.compiler.ast.LocalDeclaration;
import org.eclipse.jdt.internal.compiler.ast.TypeDeclaration;
import org.eclipse.jdt.internal.compiler.ast.TypeReference;
import org.eclipse.jdt.internal.compiler.parser.Parser;

/**
 * Entry point for the Eclipse Parser patch that lets lombok modify the Abstract Syntax Tree as generated by
 * Eclipse's parser implementations. This class is injected into the appropriate OSGi ClassLoader and can thus
 * use any classes that belong to org.eclipse.jdt.(apt.)core.
 * 
 * Note that, for any Method body, if Bit24 is set, the Eclipse parser has been patched to never attempt to
 * (re)parse it. You should set Bit24 on any MethodDeclaration object you inject into the AST:
 * 
 * {@code methodDeclaration.bits |= ASTNode.Bit24; //0x800000}
 * 
 * @author rzwitserloot
 * @author rspilker
 */
public class TransformEclipseAST {
	private final EclipseAST ast;
	//The patcher hacks this field onto CUD. It's public.
	private static final Field astCacheField;
	private static final HandlerLibrary handlers;
	
	public static boolean disableLombok = false;
	private static final HistogramTracker lombokTracker;
	private static Map<CompilationUnitDeclaration, State> transformationStates = Collections.synchronizedMap(new WeakHashMap<CompilationUnitDeclaration, State>());
	
	static {
		String v = System.getProperty("lombok.histogram");
		if (v == null) lombokTracker = null;
		else if (v.toLowerCase().equals("sysout")) lombokTracker = new HistogramTracker("lombok.histogram", System.out);
		else lombokTracker = new HistogramTracker("lombok.histogram");
	}
	
	static {
		Field f = null;
		HandlerLibrary h = null;
		
		if (System.getProperty("lombok.disable") != null) {
			disableLombok = true;
			astCacheField = null;
			handlers = null;
		} else {
			try {
				h = HandlerLibrary.load();
			} catch (Throwable t) {
				try {
					error(null, "Problem initializing lombok", t);
				} catch (Throwable t2) {
					System.err.println("Problem initializing lombok");
					t.printStackTrace();
				}
				disableLombok = true;
			}
			try {
				f = Permit.getField(CompilationUnitDeclaration.class, "$lombokAST");
			} catch (Throwable t) {
				//I guess we're in an ecj environment; we'll just not cache stuff then.
			}
			
			astCacheField = f;
			handlers = h;
		}
	}
	
	public static void transform_swapped(CompilationUnitDeclaration ast, Parser parser) {
		transform(parser, ast);
	}
	
	public static EclipseAST getAST(CompilationUnitDeclaration ast, boolean forceRebuild) {
		EclipseAST existing = null;
		if (astCacheField != null) {
			try {
				existing = (EclipseAST) astCacheField.get(ast);
			} catch (Exception e) {
				// existing remains null
			}
		}
		
		if (existing == null) {
			existing = new EclipseAST(ast);
			if (astCacheField != null) try {
				astCacheField.set(ast, existing);
			} catch (Exception ignore) {
			}
		} else {
			existing.rebuild(forceRebuild);
		}
		
		return existing;
	}
	
	/**
	 * Check if lombok already handled the given AST. This method will return
	 * <code>true</code> once for diet mode and once for full mode.
	 * 
	 * The reason for this is that Eclipse invokes the transform method multiple
	 * times during compilation and it is enough to transform it once and not
	 * repeat the whole thing over and over again.
	 * 
	 * @param ast The AST node belonging to the compilation unit (java speak for a single source file).
	 * @return <code>true</code> if this AST was already handled by lombok.
	 */
	public static boolean alreadyTransformed(CompilationUnitDeclaration ast) {
		State state = transformationStates.get(ast);
		
		if (state == State.FULL) return true;
		if (state == State.DIET) {
			if (!EclipseAST.isComplete(ast)) return true;
			transformationStates.put(ast, State.FULL);
		} else {
			transformationStates.put(ast, State.DIET);
		}
		return false;
	}
	
	/**
	 * This method is called immediately after Eclipse finishes building a CompilationUnitDeclaration, which is
	 * the top-level AST node when Eclipse parses a source file. The signature is 'magic' - you should not
	 * change it!
	 * 
	 * Eclipse's parsers often operate in diet mode, which means many parts of the AST have been left blank.
	 * Be ready to deal with just about anything being null, such as the Statement[] arrays of the Method AST nodes.
	 * 
	 * @param parser The Eclipse parser object that generated the AST. Not actually used; mostly there to satisfy parameter rules for lombok.patcher scripts.
	 * @param ast The AST node belonging to the compilation unit (java speak for a single source file).
	 */
	public static void transform(Parser parser, CompilationUnitDeclaration ast) {
		if (disableLombok) return;
		
		if (Symbols.hasSymbol("lombok.disable")) return;
		if (alreadyTransformed(ast)) return;
		
		// Do NOT abort if (ast.bits & ASTNode.HasAllMethodBodies) != 0 - that doesn't work.
		
		if (Boolean.TRUE.equals(LombokConfiguration.read(ConfigurationKeys.LOMBOK_DISABLE, EclipseAST.getAbsoluteFileLocation(ast)))) return;
		
		try {
			DebugSnapshotStore.INSTANCE.snapshot(ast, "transform entry");
			long histoToken = lombokTracker == null ? 0L : lombokTracker.start();
			EclipseAST existing = getAST(ast, false);
			new TransformEclipseAST(existing).go();
			if (lombokTracker != null) lombokTracker.end(histoToken);
			DebugSnapshotStore.INSTANCE.snapshot(ast, "transform exit");
		} catch (Throwable t) {
			DebugSnapshotStore.INSTANCE.snapshot(ast, "transform error: %s", t.getClass().getSimpleName());
			try {
				String message = "Lombok can't parse this source: " + t.toString();
				
				EclipseAST.addProblemToCompilationResult(ast.getFileName(), ast.compilationResult, false, message, 0, 0);
				t.printStackTrace();
			} catch (Throwable t2) {
				try {
					error(ast, "Can't create an error in the problems dialog while adding: " + t.toString(), t2);
				} catch (Throwable t3) {
					//This seems risky to just silently turn off lombok, but if we get this far, something pretty
					//drastic went wrong. For example, the eclipse help system's JSP compiler will trigger a lombok call,
					//but due to class loader shenanigans we'll actually get here due to a cascade of
					//ClassNotFoundErrors. This is the right action for the help system (no lombok needed for that JSP compiler,
					//of course). 'disableLombok' is static, but each context classloader (e.g. each eclipse OSGi plugin) has
					//it's own edition of this class, so this won't turn off lombok everywhere.
					disableLombok = true;
				}
			}
		}
	}
	
	public TransformEclipseAST(EclipseAST ast) {
		this.ast = ast;
	}
	
	/**
	 * First handles all lombok annotations except PrintAST, then calls all non-annotation based handlers.
	 * then handles any PrintASTs.
	 */
	public void go() {
		long nextPriority = Long.MIN_VALUE;
		for (Long d : handlers.getPriorities()) {
			if (nextPriority > d) continue;
			AnnotationVisitor visitor = new AnnotationVisitor(d);
			ast.traverse(visitor);
			// if no visitor interested for this AST, nextPriority would be MAX_VALUE and we bail out immediatetly
			nextPriority = visitor.getNextPriority();
			nextPriority = Math.min(nextPriority, handlers.callASTVisitors(ast, d, ast.isCompleteParse()));
		}
	}
	
	private static class AnnotationVisitor extends EclipseASTAdapter {
		private final long priority;
		// this is the next priority we continue to visit.
		// Long.MAX_VALUE means never. Each visit method will potentially reduce the next priority
		private long nextPriority = Long.MAX_VALUE;
		
		public AnnotationVisitor(long priority) {
			this.priority = priority;
		}
		
		public long getNextPriority() {
			return nextPriority;
		}
		
		@Override public void visitAnnotationOnField(FieldDeclaration field, EclipseNode annotationNode, Annotation annotation) {
			CompilationUnitDeclaration top = (CompilationUnitDeclaration) annotationNode.top().get();
			nextPriority = Math.min(nextPriority, handlers.handleAnnotation(top, annotationNode, annotation, priority));
		}
		
		@Override public void visitAnnotationOnMethodArgument(Argument arg, AbstractMethodDeclaration method, EclipseNode annotationNode, Annotation annotation) {
			CompilationUnitDeclaration top = (CompilationUnitDeclaration) annotationNode.top().get();
			nextPriority = Math.min(nextPriority, handlers.handleAnnotation(top, annotationNode, annotation, priority));
		}
		
		@Override public void visitAnnotationOnLocal(LocalDeclaration local, EclipseNode annotationNode, Annotation annotation) {
			CompilationUnitDeclaration top = (CompilationUnitDeclaration) annotationNode.top().get();
			nextPriority = Math.min(nextPriority, handlers.handleAnnotation(top, annotationNode, annotation, priority));
		}
		
		@Override public void visitAnnotationOnMethod(AbstractMethodDeclaration method, EclipseNode annotationNode, Annotation annotation) {
			CompilationUnitDeclaration top = (CompilationUnitDeclaration) annotationNode.top().get();
			nextPriority = Math.min(nextPriority, handlers.handleAnnotation(top, annotationNode, annotation, priority));
		}
		
		@Override public void visitAnnotationOnType(TypeDeclaration type, EclipseNode annotationNode, Annotation annotation) {
			CompilationUnitDeclaration top = (CompilationUnitDeclaration) annotationNode.top().get();
			nextPriority = Math.min(nextPriority, handlers.handleAnnotation(top, annotationNode, annotation, priority));
		}
		
		@Override public void visitAnnotationOnTypeUse(TypeReference typeUse, EclipseNode annotationNode, Annotation annotation) {
			CompilationUnitDeclaration top = (CompilationUnitDeclaration) annotationNode.top().get();
			nextPriority = Math.min(nextPriority, handlers.handleAnnotation(top, annotationNode, annotation, priority));
		}
	}
	
	private static enum State {
		DIET,
		FULL
	}
}
