/*******************************************************************************
 * Copyright (c) 2014, 2015 GK Software AG.
 * 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:
 *     Stephan Herrmann - initial API and implementation
 *******************************************************************************/
package org.aspectj.org.eclipse.jdt.internal.compiler.classfmt;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

import org.aspectj.org.eclipse.jdt.core.compiler.CharOperation;
import org.aspectj.org.eclipse.jdt.internal.compiler.env.IBinaryAnnotation;
import org.aspectj.org.eclipse.jdt.internal.compiler.env.IBinaryElementValuePair;
import org.aspectj.org.eclipse.jdt.internal.compiler.env.ITypeAnnotationWalker;
import org.aspectj.org.eclipse.jdt.internal.compiler.lookup.LookupEnvironment;
import org.aspectj.org.eclipse.jdt.internal.compiler.lookup.SignatureWrapper;
import org.aspectj.org.eclipse.jdt.internal.compiler.util.Util;

public class ExternalAnnotationProvider {

	public static final String ANNOTION_FILE_EXTENSION= "eea"; //$NON-NLS-1$
	public static final String CLASS_PREFIX = "class "; //$NON-NLS-1$
	public static final String SUPER_PREFIX = "super "; //$NON-NLS-1$

	/** Representation of a 'nullable' annotation, independent of the concrete annotation name used in Java sources. */
	public static final char NULLABLE = '0';

	/** Representation of a 'nonnull' annotation, independent of the concrete annotation name used in Java sources. */
	public static final char NONNULL = '1';

	/**
	 * Represents absence of a null annotation. Useful for removing an existing null annotation.
	 * This character is used only internally, it is not part of the Eclipse External Annotation file format.
	 */
	public static final char NO_ANNOTATION = '@';

	static final String ANNOTATION_FILE_SUFFIX = ".eea"; //$NON-NLS-1$

	private static final String TYPE_PARAMETER_PREFIX = " <"; //$NON-NLS-1$


	private String typeName;
	String typeParametersAnnotationSource;
	Map<String,String> supertypeAnnotationSources;
	private Map<String,String> methodAnnotationSources;
	private Map<String,String> fieldAnnotationSources;
	
	/**
	 * Create and initialize.
	 * @param input open input stream to read the annotations from, will be closed by the constructor.
	 * @param typeName slash-separated qualified name of a type
	 * @throws IOException various issues when accessing the annotation file
	 */
	public ExternalAnnotationProvider(InputStream input, String typeName) throws IOException {
		this.typeName = typeName;
		initialize(input);
	}

	private void initialize(InputStream input) throws IOException {
		LineNumberReader reader = new LineNumberReader(new InputStreamReader(input));
		try {
			assertClassHeader(reader.readLine(), this.typeName);

			String line;
			if ((line = reader.readLine()) == null) {
				return;
			}
			if (line.startsWith(TYPE_PARAMETER_PREFIX)) {
				if ((line = reader.readLine()) == null) // skip first line, second line may contain type parameter annotations
					return;
				if (line.startsWith(TYPE_PARAMETER_PREFIX)) {
					this.typeParametersAnnotationSource = line.substring(TYPE_PARAMETER_PREFIX.length());
					if ((line = reader.readLine()) == null)
						return;
				} 
			}
			String pendingLine;
			do {
				pendingLine = null;
				line = line.trim();
				if (line.isEmpty()) continue;
				String rawSig = null, annotSig = null;
				// selector:
				String selector = line;
				boolean isSuper = selector.startsWith(SUPER_PREFIX);
				if (isSuper)
					selector = selector.substring(SUPER_PREFIX.length());
				int errLine = -1;
				try {
					// raw signature:
					line = reader.readLine();
					if (line != null && !line.isEmpty() && line.charAt(0) == ' ') // first signature line is mandatory
						rawSig = line.substring(1);
					else
						errLine = reader.getLineNumber();
					// annotated signature:
					line = reader.readLine();
					if (line == null || line.isEmpty())
						continue; // skip since optional line with annotations is missing
					if (line.charAt(0) != ' ') {
						pendingLine = line; // push back what appears to be the next selector, not a signature
						continue;
					}
					annotSig = line.substring(1);
				} catch (Exception ex) {
					// continue to escalate below
				}
				if (rawSig == null || annotSig == null) {
					if (errLine == -1) errLine = reader.getLineNumber();
					throw new IOException("Illegal format for annotation file at line "+errLine); //$NON-NLS-1$
				}
				// discard optional meta data (separated by whitespace):
				annotSig = trimTail(annotSig);
				if (isSuper) {
					if (this.supertypeAnnotationSources == null)
						this.supertypeAnnotationSources = new HashMap<String, String>();
					this.supertypeAnnotationSources.put('L'+selector+rawSig+';', annotSig);
				} else if (rawSig.contains("(")) { //$NON-NLS-1$
					if (this.methodAnnotationSources == null)
						this.methodAnnotationSources = new HashMap<String, String>();
					this.methodAnnotationSources.put(selector+rawSig, annotSig);
				} else {
					if (this.fieldAnnotationSources == null)
						this.fieldAnnotationSources = new HashMap<String, String>();
					this.fieldAnnotationSources.put(selector+':'+rawSig, annotSig);
				}
			} while (((line = pendingLine) != null) || (line = reader.readLine()) != null);
		} finally {
			reader.close();
		}
	}

