/*
 * Copyright (c) 1998-2012 Caucho Technology -- all rights reserved
 *
 * This file is part of Resin(R) Open Source
 *
 * Each copy or derived work must preserve the copyright notice and this
 * notice unmodified.
 *
 * Resin Open Source is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Resin Open Source is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, or any warranty
 * of NON-INFRINGEMENT.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Resin Open Source; if not, write to the
 *
 *   Free Software Foundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Scott Ferguson
 */

package com.caucho.amber.manager;

import com.caucho.amber.AmberException;
import com.caucho.amber.cfg.ColumnResultConfig;
import com.caucho.amber.cfg.EntityResultConfig;
import com.caucho.amber.cfg.FieldResultConfig;
import com.caucho.amber.cfg.SqlResultSetMappingConfig;
import com.caucho.amber.entity.Entity;
import com.caucho.amber.entity.EntityItem;
import com.caucho.amber.expr.AmberExpr;
import com.caucho.amber.expr.ArgExpr;
import com.caucho.amber.expr.LoadEntityExpr;
import com.caucho.amber.query.AbstractQuery;
import com.caucho.amber.query.ResultSetImpl;
import com.caucho.amber.query.AmberSelectQuery;
import com.caucho.amber.query.UserQuery;
import com.caucho.amber.type.CalendarType;
import com.caucho.amber.type.EntityType;
import com.caucho.amber.type.UtilDateType;
import com.caucho.ejb.EJBExceptionWrapper;
import com.caucho.util.L10N;

import javax.persistence.FlushModeType;
import javax.persistence.LockModeType;
import javax.persistence.NonUniqueResultException;
import javax.persistence.NoResultException;
import javax.persistence.Parameter;
import javax.persistence.Query;
import javax.persistence.TemporalType;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Types;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * The EJB query
 */
public class QueryImpl implements Query {
  private static final L10N L = new L10N(QueryImpl.class);
  private static final Logger log
    = Logger.getLogger(QueryImpl.class.getName());

  private AbstractQuery _query;
  private UserQuery _userQuery;

  private AmberConnection _aConn;
  private int _firstResult;

  private int _currIndex;

  private FlushModeType _flushMode;

  // Constructor queries.
  private Class _primitiveColumns[];

  // Native queries.
  private String _nativeSql;
  private PreparedStatement _nativeStmt;
  private SqlResultSetMappingConfig _sqlResultSetMapping;
  private int _currEntityResult;
  private int _currColumnResult;

  /**
   * Creates a manager instance.
   */
  QueryImpl(AbstractQuery query, AmberConnection aConn)
  {
    _query = query;
    _aConn = aConn;

    _userQuery = new UserQuery(query);
    _userQuery.setSession(_aConn);
  }

  /**
   * Creates a manager instance.
   */
  QueryImpl(AmberConnection aConn)
  {
    _aConn = aConn;
  }

