package org.apache.rya.indexing.pcj.fluo.app;

/*
 * 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.io.UnsupportedEncodingException;
import java.util.Map;
import java.util.Optional;

import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.log4j.Logger;
import org.apache.rya.api.domain.RyaStatement;
import org.apache.rya.api.domain.RyaType;
import org.apache.rya.api.domain.RyaIRI;
import org.apache.rya.api.domain.VarNameUtils;
import org.apache.rya.api.model.VisibilityBindingSet;
import org.apache.rya.api.resolver.RdfToRyaConversions;
import org.eclipse.rdf4j.model.BNode;
import org.eclipse.rdf4j.model.IRI;
import org.eclipse.rdf4j.model.Resource;
import org.eclipse.rdf4j.model.Value;
import org.eclipse.rdf4j.model.ValueFactory;
import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
import org.eclipse.rdf4j.query.BindingSet;
import org.eclipse.rdf4j.query.algebra.StatementPattern;
import org.eclipse.rdf4j.query.algebra.Var;

import com.google.common.base.Objects;
import com.google.common.base.Preconditions;

/**
 * This class projects a VisibilityBindingSet onto a RyaStatement. The Binding
 * {@link Value}s that get projected onto subject, predicate and object are
 * indicated by the names {@link ConstructProjection#getSubjectSourceName()},
 * {@link ConstructProjection#getPredicateSourceName()} and
 * {@link ConstructProjection#getObjectSourceName()} and must satisfy standard
 * RDF constraints for RDF subjects, predicates and objects. The purpose of
 * projecting {@link BindingSet}s in this way is to provide functionality for
 * SPARQL Construct queries which create RDF statements from query results.
 *
 */
public class ConstructProjection {

    private static final Logger log = Logger.getLogger(ConstructProjection.class);
    private static final ValueFactory VF = SimpleValueFactory.getInstance();
    private String subjName;
    private String predName;
    private String objName;
    private Optional<Value> subjValue;
    private Optional<Value> predValue;
    private Optional<Value> objValue;
    private Var subjVar;
    private Var predVar;
    private Var objVar;

    public ConstructProjection(Var subjectVar, Var predicateVar, Var objectVar) {
        Preconditions.checkNotNull(subjectVar);
        Preconditions.checkNotNull(predicateVar);
        Preconditions.checkNotNull(objectVar);
        subjName = VarNameUtils.createSimpleConstVarName(subjectVar);
        predName = VarNameUtils.createSimpleConstVarName(predicateVar);
        objName = VarNameUtils.createSimpleConstVarName(objectVar);
        Preconditions.checkNotNull(subjName);
        Preconditions.checkNotNull(predName);
        Preconditions.checkNotNull(objName);
        this.subjVar = subjectVar;
        this.predVar = predicateVar;
        this.objVar = objectVar;
        if ((subjVar.isAnonymous() || VarNameUtils.isAnonymous(subjName)) && subjectVar.getValue() == null) {
            subjValue = Optional.of(VF.createBNode(""));
        } else {
            subjValue = Optional.ofNullable(subjectVar.getValue());
        }
        predValue = Optional.ofNullable(predicateVar.getValue());
        objValue = Optional.ofNullable(objectVar.getValue());
    }

    public ConstructProjection(StatementPattern pattern) {
        this(pattern.getSubjectVar(), pattern.getPredicateVar(), pattern.getObjectVar());
    }

    /**
     * Returns a Var with info about the Value projected onto the RyaStatement
     * subject. If the org.eclipse.rdf4j.query.algebra.Var returned by this method is
     * not constant (as indicated by {@link Var#isConstant()}, then
     * {@link Var#getName()} is the Binding name that gets projected. If the Var
     * is constant, then {@link Var#getValue()} is assigned to the subject
     * 
     * @return {@link org.eclipse.rdf4j.query.algebra.Var} containing info about
     *         Binding that gets projected onto the subject
     */
    public String getSubjectSourceName() {
        return subjName;
    }

    /**
     * Returns a Var with info about the Value projected onto the RyaStatement
     * predicate. If the org.eclipse.rdf4j.query.algebra.Var returned by this method
     * is not constant (as indicated by {@link Var#isConstant()}, then
     * {@link Var#getName()} is the Binding name that gets projected. If the Var
     * is constant, then {@link Var#getValue()} is assigned to the predicate
     * 
     * @return {@link org.eclipse.rdf4j.query.algebra.Var} containing info about
     *         Binding that gets projected onto the predicate
     */
    public String getPredicateSourceName() {
        return predName;
    }

    /**
     * Returns a Var with info about the Value projected onto the RyaStatement
     * object. If the org.eclipse.rdf4j.query.algebra.Var returned by this method is
     * not constant (as indicated by {@link Var#isConstant()}, then
     * {@link Var#getName()} is the Binding name that gets projected. If the Var
     * is constant, then {@link Var#getValue()} is assigned to the object
     * 
     * @return {@link org.eclipse.rdf4j.query.algebra.Var} containing info about
     *         Binding that gets projected onto the object
     */
    public String getObjectSourceName() {
        return objName;
    }

