package org.aksw.sparqlify.core.sparql;

import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map.Entry;
import java.util.Set;

import javax.annotation.Nullable;

import org.aksw.jena_sparql_api.views.RestrictedExpr;
import org.aksw.sparqlify.core.MakeExprPermissive;
import org.apache.jena.datatypes.RDFDatatype;
import org.apache.jena.datatypes.xsd.impl.RDFLangString;
import org.apache.jena.graph.Node;
import org.apache.jena.graph.NodeFactory;
import org.apache.jena.riot.process.normalize.CanonicalizeLiteral;
import org.apache.jena.sparql.core.Var;
import org.apache.jena.sparql.engine.binding.Binding;
import org.apache.jena.sparql.engine.binding.BindingBuilder;
import org.apache.jena.sparql.engine.binding.BindingFactory;
import org.apache.jena.sparql.expr.Expr;
import org.apache.jena.sparql.expr.NodeValue;
import org.apache.jena.sparql.util.ExprUtils;
import org.apache.jena.vocabulary.XSD;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Function;
import com.google.common.collect.Multimap;

public class ItemProcessorSparqlify
    implements Function<Binding, Binding>
{
    private static final Logger logger = LoggerFactory.getLogger(IteratorResultSetSparqlifyBinding.class);

    // Canonicalize values, e.g. 20.0 -> 2.0e1
    private static CanonicalizeLiteral canonicalizer = CanonicalizeLiteral.get();

    //private NodeExprSubstitutor substitutor;// = new NodeExprSubstitutor(sparqlVarMap);
    private Multimap<Var, RestrictedExpr> sparqlVarMap;

    //private long nextRowId;

    public static boolean isCharType(String typeName) {
        String tmp = typeName.toLowerCase();

        Set<String> charNames = new HashSet<String>(Arrays.asList("char"));

        boolean result = charNames.contains(tmp);
        return result;
    }

    public ItemProcessorSparqlify(Multimap<Var, RestrictedExpr> sparqlVarMap)
    {
        this.sparqlVarMap = sparqlVarMap;
     }

    @Override
    public Binding apply(@Nullable Binding binding) {
        Binding result = ItemProcessorSparqlify.process(sparqlVarMap, binding);
        return result;
    }


    public static Binding process(Multimap<Var, RestrictedExpr> sparqlVarMap, Binding binding) {
        boolean debugMode = true;

        BindingBuilder result = BindingFactory.builder();

        for(Entry<Var, Collection<RestrictedExpr>> entry : sparqlVarMap.asMap().entrySet()) {

            Var bindingVar = entry.getKey();
            Collection<RestrictedExpr> candidateExprs = entry.getValue();

//			if(bindingVar.getName().equals("o")) {
//				System.out.println("BindingVar o ");
//			}

            //RDFNode rdfNode = null;
            NodeValue value = null;
            //Node value = Node.NULL;

            // We distinguish on how to create a variable by the columns that are used
            // We use the most specific rdfTerm constructor
            Set<Var> usedVars = new HashSet<Var>();
            for(RestrictedExpr def : candidateExprs) {

                Expr expr = def.getExpr();


                // Check if all variables are bound
                // Null columns may appear on left joins
                boolean allBound = true;
                Set<Var> exprVars = expr.getVarsMentioned();
                for(Var var : exprVars) {
                    if(!binding.contains(var)) {
                        allBound = false;
                        break;
                    }
                }

                if(allBound) {
                    if(value != null) {
                        // If the new rdfTerm constructor makes use of only a subset of the columns
                        // from which the current node was created, we ignore the new bindings
                        if(usedVars.containsAll(expr.getVarsMentioned())) {
                            continue;
                        } else if(usedVars.equals(expr.getVarsMentioned())) {
                            throw new RuntimeException("Multiple expressions binding the variable (ambiguity) " + bindingVar + ": " + entry.getValue());
                        } else if(!expr.getVarsMentioned().containsAll(usedVars)) {
                            throw new RuntimeException("Multiple expressions binding the variable (overlap) " + bindingVar + ": " + entry.getValue());
                        }
                    }


                    expr = MakeExprPermissive.getInstance().deepCopy(expr);

                    value = ExprUtils.eval(expr, binding);
                    //rdfNode = ModelUtils.convertGraphNodeToRDFNode(value.asNode(), null);

                    if(!debugMode) {
                        break;
                    }
                }
            }

            //qs.add(entry.getKey().getName(), rdfNode);
            // TODO Add a switch for this warning/debugging message (also decide on the logging level)
            Node resultValue = value == null ? null : value.asNode();

            if(resultValue == null) {
                logger.trace("Null node for variable " + bindingVar + " - Might be undesired.");
                //throw new RuntimeException("Null node for variable " + entry.getKey() + " - Should not happen.");
            } else {

                //result.add((Var)entry.getKey(), resultValue);

                boolean isDatatypeCanonicalization = false;

                Node canonResultValue = canonicalizer.apply(resultValue);
                //System.out.println("Canonicalization: " + resultValue + " -> " + canonResultValue);
                if(!isDatatypeCanonicalization) {

                    if(canonResultValue.isLiteral()) {
                        String lex = canonResultValue.getLiteralLexicalForm();

                        if(resultValue.isLiteral()) {
                            RDFDatatype originalType = resultValue.getLiteralDatatype();


                            if(originalType != null) {
                                String typeUri = originalType.getURI();
                                boolean isStringType =  typeUri.equals(RDFLangString.rdfLangString.getURI()) ||typeUri.equals(XSD.xstring.getURI());

                            //String typeUri = resultValue.getLiteralDatatypeURI();
                                if(!isStringType) {
                                    canonResultValue = NodeFactory.createLiteral(lex, originalType);
                                }
                            }
                        } else {
                            throw new RuntimeException("Should not happen: Non-literal canonicalized to literal: " + resultValue + " became " + canonResultValue);
                        }


                    }

                }

                result.add(bindingVar, canonResultValue);
            }
        }

        return result.build();
    }


}