	/**
	 * Assert that the given line is a class header for 'typeName' (slash-separated qualified name).
	 */
	public static void assertClassHeader(String line, String typeName) throws IOException {
		if (line.startsWith(CLASS_PREFIX)) {
			line = line.substring(CLASS_PREFIX.length());
		} else {
			throw new IOException("missing class header in annotation file"); //$NON-NLS-1$
		}
		if (!trimTail(line).equals(typeName)) {
			throw new IOException("mismatching class name in annotation file, expected "+typeName+", but header said "+line); //$NON-NLS-1$ //$NON-NLS-2$
		}
	}

	/**
	 * Extract the signature from a line of an external annotation file.
	 * Answers null if line is not in the expected format.
	 */
	public static String extractSignature(String line) {
		if (line == null || line.isEmpty() || line.charAt(0) != ' ')
			return null;
		return trimTail(line.substring(1));
	}

	/** Lines may contain arbitrary trailing data, separated by white space. */
	protected static String trimTail(String line) {
		int tail = line.indexOf(' ');
		if (tail == -1)
			tail = line.indexOf('\t');
		if (tail != -1) 
			return line.substring(0, tail);
		return line;
	}

	public ITypeAnnotationWalker forTypeHeader(LookupEnvironment environment) {
		if (this.typeParametersAnnotationSource != null || this.supertypeAnnotationSources != null)
			return new DispatchingAnnotationWalker(environment);
		return ITypeAnnotationWalker.EMPTY_ANNOTATION_WALKER;
	}

	public ITypeAnnotationWalker forMethod(char[] selector, char[] signature, LookupEnvironment environment) {
		Map<String, String> sources = this.methodAnnotationSources;
		if (sources != null) {
			String source = sources.get(String.valueOf(CharOperation.concat(selector, signature)));
			if (source != null)
				return new MethodAnnotationWalker(source.toCharArray(), 0, environment);
		}
		return ITypeAnnotationWalker.EMPTY_ANNOTATION_WALKER;
	}

	public ITypeAnnotationWalker forField(char[] selector, char[] signature, LookupEnvironment environment) {
		if (this.fieldAnnotationSources != null) {
			String source = this.fieldAnnotationSources.get(String.valueOf(CharOperation.concat(selector, signature, ':')));
			if (source != null)
				return new FieldAnnotationWalker(source.toCharArray(), 0, environment);
		}
		return ITypeAnnotationWalker.EMPTY_ANNOTATION_WALKER;
	}

	public String toString() {
		StringBuilder sb = new StringBuilder();
		sb.append("External Annotations for ").append(this.typeName).append('\n'); //$NON-NLS-1$
		sb.append("Methods:\n"); //$NON-NLS-1$
		if (this.methodAnnotationSources != null)
			for (Entry<String,String> e : this.methodAnnotationSources.entrySet())
				sb.append('\t').append(e.getKey()).append('\n');
		return sb.toString();
	}

