package org.apache.rya.indexing.external.tupleSet;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.rya.api.domain.VarNameUtils;
import org.eclipse.rdf4j.common.iteration.CloseableIteration;
import org.eclipse.rdf4j.model.Value;
import org.eclipse.rdf4j.query.BindingSet;
import org.eclipse.rdf4j.query.QueryEvaluationException;
import org.eclipse.rdf4j.query.algebra.Projection;
import org.eclipse.rdf4j.query.algebra.TupleExpr;
import org.eclipse.rdf4j.query.algebra.Var;
import org.eclipse.rdf4j.query.algebra.evaluation.impl.ExternalSet;
import org.eclipse.rdf4j.query.algebra.helpers.AbstractQueryModelVisitor;

import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;

/**
 * This is an abstract class of delegating the evaluation of part
 * of a SPARQL query to an external source.  The {@link TupleExpr} returned by {@link ExternalTupleSet#getTupleExpr()}
 * represents the SPARQL string that this node evaluates, and table returned by {@link ExternalTupleSet#getTableVarMap()}
 * maps the variables of TupleExpr to the variables stored in the external store (which may be different).  The map
 * returned by {@link ExternalTupleSet#getSupportedVariableOrderMap()} provides a map of all the variable orders in which
 * data is written to the supporting, and is useful for determining which {@link BindingSet} can be passed into
 * {@link ExternalTupleSet#evaluate(BindingSet)}.
 *
 */
public abstract class ExternalTupleSet extends ExternalSet {

	public static final String VAR_ORDER_DELIM = ";";
	/**
	 * @deprecated use {@link VarNameUtils#CONSTANT_PREFIX}.
	 */
	@Deprecated
	public static final String CONST_PREFIX = VarNameUtils.CONSTANT_PREFIX;
	public static final String VALUE_DELIM = "\u0000";
	private Projection tupleExpr;
    private Map<String, String> tableVarMap = Maps.newHashMap();  //maps vars in tupleExpr to var in stored binding sets
    private Map<String, Set<String>> supportedVarOrders = Maps.newHashMap(); //indicates supported var orders
    private Map<String, Value> valMap;

    public ExternalTupleSet() {
    }

    public ExternalTupleSet(Projection tupleExpr) {
    	Preconditions.checkNotNull(tupleExpr);
        this.tupleExpr = tupleExpr;
        valMap = getValMap();
        updateTableVarMap(tupleExpr, tupleExpr);
    }

    @Override
    abstract public CloseableIteration<BindingSet, QueryEvaluationException> evaluate(BindingSet bindings) throws QueryEvaluationException;

    @Override
    public Set<String> getBindingNames() {
        return tupleExpr.getBindingNames();
    }

    @Override
    public Set<String> getAssuredBindingNames() {
        return tupleExpr.getAssuredBindingNames();
    }

    @Override
    public String getSignature() {
        return "(External Projection) " + Joiner.on(", ").join(tupleExpr.getProjectionElemList().getElements()).replaceAll("\\s+", " ");
    }

    public Projection getTupleExpr() {
        return tupleExpr;
    }

    public void setProjectionExpr(Projection tupleExpr) {
    	Preconditions.checkNotNull(tupleExpr);
    	if(this.tupleExpr == null) {
    		updateTableVarMap(tupleExpr, tupleExpr);
    	} else {
    		updateTableVarMap(tupleExpr, this.tupleExpr);
    	}
        this.tupleExpr = tupleExpr;
        valMap = getValMap();
		if (supportedVarOrders.size() != 0) {
			updateSupportedVarOrderMap();
		}
    }

    public void setTableVarMap(Map<String,String> vars) {
    	Preconditions.checkNotNull(vars);
        this.tableVarMap = vars;
    }

    public Map<String, String> getTableVarMap() {
        return this.tableVarMap;
    }

    public void setSupportedVariableOrderMap(Map<String, Set<String>> varOrders) {
    	Preconditions.checkNotNull(varOrders);
        this.supportedVarOrders = varOrders;
    }

    public void setSupportedVariableOrderMap(List<String> varOrders) {
    	Preconditions.checkNotNull(varOrders);
    	this.supportedVarOrders = createSupportedVarOrderMap(varOrders);
    }

    public Map<String, Set<String>> getSupportedVariableOrderMap() {
        return supportedVarOrders;
    }

    public Map<String, Value> getConstantValueMap() {
    	return valMap;
    }

    @Override
    public ExternalSet clone() {
        final ExternalTupleSet clone = (ExternalTupleSet) super.clone();
        clone.setProjectionExpr(this.tupleExpr.clone());
        clone.tableVarMap = Maps.newHashMap();
        for(final String s: this.tableVarMap.keySet()) {
            clone.tableVarMap.put(s,this.tableVarMap.get(s));
        }
        clone.supportedVarOrders = Maps.newHashMap();
        for(final String s: this.supportedVarOrders.keySet()) {
            clone.supportedVarOrders.put(s,this.supportedVarOrders.get(s));
        }
        return clone;
    }