  /**
   * Execute the query and return as a List.
   */
  public List getResultList()
  {
    ResultSet rs = null;

    try {
      // jpa/0y1b
      if (_aConn.isInTransaction())
        _aConn.flush();

      Class constructorClass = null;

      if (isSelectQuery()) {
        if (! isNativeQuery()) {
          AmberSelectQuery selectQuery = (AmberSelectQuery) _query;
          constructorClass = selectQuery.getConstructorClass();
        }
      }
      else
        throw new IllegalStateException(L.l("javax.persistence.Query.getResultList() can only be applied to a SELECT statement"));

      Constructor constructor = null;

      ArrayList results = new ArrayList();

      rs = executeQuery();

      ResultSetMetaData metaData = null;
      int columnCount = -1;

      int n = 0;

      Object row[] = null;

      ArrayList columns = new ArrayList();

      while (rs.next()) {
        Object object = null;

        _currIndex = 1;

        if (n == 0) {
          try {
            metaData = rs.getMetaData();

            if (metaData != null)
              columnCount = metaData.getColumnCount();
          } catch (Exception ex) {
            // Below, we work around if DB is not able
            // to retrieve result set meta data. jpa/0t00
            metaData = null;
          }

          if (columnCount <= 0)
            columnCount = 10000;

          for (int i = 1; i <= columnCount; i++) {

            int columnType = -1;

            try {
              columnType = metaData.getColumnType(i);
            } catch (Exception ex) {
            }

            try {
              if (isNativeQuery()) {
                ArrayList<EntityResultConfig> entityResults
                  = _sqlResultSetMapping.getEntityResults();

                if (_currEntityResult == entityResults.size()) {

                  ArrayList<ColumnResultConfig> columnResults
                    = _sqlResultSetMapping.getColumnResults();

                  if (_currColumnResult == columnResults.size())
                    break;
                }

                object = getInternalNative(rs);
              }
              else {
                object = getInternalObject(rs, columnType);
              }

              columns.add(object);

            } catch (IndexOutOfBoundsException ex1) {
              break;
            }
            // catch (Exception ex2) {
            //  break;
            // }
          }

          n = columns.size();
          row = columns.toArray();

          if (constructorClass != null) {

            _primitiveColumns = new Class[row.length];

            StringBuilder argValues = new StringBuilder();

            try {
              // jpa/11a4, jpa/11a5

              boolean isFirst = true;

              for (int i=0; i < n; i++) {
                if (isFirst)
                  isFirst = false;
                else
                  argValues.append(", ");

                argValues.append(row[i]);
              }

              Constructor ctors[] = constructorClass.getDeclaredConstructors();

              ArrayList<Constructor> validConstructors
                = new ArrayList<Constructor>();

              for (int j=0; j < ctors.length; j++) {

                Class paramTypes[] = ctors[j].getParameterTypes();

                if (paramTypes.length != row.length)
                  continue;

                boolean isValid = true;

                for (int k=0; k < paramTypes.length; k++) {
                  Class columnClass = row[k].getClass();

                  if (! paramTypes[k].isAssignableFrom(columnClass)) {

                    if (! paramTypes[k].isPrimitive()) {
                      isValid = false;
                      break;
                    }

                    Class primitiveType
                      = (Class) columnClass.getDeclaredField("TYPE").get(null);

                    if (paramTypes[k].isAssignableFrom(primitiveType)) {
                      // jpa/11a5
                      _primitiveColumns[k] = primitiveType;
                    }
                    else {
                      isValid = false;
                      break;
                    }
                  }
                }

                if (isValid)
                  validConstructors.add(ctors[j]);
              }

              constructor = validConstructors.get(0);

            } catch (Exception ex) {
              throw error(L.l("Unable to find constructor {0}. Make sure there is a public constructor for the given argument values ({1})", constructorClass.getName(), argValues));
            }
          }
        }
        else {
          row = new Object[n];

          for (int i=0; i < n; i++) {

            int columnType = -1;

            try {
              columnType = metaData.getColumnType(i + 1);
            } catch (Exception ex) {
            }

            if (isNativeQuery())
              row[i] = getInternalNative(rs);
            else
              row[i] = getInternalObject(rs, columnType);
          }
        }

        if (constructor == null) {
          if (n == 1)
            results.add(row[0]);
          else
            results.add(row);
        }
        else {
          try {
            for (int i=0; i < row.length; i++) {
              Class primitiveType = _primitiveColumns[i];

              if (primitiveType == null)
                continue;

              // jpa/11a5
              if (primitiveType == Boolean.TYPE)
                row[i] = ((Boolean) row[i]).booleanValue();
              else if (primitiveType == Byte.TYPE)
                row[i] = ((Byte) row[i]).byteValue();
              else if (primitiveType == Character.TYPE)
                row[i] = ((Character) row[i]).charValue();
              else if (primitiveType == Double.TYPE)
                row[i] = ((Double) row[i]).doubleValue();
              else if (primitiveType == Float.TYPE)
                row[i] = ((Float) row[i]).floatValue();
              else if (primitiveType == Integer.TYPE)
                row[i] = ((Integer) row[i]).intValue();
              else if (primitiveType == Long.TYPE)
                row[i] = ((Long) row[i]).longValue();
              else if (primitiveType == Short.TYPE)
                row[i] = ((Short) row[i]).shortValue();
            }

            object = constructor.newInstance(row);
          } catch (Exception ex) {

            StringBuilder argTypes = new StringBuilder();

            boolean isFirst = true;

            for (int i=0; i < row.length; i++) {

              if (isFirst)
                isFirst = false;
              else
                argTypes.append(", ");

              if (row[i] == null)
                argTypes.append("null");
              else
                argTypes.append(row[i].toString()); // .getClass().getName());
            }

            throw error(L.l("Unable to instantiate {0} with parameters ({1}).", constructorClass.getName(), argTypes));
          }

          results.add(object);
        }
      }

      if (log.isLoggable(Level.FINER)) {
        if (results != null) {
          log.log(Level.FINER, L.l("query result list size: {0}", results.size()));

          java.util.Iterator it = results.iterator();

          while (it.hasNext()) {
            Object o = it.next();

            if (o == null)
              log.log(Level.FINER, L.l("  result entry: null"));
            else
              log.log(Level.FINER, L.l("  result entry: {0}", o.getClass().getName()));
          }
        }
      }

      // jpa/0h19, jpa/1160
      if (! _aConn.isActiveTransaction())
        _aConn.detach();

      return results;
    } catch (RuntimeException e) {
      throw e;
    } catch (Exception e) {
      throw EJBExceptionWrapper.createRuntime(e);
    } finally {
      if (rs != null) {
        try {
          rs.close();
        } catch (SQLException e) {
          log.log(Level.FINE, e.toString(), e);
        }
      }
    }
  }

