/*******************************************************************************
 * Copyright (c) 2010, 2017 Cloudsmith Inc. and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     Cloudsmith Inc. - initial API and implementation
 *******************************************************************************/
package org.eclipse.equinox.internal.p2.metadata.index;

import java.util.ArrayList;
import java.util.Iterator;
import org.eclipse.equinox.internal.p2.metadata.expression.*;
import org.eclipse.equinox.p2.metadata.expression.*;
import org.eclipse.equinox.p2.metadata.index.IIndex;

public abstract class Index<T> implements IIndex<T> {

	protected static boolean isIndexedMember(IExpression expr, IExpression variable, String memberName) {
		if (expr instanceof Member) {
			Member member = (Member) expr;
			return member.getOperand() == variable && member.getName().equals(memberName);
		}
		return false;
	}

	protected static Object concatenateUnique(Object previous, Object toAdd) {
		if (previous == null || toAdd == null || toAdd == Boolean.FALSE)
			return toAdd;

		if (previous instanceof ArrayList<?>) {
			@SuppressWarnings("unchecked")
			ArrayList<Object> prevArr = (ArrayList<Object>) previous;
			if (!prevArr.contains(toAdd))
				prevArr.add(toAdd);
			return previous;
		}
		if (previous.equals(toAdd))
			return previous;

		ArrayList<Object> arr = new ArrayList<>();
		arr.add(previous);
		arr.add(toAdd);
		return arr;
	}

	protected Object getQueriedIDs(IEvaluationContext ctx, IExpression variable, String memberName, IExpression booleanExpr, Object queriedKeys) {
		IExpression targetExpr = booleanExpr;
		if (booleanExpr instanceof IMatchExpression<?>) {
			targetExpr = ((Unary) targetExpr).operand;
			ctx = ((IMatchExpression<?>) booleanExpr).createContext();
		}
		int type = targetExpr.getExpressionType();
		switch (type) {
			case IExpression.TYPE_EQUALS :
				Binary eqExpr = (Binary) targetExpr;
				IExpression lhs = eqExpr.lhs;
				IExpression rhs = eqExpr.rhs;
				if (isIndexedMember(lhs, variable, memberName)) {
					Object val = safeEvaluate(ctx, rhs);
					if (val == null)
						return null;
					return concatenateUnique(queriedKeys, val);
				}
				if (isIndexedMember(rhs, variable, memberName)) {
					Object val = safeEvaluate(ctx, lhs);
					if (val == null)
						return null;
					return concatenateUnique(queriedKeys, val);
				}

				// Not applicable for indexing
				return null;

			case IExpression.TYPE_AND :
				// AND is OK if at least one of the branches require the queried key
				for (IExpression expr : ExpressionUtil.getOperands(targetExpr)) {
					Object test = getQueriedIDs(ctx, variable, memberName, expr, queriedKeys);
					if (test != null) {
						if (test == Boolean.FALSE)
							// Failing exists so the AND will fail altogether
							return test;

						// It's safe to break here since an and'ing several queries
						// for different keys and the same input will yield false anyway.
						return test;
					}
				}
				return null;

			case IExpression.TYPE_OR :
				// OR is OK if all the branches require the queried key
				for (IExpression expr : ExpressionUtil.getOperands(targetExpr)) {
					Object test = getQueriedIDs(ctx, variable, memberName, expr, queriedKeys);
					if (test == null)
						// This branch did not require the key so index cannot be used
						return null;

					if (test == Boolean.FALSE)
						// Branch will always fail regardless of input, so just ignore
						continue;

					queriedKeys = test;
				}
				return queriedKeys;

			case IExpression.TYPE_EXISTS :
			case IExpression.TYPE_ALL :
				// We must evaluate the lhs to find the referenced keys
				//
				CollectionFilter cf = (CollectionFilter) targetExpr;
				Iterator<?> values;
				try {
					values = cf.getOperand().evaluateAsIterator(ctx);
				} catch (IllegalArgumentException e) {
					return null;
				}
				if (!values.hasNext())
					// No keys are requested but we know that an exists must
					// fail at this point. An all will however succeed regardless
					// of what is used as input.
					return type == IExpression.TYPE_ALL ? null : Boolean.FALSE;

				LambdaExpression lambda = cf.lambda;
				IEvaluationContext lambdaCtx = lambda.prolog(ctx);
				Variable lambdaVar = lambda.getItemVariable();
				IExpression filterExpr = lambda.getOperand();
				do {
					lambdaVar.setValue(lambdaCtx, values.next());
					queriedKeys = getQueriedIDs(lambdaCtx, variable, memberName, filterExpr, queriedKeys);
					if (queriedKeys == null)
						// No use continuing. The expression does not require the key
						return null;
				} while (values.hasNext());
				return queriedKeys;
		}
		return null;
	}

	private static Object safeEvaluate(IEvaluationContext ctx, IExpression expr) {
		try {
			return expr.evaluate(ctx);
		} catch (IllegalArgumentException e) {
			return null;
		}
	}
}
