/*
 * Copyright (C) 2009-2019 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.javac;

import static lombok.javac.JavacTreeMaker.TreeTag.treeTag;
import static lombok.javac.JavacTreeMaker.TypeTag.typeTag;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.lang.model.type.NoType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeVisitor;

import com.sun.tools.javac.code.Flags;
import com.sun.tools.javac.code.Source;
import com.sun.tools.javac.code.Symtab;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.main.JavaCompiler;
import com.sun.tools.javac.parser.Tokens.Comment;
import com.sun.tools.javac.tree.DocCommentTable;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.JCTree.JCAnnotation;
import com.sun.tools.javac.tree.JCTree.JCClassDecl;
import com.sun.tools.javac.tree.JCTree.JCCompilationUnit;
import com.sun.tools.javac.tree.JCTree.JCExpression;
import com.sun.tools.javac.tree.JCTree.JCFieldAccess;
import com.sun.tools.javac.tree.JCTree.JCIdent;
import com.sun.tools.javac.tree.JCTree.JCLiteral;
import com.sun.tools.javac.tree.JCTree.JCVariableDecl;
import com.sun.tools.javac.util.JCDiagnostic.DiagnosticPosition;

import lombok.core.ClassLiteral;
import lombok.core.FieldSelect;
import lombok.core.JavaIdentifiers;
import lombok.javac.JavacTreeMaker.TreeTag;
import lombok.javac.JavacTreeMaker.TypeTag;
import lombok.permit.Permit;

/**
 * Container for static utility methods relevant to lombok's operation on javac.
 */
public class Javac {
	private Javac() {
		// prevent instantiation
	}
	
	private static final Pattern VERSION_PARSER = Pattern.compile("^(\\d{1,6})\\.?(\\d{1,6})?.*$");
	private static final Pattern SOURCE_PARSER = Pattern.compile("^JDK(\\d{1,6})_?(\\d{1,6})?.*$");
	
	private static final AtomicInteger compilerVersion = new AtomicInteger(-1);
	
	/**
	 * Returns the version of this java compiler, i.e. the JDK that it shipped in. For example, for javac v1.7, this returns {@code 7}.
	 */
	public static int getJavaCompilerVersion() {
		int cv = compilerVersion.get();
		if (cv != -1) return cv;
		
		/* Main algorithm: Use JavaCompiler's intended method to do this */ {
			Matcher m = VERSION_PARSER.matcher(JavaCompiler.version());
			if (m.matches()) {
				int major = Integer.parseInt(m.group(1));
				if (major == 1) {
					int minor = Integer.parseInt(m.group(2));
					return setVersion(minor);
				}
				if (major >= 9) return setVersion(major);
			}
		}
		
		/* Fallback algorithm one: Check Source's values. Lets hope oracle never releases a javac that recognizes future versions for -source */ {
			String name = Source.values()[Source.values().length - 1].name();
			Matcher m = SOURCE_PARSER.matcher(name);
			if (m.matches()) {
				int major = Integer.parseInt(m.group(1));
				if (major == 1) {
					int minor = Integer.parseInt(m.group(2));
					return setVersion(minor);
				}
				if (major >= 9) return setVersion(major);
			}
		}
		return setVersion(6);
	}
	
	private static int setVersion(int version) {
		compilerVersion.set(version);
		return version;
	}
	
	private static final Class<?> DOCCOMMENTTABLE_CLASS;
	
	static {
		Class<?> c = null;
		try {
			c = Class.forName("com.sun.tools.javac.tree.DocCommentTable");
		} catch (Throwable ignore) {}
		DOCCOMMENTTABLE_CLASS = c;
	}
	
	public static boolean instanceOfDocCommentTable(Object o) {
		return DOCCOMMENTTABLE_CLASS != null && DOCCOMMENTTABLE_CLASS.isInstance(o);
	}
	
	/**
	 * Checks if the given expression (that really ought to refer to a type
	 * expression) represents a primitive type.
	 */
	public static boolean isPrimitive(JCExpression ref) {
		return JavaIdentifiers.isPrimitive(ref.toString());
	}
	
