/*
 * 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.
 */
package org.apache.rya.api.function.projection;

import static java.util.Objects.requireNonNull;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;

import org.apache.rya.api.model.VisibilityBindingSet;
import org.eclipse.rdf4j.model.BNode;
import org.eclipse.rdf4j.model.Value;
import org.eclipse.rdf4j.model.ValueFactory;
import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
import org.eclipse.rdf4j.query.algebra.BNodeGenerator;
import org.eclipse.rdf4j.query.algebra.Extension;
import org.eclipse.rdf4j.query.algebra.ExtensionElem;
import org.eclipse.rdf4j.query.algebra.Projection;
import org.eclipse.rdf4j.query.algebra.ProjectionElem;
import org.eclipse.rdf4j.query.algebra.ProjectionElemList;
import org.eclipse.rdf4j.query.algebra.TupleExpr;
import org.eclipse.rdf4j.query.algebra.ValueConstant;
import org.eclipse.rdf4j.query.algebra.ValueExpr;
import org.eclipse.rdf4j.query.impl.MapBindingSet;

import edu.umd.cs.findbugs.annotations.DefaultAnnotation;
import edu.umd.cs.findbugs.annotations.NonNull;

/**
 * Processes a {@link Projection} node from a SPARQL query.
 * </p>
 * A projection is used transform the bindings that are in a Binding Set. It may do the following things:
 * <ul>
 *   <li>Change the binding name for a value.</li>
 *   <li>Completely remove a binding from the Binding Set.</li>
 *   <li>Insert a binding that has a constant value.</li>
 *   <li>Insert a binding with a blank node that identifies some resource.</li>
 * </ul>
 * </p>
 * If you do not supply ID values for the blank nodes that may result from a projection, then a random {@link UUID}
 * is used.
 */
@DefaultAnnotation(NonNull.class)
public class ProjectionEvaluator {

    private static final ValueFactory VF = SimpleValueFactory.getInstance();

    /**
     * All off the projection elements that define what will appear in the resulting binding sets.
     */
    private final ProjectionElemList projectionElems;

    /**
     * Maps from a ProjectionElem's source name to the constant value that should be used for that name
     * in resulting binding sets.
     */
    private final Map<String, Value> constantSources = new HashMap<>();

    /**
     * A set of names for the anonymous source names. These values will be blank node UUIDs.
     */
    private final Set<String> anonymousSources = new HashSet<>();

    /**
     * Constructs an instance of {@link ProjectionEvaluator}.
     *
     * @param projectionElems - Defines the structure of the resulting value. (not null)
     * @param extensions - Extra information about the projection elements when there are anonymous constants or blank
     *   nodes within the projection elements. (not null)
     */
    public ProjectionEvaluator(final ProjectionElemList projectionElems, final Optional<Extension> extensions) {
        this.projectionElems = requireNonNull(projectionElems);
        requireNonNull(extensions);

        // Find all extensions that represent constant insertions.
        if(extensions.isPresent()) {
            for(final ExtensionElem extensionElem : extensions.get().getElements()) {
                final ValueExpr valueExpr = extensionElem.getExpr();

                // If the extension is a ValueConstant, store it so that they may be added to the binding sets.
                if(valueExpr instanceof ValueConstant) {
                    final String sourceName = extensionElem.getName();
                    final Value targetValue = ((ValueConstant) valueExpr).getValue();
                    constantSources.put(sourceName, targetValue);
                }

                // If the extension is a BNodeGenerator, keep track of the name so that we know we have to generate an ID for it.
                else if(valueExpr instanceof BNodeGenerator) {
                    final String sourceName = extensionElem.getName();
                    anonymousSources.add( sourceName );
                }
            }
        }
    }

    /**
     * Make a {@link ProjectionEvaluator} that processes the logic of a {@link Projection}.
     *
     * @param projection - Defines the projection that will be processed. (not null)
     * @return A {@link ProjectionEvaluator} for the provided {@link Projection}.
     */
    public static ProjectionEvaluator make(final Projection projection) {
        requireNonNull(projection);

        final ProjectionElemList projectionElems = projection.getProjectionElemList();

        final TupleExpr arg = projection.getArg();
        final Optional<Extension> extension = arg instanceof Extension ? Optional.of((Extension)arg) : Optional.empty();

        return new ProjectionEvaluator(projectionElems, extension);
    }

    /**
     * Applies the projection to a value. If the result has any blank nodes, those nodes will use random UUIDs.
     * If you want to control what those IDs are, then use {@link #project(VisibilityBindingSet, Map)} instead.
     *
     * @param bs - The value the projection will be applied to. (not null)
     * @return A new value that is the result of the projection.
     */
    public VisibilityBindingSet project(final VisibilityBindingSet bs) {
        return project(bs, new HashMap<>());
    }

    /**
     * Applies the projection to a value. If the result has a blank node whose ID is not mapped to a value in
     * {@code blankNodes}, then a random UUID will be used.
     *
     * @param bs - The value the projection will be applied to. (not null)
     * @param blankNodes - A map from node source names to the blank nodes that will be used for those names. (not null)
     * @return A new value that is the result of the projection.
     */
    public VisibilityBindingSet project(final VisibilityBindingSet bs, final Map<String, BNode> blankNodes) {
        requireNonNull(bs);
        requireNonNull(blankNodes);

        // Apply the projection elements against the original binding set.
        final MapBindingSet result = new MapBindingSet();
        for (final ProjectionElem elem : projectionElems.getElements()) {
            final String sourceName = elem.getSourceName();

            Value value = null;

            // If the binding set already has the source name, then use the target name.
            if (bs.hasBinding(sourceName)) {
                value = bs.getValue(elem.getSourceName());
            }

            // If the source name represents a constant value, then use the constant.
            else if(constantSources.containsKey(sourceName)) {
                value = constantSources.get(sourceName);
            }

            // If the source name represents an anonymous value, then create a Blank Node.
            else if(anonymousSources.contains(sourceName)) {
                if(blankNodes.containsKey(sourceName)) {
                    value = blankNodes.get(sourceName);
                } else {
                    value = VF.createBNode( UUID.randomUUID().toString() );
                }
            }

            // Only add the value if there is one. There may not be one if a binding is optional.
            if(value != null) {
                result.addBinding(elem.getTargetName(), value);
            }
        }

        return new VisibilityBindingSet(result, bs.getVisibility());
    }
}