/*
 * 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.geode.cache.query.internal;

import java.io.StringReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Stack;

import org.apache.logging.log4j.Logger;

import org.apache.geode.cache.query.FunctionDomainException;
import org.apache.geode.cache.query.NameResolutionException;
import org.apache.geode.cache.query.QueryInvalidException;
import org.apache.geode.cache.query.QueryInvocationTargetException;
import org.apache.geode.cache.query.TypeMismatchException;
import org.apache.geode.cache.query.internal.parse.GemFireAST;
import org.apache.geode.cache.query.internal.parse.OQLLexer;
import org.apache.geode.cache.query.internal.parse.OQLLexerTokenTypes;
import org.apache.geode.cache.query.internal.parse.OQLParser;
import org.apache.geode.cache.query.internal.types.CollectionTypeImpl;
import org.apache.geode.cache.query.internal.types.MapTypeImpl;
import org.apache.geode.cache.query.internal.types.ObjectTypeImpl;
import org.apache.geode.cache.query.internal.types.TypeUtils;
import org.apache.geode.cache.query.types.CollectionType;
import org.apache.geode.cache.query.types.MapType;
import org.apache.geode.cache.query.types.ObjectType;
import org.apache.geode.internal.Assert;
import org.apache.geode.internal.InternalDataSerializer;
import org.apache.geode.internal.i18n.LocalizedStrings;
import org.apache.geode.internal.logging.LogService;

/**
 * Class Description
 *
 * @version $Revision: 1.1 $s
 */
public class QCompiler implements OQLLexerTokenTypes {
  private static final Logger logger = LogService.getLogger();

  private Stack stack = new Stack();
  private Map imports = new HashMap();
  private final boolean isForIndexCompilation;
  private boolean traceOn;

  public QCompiler() {
    this.isForIndexCompilation = false;
  }

  public QCompiler(boolean isForIndexCompilation) {
    this.isForIndexCompilation = isForIndexCompilation;
  }

  /*
   * compile the string into a Query (returns the root CompiledValue)
   */
  public CompiledValue compileQuery(String oqlSource) {
    try {
      OQLLexer lexer = new OQLLexer(new StringReader(oqlSource));
      OQLParser parser = new OQLParser(lexer);
      // by default use Unsupported AST class, overridden for supported
      // operators in the grammer proper
      parser.setASTNodeClass("org.apache.geode.cache.query.internal.parse.ASTUnsupported");
      parser.queryProgram();
      GemFireAST n = (GemFireAST) parser.getAST();
      n.compile(this);
    } catch (Exception ex) { // This is to make sure that we are wrapping any antlr exception with
                             // GemFire Exception.
      throw new QueryInvalidException(
          LocalizedStrings.QCompiler_SYNTAX_ERROR_IN_QUERY_0.toLocalizedString(ex.getMessage()),
          ex);
    }
    Assert.assertTrue(stackSize() == 1, "stack size = " + stackSize());
    return (CompiledValue) pop();
  }

  /** Returns List<CompiledIteratorDef> */
  public List compileFromClause(String fromClause) {
    try {
      OQLLexer lexer = new OQLLexer(new StringReader(fromClause));
      OQLParser parser = new OQLParser(lexer);
      // by default use Unsupported AST class, overridden for supported
      // operators in the grammer proper
      parser.setASTNodeClass("org.apache.geode.cache.query.internal.parse.ASTUnsupported");
      parser.loneFromClause();
      GemFireAST n = (GemFireAST) parser.getAST();
      n.compile(this);
    } catch (Exception ex) { // This is to make sure that we are wrapping any antlr exception with
                             // GemFire Exception.
      throw new QueryInvalidException(
          LocalizedStrings.QCompiler_SYNTAX_ERROR_IN_QUERY_0.toLocalizedString(ex.getMessage()),
          ex);
    }
    Assert.assertTrue(stackSize() == 1, "stack size = " + stackSize());
    return (List) pop();
  }