	/**
	 * Turns an expression into a guessed intended literal. Only works for
	 * literals, as you can imagine.
	 * 
	 * Will for example turn a TrueLiteral into 'Boolean.valueOf(true)'.
	 */
	public static Object calculateGuess(JCExpression expr) {
		if (expr instanceof JCLiteral) {
			JCLiteral lit = (JCLiteral) expr;
			if (lit.getKind() == com.sun.source.tree.Tree.Kind.BOOLEAN_LITERAL) {
				return ((Number) lit.value).intValue() == 0 ? false : true;
			}
			return lit.value;
		}
		
		if (expr instanceof JCIdent || expr instanceof JCFieldAccess) {
			String x = expr.toString();
			if (x.endsWith(".class")) return new ClassLiteral(x.substring(0, x.length() - 6));
			int idx = x.lastIndexOf('.');
			if (idx > -1) x = x.substring(idx + 1);
			return new FieldSelect(x);
		}
		
		return null;
	}
	
	public static final TypeTag CTC_BOOLEAN = typeTag("BOOLEAN");
	public static final TypeTag CTC_INT = typeTag("INT");
	public static final TypeTag CTC_DOUBLE = typeTag("DOUBLE");
	public static final TypeTag CTC_FLOAT = typeTag("FLOAT");
	public static final TypeTag CTC_SHORT = typeTag("SHORT");
	public static final TypeTag CTC_BYTE = typeTag("BYTE");
	public static final TypeTag CTC_LONG = typeTag("LONG");
	public static final TypeTag CTC_CHAR = typeTag("CHAR");
	public static final TypeTag CTC_VOID = typeTag("VOID");
	public static final TypeTag CTC_NONE = typeTag("NONE");
	public static final TypeTag CTC_BOT = typeTag("BOT");
	public static final TypeTag CTC_ERROR = typeTag("ERROR");
	public static final TypeTag CTC_UNKNOWN = typeTag("UNKNOWN");
	public static final TypeTag CTC_UNDETVAR = typeTag("UNDETVAR");
	public static final TypeTag CTC_CLASS = typeTag("CLASS");
	
	public static final TreeTag CTC_NOT_EQUAL = treeTag("NE");
	public static final TreeTag CTC_LESS_THAN = treeTag("LT");
	public static final TreeTag CTC_GREATER_THAN = treeTag("GT");
	public static final TreeTag CTC_LESS_OR_EQUAL= treeTag("LE");
	public static final TreeTag CTC_GREATER_OR_EQUAL = treeTag("GE");
	public static final TreeTag CTC_POS = treeTag("POS");
	public static final TreeTag CTC_NEG = treeTag("NEG");
	public static final TreeTag CTC_NOT = treeTag("NOT");
	public static final TreeTag CTC_COMPL = treeTag("COMPL");
	public static final TreeTag CTC_BITXOR = treeTag("BITXOR");
	public static final TreeTag CTC_UNSIGNED_SHIFT_RIGHT = treeTag("USR");
	public static final TreeTag CTC_MUL = treeTag("MUL");
	public static final TreeTag CTC_DIV = treeTag("DIV");
	public static final TreeTag CTC_PLUS = treeTag("PLUS");
	public static final TreeTag CTC_MINUS = treeTag("MINUS");
	public static final TreeTag CTC_EQUAL = treeTag("EQ");
	public static final TreeTag CTC_PREINC = treeTag("PREINC");
	public static final TreeTag CTC_PREDEC = treeTag("PREDEC");
	public static final TreeTag CTC_POSTINC = treeTag("POSTINC");
	public static final TreeTag CTC_POSTDEC = treeTag("POSTDEC");
	
	private static final Method getExtendsClause, getEndPosition, storeEnd;
	
	static {
		getExtendsClause = getMethod(JCClassDecl.class, "getExtendsClause", new Class<?>[0]);
		
		if (getJavaCompilerVersion() < 8) {
			getEndPosition = getMethod(DiagnosticPosition.class, "getEndPosition", java.util.Map.class);
			storeEnd = getMethod(java.util.Map.class, "put", Object.class, Object.class);
		} else {
			getEndPosition = getMethod(DiagnosticPosition.class, "getEndPosition", "com.sun.tools.javac.tree.EndPosTable");
			Method storeEndMethodTemp;
			Class<?> endPosTable;
			try {
				endPosTable = Class.forName("com.sun.tools.javac.tree.EndPosTable");
			} catch (ClassNotFoundException ex) {
				throw sneakyThrow(ex);
			}
			try {
				storeEndMethodTemp = Permit.getMethod(endPosTable, "storeEnd", JCTree.class, int.class);
			} catch (NoSuchMethodException e) {
				try {
					endPosTable = Class.forName("com.sun.tools.javac.parser.JavacParser$AbstractEndPosTable");
					storeEndMethodTemp = Permit.getMethod(endPosTable, "storeEnd", JCTree.class, int.class);
				} catch (NoSuchMethodException ex) {
					throw sneakyThrow(ex);
				} catch (ClassNotFoundException ex) {
					throw sneakyThrow(ex);
				}
			}
			storeEnd = storeEndMethodTemp;
		}
		Permit.setAccessible(getEndPosition);
		Permit.setAccessible(storeEnd);
	}
	
