/* *******************************************************************
 * Copyright (c) 2005 Contributors.
 * 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:
 *   Adrian Colyer			Initial implementation
 * ******************************************************************/
package org.aspectj.weaver.reflect;

import java.lang.reflect.Member;

import org.aspectj.util.FuzzyBoolean;
import org.aspectj.weaver.ResolvedType;
import org.aspectj.weaver.World;
import org.aspectj.weaver.ast.And;
import org.aspectj.weaver.ast.Call;
import org.aspectj.weaver.ast.FieldGetCall;
import org.aspectj.weaver.ast.HasAnnotation;
import org.aspectj.weaver.ast.ITestVisitor;
import org.aspectj.weaver.ast.Instanceof;
import org.aspectj.weaver.ast.Literal;
import org.aspectj.weaver.ast.Not;
import org.aspectj.weaver.ast.Or;
import org.aspectj.weaver.ast.Test;
import org.aspectj.weaver.ast.Var;
import org.aspectj.weaver.internal.tools.MatchingContextBasedTest;
import org.aspectj.weaver.patterns.ExposedState;
import org.aspectj.weaver.tools.DefaultMatchingContext;
import org.aspectj.weaver.tools.JoinPointMatch;
import org.aspectj.weaver.tools.MatchingContext;
import org.aspectj.weaver.tools.PointcutParameter;
import org.aspectj.weaver.tools.ShadowMatch;

/**
 * @author colyer Implementation of ShadowMatch for reflection based worlds.
 */
public class ShadowMatchImpl implements ShadowMatch {

	private FuzzyBoolean match;
	private ExposedState state;
	private Test residualTest;
	private PointcutParameter[] params;
	private Member withinCode;
	private Member subject;
	private Class<?> withinType;
	private MatchingContext matchContext = new DefaultMatchingContext();

	public ShadowMatchImpl(FuzzyBoolean match, Test test, ExposedState state, PointcutParameter[] params) {
		this.match = match;
		this.residualTest = test;
		this.state = state;
		this.params = params;
	}

	public void setWithinCode(Member aMember) {
		this.withinCode = aMember;
	}

	public void setSubject(Member aMember) {
		this.subject = aMember;
	}

	public void setWithinType(Class<?> aClass) {
		this.withinType = aClass;
	}

	public boolean alwaysMatches() {
		return match.alwaysTrue();
	}

	public boolean maybeMatches() {
		return match.maybeTrue();
	}

	public boolean neverMatches() {
		return match.alwaysFalse();
	}

	public JoinPointMatch matchesJoinPoint(Object thisObject, Object targetObject, Object[] args) {
		if (neverMatches()) {
			return JoinPointMatchImpl.NO_MATCH;
		}
		if (new RuntimeTestEvaluator(residualTest, thisObject, targetObject, args, this.matchContext).matches()) {
			return new JoinPointMatchImpl(getPointcutParameters(thisObject, targetObject, args));
		} else {
			return JoinPointMatchImpl.NO_MATCH;
		}
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.aspectj.weaver.tools.ShadowMatch#setMatchingContext(org.aspectj.weaver.tools.MatchingContext)
	 */
	public void setMatchingContext(MatchingContext aMatchContext) {
		this.matchContext = aMatchContext;
	}

	private PointcutParameter[] getPointcutParameters(Object thisObject, Object targetObject, Object[] args) {
		Var[] vars = state.vars;
		PointcutParameterImpl[] bindings = new PointcutParameterImpl[params.length];
		for (int i = 0; i < bindings.length; i++) {
			bindings[i] = new PointcutParameterImpl(params[i].getName(), params[i].getType());
			bindings[i].setBinding(((ReflectionVar) vars[i]).getBindingAtJoinPoint(thisObject, targetObject, args, subject,
					withinCode, withinType));
		}
		return bindings;
	}

	private static class RuntimeTestEvaluator implements ITestVisitor {

		private boolean matches = true;
		private final Test test;
		private final Object thisObject;
		private final Object targetObject;
		private final Object[] args;
		private final MatchingContext matchContext;

		public RuntimeTestEvaluator(Test aTest, Object thisObject, Object targetObject, Object[] args, MatchingContext context) {
			this.test = aTest;
			this.thisObject = thisObject;
			this.targetObject = targetObject;
			this.args = args;
			this.matchContext = context;
		}

		public boolean matches() {
			test.accept(this);
			return matches;
		}

		public void visit(And e) {
			boolean leftMatches = new RuntimeTestEvaluator(e.getLeft(), thisObject, targetObject, args, matchContext).matches();
			if (!leftMatches) {
				matches = false;
			} else {
				matches = new RuntimeTestEvaluator(e.getRight(), thisObject, targetObject, args, matchContext).matches();
			}
		}

		public void visit(Instanceof instanceofTest) {
			ReflectionVar v = (ReflectionVar) instanceofTest.getVar();
			Object value = v.getBindingAtJoinPoint(thisObject, targetObject, args);
			World world = v.getType().getWorld();
			ResolvedType desiredType = instanceofTest.getType().resolve(world);
			if (value == null) {
				matches = false;
			} else {
				ResolvedType actualType = world.resolve(value.getClass().getName());
				matches = desiredType.isAssignableFrom(actualType);
			}
		}

		public void visit(MatchingContextBasedTest matchingContextTest) {
			matches = matchingContextTest.matches(this.matchContext);
		}

		public void visit(Not not) {
			matches = !new RuntimeTestEvaluator(not.getBody(), thisObject, targetObject, args, matchContext).matches();
		}

		public void visit(Or or) {
			boolean leftMatches = new RuntimeTestEvaluator(or.getLeft(), thisObject, targetObject, args, matchContext).matches();
			if (leftMatches) {
				matches = true;
			} else {
				matches = new RuntimeTestEvaluator(or.getRight(), thisObject, targetObject, args, matchContext).matches();
			}
		}

		public void visit(Literal literal) {
			if (literal == Literal.FALSE) {
				matches = false;
			} else {
				matches = true;
			}
		}

		public void visit(Call call) {
			throw new UnsupportedOperationException("Can't evaluate call test at runtime");
		}

		public void visit(FieldGetCall fieldGetCall) {
			throw new UnsupportedOperationException("Can't evaluate fieldGetCall test at runtime");
		}

		public void visit(HasAnnotation hasAnnotation) {
			ReflectionVar v = (ReflectionVar) hasAnnotation.getVar();
			Object value = v.getBindingAtJoinPoint(thisObject, targetObject, args);
			World world = v.getType().getWorld();
			ResolvedType actualVarType = world.resolve(value.getClass().getName());
			ResolvedType requiredAnnotationType = hasAnnotation.getAnnotationType().resolve(world);
			matches = actualVarType.hasAnnotation(requiredAnnotationType);
		}

	}

}
