package org.revenj.database.postgres.jinq.transform;

import java.util.ArrayList;
import java.util.List;


import ch.epfl.labos.iu.orm.queryll2.path.PathAnalysis;
import ch.epfl.labos.iu.orm.queryll2.path.PathAnalysisSimplifier;
import ch.epfl.labos.iu.orm.queryll2.symbolic.TypedValue;
import ch.epfl.labos.iu.orm.queryll2.symbolic.TypedValueVisitorException;
import org.revenj.database.postgres.jinq.jpqlquery.BinaryExpression;
import org.revenj.database.postgres.jinq.jpqlquery.CaseWhenExpression;
import org.revenj.database.postgres.jinq.jpqlquery.ColumnExpressions;
import org.revenj.database.postgres.jinq.jpqlquery.Expression;

/**
 * Subclasses of this class are used to hold the logic for applying
 * a lambda to a JPQL query (e.g. how to apply a where lambda to
 * a JPQL query, producing a new JPQL query)
 */
public abstract class RevenjQueryTransform {
	RevenjQueryTransformConfiguration config = new RevenjQueryTransformConfiguration();

	RevenjQueryTransform(RevenjQueryTransformConfiguration config) {
		this.config = config;
	}

	protected <U> ColumnExpressions<U> makeSelectExpression(
			SymbExToColumns translator,
			LambdaAnalysis lambda) throws TypedValueVisitorException {
		// Handle the case where there is only one path
		if (lambda.symbolicAnalysis.paths.size() == 1) {
			SymbExPassDown passdown = SymbExPassDown.with(null, false);
			return simplifyAndTranslateMainPathToColumns(lambda, translator, passdown);
		}

		// Multi-path case

		// Calculate the return expressions and path conditions for each path
		int numPaths = lambda.symbolicAnalysis.paths.size();
		List<ColumnExpressions<U>> returnExprs = new ArrayList<>(numPaths);
		List<Expression> pathExprs = new ArrayList<>(numPaths);
		for (int n = 0; n < numPaths; n++) {
			SymbExPassDown passdown = SymbExPassDown.with(null, false);
			returnExprs.add(simplifyAndTranslatePathToColumns(lambda, n, translator, passdown));
			pathExprs.add(pathConditionsToExpr(translator, lambda.symbolicAnalysis.paths.get(n)));
		}

		// Check that the different paths generated consistent data
		for (int n = 1; n < numPaths; n++) {
			// TODO: Check that all the readers are compatible (should be due to Java type-checking, though
			//    the user could do something silly like return Objects instead of something more specific)
			if (returnExprs.get(n).getNumColumns() != returnExprs.get(0).getNumColumns()) {
				throw new TypedValueVisitorException("Different paths returned different numbers of columns");
			}
		}

		// Merge everything into a giant CASE WHEN statement
		// TODO: EclipseLink has problems when CASE WHEN is used to return entities instead of simple fields
		ColumnExpressions<U> toReturn = new ColumnExpressions<>(returnExprs.get(0).reader);
		for (int col = 0; col < returnExprs.get(0).getNumColumns(); col++) {
			// Check if all the paths are the same for this particular column
			boolean allSame = true;
			for (int n = 1; n < numPaths; n++) {
				if (!returnExprs.get(n).columns.get(col).equals(returnExprs.get(0).columns.get(col))) {
					allSame = false;
					break;
				}
			}

			if (allSame) {
				// Everything in this column in the same
				toReturn.columns.add(returnExprs.get(0).columns.get(col));
			} else {
				// Use a CASE WHEN... to handle the different possibilities
				List<CaseWhenExpression.ConditionResult> cases = new ArrayList<>();
				for (int n = 0; n < numPaths; n++) {
					CaseWhenExpression.ConditionResult c = new CaseWhenExpression.ConditionResult();
					c.condition = pathExprs.get(n);
					c.result = returnExprs.get(n).columns.get(col);
					cases.add(c);
				}
				toReturn.columns.add(new CaseWhenExpression(cases));
			}
		}
		return toReturn;
	}

	protected <U> ColumnExpressions<U> simplifyAndTranslateMainPathToColumns(
			LambdaAnalysis lambda,
			SymbExToColumns translator,
			SymbExPassDown passdown) throws TypedValueVisitorException {
		return simplifyAndTranslatePathToColumns(lambda, 0, translator, passdown);
	}

	protected <U> ColumnExpressions<U> simplifyAndTranslatePathToColumns(
			LambdaAnalysis lambda,
			int pathIdx,
			SymbExToColumns translator,
			SymbExPassDown passdown) throws TypedValueVisitorException {
		return (ColumnExpressions<U>) PathAnalysisSimplifier
				.simplify(
						lambda.symbolicAnalysis.paths.get(pathIdx).getReturnValue(),
						config.getComparisonMethods(),
						config.getStaticComparisonMethods(),
						config.isAllEqualsSafe)
				.visit(translator, passdown);
	}

	protected Expression pathConditionsToExpr(
			SymbExToColumns translator,
			PathAnalysis path) throws TypedValueVisitorException {
		Expression conditionExpr = null;
		for (TypedValue cmp : path.getConditions()) {
			SymbExPassDown passdown = SymbExPassDown.with(null, true);
			ColumnExpressions<?> col = cmp.visit(translator, passdown);
			if (!col.isSingleColumn()) {
				throw new TypedValueVisitorException("Expecting a single column result for path condition");
			}
			Expression expr = col.getOnlyColumn();
			if (conditionExpr != null) {
				conditionExpr = new BinaryExpression(conditionExpr, "AND", expr);
			} else {
				conditionExpr = expr;
			}
		}
		return conditionExpr;
	}

	/**
	 * Returns a String that can be used to identify the type of transformation being applied.
	 * This is useful for caching the effects of query transforms.
	 */
	public abstract String getTransformationTypeCachingTag();
}