  /** Returns List<CompiledIteratorDef> or null if projectionAttrs is '*' */
  public List compileProjectionAttributes(String projectionAttributes) {
    try {
      OQLLexer lexer = new OQLLexer(new StringReader(projectionAttributes));
      OQLParser parser = new OQLParser(lexer);
      // by default use Unsupported AST class, overridden for supported
      // operators in the grammer proper
      parser.setASTNodeClass("org.apache.geode.cache.query.internal.parse.ASTUnsupported");
      parser.loneProjectionAttributes();
      GemFireAST n = (GemFireAST) parser.getAST();
      // don't compile TOK_STAR
      if (n.getType() == TOK_STAR) {
        return null;
      }
      n.compile(this);
    } catch (Exception ex) { // This is to make sure that we are wrapping any antlr exception with
                             // GemFire Exception.
      throw new QueryInvalidException(
          LocalizedStrings.QCompiler_SYNTAX_ERROR_IN_QUERY_0.toLocalizedString(ex.getMessage()),
          ex);
    }
    Assert.assertTrue(stackSize() == 1, "stack size = " + stackSize() + ";stack=" + this.stack);
    return (List) pop();
  }

  /**
   * Yogesh: compiles order by clause and push into the stack
   *
   * @param numOfChildren
   */
  public void compileOrederByClause(int numOfChildren) {
    List list = new ArrayList();
    for (int i = 0; i < numOfChildren; i++) {
      CompiledSortCriterion csc = (CompiledSortCriterion) this.stack.pop();
      list.add(0, csc);
    }
    push(list);
  }

  public void compileGroupByClause(int numOfChildren) {
    List list = new ArrayList();
    for (int i = 0; i < numOfChildren; i++) {
      Object csc = this.stack.pop();
      list.add(0, csc);
    }
    push(list);
  }

  /**
   * Yogesh: compiles sort criteria present in order by clause and push into the stack
   *
   * @param sortCriterion
   */
  public void compileSortCriteria(String sortCriterion) {

    CompiledValue obj = (CompiledValue) this.stack.pop();
    boolean criterion = false;
    if (sortCriterion.equals("desc"))
      criterion = true;
    CompiledSortCriterion csc = new CompiledSortCriterion(criterion, obj);
    push(csc);

  }

  public void compileLimit(String limitNum) {
    push(Integer.valueOf(limitNum));
  }

  /**
   * Processes import statements only. This compiler instance remembers the imports and can be used
   * to compile other strings with this context info
   */
  public void compileImports(String imports) {
    try {
      OQLLexer lexer = new OQLLexer(new StringReader(imports));
      OQLParser parser = new OQLParser(lexer);
      // by default use Unsupported AST class, overridden for supported
      // operators in the grammer proper
      parser.setASTNodeClass("org.apache.geode.cache.query.internal.parse.ASTUnsupported");
      parser.loneImports();
      GemFireAST n = (GemFireAST) parser.getAST();
      n.compile(this);
    } catch (Exception ex) { // This is to make sure that we are wrapping any antlr exception with
                             // GemFire Exception.
      throw new QueryInvalidException(
          LocalizedStrings.QCompiler_SYNTAX_ERROR_IN_QUERY_0.toLocalizedString(ex.getMessage()),
          ex);
    }
    Assert.assertTrue(stackSize() == 0, "stack size = " + stackSize() + ";stack=" + this.stack);
  }