	private static Method getMethod(Class<?> clazz, String name, Class<?>... paramTypes) {
		try {
			return Permit.getMethod(clazz, name, paramTypes);
		} catch (NoSuchMethodException e) {
			throw sneakyThrow(e);
		}
	}
	
	private static Method getMethod(Class<?> clazz, String name, String... paramTypes) {
		try {
			Class<?>[] c = new Class[paramTypes.length];
			for (int i = 0; i < paramTypes.length; i++) c[i] = Class.forName(paramTypes[i]);
			return Permit.getMethod(clazz, name, c);
		} catch (NoSuchMethodException e) {
			throw sneakyThrow(e);
		} catch (ClassNotFoundException e) {
			throw sneakyThrow(e);
		}
	}
	
	public static JCTree getExtendsClause(JCClassDecl decl) {
		try {
			return (JCTree) getExtendsClause.invoke(decl);
		} catch (IllegalAccessException e) {
			throw sneakyThrow(e);
		} catch (InvocationTargetException e) {
			throw sneakyThrow(e.getCause());
		}
	}
	
	public static Object getDocComments(JCCompilationUnit cu) {
		try {
			return JCCOMPILATIONUNIT_DOCCOMMENTS.get(cu);
		} catch (IllegalAccessException e) {
			throw sneakyThrow(e);
		}
	}
	
	public static String getDocComment(JCCompilationUnit cu, JCTree node) {
		Object dc = getDocComments(cu);
		if (dc instanceof Map) return (String) ((Map<?, ?>) dc).get(node);
		if (instanceOfDocCommentTable(dc)) return JavadocOps_8.getJavadoc(dc, node);
		return null;
	}
	
	@SuppressWarnings("unchecked")
	public static void setDocComment(JCCompilationUnit cu, JCTree node, String javadoc) {
		if (javadoc == null) return;
		Object dc = getDocComments(cu);
		if (dc instanceof Map) {
			((Map<JCTree, String>) dc).put(node, javadoc);
			return;
		}
		
		if (instanceOfDocCommentTable(dc)) {
			JavadocOps_8.setJavadoc(dc, node, javadoc);
			return;
		}
	}
	
	private static class JavadocOps_8 {
		static String getJavadoc(Object dc, JCTree node) {
			DocCommentTable dct = (DocCommentTable) dc;
			Comment javadoc = dct.getComment(node);
			if (javadoc == null) return null;
			return javadoc.getText();
		}
		
		static void setJavadoc(Object dc, JCTree node, String javadoc) {
			DocCommentTable dct = (DocCommentTable) dc;
			Comment newCmt = createJavadocComment(javadoc, node);
			dct.putComment(node, newCmt);
		}
		
		private static Comment createJavadocComment(final String text, final JCTree field) {
			return new Comment() {
				@Override public String getText() {
					return text;
				}
				
				@Override public int getSourcePos(int index) {
					return -1;
				}
				
				@Override public CommentStyle getStyle() {
					return CommentStyle.JAVADOC;
				}
				
				@Override public boolean isDeprecated() {
					return text.contains("@deprecated") && field instanceof JCVariableDecl && isFieldDeprecated(field);
				}
			};
		}
	}
	
	public static boolean isFieldDeprecated(JCTree field) {
		if (!(field instanceof JCVariableDecl)) return false;
		JCVariableDecl fieldNode = (JCVariableDecl) field;
		if ((fieldNode.mods.flags & Flags.DEPRECATED) != 0) return true;
		if (fieldNode.mods.annotations != null) for (JCAnnotation ann : fieldNode.mods.annotations) {
			String at = ann.getAnnotationType().toString();
			return at.equals("Deprecated") || at.endsWith(".Deprecated");
		}
		return false;
	}
	
