/* *******************************************************************
 * 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;

import java.io.DataInputStream;
import java.io.IOException;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.aspectj.bridge.IMessage;
import org.aspectj.bridge.ISourceLocation;
import org.aspectj.bridge.MessageUtil;
import org.aspectj.util.PartialOrder;
import org.aspectj.util.TypeSafeEnum;
import org.aspectj.weaver.ast.Var;

/*
 * The superclass of anything representing a the shadow of a join point.  A shadow represents
 * some bit of code, and encompasses both entry and exit from that code.  All shadows have a kind
 * and a signature.
 */

public abstract class Shadow {

	// every Shadow has a unique id, doesn't matter if it wraps...
	private static int nextShadowID = 100; // easier to spot than zero. // OPTIMIZE is this a bug? static?

	private final Kind kind;
	private final Member signature;
	private Member matchingSignature;
	private ResolvedMember resolvedSignature;
	protected final Shadow enclosingShadow;
	protected List<ShadowMunger> mungers = Collections.emptyList();
	protected boolean needAroundClosureStacking = false;

	public int shadowId = nextShadowID++; // every time we build a shadow, it gets a new id

	// ----
	protected Shadow(Kind kind, Member signature, Shadow enclosingShadow) {
		this.kind = kind;
		this.signature = signature;
		this.enclosingShadow = enclosingShadow;
	}

	// ----

	public abstract World getIWorld();

	public List<ShadowMunger> getMungers() {
		return mungers;
	}

	/**
	 * could this(*) pcd ever match
	 */
	public final boolean hasThis() {
		if (getKind().neverHasThis()) {
			return false;
		} else if (getKind().isEnclosingKind()) {
			return !Modifier.isStatic(getSignature().getModifiers());
		} else if (enclosingShadow == null) {
			return false;
		} else {
			return enclosingShadow.hasThis();
		}
	}

	/**
	 * the type of the this object here
	 *
	 * @throws IllegalStateException if there is no this here
	 */
	public final UnresolvedType getThisType() {
		if (!hasThis()) {
			throw new IllegalStateException("no this");
		}
		if (getKind().isEnclosingKind()) {
			return getSignature().getDeclaringType();
		} else {
			return enclosingShadow.getThisType();
		}
	}

	/**
	 * a var referencing this
	 *
	 * @throws IllegalStateException if there is no target here
	 */
	public abstract Var getThisVar();

	/**
	 * could target(*) pcd ever match
	 */
	public final boolean hasTarget() {
		if (getKind().neverHasTarget()) {
			return false;
		} else if (getKind().isTargetSameAsThis()) {
			return hasThis();
		} else {
			return !Modifier.isStatic(getSignature().getModifiers());
		}
	}

	/**
	 * the type of the target object here
	 *
	 * @throws IllegalStateException if there is no target here
	 */
	public final UnresolvedType getTargetType() {
		if (!hasTarget()) {
			throw new IllegalStateException("no target");
		}
		return getSignature().getDeclaringType();
	}

	/**
	 * a var referencing the target
	 *
	 * @throws IllegalStateException if there is no target here
	 */
	public abstract Var getTargetVar();

	public UnresolvedType[] getArgTypes() {
		if (getKind() == FieldSet) {
			return new UnresolvedType[] { getSignature().getReturnType() };
		}
		return getSignature().getParameterTypes();
	}

	public boolean isShadowForArrayConstructionJoinpoint() {
		return (getKind() == ConstructorCall && signature.getDeclaringType().isArray());
	}

	public boolean isShadowForMonitor() {
		return (getKind() == SynchronizationLock || getKind() == SynchronizationUnlock);
	}

	// will return the right length array of ints depending on how many dimensions the array has
	public ResolvedType[] getArgumentTypesForArrayConstructionShadow() {
		String s = signature.getDeclaringType().getSignature();
		int pos = s.indexOf("[");
		int dims = 1;
		while (pos < s.length()) {
			pos++;
			if (pos < s.length()) {
				dims += (s.charAt(pos) == '[' ? 1 : 0);
			}
		}
		ResolvedType intType = UnresolvedType.INT.resolve(this.getIWorld());
		if (dims == 1) {
			return new ResolvedType[] { intType };
		}
		ResolvedType[] someInts = new ResolvedType[dims];
		for (int i = 0; i < dims; i++) {
			someInts[i] = intType;
		}
		return someInts;
	}