  public void select(Map<Integer, Object> queryComponents) {

    CompiledValue limit = null;
    Object limitObject = queryComponents.remove(OQLLexerTokenTypes.LIMIT);
    if (limitObject instanceof Integer) {
      limit = new CompiledLiteral(limitObject);
    } else {
      limit = (CompiledBindArgument) limitObject;
    }
    List<CompiledSortCriterion> orderByAttrs =
        (List<CompiledSortCriterion>) queryComponents.remove(OQLLexerTokenTypes.LITERAL_order);

    List iterators = (List) queryComponents.remove(OQLLexerTokenTypes.LITERAL_from);
    List projAttrs = (List) queryComponents.remove(OQLLexerTokenTypes.PROJECTION_ATTRS);
    if (projAttrs == null) {
      // remove any * or all attribute
      queryComponents.remove(OQLLexerTokenTypes.TOK_STAR);
      queryComponents.remove(OQLLexerTokenTypes.LITERAL_all);
    }
    // "COUNT" or null
    /*
     * String aggrExpr = (String) queryComponents .remove(OQLLexerTokenTypes.LITERAL_count);
     */

    // "DISTINCT" or null
    String distinct = (String) queryComponents.remove(OQLLexerTokenTypes.LITERAL_distinct);
    List<String> hints = null;
    Object hintObject = queryComponents.remove(OQLLexerTokenTypes.LITERAL_hint);
    if (hintObject != null) {
      hints = (List<String>) hintObject;
    }

    List<CompiledValue> groupByClause =
        (List<CompiledValue>) queryComponents.remove(OQLLexerTokenTypes.LITERAL_group);

    // whatever remains , treat it as where
    // whereClause
    CompiledValue where = null;

    if (queryComponents.size() == 1) {
      where = (CompiledValue) queryComponents.values().iterator().next();
    } else if (queryComponents.size() > 1) {
      throw new QueryInvalidException("Unexpected/unsupported query clauses found");
    }
    LinkedHashMap<Integer, CompiledAggregateFunction> aggMap =
        identifyAggregateExpressions(projAttrs);
    boolean isCountOnly = checkForCountOnly(aggMap, projAttrs, groupByClause);
    if (isCountOnly) {
      projAttrs = null;
    }
    CompiledSelect select = createSelect(distinct != null, isCountOnly, where, iterators, projAttrs,
        orderByAttrs, limit, hints, groupByClause, aggMap);
    push(select);
  }

  private boolean checkForCountOnly(Map<Integer, CompiledAggregateFunction> aggregateMap,
      List projAttribs, List<CompiledValue> groupBy) {
    if (aggregateMap != null && aggregateMap.size() == 1 && projAttribs.size() == 1
        && groupBy == null) {
      for (Map.Entry<Integer, CompiledAggregateFunction> entry : aggregateMap.entrySet()) {
        CompiledAggregateFunction caf = entry.getValue();
        if (caf.getFunctionType() == OQLLexerTokenTypes.COUNT && caf.getParameter() == null) {
          return true;
        }
      }
    }
    return false;
  }

  private CompiledSelect createSelect(boolean isDistinct, boolean isCountOnly, CompiledValue where,
      List iterators, List projAttrs, List<CompiledSortCriterion> orderByAttrs, CompiledValue limit,
      List<String> hints, List<CompiledValue> groupByClause,
      LinkedHashMap<Integer, CompiledAggregateFunction> aggMap) {
    if (isCountOnly || (groupByClause == null && aggMap == null)
        || (aggMap == null && orderByAttrs == null)) {
      return new CompiledSelect(isDistinct, isCountOnly, where, iterators, projAttrs, orderByAttrs,
          limit, hints, groupByClause);
    } else {
      return new CompiledGroupBySelect(isDistinct, isCountOnly, where, iterators, projAttrs,
          orderByAttrs, limit, hints, groupByClause, aggMap);
    }
  }

  private LinkedHashMap<Integer, CompiledAggregateFunction> identifyAggregateExpressions(
      List projAttribs) {
    if (projAttribs != null) {
      LinkedHashMap<Integer, CompiledAggregateFunction> mapping =
          new LinkedHashMap<Integer, CompiledAggregateFunction>();
      int index = 0;
      for (Object o : projAttribs) {
        CompiledValue proj = (CompiledValue) ((Object[]) o)[1];
        if (proj.getType() == OQLLexerTokenTypes.AGG_FUNC) {
          mapping.put(index, (CompiledAggregateFunction) proj);
        }
        ++index;
      }
      return mapping.size() == 0 ? null : mapping;
    } else {
      return null;
    }

  }

  public void projection() {
    // find an id or null on the stack, then an expr CompiledValue
    // push an Object[2] on the stack. First element is id, second is CompiledValue
    CompiledID id = (CompiledID) pop();
    CompiledValue expr = (CompiledValue) pop();
    push(new Object[] {id == null ? null : id.getId(), expr});
  }

