/*
 * 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.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import org.apache.geode.annotations.internal.MakeNotStatic;
import org.apache.geode.cache.EntryDestroyedException;
import org.apache.geode.cache.Region;
import org.apache.geode.cache.query.AmbiguousNameException;
import org.apache.geode.cache.query.FunctionDomainException;
import org.apache.geode.cache.query.NameResolutionException;
import org.apache.geode.cache.query.QueryInvocationTargetException;
import org.apache.geode.cache.query.QueryService;
import org.apache.geode.cache.query.TypeMismatchException;
import org.apache.geode.internal.InternalDataSerializer;
import org.apache.geode.internal.cache.PartitionedRegion;
import org.apache.geode.pdx.PdxInstance;
import org.apache.geode.pdx.PdxSerializationException;
import org.apache.geode.pdx.internal.InternalPdxInstance;
import org.apache.geode.pdx.internal.PdxString;

/**
 * Class Description
 *
 * @version $Revision: 1.1 $
 */


public class CompiledOperation extends AbstractCompiledValue {
  private final CompiledValue receiver; // may be null if implicit to scope
  private final String methodName;
  private final List args;
  @MakeNotStatic
  private static final ConcurrentMap cache = new ConcurrentHashMap();


  // receiver is an ID or PATH that contains the operation name
  public CompiledOperation(CompiledValue receiver, String methodName, List args) {
    this.receiver = receiver;
    this.methodName = methodName;
    this.args = args;
  }

  @Override
  public List getChildren() {
    List list = new ArrayList();
    if (this.receiver != null) {
      list.add(this.receiver);
    }
    list.addAll(this.args);
    return list;
  }

  public String getMethodName() {
    return this.methodName;
  }

  public List getArguments() {
    return this.args;
  }


  @Override
  public int getType() {
    return METHOD_INV;
  }

  public CompiledValue getReceiver(ExecutionContext cxt) {
    // receiver may be cached in execution context
    if (this.receiver == null && cxt != null) {
      return (CompiledValue) cxt.cacheGet(this);
    }
    return this.receiver;
  }

  @Override
  public boolean hasIdentifierAtLeafNode() {
    if (this.receiver.getType() == Identifier) {
      return true;
    } else {
      return this.receiver.hasIdentifierAtLeafNode();
    }
  }

  @Override
  public CompiledValue getReceiver() {
    return this.getReceiver(null);
  }

  @Override
  public Object evaluate(ExecutionContext context) throws FunctionDomainException,
      TypeMismatchException, NameResolutionException, QueryInvocationTargetException {
    CompiledValue rcvr = getReceiver(context);

    Object result;
    Object evalRcvr;

    if (rcvr == null) { // must be intended as implicit iterator operation
      // see if it's an implicit operation name
      RuntimeIterator rcvrItr =
          context.resolveImplicitOperationName(this.methodName, this.args.size(), true);
      evalRcvr = rcvrItr.evaluate(context);
      /*
       * // evaluate on current iteration of collection if (rcvrItr != null) { result =
       * eval0(rcvrItr.evaluate(context), rcvrItr.getElementType().resolveClass(), context); }
       *
       * // function call: no functions implemented except keywords in the grammar throw new
       * TypeMismatchException("Could not resolve method named 'xyz'")
       */
    } else {
      // if not null, then explicit receiver
      evalRcvr = rcvr.evaluate(context);
    }

    // short circuit null immediately
    if (evalRcvr == null) {
      return QueryService.UNDEFINED;
    }

    if (context.isCqQueryContext() && evalRcvr instanceof Region.Entry) {
      Region.Entry re = (Region.Entry) evalRcvr;
      if (re.isDestroyed()) {
        return QueryService.UNDEFINED;
      }
      try {
        evalRcvr = re.getValue();
      } catch (EntryDestroyedException ede) {
        // Even though isDestory() check is made, the entry could
        // throw EntryDestroyedException if the value becomes null.
        return QueryService.UNDEFINED;
      }
    }

    // check if the receiver is the iterator, in which
    // case we resolve the method on the constraint rather
    // than the runtime type of the receiver
    Class resolveClass = null;
    // commented out because we currently always resolve the method
    // on the runtime types

    // CompiledValue rcvrVal = rcvrPath.getReceiver();
    // if (rcvrVal.getType() == ID)
    // {
    // CompiledValue resolvedID = context.resolve(((CompiledID)rcvrVal).getId());
    // if (resolvedID.getType() == ITERATOR)
    // {
    // resolveClass = ((RuntimeIterator)resolvedID).getBaseCollection().getConstraint();
    // }
    // }
    // if (resolveClass == null)
    if (evalRcvr instanceof PdxInstance) {
      String className = ((PdxInstance) evalRcvr).getClassName();
      try {
        resolveClass = InternalDataSerializer.getCachedClass(className);
      } catch (ClassNotFoundException cnfe) {
        throw new QueryInvocationTargetException(cnfe);
      }
    } else if (evalRcvr instanceof PdxString) {
      resolveClass = String.class;
    } else {
      resolveClass = evalRcvr.getClass();
    }

    result = eval0(evalRcvr, resolveClass, context);
    // }
    // check for PR substitution
    // check for BucketRegion substitution
    PartitionedRegion pr = context.getPartitionedRegion();
    if (pr != null && (result instanceof Region)) {
      if (pr.getFullPath().equals(((Region) result).getFullPath())) {
        result = context.getBucketRegion();
      }
    }
    return result;
  }