	public UnresolvedType[] getGenericArgTypes() {
		if (isShadowForArrayConstructionJoinpoint()) {
			return getArgumentTypesForArrayConstructionShadow();
		}
		if (isShadowForMonitor()) {
			return UnresolvedType.ARRAY_WITH_JUST_OBJECT;
		}
		ResolvedMember resolvedSig = getResolvedSignature();
		if (getKind() == FieldSet) {
			return new UnresolvedType[] { resolvedSig == null ? null : resolvedSig.getGenericReturnType() };
		}
		return resolvedSig == null ? null : resolvedSig.getGenericParameterTypes();
	}

	public UnresolvedType getArgType(int arg) {
		if (getKind() == FieldSet) {
			return getSignature().getReturnType();
		}
		return getSignature().getParameterTypes()[arg];
	}

	public int getArgCount() {
		if (getKind() == FieldSet) {
			return 1;
		}
		return getSignature().getParameterTypes().length;
	}

	// /**
	// * Return name of the argument at position 'i' at this shadow. This does not make sense for all shadows - but can be useful in
	// * the case of, for example, method-execution.
	// *
	// * @return null if it cannot be determined
	// */
	// public String getArgName(int i, World w) {
	// String[] names = getSignature().getParameterNames(w);
	// if (names == null || i >= names.length)
	// return null;
	// return names[i];
	// }

	public abstract UnresolvedType getEnclosingType();

	public abstract Var getArgVar(int i);

	public abstract Var getThisJoinPointVar();

	public abstract Var getThisJoinPointStaticPartVar();

	public abstract Var getThisEnclosingJoinPointStaticPartVar();

	public abstract Var getThisAspectInstanceVar(ResolvedType aspectType);

	// annotation variables
	public abstract Var getKindedAnnotationVar(UnresolvedType forAnnotationType);

	public abstract Var getWithinAnnotationVar(UnresolvedType forAnnotationType);

	public abstract Var getWithinCodeAnnotationVar(UnresolvedType forAnnotationType);

	public abstract Var getThisAnnotationVar(UnresolvedType forAnnotationType);

	public abstract Var getTargetAnnotationVar(UnresolvedType forAnnotationType);

	public abstract Var getArgAnnotationVar(int i, UnresolvedType forAnnotationType);

	public abstract Member getEnclosingCodeSignature();

	/**
	 * returns the kind of shadow this is, representing what happens under this shadow
	 */
	public Kind getKind() {
		return kind;
	}

	/**
	 * returns the signature of the thing under this shadow
	 */
	public Member getSignature() {
		return signature;
	}

	/**
	 * returns the signature of the thing under this shadow, with any synthetic arguments removed
	 */
	public Member getMatchingSignature() {
		return matchingSignature != null ? matchingSignature : signature;
	}

	public void setMatchingSignature(Member member) {
		this.matchingSignature = member;
	}

	/**
	 * returns the resolved signature of the thing under this shadow
	 *
	 */
	public ResolvedMember getResolvedSignature() {
		if (resolvedSignature == null) {
			resolvedSignature = signature.resolve(getIWorld());
		}
		return resolvedSignature;
	}

	public UnresolvedType getReturnType() {
		if (kind == ConstructorCall) {
			return getSignature().getDeclaringType();
		} else if (kind == FieldSet) {
			return UnresolvedType.VOID;
		} else if (kind == SynchronizationLock || kind == SynchronizationUnlock) {
			return UnresolvedType.VOID;
		}
		ResolvedMember resolvedSig = getResolvedSignature();
		return resolvedSig == null ? null : resolvedSig.getGenericReturnType();
	}

	public static String METHOD_EXECUTION = "method-execution";
	public static String METHOD_CALL = "method-call";
	public static String CONSTRUCTOR_EXECUTION = "constructor-execution";
	public static String CONSTRUCTOR_CALL = "constructor-call";
	public static String FIELD_GET = "field-get";
	public static String FIELD_SET = "field-set";
	public static String STATICINITIALIZATION = "staticinitialization";
	public static String PREINITIALIZATION = "preinitialization";
	public static String INITIALIZATION = "initialization";
	public static String EXCEPTION_HANDLER = "exception-handler";
	public static String SYNCHRONIZATION_LOCK = "lock";
	public static String SYNCHRONIZATION_UNLOCK = "unlock";
	public static String ADVICE_EXECUTION = "adviceexecution";

