/*
 * 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.lang.reflect.Method;
import java.util.Collections;
import java.util.Set;

import org.apache.geode.annotations.Immutable;
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.SelectResults;
import org.apache.geode.cache.query.TypeMismatchException;
import org.apache.geode.cache.query.internal.index.IndexCreationHelper;
import org.apache.geode.cache.query.internal.types.TypeUtils;
import org.apache.geode.cache.query.types.ObjectType;
import org.apache.geode.cache.query.types.StructType;

/**
 * Value representing a current iteration element. This is the representation used during
 * evaluation.
 *
 * A RuntimeIterator can be in one of two states. If it is independent of the current context scope
 * then its collection is evaluated lazily, in which case collection is initialized and knows its
 * elementType. The elementType field is the same value as in the collection. Otherwise, collection
 * is UNINITIALIZED and the elementType is set in any case.
 *
 * A RuntimeIterator can also be named or anonymous (name is null).
 *
 */
public class RuntimeIterator extends AbstractCompiledValue {

  // token to differentiate null from uninitialized
  @Immutable
  private static final SelectResults UNINITIALIZED = new ResultsBag(0, null);
  private Object current = UNINITIALIZED;
  private final String name;
  private SelectResults collection = UNINITIALIZED;
  private final CompiledIteratorDef cmpIteratorDefn;
  /** may be more specific than that in cmpIteratorDefn */
  private ObjectType elementType;
  /** for canonicalization */
  private String internalId = null;
  private String definition = null;
  private String index_internal_id = null;
  private int scopeID = -1;

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

  public ObjectType getElementType() {
    return elementType;
  }

  RuntimeIterator(CompiledIteratorDef cmpIteratorDefn, ObjectType elementType) {
    if (elementType == null || cmpIteratorDefn == null) {
      throw new IllegalArgumentException(
          "elementType and/or cmpIteratorDefn should not be null");
    }
    name = cmpIteratorDefn.getName();
    this.elementType = elementType;
    this.cmpIteratorDefn = cmpIteratorDefn;
  }

  CompiledIteratorDef getCmpIteratorDefn() {
    return cmpIteratorDefn;
  }

  public String getName() {
    return name;
  }

  /**
   * (Re)evaluate in the context of the current iterations through the cross-product. If this
   * iterator is not dependent on the current iteration, then just return the previously evaluated
   * collection. Otherwise, re-evaluate. Returns null if the collection itself is null or UNDEFINED
   */
  public SelectResults evaluateCollection(ExecutionContext context) throws FunctionDomainException,
      TypeMismatchException, NameResolutionException, QueryInvocationTargetException {
    if (collection != UNINITIALIZED
        && !cmpIteratorDefn.isDependentOnAnyIteratorOfScopeLessThanItsOwn(context)
        && scopeID != IndexCreationHelper.INDEX_QUERY_SCOPE_ID) {
      return collection;
    }
    // limit the scope for evaluation to this RuntimeIterator:
    // we don't want to use this iterator or subsequent ones in the from clause
    // to evaluate this collection.
    collection = cmpIteratorDefn.evaluateCollection(context, this);
    if (collection == null) {
      return null;
    }
    // if we already have a more specific elementType, set it in the collection
    if (!elementType.equals(TypeUtils.OBJECT_TYPE)) {
      collection.setElementType(elementType);
    } else {
      // The elementType in the Collection obtained is more specific . So use that type.
      elementType = collection.getCollectionType().getElementType();
    }
    return collection;
  }

  @Override
  public Set computeDependencies(ExecutionContext context) {
    // called as the receiver of Path or Operation
    return Collections.singleton(this);
  }

  @Override
  public boolean isDependentOnIterator(RuntimeIterator itr, ExecutionContext context) {
    if (itr == this) {
      return true; // never true(?)
    }
    return cmpIteratorDefn.isDependentOnIterator(itr, context);
  }

  @Override
  public boolean isDependentOnCurrentScope(ExecutionContext context) {
    return cmpIteratorDefn.isDependentOnCurrentScope(context);
  }

  public void setCurrent(Object current) {
    this.current = current;
  }

  @Override
  public Object evaluate(ExecutionContext context) {
    Support.Assert(current != UNINITIALIZED,
        "error to evaluate RuntimeIterator without setting current first");
    return current;
  }

  boolean containsProperty(ExecutionContext context, String name, int numArgs,
      boolean mustBeMethod) {
    // first handle structs
    if ((elementType instanceof StructType) && !mustBeMethod) {
      // check field names
      String[] fieldName = ((StructType) elementType).getFieldNames();
      for (String s : fieldName) {
        if (name.equals(s)) {
          return true;
        }
      }
    }
    Class<?> clazz = elementType.resolveClass();
    if (numArgs > 0 || mustBeMethod) {
      // if numArgs==0, then just look up method directly
      // instead of sifting through all methods
      if (numArgs == 0) {
        try {
          clazz.getMethod(name, (Class[]) null);
          return true;
        } catch (NoSuchMethodException e) {
          return false;
        }
      }
      // enumerate methods and match with name
      // here, only check for a method with the same number of arguments.
      // we'll check for ambiguous method invocation when the method is
      // actually fully resolved and invoked
      Method[] methods = clazz.getMethods();
      for (Method m : methods) {
        if (m.getName().equals(name) && m.getParameterTypes().length == numArgs) {
          return true;
        }
      }
      return false;
    }
    // if there are zero arguments and it's an attribute, then defer to
    // AttributeDescriptor to see if there's a match
    return new AttributeDescriptor(context.getCache().getPdxRegistry(), name)
        .validateReadType(clazz);
  }

  @Override
  public String toString() {
    return getClass().getName() + " (name=" + name + " collection expr=" + cmpIteratorDefn + ")";
  }

  // Canonicalization
  void setInternalId(String id) {
    // it's okay for this to be set more than once; a RuntimeIterator
    // can be bound to a scope to compute dependencies, then
    // re-bound to a different scope later at eval time.
    // Support.Assert((internalId == null), "Internal ID is already set");
    Support.Assert((id != null), "Internal ID can not be null");
    internalId = id;
  }

  public String getInternalId() {
    // Support.Assert((internalId != null), "Internal ID is not yet set");
    return internalId;
  }

  public void setDefinition(String def) {
    Support.Assert((definition == null), "Definition is already set");
    Support.Assert((def != null), "Definition can not be null");
    definition = def;
  }

  public void setIndexInternalID(String index_id) {
    index_internal_id = index_id;
  }

  public String getIndexInternalID() {
    return index_internal_id;
  }

  public String getDefinition() {
    Support.Assert((definition != null), "Definition is not yet set");
    return definition;
  }

  @Override
  public void generateCanonicalizedExpression(StringBuilder clauseBuffer,
      ExecutionContext context) {
    // prepend the internal iterator variable name for this RunTimeIterator
    int currScopeID = context.currentScope().getScopeID();
    if (currScopeID == scopeID) {
      // should have been set at this point
      clauseBuffer.insert(0, index_internal_id == null ? internalId : index_internal_id);
    } else {
      clauseBuffer.insert(0, internalId).insert(0, '_').insert(0, scopeID).insert(0, "scope");
    }
  }

  void setScopeID(int scopeID) {
    this.scopeID = scopeID;
  }

  int getScopeID() {
    return scopeID;
  }
}