	abstract class SingleMarkerAnnotation implements IBinaryAnnotation {
		@Override
		public IBinaryElementValuePair[] getElementValuePairs() {
			return ElementValuePairInfo.NoMembers;
		}
		protected char[] getBinaryTypeName(char[][] name) {
			return CharOperation.concat('L', CharOperation.concatWith(name, '/'), ';');
		}
	}

	SingleMarkerAnnotation NULLABLE_ANNOTATION, NONNULL_ANNOTATION;

	void initAnnotations(final LookupEnvironment environment) {
		if (this.NULLABLE_ANNOTATION == null) {
			this.NULLABLE_ANNOTATION = new SingleMarkerAnnotation() {
				@Override public char[] getTypeName() { return getBinaryTypeName(environment.getNullableAnnotationName()); }
			};
		}
		if (this.NONNULL_ANNOTATION == null) {
			this.NONNULL_ANNOTATION = new SingleMarkerAnnotation() {
				@Override public char[] getTypeName() { return getBinaryTypeName(environment.getNonNullAnnotationName()); }
			};
		}
	}

	/**
	 * Walker for top-level elements of a type (type parameters & super types),
	 * which dispatches to specialized walkers for those details.
	 */
	class DispatchingAnnotationWalker implements ITypeAnnotationWalker {

		private LookupEnvironment environment;
		private TypeParametersAnnotationWalker typeParametersWalker;

		public DispatchingAnnotationWalker(LookupEnvironment environment) {
			this.environment = environment;
		}
		public ITypeAnnotationWalker toTypeParameter(boolean isClassTypeParameter, int rank) {
			String source = ExternalAnnotationProvider.this.typeParametersAnnotationSource;
			if (source != null) {
				if (this.typeParametersWalker == null) 
					this.typeParametersWalker = new TypeParametersAnnotationWalker(source.toCharArray(), 0, 0, null, this.environment);
				return this.typeParametersWalker.toTypeParameter(isClassTypeParameter, rank);
			}
			return this;
		}
		public ITypeAnnotationWalker toTypeParameterBounds(boolean isClassTypeParameter, int parameterRank) {
			if (this.typeParametersWalker != null)
				return this.typeParametersWalker.toTypeParameterBounds(isClassTypeParameter, parameterRank);
			return this;
		}
		public ITypeAnnotationWalker toSupertype(short index, char[] superTypeSignature) {
			Map<String, String> sources = ExternalAnnotationProvider.this.supertypeAnnotationSources;
			if (sources != null) {
				String source = sources.get(String.valueOf(superTypeSignature));
				if (source != null)
					return new SuperTypesAnnotationWalker(source.toCharArray(), this.environment);
			}
			return this;
		}
		// the rest is borrowed from EMPTY_ANNOTATION_WALKER:
		public ITypeAnnotationWalker toField() { return this; }
		public ITypeAnnotationWalker toThrows(int rank) { return this; }
		public ITypeAnnotationWalker toTypeArgument(int rank) { return this; }
		public ITypeAnnotationWalker toMethodParameter(short index) { return this; }
		public ITypeAnnotationWalker toTypeBound(short boundIndex) { return this; }
		public ITypeAnnotationWalker toMethodReturn() { return this; }
		public ITypeAnnotationWalker toReceiver() { return this; }
		public ITypeAnnotationWalker toWildcardBound() { return this; }
		public ITypeAnnotationWalker toNextArrayDimension() { return this; }
		public ITypeAnnotationWalker toNextNestedType() { return this; }
		public IBinaryAnnotation[] getAnnotationsAtCursor(int currentTypeId) { return NO_ANNOTATIONS; }
	}

	abstract class BasicAnnotationWalker implements ITypeAnnotationWalker {
		
		char[] source;
		SignatureWrapper wrapper;
		int pos;
		int prevTypeArgStart;
		int currentTypeBound;
		LookupEnvironment environment;

		BasicAnnotationWalker(char[] source, int pos, LookupEnvironment environment) {
			this.source = source;
			this.pos = pos;
			this.environment = environment;
			initAnnotations(environment);
		}
		