	/**
	 * These names are the ones that will be returned by thisJoinPoint.getKind() Those need to be documented somewhere
	 */
	public static final Kind MethodCall = new Kind(METHOD_CALL, 1, true);
	public static final Kind ConstructorCall = new Kind(CONSTRUCTOR_CALL, 2, true);
	public static final Kind MethodExecution = new Kind(METHOD_EXECUTION, 3, false);
	public static final Kind ConstructorExecution = new Kind(CONSTRUCTOR_EXECUTION, 4, false);
	public static final Kind FieldGet = new Kind(FIELD_GET, 5, true);
	public static final Kind FieldSet = new Kind(FIELD_SET, 6, true);
	public static final Kind StaticInitialization = new Kind(STATICINITIALIZATION, 7, false);
	public static final Kind PreInitialization = new Kind(PREINITIALIZATION, 8, false);
	public static final Kind AdviceExecution = new Kind(ADVICE_EXECUTION, 9, false);
	public static final Kind Initialization = new Kind(INITIALIZATION, 10, false);
	public static final Kind ExceptionHandler = new Kind(EXCEPTION_HANDLER, 11, true);
	public static final Kind SynchronizationLock = new Kind(SYNCHRONIZATION_LOCK, 12, true);
	public static final Kind SynchronizationUnlock = new Kind(SYNCHRONIZATION_UNLOCK, 13, true);

	// Bits here are 1<<(Kind.getKey()) - and unfortunately keys didn't start at zero so bits here start at 2
	public static final int MethodCallBit = 0x002;
	public static final int ConstructorCallBit = 0x004;
	public static final int MethodExecutionBit = 0x008;
	public static final int ConstructorExecutionBit = 0x010;
	public static final int FieldGetBit = 0x020;
	public static final int FieldSetBit = 0x040;
	public static final int StaticInitializationBit = 0x080;
	public static final int PreInitializationBit = 0x100;
	public static final int AdviceExecutionBit = 0x200;
	public static final int InitializationBit = 0x400;
	public static final int ExceptionHandlerBit = 0x800;
	public static final int SynchronizationLockBit = 0x1000;
	public static final int SynchronizationUnlockBit = 0x2000;

	public static final int MAX_SHADOW_KIND = 13;
	public static final Kind[] SHADOW_KINDS = new Kind[] { MethodCall, ConstructorCall, MethodExecution, ConstructorExecution,
			FieldGet, FieldSet, StaticInitialization, PreInitialization, AdviceExecution, Initialization, ExceptionHandler,
			SynchronizationLock, SynchronizationUnlock };

	public static final int ALL_SHADOW_KINDS_BITS;
	public static final int NO_SHADOW_KINDS_BITS;

	static {
		ALL_SHADOW_KINDS_BITS = 0x3ffe;
		NO_SHADOW_KINDS_BITS = 0x0000;
	}

	/**
	 * Return count of how many bits set in the supplied parameter.
	 */
	public static int howMany(int i) {
		int count = 0;
        for (Kind shadowKind : SHADOW_KINDS) {
            if ((i & shadowKind.bit) != 0) {
                count++;
            }
        }
		return count;
	}

	/**
	 * A type-safe enum representing the kind of shadows
	 */
	public static final class Kind extends TypeSafeEnum {
		// private boolean argsOnStack; //XXX unused

		public int bit;

		public Kind(String name, int key, boolean argsOnStack) {
			super(name, key);
			bit = 1 << key;
			// this.argsOnStack = argsOnStack;
		}

		public String toLegalJavaIdentifier() {
			return getName().replace('-', '_');
		}

		public boolean argsOnStack() {
			return !isTargetSameAsThis();
		}

		// false for handlers
		public boolean allowsExtraction() {
			return true;
		}

		public boolean isSet(int i) {
			return (i & bit) != 0;
		}

		// XXX revisit along with removal of priorities
		public boolean hasHighPriorityExceptions() {
			return !isTargetSameAsThis();
		}

		private final static int hasReturnValueFlag = MethodCallBit | ConstructorCallBit | MethodExecutionBit | FieldGetBit
				| AdviceExecutionBit;

		/**
		 * These shadow kinds have return values that can be bound in after returning(Dooberry doo) advice.
		 *
		 * @return
		 */
		public boolean hasReturnValue() {
			return (bit & hasReturnValueFlag) != 0;
		}

		private final static int isEnclosingKindFlag = MethodExecutionBit | ConstructorExecutionBit | AdviceExecutionBit
				| StaticInitializationBit | InitializationBit;

		/**
		 * These are all the shadows that contains other shadows within them and are often directly associated with methods.
		 */
		public boolean isEnclosingKind() {
			return (bit & isEnclosingKindFlag) != 0;
		}