  /**
   * Returns a single result.
   */
  public Object getSingleResult()
  {
    ResultSet rs = null;

    try {
      if (! isSelectQuery())
        throw new IllegalStateException(L.l("javax.persistence.Query.getSingleResult() can only be applied to a SELECT statement"));

      rs = executeQuery();

      Object value = null;

      if (rs.next())
        value = rs.getObject(1);
      else // jpa/1004
        throw new NoResultException("Query returned no results for getSingleResult()");

      // jpa/1005
      if (rs.next())
        throw new NonUniqueResultException("Query returned more than one result for getSingleResult()");

      return value;
    }
    catch (RuntimeException e) {
      throw e;
    }
    catch (Exception e) {
      throw EJBExceptionWrapper.createRuntime(e);
    }
    finally {
      if (rs != null) {
        try {
          rs.close();
        } catch (SQLException e) {
          log.log(Level.FINE, e.toString(), e);
        }

        // jpa/0h19
        if (! _aConn.isActiveTransaction())
          _aConn.detach();
      }
    }
  }

  /**
   * Execute an update or delete.
   */
  public int executeUpdate()
  {
    try {
      // jpa/1006
      if (isSelectQuery())
        throw new IllegalStateException(L.l("javax.persistence.Query.executeUpdate() cannot be applied to a SELECT statement"));

      if (_flushMode == FlushModeType.AUTO)
        _aConn.flushNoChecks();

      return _userQuery.executeUpdate();
    } catch (Exception e) {
      throw EJBExceptionWrapper.createRuntime(e);
    }
  }

  /**
   * Executes the query returning a result set.
   */
  protected ResultSet executeQuery()
    throws SQLException
  {
    ResultSet rs = null;
    PreparedStatement pstmt = null;

    try {
      if (_flushMode == FlushModeType.AUTO)
        _aConn.flushNoChecks();

      if (_nativeSql == null) {
        // JPA query.
        rs = _userQuery.executeQuery();
      }
      else {
        // Native query stmt is prepared in setNativeSql().
        rs = _nativeStmt.executeQuery();
      }
    }
    catch (SQLException e) {
      if (rs != null)
        rs.close();

      if (pstmt != null)
        _aConn.closeStatement(_nativeSql);

      throw e;
    }

    return rs;
  }

  /**
   * Sets the maximum result returned.
   */
  public Query setMaxResults(int maxResults)
  {
    if (maxResults < 0)
      throw new IllegalArgumentException(L.l("setMaxResults() needs a non-negative argument, '{0}' is not allowed", maxResults));

    _userQuery.setMaxResults(maxResults);

    return this;
  }

  /**
   * Sets the position of the first result.
   */
  public Query setFirstResult(int startPosition)
  {
    if (startPosition < 0)
      throw new IllegalArgumentException("setFirstResult() requires a non-negative argument");

    _userQuery.setFirstResult(startPosition);

    return this;
  }