	public static void initDocComments(JCCompilationUnit cu) {
		try {
			JCCOMPILATIONUNIT_DOCCOMMENTS.set(cu, new HashMap<Object, String>());
		} catch (IllegalArgumentException e) {
			// That's fine - we're on JDK8, we'll fix that later.
		} catch (IllegalAccessException e) {
			throw sneakyThrow(e);
		}
	}
	
	public static int getEndPosition(DiagnosticPosition pos, JCCompilationUnit top) {
		try {
			Object endPositions = JCCOMPILATIONUNIT_ENDPOSITIONS.get(top);
			return (Integer) getEndPosition.invoke(pos, endPositions);
		} catch (IllegalAccessException e) {
			throw sneakyThrow(e);
		} catch (InvocationTargetException e) {
			throw sneakyThrow(e.getCause());
		}
	}
	
	public static void storeEnd(JCTree tree, int pos, JCCompilationUnit top) {
		try {
			Object endPositions = JCCOMPILATIONUNIT_ENDPOSITIONS.get(top);
			storeEnd.invoke(endPositions, tree, pos);
		} catch (IllegalAccessException e) {
			throw sneakyThrow(e);
		} catch (InvocationTargetException e) {
			throw sneakyThrow(e.getCause());
		}
	}

	private static final Class<?> JC_VOID_TYPE, JC_NO_TYPE;
	
	static {
		Class<?> c = null;
		try {
			c = Class.forName("com.sun.tools.javac.code.Type$JCVoidType");
		} catch (Throwable ignore) {}
		JC_VOID_TYPE = c;
		c = null;
		try {
			c = Class.forName("com.sun.tools.javac.code.Type$JCNoType");
		} catch (Throwable ignore) {}
		JC_NO_TYPE = c;
	}
	
	private static final Field symtabVoidType = getFieldIfExists(Symtab.class, "voidType");
	
	private static Field getFieldIfExists(Class<?> c, String fieldName) {
		try {
			return Permit.getField(c, "voidType");
		} catch (Exception e) {
			return null;
		}
	}
	
	public static Type createVoidType(Symtab symbolTable, TypeTag tag) {
		if (symtabVoidType != null) try {
			return (Type) symtabVoidType.get(symbolTable);
		} catch (IllegalAccessException ignore) {}
		
		if (Javac.getJavaCompilerVersion() < 8) {
			return new JCNoType(((Integer) tag.value).intValue());
		} else {
			try {
				if (CTC_VOID.equals(tag)) {
					return (Type) JC_VOID_TYPE.getConstructor().newInstance();
				} else {
					return (Type) JC_NO_TYPE.getConstructor().newInstance();
				}
			} catch (InvocationTargetException e) {
				throw sneakyThrow(e.getCause());
			} catch (NoSuchMethodException e) {
				throw sneakyThrow(e);
			} catch (IllegalAccessException e) {
				throw sneakyThrow(e);
			} catch (InstantiationException e) {
				throw sneakyThrow(e);
			}
		}
	}
	
	private static class JCNoType extends Type implements NoType {
		public JCNoType(int tag) {
			super(tag, null);
		}
		
		@Override
		public TypeKind getKind() {
			if (tag == ((Integer) CTC_VOID.value).intValue()) return TypeKind.VOID;
			if (tag == ((Integer) CTC_NONE.value).intValue()) return TypeKind.NONE;
			throw new AssertionError("Unexpected tag: " + tag);
		}
		
		@Override
		public <R, P> R accept(TypeVisitor<R, P> v, P p) {
			return v.visitNoType(this, p);
		}
	}
	
	private static final Field JCCOMPILATIONUNIT_ENDPOSITIONS, JCCOMPILATIONUNIT_DOCCOMMENTS;
	static {
		Field f = null;
		try {
			f = Permit.getField(JCCompilationUnit.class, "endPositions");
		} catch (NoSuchFieldException e) {}
		JCCOMPILATIONUNIT_ENDPOSITIONS = f;
		
		f = null;
		try {
			f = Permit.getField(JCCompilationUnit.class, "docComments");
		} catch (NoSuchFieldException e) {}
		JCCOMPILATIONUNIT_DOCCOMMENTS = f;
	}
	
	static RuntimeException sneakyThrow(Throwable t) {
		if (t == null) throw new NullPointerException("t");
		Javac.<RuntimeException>sneakyThrow0(t);
		return null;
	}
	
	@SuppressWarnings("unchecked")
	private static <T extends Throwable> void sneakyThrow0(Throwable t) throws T {
		throw (T)t;
	}
}
