/* *******************************************************************
 * Copyright (c) 2002 Palo Alto Research Center, Incorporated (PARC).
 * All rights reserved.
 * This program and the accompanying materials are made available
 * under the terms of the Eclipse Public License v 2.0
 * which accompanies this distribution and is available at
 * https://www.eclipse.org/org/documents/epl-2.0/EPL-2.0.txt
 *
 * Contributors:
 *     PARC     initial implementation
 * ******************************************************************/

package org.aspectj.weaver.patterns;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.aspectj.bridge.ISourceLocation;
import org.aspectj.util.FuzzyBoolean;
import org.aspectj.weaver.CompressingDataOutputStream;
import org.aspectj.weaver.ISourceContext;
import org.aspectj.weaver.IntMap;
import org.aspectj.weaver.ResolvableTypeList;
import org.aspectj.weaver.ResolvedType;
import org.aspectj.weaver.UnresolvedType;
import org.aspectj.weaver.VersionedDataInputStream;
import org.aspectj.weaver.World;

public class TypePatternList extends PatternNode {
	private TypePattern[] typePatterns;
	int ellipsisCount = 0;

	public static final TypePatternList EMPTY = new TypePatternList(new TypePattern[] {});

	public static final TypePatternList ANY = new TypePatternList(new TypePattern[] { new EllipsisTypePattern() }); // can't use

	// TypePattern.ELLIPSIS
	// because of
	// circular
	// static
	// dependency
	// that
	// introduces

	public TypePatternList() {
		typePatterns = new TypePattern[0];
		ellipsisCount = 0;
	}

	public TypePatternList(TypePattern[] arguments) {
		this.typePatterns = arguments;
		for (TypePattern argument : arguments) {
			if (argument == TypePattern.ELLIPSIS) {
				ellipsisCount++;
			}
		}
	}

	public TypePatternList(List<TypePattern> l) {
		this(l.toArray(new TypePattern[0]));
	}

	public int size() {
		return typePatterns.length;
	}

	public TypePattern get(int index) {
		return typePatterns[index];
	}

	@Override
	public String toString() {
		StringBuilder buf = new StringBuilder();
		buf.append("(");
		for (int i = 0, len = typePatterns.length; i < len; i++) {
			TypePattern type = typePatterns[i];
			if (i > 0) {
				buf.append(", ");
			}
			if (type == TypePattern.ELLIPSIS) {
				buf.append("..");
			} else {
				buf.append(type.toString());
			}
		}
		buf.append(")");
		return buf.toString();
	}

	/*
	 * return true iff this pattern could ever match a signature with the given number of parameters
	 */
	public boolean canMatchSignatureWithNParameters(int numParams) {
		if (ellipsisCount == 0) {
			return numParams == size();
		} else {
			return (size() - ellipsisCount) <= numParams;
		}
	}

	public FuzzyBoolean matches(ResolvedType[] types, TypePattern.MatchKind kind) {
		return matches(types, kind, null);
	}

	// XXX shares much code with WildTypePattern and with NamePattern
	/**
	 * When called with TypePattern.STATIC this will always return either FuzzyBoolean.YES or FuzzyBoolean.NO.
	 *
	 * When called with TypePattern.DYNAMIC this could return MAYBE if at runtime it would be possible for arguments of the given
	 * static types to dynamically match this, but it is not known for certain.
	 *
	 * This method will never return FuzzyBoolean.NEVER
	 */
	public FuzzyBoolean matches(ResolvedType[] types, TypePattern.MatchKind kind, ResolvedType[][] parameterAnnotations) {
		int nameLength = types.length;
		int patternLength = typePatterns.length;

		int nameIndex = 0;
		int patternIndex = 0;

		if (ellipsisCount == 0) {
			if (nameLength != patternLength) {
				return FuzzyBoolean.NO;
			}
			FuzzyBoolean finalReturn = FuzzyBoolean.YES;
			while (patternIndex < patternLength) {
				ResolvedType t = types[nameIndex];
				FuzzyBoolean ret = null;
				try {
					if (parameterAnnotations != null) {
						t.temporaryAnnotationTypes = parameterAnnotations[nameIndex];
					}
					ret = typePatterns[patternIndex].matches(t, kind);
				} finally {
					t.temporaryAnnotationTypes = null;
				}
				patternIndex++;
				nameIndex++;
				if (ret == FuzzyBoolean.NO) {
					return ret;
				}
				if (ret == FuzzyBoolean.MAYBE) {
					finalReturn = ret;
				}
			}
			return finalReturn;
		} else if (ellipsisCount == 1) {
			if (nameLength < patternLength - 1) {
				return FuzzyBoolean.NO;
			}
			FuzzyBoolean finalReturn = FuzzyBoolean.YES;
			while (patternIndex < patternLength) {
				TypePattern p = typePatterns[patternIndex++];
				if (p == TypePattern.ELLIPSIS) {
					nameIndex = nameLength - (patternLength - patternIndex);
				} else {
					ResolvedType t = types[nameIndex];
					FuzzyBoolean ret = null;
					try {
						if (parameterAnnotations != null) {
							t.temporaryAnnotationTypes = parameterAnnotations[nameIndex];
						}
						ret = p.matches(t, kind);
					} finally {
						t.temporaryAnnotationTypes = null;
					}
					nameIndex++;
					if (ret == FuzzyBoolean.NO) {
						return ret;
					}
					if (ret == FuzzyBoolean.MAYBE) {
						finalReturn = ret;
					}
				}
			}
			return finalReturn;
		} else {
			// System.err.print("match(" + arguments + ", " + types + ") -> ");
			FuzzyBoolean b = outOfStar(typePatterns, types, 0, 0, patternLength - ellipsisCount, nameLength, ellipsisCount, kind,
					parameterAnnotations);
			// System.err.println(b);
			return b;
		}
	}