		private final static int isTargetSameAsThisFlag = MethodExecutionBit | ConstructorExecutionBit | StaticInitializationBit
				| PreInitializationBit | AdviceExecutionBit | InitializationBit;

		public boolean isTargetSameAsThis() {
			return (bit & isTargetSameAsThisFlag) != 0;
		}

		private final static int neverHasTargetFlag = ConstructorCallBit | ExceptionHandlerBit | PreInitializationBit
				| StaticInitializationBit | SynchronizationLockBit | SynchronizationUnlockBit;

		public boolean neverHasTarget() {
			return (bit & neverHasTargetFlag) != 0;
		}

		private final static int neverHasThisFlag = PreInitializationBit | StaticInitializationBit;

		public boolean neverHasThis() {
			return (bit & neverHasThisFlag) != 0;
		}

		public String getSimpleName() {
			int dash = getName().lastIndexOf('-');
			if (dash == -1) {
				return getName();
			} else {
				return getName().substring(dash + 1);
			}
		}

		public static Kind read(DataInputStream s) throws IOException {
			int key = s.readByte();
			switch (key) {
			case 1:
				return MethodCall;
			case 2:
				return ConstructorCall;
			case 3:
				return MethodExecution;
			case 4:
				return ConstructorExecution;
			case 5:
				return FieldGet;
			case 6:
				return FieldSet;
			case 7:
				return StaticInitialization;
			case 8:
				return PreInitialization;
			case 9:
				return AdviceExecution;
			case 10:
				return Initialization;
			case 11:
				return ExceptionHandler;
			case 12:
				return SynchronizationLock;
			case 13:
				return SynchronizationUnlock;
			}
			throw new BCException("unknown kind: " + key);
		}
	}

	/**
	 * Only does the check if the munger requires it (@AJ aspects don't)
	 *
	 * @param munger
	 * @return
	 */
	protected boolean checkMunger(ShadowMunger munger) {
		if (munger.mustCheckExceptions()) {
            for (ResolvedType resolvedType : munger.getThrownExceptions()) {
                if (!checkCanThrow(munger, resolvedType)) {
                    return false;
                }
            }
		}
		return true;
	}

	protected boolean checkCanThrow(ShadowMunger munger, ResolvedType resolvedTypeX) {
		if (getKind() == ExceptionHandler) {
			// XXX much too lenient rules here, need to walk up exception handlers
			return true;
		}

		if (!isDeclaredException(resolvedTypeX, getSignature())) {
			getIWorld().showMessage(IMessage.ERROR, WeaverMessages.format(WeaverMessages.CANT_THROW_CHECKED, resolvedTypeX, this), // from
					// advice
					// in
					// \
					// '"
					// +
					// munger
					// .
					// +
					// "\'"
					// ,
					getSourceLocation(), munger.getSourceLocation());
		}

		return true;
	}

	private boolean isDeclaredException(ResolvedType resolvedTypeX, Member member) {
		ResolvedType[] excs = getIWorld().resolve(member.getExceptions(getIWorld()));
        for (ResolvedType exc : excs) {
            if (exc.isAssignableFrom(resolvedTypeX)) {
                return true;
            }
        }
		return false;
	}

	public void addMunger(ShadowMunger munger) {
		if (checkMunger(munger)) {
			if (mungers == Collections.EMPTY_LIST) {
				mungers = new ArrayList<>();
			}
			this.mungers.add(munger);
		}
	}

	public final void implement() {
		sortMungers();
		if (mungers == null) {
			return;
		}
		prepareForMungers();
		implementMungers();
	}

	private void sortMungers() {

		List<ShadowMunger> sorted = PartialOrder.sort(mungers);

		// Bunch of code to work out whether to report xlints for advice that isn't ordered at this Joinpoint
		possiblyReportUnorderedAdvice(sorted);

		if (sorted == null) {
			// this means that we have circular dependencies
			for (ShadowMunger m : mungers) {
				getIWorld().getMessageHandler().handleMessage(
						MessageUtil.error(WeaverMessages.format(WeaverMessages.CIRCULAR_DEPENDENCY, this), m.getSourceLocation()));
			}
		}
		mungers = sorted;
	}

