/**
 * 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.atlas.gremlin.optimizer;

import java.util.ArrayList;
import java.util.List;

import org.apache.atlas.gremlin.GremlinExpressionFactory;
import org.apache.atlas.groovy.AbstractFunctionExpression;
import org.apache.atlas.groovy.FunctionCallExpression;
import org.apache.atlas.groovy.GroovyExpression;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Optimizer that pulls has expressions out of an 'and' expression.
 *
 * For example:
 *
 * g.V().and(has('x'),has('y')
 *
 * is optimized to:
 *
 * g.V().has('x').has('y')
 *
 * There are certain cases where it is not safe to move an expression out
 * of the 'and'.  For example, in the expression
 *
 * g.V().and(has('x').out('y'),has('z'))
 *
 * has('x').out('y') cannot be moved out of the 'and', since it changes the value of the traverser.
 *
 * At this time, the ExpandAndsOptimizer is not able to handle this scenario, so we don't extract
 * that expression.  In this case, the result is:
 *
 * g.V().has('z').and(has('x').out('y'))
 *
 * The optimizer will call ExpandAndsOptimization recursively on the children, so
 * there is no need to recursively update the children here.
 *
 * @param expr
 * @param context
 * @return the expressions that should be unioned together to get the query result
 */
public class ExpandAndsOptimization implements GremlinOptimization {

    private static final Logger logger_ = LoggerFactory.getLogger(ExpandAndsOptimization.class);


    private final GremlinExpressionFactory factory;

    public ExpandAndsOptimization(GremlinExpressionFactory factory) {
        this.factory = factory;
    }

    @Override
    public boolean appliesTo(GroovyExpression expr, OptimizationContext contxt) {
        return expr instanceof FunctionCallExpression && ((FunctionCallExpression)expr).getFunctionName().equals("and");
    }

    /**
     * Expands the given and expression.  There is no need to recursively
     * expand the children here.  This method is called recursively by
     * GremlinQueryOptimier on the children.
     *
     */
    @Override
    public GroovyExpression apply(GroovyExpression expr, OptimizationContext context) {

        FunctionCallExpression exprAsFunction = (FunctionCallExpression)expr;
        GroovyExpression result = exprAsFunction.getCaller();

        List<GroovyExpression> nonExtractableArguments = new ArrayList<>();
        for(GroovyExpression argument : exprAsFunction.getArguments()) {

            if (GremlinQueryOptimizer.isExtractable(argument)) {
                //Set the caller of the deepest expression in the call hierarchy
                //of the argument to point to the current result.
                //For example, if result is "g.V()" and the updatedArgument is "has('x').has('y')",
                //updatedArgument would be a tree like this:
                //
                //                  has('y')
                //                 /
                //                / caller
                //              |/_
                //           has('x')
                //             /
                //            / caller
                //          |/_
                //        (null)
                //
                //We would set the caller of has('x') to be g.V(), so result would become g.V().has('x').has('y').
                //
                // Note: This operation is currently done by making a copy of the argument tree.  That should
                // be changed.
                result = GremlinQueryOptimizer.copyWithNewLeafNode(
                        (AbstractFunctionExpression) argument, result);
            } else {
                logger_.warn("Found non-extractable argument '{}' in the 'and' expression '{}'",argument.toString(), expr.toString());
                nonExtractableArguments.add(argument);
            }
        }

        if (!nonExtractableArguments.isEmpty()) {
            //add a final 'and' call with the arguments that could not be extracted
            result =  factory.generateLogicalExpression(result, "and", nonExtractableArguments);
        }
        return result;
    }

    @Override
    public boolean isApplyRecursively() {
        return true;
    }
}