	private static FuzzyBoolean outOfStar(final TypePattern[] pattern, final ResolvedType[] target, int pi, int ti, int pLeft,
			int tLeft, final int starsLeft, TypePattern.MatchKind kind, ResolvedType[][] parameterAnnotations) {
		if (pLeft > tLeft) {
			return FuzzyBoolean.NO;
		}
		FuzzyBoolean finalReturn = FuzzyBoolean.YES;
		while (true) {
			// invariant: if (tLeft > 0) then (ti < target.length && pi < pattern.length)
			if (tLeft == 0) {
				return finalReturn;
			}
			if (pLeft == 0) {
				if (starsLeft > 0) {
					return finalReturn;
				} else {
					return FuzzyBoolean.NO;
				}
			}
			if (pattern[pi] == TypePattern.ELLIPSIS) {
				return inStar(pattern, target, pi + 1, ti, pLeft, tLeft, starsLeft - 1, kind, parameterAnnotations);
			}
			FuzzyBoolean ret = null;
			try {
				if (parameterAnnotations != null) {
					target[ti].temporaryAnnotationTypes = parameterAnnotations[ti];
				}
				ret = pattern[pi].matches(target[ti], kind);
			} finally {
				target[ti].temporaryAnnotationTypes = null;
			}
			if (ret == FuzzyBoolean.NO) {
				return ret;
			}
			if (ret == FuzzyBoolean.MAYBE) {
				finalReturn = ret;
			}
			pi++;
			ti++;
			pLeft--;
			tLeft--;
		}
	}

	private static FuzzyBoolean inStar(final TypePattern[] pattern, final ResolvedType[] target, int pi, int ti, final int pLeft,
			int tLeft, int starsLeft, TypePattern.MatchKind kind, ResolvedType[][] parameterAnnotations) {
		// invariant: pLeft > 0, so we know we'll run out of stars and find a real char in pattern
		TypePattern patternChar = pattern[pi];
		while (patternChar == TypePattern.ELLIPSIS) {
			starsLeft--;
			patternChar = pattern[++pi];
		}
		while (true) {
			// invariant: if (tLeft > 0) then (ti < target.length)
			if (pLeft > tLeft) {
				return FuzzyBoolean.NO;
			}

			FuzzyBoolean ff = null;
			try {
				if (parameterAnnotations != null) {
					target[ti].temporaryAnnotationTypes = parameterAnnotations[ti];
				}
				ff = patternChar.matches(target[ti], kind);
			} finally {
				target[ti].temporaryAnnotationTypes = null;
			}

			if (ff.maybeTrue()) {
				FuzzyBoolean xx = outOfStar(pattern, target, pi + 1, ti + 1, pLeft - 1, tLeft - 1, starsLeft, kind,
						parameterAnnotations);
				if (xx.maybeTrue()) {
					return ff.and(xx);
				}
			}
			ti++;
			tLeft--;
		}
	}