  /**
   * Sets a hint.
   */
  public Query setHint(String hintName, Object value)
  {
    return this;
  }

  /**
   * Sets a named parameter.
   */
  public Query setParameter(String name, Object value)
  {
    ArrayList<String> mapping = _query.getPreparedMapping();

    int n = mapping.size();

    boolean found = false;

    for (int i=0; i < n; i++) {
      if (mapping.get(i).equals(name)) {
        // jpa/10ee
        ArgExpr args[] = _userQuery.getQuery().getArgList();
        setInternalParameter(args[i], i+1, value);
        found = true;
      }
    }

    if (! found)
      throw new IllegalArgumentException(L.l("Parameter name '{0}' is invalid", name));

    return this;
  }

  /**
   * Sets a date parameter.
   */
  public Query setParameter(String name, Date value, TemporalType type)
  {
    ArrayList<String> mapping = _query.getPreparedMapping();

    int n = mapping.size();

    boolean found = false;

    for (int i=0; i < n; i++) {
      if (mapping.get(i).equals(name)) {
        setParameter(i+1, value, type);
        found = true;
      }
    }

    if (! found)
      throw new IllegalArgumentException(L.l("Parameter name '{0}' is invalid", name));

    return this;
  }

  /**
   * Sets a calendar parameter.
   */
  public Query setParameter(String name, Calendar value, TemporalType type)
  {
    ArrayList<String> mapping = _query.getPreparedMapping();

    int n = mapping.size();

    boolean found = false;

    for (int i=0; i < n; i++) {
      if (mapping.get(i).equals(name)) {
        setParameter(i+1, value, type);
        found = true;
      }
    }

    if (! found)
      throw new IllegalArgumentException(L.l("Parameter name '{0}' is invalid", name));

    return this;
  }

  /**
   * Sets an indexed parameter.
   */
  public Query setParameter(int index, Object value)
  {
    if (_nativeSql == null) {
      // jpa/141h
      ArgExpr arg = checkParameterIndex(index);

      setInternalParameter(arg, index, value);
    }
    else {
      setInternalParameter(_nativeStmt, null, index, value);
    }

    return this;
  }

  /**
   * Sets a date parameter.
   */
  public Query setParameter(int index, Date value, TemporalType type)
  {
    try {

      checkParameterIndex(index);

      if (value == null)
        _userQuery.setNull(index, Types.JAVA_OBJECT);
      else {
        switch (type) {
        case TIME:
          _userQuery.setObject(index, value, UtilDateType.TEMPORAL_TIME_TYPE);
          break;

        case DATE:
          _userQuery.setObject(index, value, UtilDateType.TEMPORAL_DATE_TYPE);
          break;

        default:
          _userQuery.setObject(index, value, UtilDateType.TEMPORAL_TIMESTAMP_TYPE);
        }
      }

      return this;
    } catch (IndexOutOfBoundsException e) {
      throw new IllegalArgumentException(L.l("Parameter index '{0}' is not valid for setParameter()", index));
    }
  }

  /**
   * Sets a calendar parameter.
   */
  public Query setParameter(int index, Calendar value, TemporalType type)
  {
    try {

      checkParameterIndex(index);

      if (value == null)
        _userQuery.setNull(index, Types.JAVA_OBJECT);
      else {
        switch (type) {
        case TIME:
          _userQuery.setObject(index, value, CalendarType.TEMPORAL_TIME_TYPE);
          break;

        case DATE:
          _userQuery.setObject(index, value, CalendarType.TEMPORAL_DATE_TYPE);
          break;

        default:
          _userQuery.setObject(index, value, CalendarType.TEMPORAL_TIMESTAMP_TYPE);
        }
      }

      return this;
    } catch (IndexOutOfBoundsException e) {
      throw new IllegalArgumentException(L.l("Parameter index '{0}' is not valid for setParameter()", index));
    }
  }

  /**
   * Sets the flush mode type.
   */
  public Query setFlushMode(FlushModeType mode)
  {
    _flushMode = mode;

    return this;
  }

  //
  // extensions

  /**
   * Sets an indexed parameter.
   */
  public Query setDouble(int index, double value)
  {
    _userQuery.setDouble(index, value);

    return this;
  }

