package org.apache.rya.indexing.IndexPlanValidator;

/*
 * 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.Iterator;
import java.util.Set;

import org.apache.rya.api.domain.VarNameUtils;
import org.apache.rya.indexing.external.tupleSet.ExternalTupleSet;
import org.eclipse.rdf4j.query.algebra.BindingSetAssignment;
import org.eclipse.rdf4j.query.algebra.Filter;
import org.eclipse.rdf4j.query.algebra.Join;
import org.eclipse.rdf4j.query.algebra.Projection;
import org.eclipse.rdf4j.query.algebra.QueryModelNode;
import org.eclipse.rdf4j.query.algebra.StatementPattern;
import org.eclipse.rdf4j.query.algebra.TupleExpr;
import org.eclipse.rdf4j.query.algebra.helpers.AbstractQueryModelVisitor;

import com.google.common.collect.Sets;

public class ThreshholdPlanSelector implements IndexedQueryPlanSelector {

    private TupleExpr query;
    private int queryNodeCount = 0;

    public ThreshholdPlanSelector(TupleExpr query) {
        this.query = query;
        QueryNodeCount qnc = new QueryNodeCount();
        query.visit(qnc);

        this.queryNodeCount = qnc.getNodeCount();
        
        if(queryNodeCount == 0) {
            throw new IllegalArgumentException("TupleExpr must contain at least one node!");
        }
    }

    
    
    
    @Override
    public TupleExpr getThreshholdQueryPlan(Iterator<TupleExpr> tuples, double threshhold, double indexWeight,
            double commonVarWeight, double extProdWeight) {

        if (threshhold < 0 || threshhold > 1) {
            throw new IllegalArgumentException("Threshhold must be between 0 and 1!");
        }
        double minCost = Double.MAX_VALUE;
        TupleExpr minTup = null;

        double tempCost = 0;
        TupleExpr tempTup = null;

        
        
        while (tuples.hasNext()) {

            tempTup = tuples.next();
            tempCost = getCost(tempTup, indexWeight, commonVarWeight, extProdWeight);

            if (tempCost < minCost) {
                minCost = tempCost;
                minTup = tempTup;
            }

            if (minCost <= threshhold) {
                return minTup;
            }

        }

        return minTup;
    }

    public double getCost(TupleExpr te, double indexWeight, double commonVarWeight, double dirProdWeight) {

        if (indexWeight + commonVarWeight + dirProdWeight != 1) {
            throw new IllegalArgumentException("Weights must sum to 1!");
        }
        
        if(te == null) {
            throw new IllegalArgumentException("TupleExpr cannot be null!");
        }

        QueryNodeCount qnc = new QueryNodeCount();
        te.visit(qnc);

        double nodeCount = qnc.getNodeCount();
        double commonJoinVars = qnc.getCommonJoinVarCount();
        double joinVars = qnc.getJoinVarCount();
        double joinCount = qnc.getJoinCount();
        double dirProdCount = qnc.getDirProdCount();
        double dirProductScale;
        
        if(queryNodeCount > nodeCount) {
            dirProductScale = 1/ (queryNodeCount - nodeCount);
        } else {
            dirProductScale = 1/ (queryNodeCount - nodeCount + 1);
        }
        
        double joinVarRatio;
        double dirProductRatio;
        
        if(joinVars != 0) {
            joinVarRatio = (joinVars - commonJoinVars) / joinVars;
        } else {
            joinVarRatio = 0;
        }
        
        if(joinCount != 0) {
            dirProductRatio = dirProdCount / joinCount;
        } else {
            dirProductRatio = 0;
        }
        
        
        double cost = indexWeight * (nodeCount / queryNodeCount) + commonVarWeight*joinVarRatio
                + dirProdWeight *dirProductRatio*dirProductScale;
        
//        System.out.println("Tuple is " + te + " and cost is " + cost);
//        System.out.println("Node count is " + nodeCount + " and query node count is " + queryNodeCount);
//        System.out.println("Common join vars are " + commonJoinVars + " and join vars " + joinVars);
//        System.out.println("Join count is " + joinCount + " and direct prod count is " + dirProdCount);
        
        return cost;
    }

    public static class QueryNodeCount extends AbstractQueryModelVisitor<RuntimeException> {

        private int nodeCount = 0;
        private int commonJoinVars = 0;
        private int joinVars = 0;
        private int joinCount = 0;
        private int dirProdCount = 0;

        public int getCommonJoinVarCount() {
            return commonJoinVars;
        }

        public int getJoinVarCount() {
            return joinVars;
        }

        public int getNodeCount() {
            return nodeCount;
        }

        public int getJoinCount() {
            return joinCount;
        }

        public int getDirProdCount() {
            return dirProdCount;
        }

        public void meet(Projection node) {
            node.getArg().visit(this);
        }

        public void meetNode(QueryModelNode node) {
            if (node instanceof ExternalTupleSet) {
                nodeCount += 1;
                return;
            }
            super.meetNode(node);
            return;
        }

        @Override
        public void meet(StatementPattern node) {
            nodeCount += 1;
            return;
        }

        @Override
        public void meet(Filter node) {
            nodeCount += 1;
            node.getArg().visit(this);
        }

        public void meet(BindingSetAssignment node) {
            nodeCount += 1;
            return;
        }

        @Override
        public void meet(Join node) {

            int tempCount = 0;

            Set<String> lNames = node.getLeftArg().getAssuredBindingNames();
            Set<String> rNames = node.getRightArg().getAssuredBindingNames();
            
            for(String s: node.getLeftArg().getBindingNames()) {
                if (VarNameUtils.isConstant(s)) {
                    lNames.remove(s);
                }
            }
            
            for(String s: node.getRightArg().getBindingNames()) {
                if (VarNameUtils.isConstant(s)) {
                    rNames.remove(s);
                }
            }
           
            
            joinVars += Math.min(lNames.size(), rNames.size());
            tempCount = Sets.intersection(lNames, rNames).size();
            if (tempCount == 0) {
                dirProdCount += 1;
            } else {
                commonJoinVars += tempCount;
            }
            joinCount += 1;

            super.meet(node);

        }

    }

}