		SignatureWrapper wrapperWithStart(int start) {
			if (this.wrapper == null)
				this.wrapper = new SignatureWrapper(this.source);
			this.wrapper.start = start;
			return this.wrapper;
		}

		@Override
		public ITypeAnnotationWalker toReceiver() {
			return ITypeAnnotationWalker.EMPTY_ANNOTATION_WALKER;
		}

		@Override
		public ITypeAnnotationWalker toTypeParameter(boolean isClassTypeParameter, int rank) {
			return ITypeAnnotationWalker.EMPTY_ANNOTATION_WALKER;
		}

		@Override
		public ITypeAnnotationWalker toTypeParameterBounds(boolean isClassTypeParameter, int parameterRank) {
			return ITypeAnnotationWalker.EMPTY_ANNOTATION_WALKER;
		}

		@Override
		public ITypeAnnotationWalker toTypeBound(short boundIndex) {
			return ITypeAnnotationWalker.EMPTY_ANNOTATION_WALKER;
		}

		@Override
		public ITypeAnnotationWalker toSupertype(short index, char[] superTypeSignature) {
			return ITypeAnnotationWalker.EMPTY_ANNOTATION_WALKER;
		}

		@Override
		public ITypeAnnotationWalker toTypeArgument(int rank) {
			if (rank == 0) {
				int start = CharOperation.indexOf('<', this.source, this.pos) + 1;
				this.prevTypeArgStart = start;
				return new MethodAnnotationWalker(this.source, start, this.environment);
			}
			int next = this.prevTypeArgStart;
			switch (this.source[next]) {
				case '*': 
					break;
				case '-': 
				case '+':
					next++;
					//$FALL-THROUGH$
				default:
					next = wrapperWithStart(next).computeEnd();
			}
			next++;
		    this.prevTypeArgStart = next;
		    return new MethodAnnotationWalker(this.source, next,	this.environment);
		}

		@Override
		public ITypeAnnotationWalker toWildcardBound() {
			switch (this.source[this.pos]) {
				case '-': 
				case '+':
					return new MethodAnnotationWalker(this.source, this.pos+1, this.environment);
				default: // includes unbounded '*'
					return ITypeAnnotationWalker.EMPTY_ANNOTATION_WALKER;
			}			
		}

		@Override
		public ITypeAnnotationWalker toNextArrayDimension() {
			if (this.source[this.pos] == '[') {
				int newPos = this.pos+1;
				switch (this.source[newPos]) {
					case NULLABLE: case NONNULL: newPos++; break;
				}
				return new MethodAnnotationWalker(this.source, newPos, this.environment);
			}
			return ITypeAnnotationWalker.EMPTY_ANNOTATION_WALKER;
		}

		@Override
		public ITypeAnnotationWalker toNextNestedType() {
			return this; // FIXME(stephan)
		}

		@Override
		public IBinaryAnnotation[] getAnnotationsAtCursor(int currentTypeId) {
			if (this.pos != -1 && this.pos < this.source.length-2) {
				switch (this.source[this.pos]) {
					case 'T':
					case 'L':
					case '[':
					case '*':
					case '+':
					case '-':
						switch (this.source[this.pos+1]) {
							case NULLABLE:
								return new IBinaryAnnotation[]{ ExternalAnnotationProvider.this.NULLABLE_ANNOTATION };
							case NONNULL:
								return new IBinaryAnnotation[]{ ExternalAnnotationProvider.this.NONNULL_ANNOTATION };
						}
				}				
			}
			return null;
		}
	}

	/**
	 * Walker that may serve the annotations on type parameters of the current class or method.
	 */
	public class TypeParametersAnnotationWalker extends BasicAnnotationWalker {

		int[] rankStarts; // indices of start positions for type parameters per rank
		int currentRank;