  /**
   * Sets the sql for native queries.
   */
  protected void setNativeSql(String sql)
    throws SQLException
  {
    _nativeSql = sql;
    _nativeStmt = _aConn.prepareStatement(sql);
  }

  /**
   * Sets the sql result set mapping for native queries.
   */
  protected void setSqlResultSetMapping(SqlResultSetMappingConfig map)
  {
    _sqlResultSetMapping = map;
  }

  /**
   * Returns true for SELECT queries.
   */
  private boolean isSelectQuery()
  {
    if (_query instanceof AmberSelectQuery)
      return true;

    if (isNativeQuery()) {
      String sql = _nativeSql.trim().toUpperCase(Locale.ENGLISH);

      if (sql.startsWith("SELECT"))
        return true;
    }

    return false;
  }

  /**
   * Returns true for native queries.
   */
  private boolean isNativeQuery()
  {
    return _nativeSql != null;
  }

  /**
   * Creates an error.
   */
  private AmberException error(String msg)
  {
    msg += "\nin \"" + _query.getQueryString() + "\"";

    return new AmberException(msg);
  }

  /**
   * Native queries. Returns the object using the
   * result set mapping.
   */
  private Object getInternalNative(ResultSet rs)
    throws Exception
  {
    // jpa/0y1-

    int oldEntityResult = _currEntityResult;

    ArrayList<EntityResultConfig> entityResults
      = _sqlResultSetMapping.getEntityResults();

    if (oldEntityResult == entityResults.size()) {

      ArrayList<ColumnResultConfig> columnResults
        = _sqlResultSetMapping.getColumnResults();

      int oldColumnResult = _currColumnResult;

      if (_currColumnResult == columnResults.size()) {
        _currColumnResult = 0;
      }

      // jpa/0y19
      if (entityResults.size() == 0
          || oldColumnResult < columnResults.size()) {
        _currColumnResult++;

        if (columnResults.size() > 0) {
          Object object = rs.getObject(_currIndex++);

          return object;
        }
      }

      oldEntityResult = 0;
      _currEntityResult = 0;
    }

    _currEntityResult++;

    EntityResultConfig entityResult = entityResults.get(oldEntityResult);

    String className = entityResult.getEntityClass();

    EntityType entityType = _aConn.getPersistenceUnit().getEntityType(className);

    if (entityType == null)
      throw new IllegalStateException(L.l("Unable to locate entity '{0}' for native query.", className));

    int oldIndex = _currIndex;

    // jpa/0y15 _currIndex++;

    /* jpa/0y14
    EntityItem item = entityType.getHome().findItem(_aConn, rs, oldIndex);

    if (item == null)
      return null;

    Entity entity = item.getEntity();
    */

    int keyLength = entityType.getId().getKeyCount();

    ArrayList<FieldResultConfig> fieldResults
      = entityResult.getFieldResults();

    Entity entity = null;

    int consumed = 0;

    try {
      // jpa/0y14
      entity = (Entity) _aConn.load(className, rs.getObject(oldIndex));

      ArrayList<String> columnNameList = new ArrayList<String>();
      entityType.generateNativeColumnNames(columnNameList);
      String []columnNames = new String[columnNameList.size()];
      columnNameList.toArray(columnNames);
      entity.__caucho_load_native(rs, columnNames);

      // jpa/0y10
      //consumed = entity.__caucho_load(_aConn, rs, oldIndex + keyLength);
    } catch (Exception e) {
      // jpa/0y1a: invalid query.
      throw new IllegalStateException(L.l("Unable to load an entity of class '{0}' using a native query. When mapped to @EntityResult, a native query should select all fields for the corresponding entity in '{1}'", className, _nativeSql));
    }

    // item.setNumberOfLoadingColumns(consumed);

    _currIndex++;

    // jpa/0y12, jpa/0y15
    _currIndex += consumed;

    return entity;
  }