    /**
     * @return Value set for RyaStatement subject (if present)
     */
    public Optional<Value> getSubjValue() {
        return subjValue;
    }

    /**
     * @return Value set for RyaStatement predicate (if present)
     */
    public Optional<Value> getPredValue() {
        return predValue;
    }

    /**
     * @return Value set for RyaStatement object (if present)
     */
    public Optional<Value> getObjValue() {
        return objValue;
    }
    

    /**
     * @return SubjectPattern representation of this ConstructProjection
     *         containing the {@link ConstructProjection#getSubjectSourceName},
     *         {@link ConstructProjection#getPredicateSourceName},
     *         {@link ConstructProjection#getObjectSourceName}
     */
    public StatementPattern getStatementPatternRepresentation() {
        return new StatementPattern(subjVar, predVar, objVar);
    }

    /**
     * Projects a given BindingSet onto a RyaStatement. The subject, predicate,
     * and object are extracted from the input VisibilityBindingSet (if the
     * getSubjectSourceName, getPredicateSourceName, getObjectSourceName is resp.
     * non-constant) and from the Var Value itself (if getSubjectSourceName,
     * predicateSource, getObjectSourceName is resp. constant).
     * 
     * 
     * @param vBs
     *            - Visibility BindingSet that gets projected onto an RDF
     *            Statement BindingSet with Binding names subject, predicate and
     *            object
     * @param   bNodes - Optional Map used to pass {@link BNode}s for given variable names into
     *          multiple {@link ConstructProjection}s.  This allows a ConstructGraph to create
     *          RyaStatements with the same BNode for a given variable name across multiple ConstructProjections.
     * @return - RyaStatement whose values are determined by
     *         {@link ConstructProjection#getSubjectSourceName()},
     *         {@link ConstructProjection#getPredicateSourceName()},
     *         {@link ConstructProjection#getObjectSourceName()}.
     * 
     */
    public RyaStatement projectBindingSet(VisibilityBindingSet vBs, Map<String, BNode> bNodes) {
     
        Preconditions.checkNotNull(vBs);
        Preconditions.checkNotNull(bNodes);
        
        Value subj = getValue(subjName, subjValue, vBs, bNodes);
        Value pred = getValue(predName, predValue, vBs, bNodes);
        Value obj = getValue(objName, objValue, vBs, bNodes);
        
        Preconditions.checkNotNull(subj);
        Preconditions.checkNotNull(pred);
        Preconditions.checkNotNull(obj);
        Preconditions.checkArgument(subj instanceof Resource);
        Preconditions.checkArgument(pred instanceof IRI);

        RyaIRI subjType = RdfToRyaConversions.convertResource((Resource) subj);
        RyaIRI predType = RdfToRyaConversions.convertIRI((IRI) pred);
        RyaType objectType = RdfToRyaConversions.convertValue(obj);

        RyaStatement statement = new RyaStatement(subjType, predType, objectType);
        try {
            statement.setColumnVisibility(vBs.getVisibility().getBytes("UTF-8"));
        } catch (UnsupportedEncodingException e) {
            log.trace("Unable to decode column visibility.  RyaStatement being created without column visibility.");
        }
        return statement;
    }
    
    private Value getValue(String name, Optional<Value> optValue, VisibilityBindingSet bs, Map<String, BNode> bNodes) {
        Value returnValue = null;
        if (optValue.isPresent()) {
            Value tempValue = optValue.get();
            if(tempValue instanceof BNode) {
                Preconditions.checkArgument(bNodes.containsKey(name));
                returnValue = bNodes.get(name);
            } else {
                returnValue = tempValue;
            }
        } else {
            returnValue = bs.getValue(name);
        }
        return returnValue;
    }

    @Override
    public boolean equals(final Object o) {
        if (this == o) {
            return true;
        }

        if (o instanceof ConstructProjection) {
            ConstructProjection projection = (ConstructProjection) o;
            return new EqualsBuilder().append(this.subjName, projection.subjName).append(this.predName, projection.predName)
                    .append(this.objName, projection.objName).append(this.subjValue, projection.subjValue)
                    .append(this.predValue, projection.predValue).append(this.objValue, projection.objValue).isEquals();
        }
        return false;

    }

    @Override
    public int hashCode() {
        return Objects.hashCode(this.subjName, this.predName, this.objName, this.subjValue, this.predValue, this.objValue);
    }

    @Override
    public String toString() {
        return new StringBuilder().append("Construct Projection {\n").append("   Subject Name: " + subjName + "\n")
                .append("   Subject Value: " + subjValue + "\n").append("   Predicate Name: " + predName + "\n")
                .append("   Predicate Value: " + predValue + "\n").append("   Object Name: " + objName + "\n")
                .append("   Object Value: " + objValue + "\n").append("}").toString();
    }

}