		TypeParametersAnnotationWalker(char[] source, int pos, int rank, int[] rankStarts, LookupEnvironment environment) {
			super(source, pos, environment);
			this.currentRank = rank;
			if (rankStarts != null) {
				this.rankStarts = rankStarts;
			} else {
				// eagerly scan all type parameters:
				int length = source.length;
				rankStarts = new int[length];
				int curRank = 0;
				// next block cf. BinaryTypeBinding.createTypeVariables():
				int depth = 0;
				boolean pendingVariable = true;
				scanVariables: {
					for (int i = pos; i < length; i++) {
						switch(this.source[i]) {
							case Util.C_GENERIC_START :
								depth++;
								break;
							case Util.C_GENERIC_END :
								if (--depth < 0)
									break scanVariables;
								break;
							case Util.C_NAME_END :
								if ((depth == 0) && (i +1 < length) && (this.source[i+1] != Util.C_COLON))
									pendingVariable = true;
								break;
							default:
								if (pendingVariable) {
									pendingVariable = false;
									rankStarts[curRank++] = i;
								}
						}
					}
				}
				System.arraycopy(rankStarts, 0, this.rankStarts = new int[curRank], 0, curRank);
			}
		}
		
		@Override
		public ITypeAnnotationWalker toTypeParameter(boolean isClassTypeParameter, int rank) {
			if (rank == this.currentRank)
				return this;
			if (rank < this.rankStarts.length)
				return new TypeParametersAnnotationWalker(this.source, this.rankStarts[rank], rank, this.rankStarts, this.environment);
			return ITypeAnnotationWalker.EMPTY_ANNOTATION_WALKER;
		}

		@Override
		public ITypeAnnotationWalker toTypeParameterBounds(boolean isClassTypeParameter, int parameterRank) {
			return new TypeParametersAnnotationWalker(this.source, this.rankStarts[parameterRank], parameterRank, this.rankStarts, this.environment);
		}

		@Override
		public ITypeAnnotationWalker toTypeBound(short boundIndex) {
			// assume we are positioned either at the start of the bounded type parameter
			// or at the start of a previous type bound
			int p = this.pos;
			int i = this.currentTypeBound;
			while(true) {
				// each bound is prefixed with ':'
				int colon = CharOperation.indexOf(Util.C_COLON, this.source, p);
				if (colon != -1)
					p = colon + 1;
				if (++i > boundIndex) break;
				// skip next type:
				p = wrapperWithStart(p).computeEnd()+1;
			}
			this.pos = p;
			this.currentTypeBound = boundIndex;
			return this;
		}

		@Override
		public ITypeAnnotationWalker toField() {
			throw new UnsupportedOperationException("Cannot navigate to fields"); //$NON-NLS-1$
		}

		@Override
		public ITypeAnnotationWalker toMethodReturn() {
			throw new UnsupportedOperationException("Cannot navigate to method return"); //$NON-NLS-1$
		}

		@Override
		public ITypeAnnotationWalker toMethodParameter(short index) {
			throw new UnsupportedOperationException("Cannot navigate to method parameter"); //$NON-NLS-1$
		}

		@Override
		public ITypeAnnotationWalker toThrows(int index) {
			throw new UnsupportedOperationException("Cannot navigate to throws"); //$NON-NLS-1$
		}

		@Override
		public IBinaryAnnotation[] getAnnotationsAtCursor(int currentTypeId) {
			if (this.pos != -1 && this.pos < this.source.length-1) {
				switch (this.source[this.pos]) {
					case NULLABLE:
						return new IBinaryAnnotation[]{ ExternalAnnotationProvider.this.NULLABLE_ANNOTATION };
					case NONNULL:
						return new IBinaryAnnotation[]{ ExternalAnnotationProvider.this.NONNULL_ANNOTATION };
				}				
			}
			return super.getAnnotationsAtCursor(currentTypeId);
		}
	}

	/** Walker serving type annotations on a type's supertypes. */
	class SuperTypesAnnotationWalker extends BasicAnnotationWalker {

		SuperTypesAnnotationWalker(char[] source, LookupEnvironment environment) {
			super(source, 0, environment);
		}

		// actual implementation is inherited, main entries: toTypeArgument & getAnnotationsAtCursor

		@Override
		public ITypeAnnotationWalker toField() {
			throw new UnsupportedOperationException("Supertype has no field annotations"); //$NON-NLS-1$
		}

		@Override
		public ITypeAnnotationWalker toMethodReturn() {
			throw new UnsupportedOperationException("Supertype has no method return"); //$NON-NLS-1$
		}