  /**
   * Returns the object using the correct
   * result set getter based on SQL type.
   */
  private Object getInternalObject(ResultSet rs,
                                   int columnType)
    throws Exception
  {
    // jpa/110-, jpa/11a4, and jpa/11z1

    int oldIndex = _currIndex;

    _currIndex++;

    Object object = rs.getObject(oldIndex);

    if (object instanceof Entity) {
      // _currIndex += ((ResultSetImpl) rs).getNumberOfLoadingColumns();

      ArrayList<AmberExpr> resultList
        = ((AmberSelectQuery) _query).getResultList();

      AmberExpr expr = resultList.get(oldIndex-1);

      // jpa/1160
      if (expr instanceof LoadEntityExpr) {
        LoadEntityExpr entityExpr = (LoadEntityExpr) expr;
        joinFetch((ResultSetImpl) rs, entityExpr, (Entity) object);
      }

      return object;
    }

    if (object == null)
      return null;

    switch (columnType) {
    case Types.BIT:
    case Types.BOOLEAN:
      //      try {
      //        object = rs.getInt(oldIndex);
      //      } catch (Exception ex) {
      if (! (object instanceof Boolean))
        object = rs.getBoolean(oldIndex);
      //      }
      break;

    case Types.TINYINT:
      if (! (object instanceof Number))
        object = rs.getByte(oldIndex);
      break;

    case Types.SMALLINT:
      if (! (object instanceof Number))
        object = rs.getShort(oldIndex);
      break;

    case Types.INTEGER:
      if (! (object instanceof Number))
        object = rs.getLong(oldIndex);
      break;

    case Types.DECIMAL:
    case Types.DOUBLE:
    case Types.NUMERIC:
    case Types.REAL:
      if (! (object instanceof Number))
        object = rs.getDouble(oldIndex);
      break;

    case Types.FLOAT:
      if (! (object instanceof Number))
        object = rs.getFloat(oldIndex);
      break;

    // It was fetched with getObject (see top).
    // default:
    //  object = rs.getObject(oldIndex);
    }

    return object;
  }

  private ArgExpr checkParameterIndex(int index)
  {
    // jpa/141h

    ArgExpr args[] = _userQuery.getQuery().getArgList();

    int len = args.length;

    for (int i = 0; i < len; i++) {
      if (args[i].getIndex() == index)
        return args[i];
    }

    throw new IllegalArgumentException(L.l("Parameter index '{0}' is invalid for query {1}", index, _userQuery.getQuery()));
  }

  private void joinFetch(ResultSetImpl rs,
                         LoadEntityExpr entityExpr,
                         Entity entity)
  {
    String property = rs.getJoinFetchMap().get(entityExpr.getExpr());

    EntityType entityType = entity.__caucho_getEntityType();

    Iterator eagerFieldsIterator = null;

    HashSet<String> eagerFieldNames = entityType.getEagerFieldNames();

    if (eagerFieldNames != null)
      eagerFieldsIterator = eagerFieldNames.iterator();

    // XXX: needs to handle field-based access
    if (! entityType.isFieldAccess()) {
      if (property == null)
        if ((eagerFieldsIterator != null) && eagerFieldsIterator.hasNext())
          property = (String) eagerFieldsIterator.next();
    }

    if (property != null) {
      try {
        Class cl = entityType.getInstanceClass();

        do {
          String methodName = "get" +
            Character.toUpperCase(property.charAt(0)) +
            property.substring(1);

          Method method = cl.getDeclaredMethod(methodName, null);

          Object field = method.invoke(entity, null);

          // XXX: for now, invoke the toString() method on
          // the collection to fetch all the objects (join fetch).

          if (field == null) {
            try {
              methodName = "__caucho_item_" + methodName;

              method = cl.getDeclaredMethod(methodName, new Class[] {AmberConnection.class});

              field = method.invoke(entity, _aConn);
            } catch (Exception ex) {
            }
          }

          if (field != null) {
            Class fieldClass = field.getClass();

            method = fieldClass.getMethod("toString", null);

            method.invoke(field, null);
          }

          property = null;

          if ((eagerFieldsIterator != null) && (eagerFieldsIterator.hasNext()))
            property = (String) eagerFieldsIterator.next();
        }
        while (property != null);

      } catch (NoSuchMethodException e) {
        log.log(Level.FINER, e.toString(), e);
      } catch (IllegalAccessException e) {
        log.log(Level.FINER, e.toString(), e);
      } catch (java.lang.reflect.InvocationTargetException e) {
        log.log(Level.FINER, e.toString(), e);
      }
    }
  }

