/*******************************************************************************
 * Copyright (c) 2015 Eclipse RDF4J contributors, Aduna, and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Distribution License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *******************************************************************************/
package org.eclipse.rdf4j.query.algebra.evaluation.federation;

import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Set;

import org.eclipse.rdf4j.common.iteration.AbstractCloseableIteration;
import org.eclipse.rdf4j.common.iteration.CloseableIteration;
import org.eclipse.rdf4j.common.iteration.DistinctIteration;
import org.eclipse.rdf4j.common.iteration.EmptyIteration;
import org.eclipse.rdf4j.common.iteration.SingletonIteration;
import org.eclipse.rdf4j.common.iteration.UnionIteration;
import org.eclipse.rdf4j.model.Value;
import org.eclipse.rdf4j.model.ValueFactory;
import org.eclipse.rdf4j.query.BindingSet;
import org.eclipse.rdf4j.query.QueryEvaluationException;
import org.eclipse.rdf4j.query.algebra.Service;
import org.eclipse.rdf4j.query.algebra.TupleExpr;
import org.eclipse.rdf4j.query.algebra.TupleFunctionCall;
import org.eclipse.rdf4j.query.algebra.ValueConstant;
import org.eclipse.rdf4j.query.algebra.ValueExpr;
import org.eclipse.rdf4j.query.algebra.Var;
import org.eclipse.rdf4j.query.algebra.evaluation.QueryBindingSet;
import org.eclipse.rdf4j.query.algebra.evaluation.ValueExprEvaluationException;
import org.eclipse.rdf4j.query.algebra.evaluation.function.TupleFunction;
import org.eclipse.rdf4j.query.algebra.evaluation.function.TupleFunctionRegistry;
import org.eclipse.rdf4j.query.algebra.evaluation.impl.TupleFunctionEvaluationStrategy;
import org.eclipse.rdf4j.query.algebra.evaluation.util.QueryEvaluationUtil;
import org.eclipse.rdf4j.repository.sparql.federation.SilentIteration;

/**
 * A federated service that can evaluate {@link TupleFunction}s.
 */
public class TupleFunctionFederatedService implements FederatedService {

	private final TupleFunctionRegistry tupleFunctionRegistry;

	private final ValueFactory vf;

	private volatile boolean isInitialized;

	public TupleFunctionFederatedService(TupleFunctionRegistry tupleFunctionRegistry, ValueFactory vf) {
		this.tupleFunctionRegistry = tupleFunctionRegistry;
		this.vf = vf;
	}

	@Override
	public boolean isInitialized() {
		return isInitialized;
	}

	@Override
	public void initialize() {
		isInitialized = true;
	}

	@Override
	public void shutdown() {
		isInitialized = false;
	}

	@Override
	public boolean ask(Service service, BindingSet bindings, String baseUri) throws QueryEvaluationException {
		try (final CloseableIteration<BindingSet, QueryEvaluationException> iter = evaluate(service,
				new SingletonIteration<>(bindings), baseUri);) {
			while (iter.hasNext()) {
				BindingSet bs = iter.next();
				String firstVar = service.getBindingNames().iterator().next();
				return QueryEvaluationUtil.getEffectiveBooleanValue(bs.getValue(firstVar));
			}
		}
		return false;
	}

	@Override
	public CloseableIteration<BindingSet, QueryEvaluationException> select(Service service,
			final Set<String> projectionVars, BindingSet bindings, String baseUri) throws QueryEvaluationException {
		final CloseableIteration<BindingSet, QueryEvaluationException> iter, eval;
		eval = evaluate(service, new SingletonIteration<>(bindings), baseUri);
		iter = service.isSilent() ? new SilentIteration(eval) : eval;
		if (service.getBindingNames().equals(projectionVars)) {
			return iter;
		}

		return new AbstractCloseableIteration<BindingSet, QueryEvaluationException>() {

			@Override
			public boolean hasNext() throws QueryEvaluationException {
				if (isClosed()) {
					return false;
				}
				boolean result = iter.hasNext();
				if (!result) {
					close();
				}
				return result;
			}

			@Override
			public BindingSet next() throws QueryEvaluationException {
				if (isClosed()) {
					throw new NoSuchElementException("The iteration has been closed.");
				}
				try {
					QueryBindingSet projected = new QueryBindingSet();
					BindingSet result = iter.next();
					for (String var : projectionVars) {
						Value v = result.getValue(var);
						projected.addBinding(var, v);
					}
					return projected;
				} catch (NoSuchElementException e) {
					close();
					throw e;
				}
			}

			@Override
			public void remove() throws QueryEvaluationException {
				if (isClosed()) {
					throw new IllegalStateException("The iteration has been closed.");
				}
				try {
					iter.remove();
				} catch (IllegalStateException e) {
					close();
					throw e;
				}
			}

			@Override
			public void handleClose() throws QueryEvaluationException {
				try {
					super.handleClose();
				} finally {
					iter.close();
				}
			}
		};
	}

	@Override
	public final CloseableIteration<BindingSet, QueryEvaluationException> evaluate(Service service,
			CloseableIteration<BindingSet, QueryEvaluationException> bindings, String baseUri)
			throws QueryEvaluationException {
		if (!bindings.hasNext()) {
			return new EmptyIteration<>();
		}

		TupleExpr expr = service.getArg();
		if (!(expr instanceof TupleFunctionCall)) {
			return new EmptyIteration<>();
		}

		TupleFunctionCall funcCall = (TupleFunctionCall) expr;
		TupleFunction func = tupleFunctionRegistry.get(funcCall.getURI())
				.orElseThrow(() -> new QueryEvaluationException("Unknown tuple function '" + funcCall.getURI() + "'"));

		List<ValueExpr> argExprs = funcCall.getArgs();

		List<CloseableIteration<BindingSet, QueryEvaluationException>> resultIters = new ArrayList<>();
		while (bindings.hasNext()) {
			BindingSet bs = bindings.next();
			Value[] argValues = new Value[argExprs.size()];
			for (int i = 0; i < argExprs.size(); i++) {
				ValueExpr argExpr = argExprs.get(i);
				Value argValue;
				if (argExpr instanceof Var) {
					argValue = getValue((Var) argExpr, bs);
				} else if (argExpr instanceof ValueConstant) {
					argValue = ((ValueConstant) argExpr).getValue();
				} else {
					throw new ValueExprEvaluationException(
							"Unsupported ValueExpr for argument " + i + ": " + argExpr.getClass().getSimpleName());
				}
				argValues[i] = argValue;
			}
			resultIters
					.add(TupleFunctionEvaluationStrategy.evaluate(func, funcCall.getResultVars(), bs, vf, argValues));
		}
		return (resultIters.size() > 1) ? new DistinctIteration<>(new UnionIteration<>(resultIters))
				: resultIters.get(0);
	}

	private static Value getValue(Var var, BindingSet bs) throws ValueExprEvaluationException {
		Value v = var.getValue();
		if (v == null) {
			v = bs.getValue(var.getName());
		}
		if (v == null) {
			throw new ValueExprEvaluationException("No value for binding: " + var.getName());
		}
		return v;
	}
}
