/*
 * 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.query

import org.apache.atlas.repository.graphdb.AtlasGraph
import org.apache.atlas.query.Expressions._
import org.slf4j.{Logger, LoggerFactory}
import org.apache.atlas.util.AtlasRepositoryConfiguration
import org.apache.atlas.utils.LruCache
import org.apache.atlas.util.CompiledQueryCacheKey
import java.util.Collections

object QueryProcessor {
    val LOG : Logger = LoggerFactory.getLogger("org.apache.atlas.query.QueryProcessor")

    val compiledQueryCache = Collections.synchronizedMap(new LruCache[CompiledQueryCacheKey, GremlinQuery](
                        AtlasRepositoryConfiguration.getCompiledQueryCacheCapacity(),
                        AtlasRepositoryConfiguration.getCompiledQueryCacheEvictionWarningThrottle()));

    def evaluate(e: Expression, g: AtlasGraph[_,_], gP : GraphPersistenceStrategies = null):
    GremlinQueryResult = {

        var strategy = gP;
        if(strategy == null) {
            strategy = GraphPersistenceStrategy1(g);
        }

        //convert the query expression to DSL so we can check whether or not it is in the compiled
        //query cache and avoid validating/translating it again if it is.
        val dsl = e.toString();
        val cacheKey = new CompiledQueryCacheKey(dsl);
        var q = compiledQueryCache.get(cacheKey);
        if(q == null) {

            //query was not found in the compiled query cache.  Validate
            //and translate it, then cache the result.

            val e1 = validate(e)
            q = new GremlinTranslator(e1, strategy).translate()
            compiledQueryCache.put(cacheKey, q);
            if(LOG.isDebugEnabled()) {
                LOG.debug("Validated Query: " + e1)
                LOG.debug("Expression Tree:\n" + e1.treeString);
            }
        }
        if(LOG.isDebugEnabled()) {
            LOG.debug("DSL Query: " + dsl);
            LOG.debug("Gremlin Query: " + q.queryStr)
        }
        new GremlinEvaluator(q, strategy, g).evaluate()
    }

    def validate(e: Expression): Expression = {

        val e1 = e.transformUp(refineIdExpressionType);
        val e2 = e1.transformUp(new Resolver(None,e1.namedExpressions))

        e2.traverseUp {
            case x: Expression if !x.resolved =>
                throw new ExpressionException(x, s"Failed to resolved expression $x")
        }

        /*
         * trigger computation of dataType of expression tree
         */
        e2.dataType

        /*
         * ensure fieldReferences match the input expression's dataType
         */
        val e3 = e2.transformUp(FieldValidator)
        val e4 = e3.transformUp(new Resolver(None,e3.namedExpressions))

        e4.dataType

        e4
    }

    val convertToFieldIdExpression : PartialFunction[Expression,Expression] = {
        case IdExpression(name, IdExpressionType.Unresolved) => IdExpression(name, IdExpressionType.NonType);
    }


    //this function is called in a depth first manner on the expression tree to set the exprType in IdExpressions
    //when we know them.  Since Expression classes are immutable, in order to do this we need to create new instances
    //of the case.  The logic here enumerates the cases that have been identified where the given IdExpression
    //cannot resolve to a class or trait.  This is the case in any places where a field value must be used.
    //For example, you cannot add two classes together or compare traits.  Any IdExpressions in those contexts
    //refer to unqualified attribute names.  On a similar note, select clauses need to product an actual value.
    //For example, in 'from DB select name' or 'from DB select name as n', name must be an attribute.
     val refineIdExpressionType : PartialFunction[Expression,Expression] = {

         //spit out the individual cases to minimize the object churn.  Specifically, for ComparsionExpressions where neither
         //child is an IdExpression, there is no need to create a new ComparsionExpression object since neither child will
         //change.  This applies to ArithmeticExpression as well.
         case c@ComparisonExpression(symbol, l@IdExpression(_,IdExpressionType.Unresolved) , r@IdExpression(_,IdExpressionType.Unresolved)) => {
             ComparisonExpression(symbol, convertToFieldIdExpression(l), convertToFieldIdExpression(r))
         }
         case c@ComparisonExpression(symbol, l@IdExpression(_,IdExpressionType.Unresolved) , r) => ComparisonExpression(symbol, convertToFieldIdExpression(l), r)
         case c@ComparisonExpression(symbol, l, r@IdExpression(_,IdExpressionType.Unresolved)) => ComparisonExpression(symbol, l, convertToFieldIdExpression(r))

         case e@ArithmeticExpression(symbol, l@IdExpression(_,IdExpressionType.Unresolved) , r@IdExpression(_,IdExpressionType.Unresolved)) => {
             ArithmeticExpression(symbol, convertToFieldIdExpression(l), convertToFieldIdExpression(r))
         }
         case e@ArithmeticExpression(symbol, l@IdExpression(_,IdExpressionType.Unresolved) , r) => ArithmeticExpression(symbol, convertToFieldIdExpression(l), r)
         case e@ArithmeticExpression(symbol, l, r@IdExpression(_,IdExpressionType.Unresolved)) => ArithmeticExpression(symbol, l, convertToFieldIdExpression(r))

         case s@SelectExpression(child, selectList, forGroupBy) => {
             var changed = false
             val newSelectList = selectList.map {

                 expr => expr match {
                     case e@IdExpression(_,IdExpressionType.Unresolved) => { changed=true; convertToFieldIdExpression(e) }
                     case AliasExpression(child@IdExpression(_,IdExpressionType.Unresolved), alias) => {changed=true; AliasExpression(convertToFieldIdExpression(child), alias)}
                     case x => x
                  }
             }
             if(changed) {
                 SelectExpression(child, newSelectList, forGroupBy)
             }
             else {
                 s
             }
         }

    }
}