  /**
   * Sets an indexed parameter.
   */
  private Query setInternalParameter(ArgExpr arg,
                                     int index,
                                     Object value)
  {
    try {
      if (value == null) {
        _userQuery.setNull(index, java.sql.Types.JAVA_OBJECT);
        return this;
      }

      // jpa/141i
      boolean valueIsNumber = value instanceof Number;

      if (valueIsNumber) {
        // jpa/0w20: type is null.
        if (arg.getType() != null) {
          boolean typeIsNumber = arg.getType().isNumeric();

          if (! typeIsNumber)
            throw new IllegalArgumentException(L.l("Type mismatch for parameter index '{0}'. Value '{1}' for type '{2}' is not valid in query '{3}'", index, value, arg.getType().getClass().getName(), _userQuery.getQuery().getSQL()));
        }
      }

      setInternalParameter(null, _userQuery, index, value);

      return this;
    } catch (IndexOutOfBoundsException e) {
      log.log(Level.FINE, e.toString(), e);

      throw new IllegalArgumentException(L.l("Parameter index '{0}' is not valid for setParameter()", index));
    }
  }

  /**
   * Sets an indexed parameter.
   */
  private static void setInternalParameter(PreparedStatement pstmt,
                                           UserQuery userQuery,
                                           int index,
                                           Object value)
  {
    try {
      if (value instanceof Byte) {
        byte arg = ((Byte) value).byteValue();

        if (pstmt == null)
          userQuery.setByte(index, arg);
        else
          pstmt.setByte(index, arg);
      }
      else if (value instanceof Short) {
        short arg = ((Short) value).shortValue();

        if (pstmt == null)
          userQuery.setShort(index, arg);
        else
          pstmt.setShort(index, arg);
      }
      else if (value instanceof Integer) {
        int arg = ((Integer) value).intValue();

        if (pstmt == null)
          userQuery.setInt(index, arg);
        else
          pstmt.setInt(index, arg);
      }
      else if (value instanceof Long) {
        long arg = ((Long) value).longValue();

        if (pstmt == null)
          userQuery.setLong(index, arg);
        else
          pstmt.setLong(index, arg);
      }
      else if (value instanceof Float) {
        float arg = ((Float) value).floatValue();

        if (pstmt == null)
          userQuery.setFloat(index, arg);
        else
          pstmt.setFloat(index, arg);
      }
      else if (value instanceof Double) { // jpa/141a
        double arg = ((Double) value).doubleValue();

        if (pstmt == null)
          userQuery.setDouble(index, arg);
        else
          pstmt.setDouble(index, arg);
      }
      else if (value instanceof Character) {
        if (pstmt == null)
          userQuery.setString(index, value.toString());
        else
          pstmt.setString(index, value.toString());
      }
      else if (value instanceof Entity) {
        // XXX: needs to handle Compound PK

        Object pk = ((Entity) value).__caucho_getPrimaryKey();

        if (pstmt == null)
          userQuery.setObject(index, pk);
        else
          pstmt.setObject(index, pk);
      }
      else {
        if (pstmt == null)
          userQuery.setObject(index, value);
        else
          pstmt.setObject(index, value);
      }
    } catch (Exception e) {
      log.log(Level.FINE, e.toString(), e);

      throw new IllegalArgumentException(L.l("Parameter index '{0}' is not valid for setParameter()", index));
    }
  }

  /**
   * The maximum number of results to retrieve.
   *
   * @Since JPA 2.0
   */
  public int getMaxResults()
  {
    throw new UnsupportedOperationException(getClass().getName());
  }

  /**
   * The first to retrieve.
   *
   * @Since JPA 2.0
   */
  public int getFirstResult()
  {
    throw new UnsupportedOperationException(getClass().getName());
  }

  /**
   * Returns the implementation-specific hints
   *
   * @Since JPA 2.0
   */
  public Map getHints()
  {
    throw new UnsupportedOperationException(getClass().getName());
  }

  /**
   * Returns the supported hints
   *
   * @Since JPA 2.0
   */
  public Set<String> getSupportedHints()
  {
    throw new UnsupportedOperationException(getClass().getName());
  }