  public void aggregateFunction(CompiledValue expr, int aggFuncType, boolean distinctOnly) {
    push(new CompiledAggregateFunction(expr, aggFuncType, distinctOnly));
  }

  public void iteratorDef() {
    // find type id and colln on the stack

    ObjectType type = assembleType(); // can be null
    CompiledID id = (CompiledID) TypeUtils.checkCast(pop(), CompiledID.class); // can be null
    CompiledValue colln = (CompiledValue) TypeUtils.checkCast(pop(), CompiledValue.class);

    if (type == null) {
      type = TypeUtils.OBJECT_TYPE;
    }

    push(new CompiledIteratorDef(id == null ? null : id.getId(), type, colln));
  }


  public void undefinedExpr(boolean is_defined) {
    CompiledValue value = (CompiledValue) pop();
    push(new CompiledUndefined(value, is_defined));
  }

  public void function(int function, int numOfChildren) {
    CompiledValue[] cvArr = new CompiledValue[numOfChildren];
    for (int i = numOfChildren - 1; i >= 0; i--) {
      cvArr[i] = (CompiledValue) pop();
    }
    push(new CompiledFunction(cvArr, function));
  }

  public void inExpr() {
    CompiledValue collnExpr = (CompiledValue) TypeUtils.checkCast(pop(), CompiledValue.class);
    CompiledValue elm = (CompiledValue) TypeUtils.checkCast(pop(), CompiledValue.class);
    push(new CompiledIn(elm, collnExpr));
  }

  public void constructObject(Class clazz) {
    // find argList on stack
    // only support SET for now
    Assert.assertTrue(clazz == ResultsSet.class);
    List argList = (List) TypeUtils.checkCast(pop(), List.class);
    push(new CompiledConstruction(clazz, argList));
  }

  public void pushId(String id) {
    push(new CompiledID(id));
  }

  public void pushRegion(String regionPath) {
    push(new CompiledRegion(regionPath));
  }

  public void appendPathComponent(String id) {
    CompiledValue rcvr = (CompiledValue) pop();
    push(new CompiledPath(rcvr, id));
  }


  public void pushBindArgument(int i) {
    push(new CompiledBindArgument(i));
  }


  public void pushLiteral(Object obj) {
    push(new CompiledLiteral(obj));
  }

  public void pushNull() // used as a placeholder for a missing clause
  {
    push(null);
  }


  public void combine(int num) {
    List list = new ArrayList();
    for (int i = 0; i < num; i++) {
      list.add(0, pop());
    }
    push(list);
  }


  public void methodInvocation() {
    // find on stack:
    // argList, methodName, receiver (which may be null if receiver is implicit)
    List argList = (List) TypeUtils.checkCast(pop(), List.class);
    CompiledID methodName = (CompiledID) TypeUtils.checkCast(pop(), CompiledID.class);
    CompiledValue rcvr = (CompiledValue) TypeUtils.checkCast(pop(), CompiledValue.class);
    push(new CompiledOperation(rcvr, methodName.getId(), argList));
  }