	public FuzzyBoolean matches(ResolvableTypeList types, TypePattern.MatchKind kind, ResolvedType[][] parameterAnnotations) {
		int nameLength = types.length;
		int patternLength = typePatterns.length;

		int nameIndex = 0;
		int patternIndex = 0;

		if (ellipsisCount == 0) {
			if (nameLength != patternLength) {
				return FuzzyBoolean.NO;
			}
			FuzzyBoolean finalReturn = FuzzyBoolean.YES;
			while (patternIndex < patternLength) {
				ResolvedType t = types.getResolved(nameIndex);
				FuzzyBoolean ret = null;
				try {
					if (parameterAnnotations != null) {
						t.temporaryAnnotationTypes = parameterAnnotations[nameIndex];
					}
					ret = typePatterns[patternIndex].matches(t, kind);
				} finally {
					t.temporaryAnnotationTypes = null;
				}
				patternIndex++;
				nameIndex++;
				if (ret == FuzzyBoolean.NO) {
					return ret;
				}
				if (ret == FuzzyBoolean.MAYBE) {
					finalReturn = ret;
				}
			}
			return finalReturn;
		} else if (ellipsisCount == 1) {
			if (nameLength < patternLength - 1) {
				return FuzzyBoolean.NO;
			}
			FuzzyBoolean finalReturn = FuzzyBoolean.YES;
			while (patternIndex < patternLength) {
				TypePattern p = typePatterns[patternIndex++];
				if (p == TypePattern.ELLIPSIS) {
					nameIndex = nameLength - (patternLength - patternIndex);
				} else {
					ResolvedType t = types.getResolved(nameIndex);
					FuzzyBoolean ret = null;
					try {
						if (parameterAnnotations != null) {
							t.temporaryAnnotationTypes = parameterAnnotations[nameIndex];
						}
						ret = p.matches(t, kind);
					} finally {
						t.temporaryAnnotationTypes = null;
					}
					nameIndex++;
					if (ret == FuzzyBoolean.NO) {
						return ret;
					}
					if (ret == FuzzyBoolean.MAYBE) {
						finalReturn = ret;
					}
				}
			}
			return finalReturn;
		} else {
			// System.err.print("match(" + arguments + ", " + types + ") -> ");
			FuzzyBoolean b = outOfStar(typePatterns, types, 0, 0, patternLength - ellipsisCount, nameLength, ellipsisCount, kind,
					parameterAnnotations);
			// System.err.println(b);
			return b;
		}
	}

	private static FuzzyBoolean outOfStar(final TypePattern[] pattern, ResolvableTypeList target, int pi, int ti, int pLeft,
			int tLeft, final int starsLeft, TypePattern.MatchKind kind, ResolvedType[][] parameterAnnotations) {
		if (pLeft > tLeft) {
			return FuzzyBoolean.NO;
		}
		FuzzyBoolean finalReturn = FuzzyBoolean.YES;
		while (true) {
			// invariant: if (tLeft > 0) then (ti < target.length && pi < pattern.length)
			if (tLeft == 0) {
				return finalReturn;
			}
			if (pLeft == 0) {
				if (starsLeft > 0) {
					return finalReturn;
				} else {
					return FuzzyBoolean.NO;
				}
			}
			if (pattern[pi] == TypePattern.ELLIPSIS) {
				return inStar(pattern, target, pi + 1, ti, pLeft, tLeft, starsLeft - 1, kind, parameterAnnotations);
			}
			FuzzyBoolean ret = null;
			ResolvedType type = target.getResolved(ti);
			try {
				if (parameterAnnotations != null) {
					type.temporaryAnnotationTypes = parameterAnnotations[ti];
				}
				ret = pattern[pi].matches(type, kind);
			} finally {
				type.temporaryAnnotationTypes = null;
			}
			if (ret == FuzzyBoolean.NO) {
				return ret;
			}
			if (ret == FuzzyBoolean.MAYBE) {
				finalReturn = ret;
			}
			pi++;
			ti++;
			pLeft--;
			tLeft--;
		}
	}

	private static FuzzyBoolean inStar(final TypePattern[] pattern, ResolvableTypeList target, int pi, int ti, final int pLeft,
			int tLeft, int starsLeft, TypePattern.MatchKind kind, ResolvedType[][] parameterAnnotations) {
		// invariant: pLeft > 0, so we know we'll run out of stars and find a real char in pattern
		TypePattern patternChar = pattern[pi];
		while (patternChar == TypePattern.ELLIPSIS) {
			starsLeft--;
			patternChar = pattern[++pi];
		}
		while (true) {
			// invariant: if (tLeft > 0) then (ti < target.length)
			if (pLeft > tLeft) {
				return FuzzyBoolean.NO;
			}

			ResolvedType type = target.getResolved(ti);
			FuzzyBoolean ff = null;
			try {
				if (parameterAnnotations != null) {
					type.temporaryAnnotationTypes = parameterAnnotations[ti];
				}
				ff = patternChar.matches(type, kind);
			} finally {
				type.temporaryAnnotationTypes = null;
			}

			if (ff.maybeTrue()) {
				FuzzyBoolean xx = outOfStar(pattern, target, pi + 1, ti + 1, pLeft - 1, tLeft - 1, starsLeft, kind,
						parameterAnnotations);
				if (xx.maybeTrue()) {
					return ff.and(xx);
				}
			}
			ti++;
			tLeft--;
		}
	}

