/*
 * Copyright (C) 2009-2013 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 java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Pattern;

import org.eclipse.jdt.internal.compiler.ast.ASTNode;
import org.eclipse.jdt.internal.compiler.ast.AbstractMethodDeclaration;
import org.eclipse.jdt.internal.compiler.ast.Annotation;
import org.eclipse.jdt.internal.compiler.ast.ClassLiteralAccess;
import org.eclipse.jdt.internal.compiler.ast.Clinit;
import org.eclipse.jdt.internal.compiler.ast.Expression;
import org.eclipse.jdt.internal.compiler.ast.FieldDeclaration;
import org.eclipse.jdt.internal.compiler.ast.Literal;
import org.eclipse.jdt.internal.compiler.ast.QualifiedNameReference;
import org.eclipse.jdt.internal.compiler.ast.SingleNameReference;
import org.eclipse.jdt.internal.compiler.ast.TryStatement;
import org.eclipse.jdt.internal.compiler.ast.TypeDeclaration;
import org.eclipse.jdt.internal.compiler.ast.TypeReference;
import org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants;
import org.eclipse.jdt.internal.compiler.impl.CompilerOptions;
import org.eclipse.jdt.internal.compiler.lookup.TypeIds;

public class Eclipse {
	private static final Annotation[] EMPTY_ANNOTATIONS_ARRAY = new Annotation[0];
	/**
	 * Eclipse's Parser class is instrumented to not attempt to fill in the body of any method or initializer
	 * or field initialization if this flag is set. Set it on the flag field of
	 * any method, field, or initializer you create!
	 */
	public static final int ECLIPSE_DO_NOT_TOUCH_FLAG = ASTNode.Bit24;
	
	private Eclipse() {
		//Prevent instantiation
	}
	
	/**
	 * For 'speed' reasons, Eclipse works a lot with char arrays. I have my doubts this was a fruitful exercise,
	 * but we need to deal with it. This turns [[java][lang][String]] into "java.lang.String".
	 */
	public static String toQualifiedName(char[][] typeName) {
		int len = typeName.length - 1;
		for (char[] c : typeName) len += c.length;
		StringBuilder sb = new StringBuilder(len);
		boolean first = true;
		for (char[] c : typeName) {
			sb.append(first ? "" : ".").append(c);
			first = false;
		}
		return sb.toString();
	}
	
	public static char[][] fromQualifiedName(String typeName) {
		String[] split = typeName.split("\\.");
		char[][] result = new char[split.length][];
		for (int i = 0; i < split.length; i++) {
			result[i] = split[i].toCharArray();
		}
		return result;
	}
	
	public static long pos(ASTNode node) {
		return ((long) node.sourceStart << 32) | (node.sourceEnd & 0xFFFFFFFFL);
	}
	
	public static long[] poss(ASTNode node, int repeat) {
		long p = ((long) node.sourceStart << 32) | (node.sourceEnd & 0xFFFFFFFFL);
		long[] out = new long[repeat];
		Arrays.fill(out, p);
		return out;
	}
	
	/**
	 * Checks if an eclipse-style array-of-array-of-characters to represent a fully qualified name ('foo.bar.baz'), matches a plain
	 * string containing the same fully qualified name with dots in the string.
	 */
	public static boolean nameEquals(char[][] typeName, String string) {
		StringBuilder sb = new StringBuilder();
		boolean first = true;
		for (char[] elem : typeName) {
			if (first) first = false;
			else sb.append('.');
			sb.append(elem);
		}
		
		return string.contentEquals(sb);
	}
	
	public static boolean hasClinit(TypeDeclaration parent) {
		if (parent.methods == null) return false;
		
		for (AbstractMethodDeclaration method : parent.methods) {
			if (method instanceof Clinit) return true;
		}
		return false;
	}
	
	/**
	 * Searches the given field node for annotations and returns each one that matches the provided regular expression pattern.
	 * 
	 * Only the simple name is checked - the package and any containing class are ignored.
	 */
	public static Annotation[] findAnnotations(FieldDeclaration field, Pattern namePattern) {
		List<Annotation> result = new ArrayList<Annotation>();
		if (field.annotations == null) return EMPTY_ANNOTATIONS_ARRAY;
		for (Annotation annotation : field.annotations) {
			TypeReference typeRef = annotation.type;
			if (typeRef != null && typeRef.getTypeName() != null) {
				char[][] typeName = typeRef.getTypeName();
				String suspect = new String(typeName[typeName.length - 1]);
				if (namePattern.matcher(suspect).matches()) {
					result.add(annotation);
				}
			}
		}	
		return result.toArray(EMPTY_ANNOTATIONS_ARRAY);
	}
	
	/** Matches any of the 8 primitive names, such as {@code boolean}. */
	private static final Pattern PRIMITIVE_TYPE_NAME_PATTERN = Pattern.compile(
			"^(boolean|byte|short|int|long|float|double|char)$");
	
	/**
	 * Checks if the given type reference represents a primitive type.
	 */
	public static boolean isPrimitive(TypeReference ref) {
		if (ref.dimensions() > 0) return false;
		return PRIMITIVE_TYPE_NAME_PATTERN.matcher(toQualifiedName(ref.getTypeName())).matches();
	}
	
	/**
	 * Returns the actual value of the given Literal or Literal-like node.
	 */
	public static Object calculateValue(Expression e) {
		if (e instanceof Literal) {
			((Literal)e).computeConstant();
			switch (e.constant.typeID()) {
			case TypeIds.T_int: return e.constant.intValue();
			case TypeIds.T_byte: return e.constant.byteValue();
			case TypeIds.T_short: return e.constant.shortValue();
			case TypeIds.T_char: return e.constant.charValue();
			case TypeIds.T_float: return e.constant.floatValue();
			case TypeIds.T_double: return e.constant.doubleValue();
			case TypeIds.T_boolean: return e.constant.booleanValue();
			case TypeIds.T_long: return e.constant.longValue();
			case TypeIds.T_JavaLangString: return e.constant.stringValue();
			default: return null;
			}
		} else if (e instanceof ClassLiteralAccess) {
			return Eclipse.toQualifiedName(((ClassLiteralAccess)e).type.getTypeName());
		} else if (e instanceof SingleNameReference) {
			return new String(((SingleNameReference)e).token);
		} else if (e instanceof QualifiedNameReference) {
			String qName = Eclipse.toQualifiedName(((QualifiedNameReference)e).tokens);
			int idx = qName.lastIndexOf('.');
			return idx == -1 ? qName : qName.substring(idx+1);
		}
		
		return null;
	}
	
	private static long latestEcjCompilerVersionConstantCached = 0;
	
	public static long getLatestEcjCompilerVersionConstant() {
		if (latestEcjCompilerVersionConstantCached != 0) return latestEcjCompilerVersionConstantCached;
		
		int highestVersionSoFar = 0;
		for (Field f : ClassFileConstants.class.getDeclaredFields()) {
			try {
				if (f.getName().startsWith("JDK1_")) {
					int thisVersion = Integer.parseInt(f.getName().substring("JDK1_".length()));
					if (thisVersion > highestVersionSoFar) {
						highestVersionSoFar = thisVersion;
						latestEcjCompilerVersionConstantCached = (Long) f.get(null);
					}
				}
			} catch (Exception ignore) {}
		}
		
		if (highestVersionSoFar > 6 && !ecjSupportsJava7Features()) {
			latestEcjCompilerVersionConstantCached = ClassFileConstants.JDK1_6;
		}
		return latestEcjCompilerVersionConstantCached;
	}
	
	private static int ecjCompilerVersionCached = -1;
	public static int getEcjCompilerVersion() {
		if (ecjCompilerVersionCached >= 0) return ecjCompilerVersionCached;
		
		for (Field f : CompilerOptions.class.getDeclaredFields()) {
			try {
				if (f.getName().startsWith("VERSION_1_")) {
					ecjCompilerVersionCached = Math.max(ecjCompilerVersionCached, Integer.parseInt(f.getName().substring("VERSION_1_".length())));
				}
			} catch (Exception ignore) {}
		}
		
		if (ecjCompilerVersionCached < 5) ecjCompilerVersionCached = 5;
		if (!ecjSupportsJava7Features()) ecjCompilerVersionCached = Math.min(6, ecjCompilerVersionCached);
		return ecjCompilerVersionCached;
	}
	
	/**
	 * Certain ECJ versions that only go up to -source 6 report that they support -source 7 and even fail to error when -source 7 is applied.
	 * We detect this and correctly say that no more than -source 6 is supported. (when this is the case, this method returns false).
	 */
	private static boolean ecjSupportsJava7Features() {
		try {
			TryStatement.class.getDeclaredField("resources");
			return true;
		} catch (NoSuchFieldException e) {
			return false;
		}
	}
}