  /**
   * Returns the named parameters as a map
   *
   * @since JPA 2.0
   */
  public Map getNamedParameters()
  {
    throw new UnsupportedOperationException(getClass().getName());
  }

  /**
   * Returns the positional parameters as a list
   *
   * @since JPA 2.0
   */
  public List getPositionalParameters()
  {
    throw new UnsupportedOperationException(getClass().getName());
  }

  /**
   * Gets the flush type.
   *
   * @since JPA 2.0
   */
  public FlushModeType getFlushMode()
  {
    throw new UnsupportedOperationException(getClass().getName());
  }

  /**
   * Sets the lock type.
   *
   * @since JPA 2.0
   */
  public Query setLockMode(LockModeType lockMode)
  {
    throw new UnsupportedOperationException(getClass().getName());
  }

  /**
   * Gets the lock type.
   *
   * @since JPA 2.0
   */
  public LockModeType getLockMode()
  {
    throw new UnsupportedOperationException(getClass().getName());
  }

  /* (non-Javadoc)
   * @see javax.persistence.Query#getParameter(java.lang.String)
   */
  @Override
  public Parameter<?> getParameter(String name)
  {
    // TODO Auto-generated method stub
    return null;
  }

  /* (non-Javadoc)
   * @see javax.persistence.Query#getParameter(java.lang.String, java.lang.Class)
   */
  @Override
  public <T> Parameter<T> getParameter(String name, Class<T> type)
  {
    // TODO Auto-generated method stub
    return null;
  }

  /* (non-Javadoc)
   * @see javax.persistence.Query#getParameter(int)
   */
  @Override
  public Parameter<?> getParameter(int pos)
  {
    // TODO Auto-generated method stub
    return null;
  }

  /* (non-Javadoc)
   * @see javax.persistence.Query#getParameter(int, java.lang.Class)
   */
  @Override
  public <T> Parameter<T> getParameter(int position, Class<T> type)
  {
    // TODO Auto-generated method stub
    return null;
  }

  /* (non-Javadoc)
   * @see javax.persistence.Query#getParameterValue(javax.persistence.Parameter)
   */
  @Override
  public <T> T getParameterValue(Parameter<T> param)
  {
    // TODO Auto-generated method stub
    return null;
  }

  /* (non-Javadoc)
   * @see javax.persistence.Query#getParameterValue(java.lang.String)
   */
  @Override
  public Object getParameterValue(String name)
  {
    // TODO Auto-generated method stub
    return null;
  }

  /* (non-Javadoc)
   * @see javax.persistence.Query#getParameterValue(int)
   */
  @Override
  public Object getParameterValue(int position)
  {
    // TODO Auto-generated method stub
    return null;
  }

  /* (non-Javadoc)
   * @see javax.persistence.Query#getParameters()
   */
  @Override
  public Set<Parameter<?>> getParameters()
  {
    // TODO Auto-generated method stub
    return null;
  }

  /* (non-Javadoc)
   * @see javax.persistence.Query#isBound(javax.persistence.Parameter)
   */
  @Override
  public boolean isBound(Parameter<?> param)
  {
    // TODO Auto-generated method stub
    return false;
  }

  /* (non-Javadoc)
   * @see javax.persistence.Query#setParameter(javax.persistence.Parameter, java.lang.Object)
   */
  @Override
  public <T> Query setParameter(Parameter<T> param, T value)
  {
    // TODO Auto-generated method stub
    return null;
  }

  /* (non-Javadoc)
   * @see javax.persistence.Query#setParameter(javax.persistence.Parameter, java.util.Calendar, javax.persistence.TemporalType)
   */
  @Override
  public Query setParameter(Parameter<Calendar> param, Calendar date,
                            TemporalType type)
  {
    // TODO Auto-generated method stub
    return null;
  }

  @Override
  public Query setParameter(Parameter<Date> param,
                                      Date value,
                                      TemporalType temporalType)
  {
    // TODO Auto-generated method stub
    return null;
  }/* (non-Javadoc)
   * @see javax.persistence.Query#unwrap(java.lang.Class)
   */
  @Override
  public <T> T unwrap(Class<T> cl)
  {
    // TODO Auto-generated method stub
    return null;
  }
}