  @Override
  public Set computeDependencies(ExecutionContext context)
      throws TypeMismatchException, AmbiguousNameException, NameResolutionException {
    List args = this.args;
    Iterator i = args.iterator();
    while (i.hasNext()) {
      context.addDependencies(this, ((CompiledValue) i.next()).computeDependencies(context));
    }

    CompiledValue rcvr = getReceiver(context);
    if (rcvr == null) // implicit iterator operation
    {
      // see if it's an implicit operation name
      RuntimeIterator rcvrItr =
          context.resolveImplicitOperationName(this.methodName, this.args.size(), true);
      if (rcvrItr == null) { // no receiver resolved
        // function call: no functions implemented except keywords in the grammar
        throw new TypeMismatchException(
            String.format("Could not resolve method named ' %s '",
                this.methodName));
      }
      // cache the receiver so we don't have to resolve it again
      context.cachePut(this, rcvrItr);
      return context.addDependency(this, rcvrItr);
    }

    // receiver is explicit
    return context.addDependencies(this, rcvr.computeDependencies(context));
  }

  @edu.umd.cs.findbugs.annotations.SuppressWarnings(
      value = "RV_RETURN_VALUE_OF_PUTIFABSENT_IGNORED",
      justification = "Does not matter if the methodDispatch that isn't stored in the map is used")
  private Object eval0(Object receiver, Class resolutionType, ExecutionContext context)
      throws TypeMismatchException, FunctionDomainException, NameResolutionException,
      QueryInvocationTargetException {
    if (receiver == null || receiver == QueryService.UNDEFINED)
      return QueryService.UNDEFINED;

    List args = new ArrayList();
    List argTypes = new ArrayList();
    Iterator i = this.args.iterator();
    while (i.hasNext()) {
      CompiledValue arg = (CompiledValue) i.next();
      Object o = arg.evaluate(context);

      // undefined arg produces undefines method result
      if (o == QueryService.UNDEFINED)
        return QueryService.UNDEFINED;

      args.add(o);
      // pass in null for the type if the runtime value is null
      if (o == null)
        argTypes.add(null);
      // commented out because we currently always use the runtime type for args
      // else if (arg.getType() == Identifier)
      // {
      // CompiledValue resolved = context.resolve(((CompiledID)arg).getId());
      // if (resolved != null && resolved.getType() == ITERATOR)
      // argTypes.add(((RuntimeIterator)resolved).getBaseCollection().getConstraint());
      // else
      // argTypes.add(o.getClass());
      // }
      else
        argTypes.add(o.getClass()); // otherwise use the runtime type
    }

    // see if in cache
    MethodDispatch methodDispatch;
    List key = Arrays.asList(new Object[] {resolutionType, this.methodName, argTypes});
    methodDispatch = (MethodDispatch) CompiledOperation.cache.get(key);
    if (methodDispatch == null) {
      try {
        methodDispatch =
            new MethodDispatch(context.getCache().getQueryService().getMethodInvocationAuthorizer(),
                resolutionType, this.methodName, argTypes);
      } catch (NameResolutionException nre) {
        if (!org.apache.geode.cache.query.Struct.class.isAssignableFrom(resolutionType)
            && (DefaultQueryService.QUERY_HETEROGENEOUS_OBJECTS
                || DefaultQueryService.TEST_QUERY_HETEROGENEOUS_OBJECTS)) {
          return QueryService.UNDEFINED;
        } else {
          throw nre;
        }
      }
      // cache
      CompiledOperation.cache.putIfAbsent(key, methodDispatch);
    }
    if (receiver instanceof InternalPdxInstance) {
      try {
        receiver = ((InternalPdxInstance) receiver).getCachedObject();
      } catch (PdxSerializationException ex) {
        throw new QueryInvocationTargetException(ex);
      }
    } else if (receiver instanceof PdxString) {
      receiver = ((PdxString) receiver).toString();
    }
    return methodDispatch.invoke(receiver, args);
  }

  // Asif :Function for generating from clause
  @Override
  public void generateCanonicalizedExpression(StringBuilder clauseBuffer, ExecutionContext context)
      throws AmbiguousNameException, TypeMismatchException, NameResolutionException {
    // Asif: if the method name starts with getABC & argument list is empty
    // then canonicalize it to aBC
    int len;
    if (this.methodName.startsWith("get") && (len = this.methodName.length()) > 3
        && (this.args == null || this.args.isEmpty())) {
      clauseBuffer.insert(0, len > 4 ? this.methodName.substring(4) : "");
      clauseBuffer.insert(0, Character.toLowerCase(this.methodName.charAt(3)));
    } else if (this.args == null || this.args.isEmpty()) {
      clauseBuffer.insert(0, "()").insert(0, this.methodName);
    } else {
      // The method contains arguments which need to be canonicalized
      clauseBuffer.insert(0, ')');
      CompiledValue cv = null;
      for (int j = this.args.size(); j > 0;) {
        cv = (CompiledValue) this.args.get(--j);
        cv.generateCanonicalizedExpression(clauseBuffer, context);
        clauseBuffer.insert(0, ',');
      }
      clauseBuffer.deleteCharAt(0).insert(0, '(').insert(0, this.methodName);

    }
    clauseBuffer.insert(0, '.');
    CompiledValue rcvr = this.receiver;
    if (rcvr == null) {
      // must be intended as implicit iterator operation
      // see if it's an implicit operation name. The receiver will now be RuntimeIterator
      rcvr = context.resolveImplicitOperationName(this.methodName, this.args.size(), true);
    }
    rcvr.generateCanonicalizedExpression(clauseBuffer, context);
  }

}