		@Override
		public ITypeAnnotationWalker toMethodParameter(short index) {
			throw new UnsupportedOperationException("Supertype has no method parameter"); //$NON-NLS-1$
		}

		@Override
		public ITypeAnnotationWalker toThrows(int index) {
			throw new UnsupportedOperationException("Supertype has no throws"); //$NON-NLS-1$
		}
	}

	public interface IMethodAnnotationWalker extends ITypeAnnotationWalker {
		int getParameterCount();
	}
	class MethodAnnotationWalker extends BasicAnnotationWalker implements IMethodAnnotationWalker {

		int prevParamStart;
		TypeParametersAnnotationWalker typeParametersWalker;

		MethodAnnotationWalker(char[] source, int pos, LookupEnvironment environment) {
			super(source, pos, environment);
		}
	
		int typeEnd(int start) {
			while (this.source[start] == '[') {
				start++;
				char an = this.source[start];
				if (an == NULLABLE || an == NONNULL)
					start++;
			}
			int end = wrapperWithStart(start).computeEnd();
			return end;
		}
		
		@Override
		public ITypeAnnotationWalker toTypeParameter(boolean isClassTypeParameter, int rank) {
			if (this.source[0] == '<') {
				if (this.typeParametersWalker == null)
					return this.typeParametersWalker = new TypeParametersAnnotationWalker(this.source, this.pos+1, rank, null, this.environment);
				return this.typeParametersWalker.toTypeParameter(isClassTypeParameter, rank);
			}
			return ITypeAnnotationWalker.EMPTY_ANNOTATION_WALKER;
		}

		@Override
		public ITypeAnnotationWalker toTypeParameterBounds(boolean isClassTypeParameter, int parameterRank) {
			if (this.typeParametersWalker != null)
				return this.typeParametersWalker.toTypeParameterBounds(isClassTypeParameter, parameterRank);
			return ITypeAnnotationWalker.EMPTY_ANNOTATION_WALKER;
		}

		@Override
		public ITypeAnnotationWalker toMethodReturn() {
			int close = CharOperation.indexOf(')', this.source);
			if (close != -1) {
				// optimization, see toMethodParameter.
				this.pos = close+1;
				return this;
			}
			return ITypeAnnotationWalker.EMPTY_ANNOTATION_WALKER;
		}

		@Override
		public ITypeAnnotationWalker toMethodParameter(short index) {
			if (index == 0) {
				int start = CharOperation.indexOf('(', this.source) + 1;
				this.prevParamStart = start;
				// optimization: normally we should create a new walker with pos=start,
				// but since we know the order how BTB/LE call us, we can safely use one walker for all parameters:
				this.pos = start;
				return this;
			}
			int end = typeEnd(this.prevParamStart); // leverage the fact that all parameters are evaluated in order
			end++;
		    this.prevParamStart = end;
		    // optimization, see above.
		    this.pos = end;
		    return this;
		}

		@Override
		public ITypeAnnotationWalker toThrows(int index) {
			return this;
		}

		@Override
		public ITypeAnnotationWalker toField() {
			throw new UnsupportedOperationException("Methods have no fields"); //$NON-NLS-1$
		}

		@Override
		public int getParameterCount() {
			int count = 0;
			int start = CharOperation.indexOf('(', this.source) + 1;
			while (start < this.source.length && this.source[start] != ')') {
				start = typeEnd(start) + 1;
				count++;
			}
			return count;
		}
	}
	
	class FieldAnnotationWalker extends BasicAnnotationWalker {
		public FieldAnnotationWalker(char[] source, int pos, LookupEnvironment environment) {
			super(source, pos, environment);
		}

		@Override
		public ITypeAnnotationWalker toField() {
			return this;
		}

		@Override
		public ITypeAnnotationWalker toMethodReturn() {
			throw new UnsupportedOperationException("Field has no method return"); //$NON-NLS-1$
		}

		@Override
		public ITypeAnnotationWalker toMethodParameter(short index) {
			throw new UnsupportedOperationException("Field has no method parameter"); //$NON-NLS-1$
		}

		@Override
		public ITypeAnnotationWalker toThrows(int index) {
			throw new UnsupportedOperationException("Field has no throws"); //$NON-NLS-1$
		}
	}
}