	public Map<String, Set<String>> getSupportedVariableOrders() {
		return supportedVarOrders;
	}

	public boolean supportsBindingSet(Set<String> bindingNames) {
		final Collection<Set<String>> values = supportedVarOrders.values();
		final Set<String> bNames = Sets.newHashSet();
		final Set<String> bNamesWithConstants = Sets.newHashSet();

		for (final String s : this.getTupleExpr().getBindingNames()) {
			if (bindingNames.contains(s)) {
				bNames.add(s);
				bNamesWithConstants.add(s);
			} else if(VarNameUtils.isConstant(s)) {
				bNamesWithConstants.add(s);
			}
		}
		return values.contains(bNames) || values.contains(bNamesWithConstants);
	}

	/**
	 * @param tupleMatch
	 *            - project expression - call after setting {@link tupleExpr} to
	 *            map new variables to old -- the variables in the binding list
	 *            of the new tupleExpr (tupleMatch) must map to the
	 *            corresponding variables in the binding list of the old
	 *            tupleExpr
	 */
	private void updateTableVarMap(TupleExpr newTuple, TupleExpr oldTuple) {

		final List<String> replacementVars = Lists.newArrayList(newTuple
				.getBindingNames());
		final List<String> tableVars = Lists.newArrayList(oldTuple
				.getBindingNames());

		final Map<String, String> tableMap = Maps.newHashMap();

		for (int i = 0; i < tableVars.size(); i++) {
			tableMap.put(replacementVars.get(i), tableVars.get(i));
		}
		this.setTableVarMap(tableMap);
	}

	/**
	 * call after setting {@link tableVarMap} to update map of supported
	 * variables in terms of variables in new tupleExpr
	 */
	private void updateSupportedVarOrderMap() {

		Preconditions.checkArgument(supportedVarOrders.size() != 0);
		final Map<String, Set<String>> newSupportedOrders = Maps.newHashMap();
		final BiMap<String, String> biMap = HashBiMap.create(tableVarMap)
				.inverse();
		Set<String> temp = null;
		final Set<String> keys = supportedVarOrders.keySet();

		for (final String s : keys) {
			temp = supportedVarOrders.get(s);
			final Set<String> newSet = Sets.newHashSet();

			for (final String t : temp) {
				newSet.add(biMap.get(t));
			}

			final String[] tempStrings = s.split(VAR_ORDER_DELIM);
			String v = "";
			for (final String u : tempStrings) {
				if (v.length() == 0) {
					v = v + biMap.get(u);
				} else {
					v = v + VAR_ORDER_DELIM + biMap.get(u);
				}
			}
			newSupportedOrders.put(v, newSet);
		}
		supportedVarOrders = newSupportedOrders;
	}

	 /**
    *
    * @param orders
    * @return - map with all possible orders in which results are written to the table
    */
   private Map<String, Set<String>> createSupportedVarOrderMap(List<String> orders) {
	   final Map<String, Set<String>> supportedVars = Maps.newHashMap();

       for (final String t : orders) {
           final String[] tempOrder = t.split(VAR_ORDER_DELIM);
           final Set<String> varSet = Sets.newHashSet();
           String u = "";
           for (final String s : tempOrder) {
               if(u.length() == 0) {
                   u = s;
               } else{
                   u = u+ VAR_ORDER_DELIM + s;
               }
               varSet.add(s);
               supportedVars.put(u, new HashSet<String>(varSet));
           }
       }
       return supportedVars;
   }

    @Override
    public boolean equals(Object other) {
        if (!(other instanceof ExternalTupleSet)) {
            return false;
        } else {
            final ExternalTupleSet arg = (ExternalTupleSet) other;
            return this.getTupleExpr().equals(arg.getTupleExpr());
        }
    }

    @Override
    public int hashCode() {
        int result = 17;
        result = 31*result + tupleExpr.hashCode();
        return result;
    }

    private Map<String, Value> getValMap() {
		ValueMapVisitor valMapVis = new ValueMapVisitor();
		tupleExpr.visit(valMapVis);
		return valMapVis.getValMap();
	}


	/**
	 *
	 * Extracts the values associated with constant labels in the query Used to
	 * create binding sets from range scan
	 */
	private class ValueMapVisitor extends
            AbstractQueryModelVisitor<RuntimeException> {
		Map<String, Value> valMap = Maps.newHashMap();

		public Map<String, Value> getValMap() {
			return valMap;
		}

		@Override
		public void meet(Var node) {
			if (VarNameUtils.isConstant(node.getName())) {
				valMap.put(node.getName(), node.getValue());
			}
		}
	}


}