  public void indexOp() {
    // find the List of index expressions and the receiver on the stack
    Object indexParams = pop();
    final CompiledValue rcvr = (CompiledValue) TypeUtils.checkCast(pop(), CompiledValue.class);
    CompiledValue indexExpr = CompiledValue.MAP_INDEX_ALL_KEYS;

    if (indexParams != null) {
      final List indexList = (List) TypeUtils.checkCast(indexParams, List.class);
      if (!isForIndexCompilation && indexList.size() != 1) {
        throw new UnsupportedOperationException(
            LocalizedStrings.QCompiler_ONLY_ONE_INDEX_EXPRESSION_SUPPORTED.toLocalizedString());
      }
      if (indexList.size() == 1) {
        indexExpr = (CompiledValue) TypeUtils.checkCast(indexList.get(0), CompiledValue.class);

        if (indexExpr.getType() == TOK_COLON) {
          throw new UnsupportedOperationException(
              LocalizedStrings.QCompiler_RANGES_NOT_SUPPORTED_IN_INDEX_OPERATORS
                  .toLocalizedString());
        }
        indexExpr = (CompiledValue) TypeUtils.checkCast(indexList.get(0), CompiledValue.class);
        push(new CompiledIndexOperation(rcvr, indexExpr));
      } else {
        assert this.isForIndexCompilation;

        MapIndexable mi = new MapIndexOperation(rcvr, indexList);
        push(mi);
      }
    } else {
      if (!this.isForIndexCompilation) {
        throw new QueryInvalidException(LocalizedStrings.QCompiler_SYNTAX_ERROR_IN_QUERY_0
            .toLocalizedString("* use incorrect"));
      }
      push(new CompiledIndexOperation(rcvr, indexExpr));
    }

  }



  /**
   * Creates appropriate CompiledValue for the like predicate based on the sargability of the String
   * or otherwise. It also works on the last character to see if the sargable like predicate results
   * in a CompiledJunction or a Comparison. Currently we are supporting only the '%' terminated
   * "like" predicate.
   *
   * @param var The CompiledValue representing the variable
   * @param patternOrBindParam The CompiledLiteral reprsenting the pattern of the like predicate
   * @return CompiledValue representing the "like" predicate
   *
   */
  CompiledValue createCompiledValueForLikePredicate(CompiledValue var,
      CompiledValue patternOrBindParam) {
    if (!(patternOrBindParam.getType() == CompiledBindArgument.QUERY_PARAM)) {
      CompiledLiteral pattern = (CompiledLiteral) patternOrBindParam;
      if (pattern._obj == null) {
        throw new UnsupportedOperationException(
            "Null values are not supported with LIKE predicate.");
      }
    }
    // From 6.6 Like is enhanced to support special character (% and _) at any
    // position of the string.
    return new CompiledLike(var, patternOrBindParam);
  }


  public void like() {
    CompiledValue v2 = (CompiledValue) pop();
    CompiledValue v1 = (CompiledValue) pop();
    CompiledValue cv = createCompiledValueForLikePredicate(v1, v2);
    push(cv);
  }



  public void compare(int opKind) {
    CompiledValue v2 = (CompiledValue) pop();
    CompiledValue v1 = (CompiledValue) pop();
    push(new CompiledComparison(v1, v2, opKind));
  }

  public void or(int numTerms) {
    junction(numTerms, LITERAL_or);
  }

  public void and(int numTerms) {
    junction(numTerms, LITERAL_and);
  }

  private void junction(int numTerms, int operator) {
    /*
     * if any of the operands are junctions with same operator as this one then flatten
     */
    List operands = new ArrayList(numTerms);
    for (int i = 0; i < numTerms; i++) {
      CompiledValue operand = (CompiledValue) pop();
      // flatten if we can
      if (operand instanceof CompiledJunction
          && ((CompiledJunction) operand).getOperator() == operator) {
        CompiledJunction junction = (CompiledJunction) operand;
        List jOperands = junction.getOperands();
        for (int j = 0; j < jOperands.size(); j++)
          operands.add(jOperands.get(j));
      } else
        operands.add(operand);
    }

    push(new CompiledJunction(
        (CompiledValue[]) operands.toArray(new CompiledValue[operands.size()]), operator));
  }



  public void not() {
    Object obj = this.stack.peek();
    Assert.assertTrue(obj instanceof CompiledValue);

    if (obj instanceof Negatable)
      ((Negatable) obj).negate();
    else
      push(new CompiledNegation((CompiledValue) pop()));
  }

  public void unaryMinus() {
    Object obj = this.stack.peek();
    Assert.assertTrue(obj instanceof CompiledValue);
    push(new CompiledUnaryMinus((CompiledValue) pop()));

  }

  public void typecast() {
    // pop expr and type, apply type, then push result
    AbstractCompiledValue cmpVal =
        (AbstractCompiledValue) TypeUtils.checkCast(pop(), AbstractCompiledValue.class);
    ObjectType objType = assembleType();
    cmpVal.setTypecast(objType);
    push(cmpVal);
  }