	// not quite optimal... but the xlint is ignore by default
	private void possiblyReportUnorderedAdvice(List<ShadowMunger> sorted) {
		if (sorted != null && getIWorld().getLint().unorderedAdviceAtShadow.isEnabled() && mungers.size() > 1) {

			// Stores a set of strings of the form 'aspect1:aspect2' which indicates there is no
			// precedence specified between the two aspects at this shadow.
			Set<String> clashingAspects = new HashSet<>();
			int max = mungers.size();

			// Compare every pair of advice mungers
			for (int i = max - 1; i >= 0; i--) {
				for (int j = 0; j < i; j++) {
					ShadowMunger a = mungers.get(i);
					ShadowMunger b = mungers.get(j);

					// Make sure they are the right type
					if (a instanceof Advice && b instanceof Advice) {
						Advice adviceA = (Advice) a;
						Advice adviceB = (Advice) b;
						if (!adviceA.concreteAspect.equals(adviceB.concreteAspect)) {
							AdviceKind adviceKindA = adviceA.getKind();
							AdviceKind adviceKindB = adviceB.getKind();

							// make sure they are the nice ones (<6) and not any synthetic advice ones we
							// create to support other features of the language.
							if (adviceKindA.getKey() < (byte) 6 && adviceKindB.getKey() < (byte) 6
									&& adviceKindA.getPrecedence() == adviceKindB.getPrecedence()) {

								// Ask the world if it knows about precedence between these
								Integer order = getIWorld().getPrecedenceIfAny(adviceA.concreteAspect, adviceB.concreteAspect);

								if (order != null && order.equals(0)) {
									String key = adviceA.getDeclaringAspect() + ":" + adviceB.getDeclaringAspect();
									String possibleExistingKey = adviceB.getDeclaringAspect() + ":" + adviceA.getDeclaringAspect();
									if (!clashingAspects.contains(possibleExistingKey)) {
										clashingAspects.add(key);
									}
								}
							}
						}
					}
				}
			}
            for (String element : clashingAspects) {
                String aspect1 = element.substring(0, element.indexOf(":"));
                String aspect2 = element.substring(element.indexOf(":") + 1);
                getIWorld().getLint().unorderedAdviceAtShadow.signal(new String[]{this.toString(), aspect1, aspect2},
                        this.getSourceLocation(), null);
            }
		}
	}

	/**
	 * Prepare the shadow for implementation. After this is done, the shadow should be in such a position that each munger simply
	 * needs to be implemented.
	 */
	protected void prepareForMungers() {
		throw new RuntimeException("Generic shadows cannot be prepared");
	}

	/** Actually implement the (non-empty) mungers associated with this shadow */
	private void implementMungers() {
		World world = getIWorld();
		needAroundClosureStacking = false;
		int annotationStyleWithAroundAndProceedCount = 0;
		for (ShadowMunger munger: mungers) {
			if (munger.getDeclaringType()!= null &&
				munger.getDeclaringType().isAnnotationStyleAspect() &&
				munger.isAroundAdvice() &&
				munger.bindsProceedingJoinPoint()) {
				annotationStyleWithAroundAndProceedCount++;
				if (annotationStyleWithAroundAndProceedCount>1) {
					needAroundClosureStacking = true;
					break;
				}
			}
		}
		for (ShadowMunger munger : mungers) {
			if (munger.implementOn(this)) {
				world.reportMatch(munger, this);
			}
		}
	}

	public abstract ISourceLocation getSourceLocation();

	// ---- utility

	public String toString() {
		return getKind() + "(" + getSignature() + ")"; // + getSourceLines();
	}

	public String toResolvedString(World world) {
		StringBuilder sb = new StringBuilder();
		sb.append(getKind());
		sb.append("(");
		Member m = getSignature();
		if (m == null) {
			sb.append("<<missing signature>>");
		} else {
			ResolvedMember rm = world.resolve(m);
			if (rm == null) {
				sb.append("<<unresolvableMember:").append(m).append(">>");
			} else {
				String genString = rm.toGenericString();
				if (genString == null) {
					sb.append("<<unableToGetGenericStringFor:").append(rm).append(">>");
				} else {
					sb.append(genString);
				}

			}
		}
		sb.append(")");
		return sb.toString();
		// was: return getKind() + "(" + world.resolve(getSignature()).toGenericString() + ")";
	}

	/**
	 * Convert a bit array for the shadow kinds into a set of them... should only be used for testing - mainline code should do bit
	 * manipulation!
	 */
	public static Set<Kind> toSet(int i) {
		Set<Kind> results = new HashSet<>();
		for (int j = 0; j < Shadow.SHADOW_KINDS.length; j++) {
			Kind k = Shadow.SHADOW_KINDS[j];
			if (k.isSet(i)) {
				results.add(k);
			}
		}
		return results;
	}

}