	/**
	 * Return a version of this type pattern list in which all type variable references are replaced by their corresponding entry in
	 * the map
	 *
	 * @param typeVariableMap
	 * @return
	 */
	public TypePatternList parameterizeWith(Map<String,UnresolvedType> typeVariableMap, World w) {
		TypePattern[] parameterizedPatterns = new TypePattern[typePatterns.length];
		for (int i = 0; i < parameterizedPatterns.length; i++) {
			parameterizedPatterns[i] = typePatterns[i].parameterizeWith(typeVariableMap, w);
		}
		return new TypePatternList(parameterizedPatterns);
	}

	public TypePatternList resolveBindings(IScope scope, Bindings bindings, boolean allowBinding, boolean requireExactType) {
		for (int i = 0; i < typePatterns.length; i++) {
			TypePattern p = typePatterns[i];
			if (p != null) {
				typePatterns[i] = typePatterns[i].resolveBindings(scope, bindings, allowBinding, requireExactType);
			}
		}
		return this;
	}

	public TypePatternList resolveReferences(IntMap bindings) {
		int len = typePatterns.length;
		TypePattern[] ret = new TypePattern[len];
		for (int i = 0; i < len; i++) {
			ret[i] = typePatterns[i].remapAdviceFormals(bindings);
		}
		return new TypePatternList(ret);
	}

	public void postRead(ResolvedType enclosingType) {
		for (TypePattern p : typePatterns) {
			p.postRead(enclosingType);
		}
	}

	@Override
	public boolean equals(Object other) {
		if (!(other instanceof TypePatternList)) {
			return false;
		}
		TypePatternList o = (TypePatternList) other;
		int len = o.typePatterns.length;
		if (len != this.typePatterns.length) {
			return false;
		}
		for (int i = 0; i < len; i++) {
			if (!this.typePatterns[i].equals(o.typePatterns[i])) {
				return false;
			}
		}
		return true;
	}

	@Override
	public int hashCode() {
		int result = 41;
		for (TypePattern typePattern : typePatterns) {
			result = 37 * result + typePattern.hashCode();
		}
		return result;
	}

	public static TypePatternList read(VersionedDataInputStream s, ISourceContext context) throws IOException {
		short len = s.readShort();
		TypePattern[] arguments = new TypePattern[len];
		for (int i = 0; i < len; i++) {
			arguments[i] = TypePattern.read(s, context);
		}
		TypePatternList ret = new TypePatternList(arguments);
		if (!s.isAtLeast169()) {
			ret.readLocation(context, s);
		}
		return ret;
	}

	@Override
	public int getEnd() {
		throw new IllegalStateException();
	}

	@Override
	public ISourceContext getSourceContext() {
		throw new IllegalStateException();
	}

	@Override
	public ISourceLocation getSourceLocation() {
		throw new IllegalStateException();
	}

	@Override
	public int getStart() {
		throw new IllegalStateException();
	}

	@Override
	public void write(CompressingDataOutputStream s) throws IOException {
		s.writeShort(typePatterns.length);
		for (TypePattern typePattern : typePatterns) {
			typePattern.write(s);
		}
		// writeLocation(s);
	}

	public TypePattern[] getTypePatterns() {
		return typePatterns;
	}

	public List<UnresolvedType> getExactTypes() {
		List<UnresolvedType> ret = new ArrayList<>();
		for (TypePattern typePattern : typePatterns) {
			UnresolvedType t = typePattern.getExactType();
			if (!ResolvedType.isMissing(t)) {
				ret.add(t);
			}
		}
		return ret;
	}

	@Override
	public Object accept(PatternNodeVisitor visitor, Object data) {
		return visitor.visit(this, data);
	}

	@Override
	public Object traverse(PatternNodeVisitor visitor, Object data) {
		Object ret = accept(visitor, data);
		if (typePatterns != null) {
			for (TypePattern typePattern : typePatterns) {
				typePattern.traverse(visitor, ret);
			}
		}
		return ret;
	}

	public boolean areAllExactWithNoSubtypesAllowed() {
		for (TypePattern array_element : typePatterns) {
			if (!(array_element instanceof ExactTypePattern)) {
				return false;
			} else {
				ExactTypePattern etp = (ExactTypePattern) array_element;
				if (etp.isIncludeSubtypes()) {
					return false;
				}
			}
		}
		return true;
	}

	public String[] maybeGetCleanNames() {
		String[] theParamNames = new String[typePatterns.length];
		for (int i = 0; i < typePatterns.length; i++) {
			TypePattern string = typePatterns[i];
			if (!(string instanceof ExactTypePattern)) {
				return null;
			}
			theParamNames[i] = ((ExactTypePattern) string).getExactType().getName();
		}
		return theParamNames;
	}
}