  // returns null if null is on the stack
  public ObjectType assembleType() {
    ObjectType objType = (ObjectType) TypeUtils.checkCast(pop(), ObjectType.class);
    if (objType instanceof CollectionType) {
      // pop the elementType
      ObjectType elementType = assembleType();

      if (objType instanceof MapType) {
        // pop the key type
        ObjectType keyType = assembleType();
        return new MapTypeImpl(objType.resolveClass(), keyType, elementType);
      }
      return new CollectionTypeImpl(objType.resolveClass(), elementType);
    }
    return objType;
  }

  public void traceRequest() {
    this.traceOn = true;
  }

  public boolean isTraceRequested() {
    return traceOn;
  }

  public void setHint(int numOfChildren) {
    ArrayList list = new ArrayList();
    for (int i = 0; i < numOfChildren; i++) {
      String hi = (String) this.stack.pop();
      list.add(0, hi);
    }
    push(list);
    // setHints(list);
  }

  public void setHintIdentifier(String text) {
    push(text);
  }

  public void importName(String qualifiedName, String asName) {
    if (asName == null) {
      // if no AS, then use the short name from qualifiedName
      // as the AS
      int idx = qualifiedName.lastIndexOf('.');
      if (idx >= 0) {
        asName = qualifiedName.substring(idx + 1);
      } else {
        asName = qualifiedName;
      }
    }
    if (logger.isTraceEnabled()) {
      logger.trace("QCompiler.importName: {},{}", asName, qualifiedName);
    }
    this.imports.put(asName, qualifiedName);
  }


  public Object pop() {
    Object obj = this.stack.pop();
    if (logger.isTraceEnabled()) {
      logger.trace("QCompiler.pop: {}", obj);
    }
    return obj;
  }

  public void push(Object obj) {
    if (logger.isTraceEnabled()) {
      logger.trace("QCompiler.push: {}", obj);
    }
    this.stack.push(obj);
  }

  public int stackSize() {
    return this.stack.size();
  }

  public ObjectType resolveType(String typeName) {
    if (typeName == null) {
      if (logger.isTraceEnabled()) {
        logger.trace("QCompiler.resolveType= {}", Object.class.getName());
      }
      return TypeUtils.OBJECT_TYPE;
    }
    // resolve with imports
    String as = null;
    if (this.imports != null) {
      as = (String) this.imports.get(typeName);
    }
    if (as != null)
      typeName = as;

    Class resultClass;
    try {
      resultClass = InternalDataSerializer.getCachedClass(typeName);
    } catch (ClassNotFoundException e) {
      throw new QueryInvalidException(
          LocalizedStrings.QCompiler_TYPE_NOT_FOUND_0.toLocalizedString(typeName), e);
    }
    if (logger.isTraceEnabled()) {
      logger.trace("QCompiler.resolveType= {}", resultClass.getName());
    }
    return new ObjectTypeImpl(resultClass);
  }

  private static class MapIndexOperation extends AbstractCompiledValue implements MapIndexable {
    private final CompiledValue rcvr;
    private final List<CompiledValue> indexList;

    public MapIndexOperation(CompiledValue rcvr, List<CompiledValue> indexList) {
      this.rcvr = rcvr;
      this.indexList = indexList;
    }

    public CompiledValue getRecieverSansIndexArgs() {
      return rcvr;
    }

    public CompiledValue getMapLookupKey() {
      throw new UnsupportedOperationException("Function invocation not expected");
    }

    public List<CompiledValue> getIndexingKeys() {
      return (List<CompiledValue>) indexList;
    }

    public Object evaluate(ExecutionContext context) throws FunctionDomainException,
        TypeMismatchException, NameResolutionException, QueryInvocationTargetException {
      throw new UnsupportedOperationException("Method execution not expected");
    }

    public int getType() {
      throw new UnsupportedOperationException("Method execution not expected");
    }
  }

}
