/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2008, Red Hat Middleware LLC, and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.ejb.plugins.cmp.jdbc;

import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.StringTokenizer;
import java.util.Collections;
import java.util.Iterator;
import java.util.AbstractCollection;
import java.util.NoSuchElementException;
import java.util.Set;
import javax.ejb.FinderException;
import javax.ejb.EJBException;
import javax.transaction.Synchronization;

import org.jboss.deployment.DeploymentException;
import org.jboss.ejb.EntityEnterpriseContext;
import org.jboss.ejb.GenericEntityObjectFactory;
import org.jboss.ejb.plugins.cmp.jdbc.bridge.JDBCCMPFieldBridge;
import org.jboss.ejb.plugins.cmp.jdbc.bridge.JDBCEntityBridge;
import org.jboss.ejb.plugins.cmp.jdbc.bridge.JDBCCMRFieldBridge;
import org.jboss.ejb.plugins.cmp.jdbc.bridge.JDBCFieldBridge;
import org.jboss.ejb.plugins.cmp.jdbc.metadata.JDBCQueryMetaData;
import org.jboss.ejb.plugins.cmp.jdbc.metadata.JDBCLeftJoinMetaData;
import org.jboss.ejb.plugins.cmp.jdbc.metadata.JDBCRelationMetaData;
import org.jboss.ejb.plugins.cmp.ejbql.SelectFunction;
import org.jboss.logging.Logger;

/**
 * Abstract superclass of finder commands that return collections.
 * Provides the handleResult() implementation that these all need.
 * 
 * @author <a href="mailto:dain@daingroup.com">Dain Sundstrom</a>
 * @author <a href="mailto:rickard.oberg@telkel.com">Rickard Oberg</a>
 * @author <a href="mailto:marc.fleury@telkel.com">Marc Fleury</a>
 * @author <a href="mailto:shevlandj@kpi.com.au">Joe Shevland</a>
 * @author <a href="mailto:justin@j-m-f.demon.co.uk">Justin Forder</a>
 * @author <a href="mailto:alex@jboss.org">Alex Loubyansky</a>
 * @version $Revision: 81030 $
 */
public abstract class JDBCAbstractQueryCommand implements JDBCQueryCommand
{
   private JDBCQueryMetaData queryMetaData;
   protected Logger log;

   private JDBCStoreManager selectManager;
   private JDBCEntityBridge selectEntity;
   private JDBCCMPFieldBridge selectField;
   private SelectFunction selectFunction;
   private boolean[] eagerLoadMask;
   private String eagerLoadGroup;
   private String sql;
   private int offsetParam;
   private int offsetValue;
   private int limitParam;
   private int limitValue;
   private List parameters = new ArrayList(0);
   private List onFindCMRList = Collections.EMPTY_LIST;
   private QueryCollectionFactory collectionFactory;

   public JDBCAbstractQueryCommand(JDBCStoreManager manager, JDBCQueryMetaData q)
      throws DeploymentException
   {
      this.log = Logger.getLogger(this.getClass().getName() +
         "." +
         manager.getMetaData().getName() +
         "#" +
         q.getMethod().getName());

      queryMetaData = q;
      collectionFactory = q.isLazyResultSetLoading() ?
         new LazyCollectionFactory() :
         (QueryCollectionFactory) new EagerCollectionFactory();

//      setDefaultOffset(q.getOffsetParam());
//      setDefaultLimit(q.getLimitParam());
      setSelectEntity((JDBCEntityBridge) manager.getEntityBridge());
   }

   public void setOffsetValue(int offsetValue)
   {
      this.offsetValue = offsetValue;
   }

   public void setLimitValue(int limitValue)
   {
      this.limitValue = limitValue;
   }

   public void setOffsetParam(int offsetParam)
   {
      this.offsetParam = offsetParam;
   }

   public void setLimitParam(int limitParam)
   {
      this.limitParam = limitParam;
   }

   public void setOnFindCMRList(List onFindCMRList)
   {
      this.onFindCMRList = onFindCMRList;
   }

   public JDBCStoreManager getSelectManager()
   {
      return selectManager;
   }

   public Collection execute(Method finderMethod,
                             Object[] args,
                             EntityEnterpriseContext ctx,
                             GenericEntityObjectFactory factory)
      throws FinderException
   {
      int offset = toInt(args, offsetParam, offsetValue);
      int limit = toInt(args, limitParam, limitValue);
      return execute(sql,
         args,
         offset,
         limit,
         selectEntity,
         selectField,
         selectFunction,
         selectManager,
         eagerLoadMask,
         parameters,
         onFindCMRList,
         queryMetaData,
         factory,
         log);
   }

   protected static int toInt(Object[] params, int paramNumber, int defaultValue)
   {
      if(paramNumber == 0)
      {
         return defaultValue;
      }
      Integer arg = (Integer) params[paramNumber - 1];
      return arg.intValue();
   }

   protected Collection execute(String sql,
                                Object[] args,
                                int offset,
                                int limit,
                                JDBCEntityBridge selectEntity,
                                JDBCCMPFieldBridge selectField,
                                SelectFunction selectFunction,
                                JDBCStoreManager selectManager,
                                boolean[] eagerLoadMask,
                                List parameters,
                                List onFindCMRList,
                                JDBCQueryMetaData queryMetaData,
                                GenericEntityObjectFactory factory,
                                Logger log)
      throws FinderException
   {
      int count = offset;
      Connection con = null;
      PreparedStatement ps = null;
      ResultSet rs = null;
      final JDBCEntityBridge entityBridge = (JDBCEntityBridge)selectManager.getEntityBridge();
      boolean throwRuntimeExceptions = entityBridge.getMetaData().getThrowRuntimeExceptions();

      // if metadata is true, the getconnection is done inside 
      // its own try catch block to throw a runtime exception (EJBException)
      if (throwRuntimeExceptions)
      {
          try 
          {
              con = entityBridge.getDataSource().getConnection();
          } 
          catch (SQLException sqle) 
          {
              javax.ejb.EJBException ejbe = new javax.ejb.EJBException("Could not get a connection; " + sqle);
              ejbe.initCause(sqle);
              throw ejbe;
          } 
      }


      try
      {
         // create the statement
         if(log.isDebugEnabled())
         {
            log.debug("Executing SQL: " + sql);
            if(limit != 0 || offset != 0)
            {
               log.debug("Query offset=" + offset + ", limit=" + limit);
            }
         }

         // if metadata is false, the getconnection is done inside this try catch block
         if ( ! throwRuntimeExceptions)
         {
             con = entityBridge.getDataSource().getConnection();             
         }
         ps = con.prepareStatement(sql);

         // Set the fetch size of the statement
         if(entityBridge.getFetchSize() > 0)
         {
            ps.setFetchSize(entityBridge.getFetchSize());
         }

         // set the parameters
         for(int i = 0; i < parameters.size(); i++)
         {
            QueryParameter parameter = (QueryParameter) parameters.get(i);
            parameter.set(log, ps, i + 1, args);
         }

         // execute statement
         rs = ps.executeQuery();

         // skip 'offset' results
         while(count > 0 && rs.next())
         {
            count--;
         }

         count = limit;
      }
      catch(Exception e)
      {
         JDBCUtil.safeClose(rs);
         JDBCUtil.safeClose(ps);
         JDBCUtil.safeClose(con);

         log.error("Find failed", e);
         FinderException fe = new FinderException("Find failed: " + e);
         fe.initCause(e);
         throw fe;
      }

      return collectionFactory.createCollection(con,
         ps,
         rs,
         limit,
         count,
         selectEntity,
         selectField,
         selectFunction,
         selectManager,
         onFindCMRList,
         eagerLoadMask,
         factory);
   }

   protected Logger getLog()
   {
      return log;
   }

   protected void setSQL(String sql)
   {
      this.sql = sql;
      if(log.isDebugEnabled())
      {
         log.debug("SQL: " + sql);
      }
   }

   protected void setParameterList(List p)
   {
      for(int i = 0; i < p.size(); i++)
      {
         if(!(p.get(i) instanceof QueryParameter))
         {
            throw new IllegalArgumentException("Element " +
               i +
               " of list " +
               "is not an instance of QueryParameter, but " +
               p.get(i).getClass().getName());
         }
      }
      parameters = new ArrayList(p);
   }

   protected JDBCEntityBridge getSelectEntity()
   {
      return selectEntity;
   }

   protected void setSelectEntity(JDBCEntityBridge selectEntity)
      throws DeploymentException
   {
      if(queryMetaData.getMethod().getName().startsWith("find") &&
         this.selectEntity != null && this.selectEntity != selectEntity)
      {
         throw new DeploymentException("Finder " + queryMetaData.getMethod().getName() +
            " defined on " + this.selectEntity.getEntityName() +
            " should return only instances of " + this.selectEntity.getEntityName() +
            " but the query results in instances of " + selectEntity.getEntityName());
      }

      this.selectField = null;
      this.selectFunction = null;
      this.selectEntity = selectEntity;
      this.selectManager = (JDBCStoreManager) selectEntity.getManager();
   }

   protected JDBCCMPFieldBridge getSelectField()
   {
      return selectField;
   }

   protected void setSelectField(JDBCCMPFieldBridge selectField)
   {
      this.selectEntity = null;
      this.selectFunction = null;
      this.selectField = selectField;
      this.selectManager = (JDBCStoreManager) selectField.getManager();
   }

   protected void setSelectFunction(SelectFunction func, JDBCStoreManager manager)
   {
      this.selectEntity = null;
      this.selectField = null;
      this.selectFunction = func;
      this.selectManager = manager;
   }

   protected void setEagerLoadGroup(String eagerLoadGroup)
   {
      this.eagerLoadGroup = eagerLoadGroup;
      boolean[] originalMask = selectEntity.getLoadGroupMask(eagerLoadGroup);
      this.eagerLoadMask = new boolean[originalMask.length];
      System.arraycopy(originalMask, 0, eagerLoadMask, 0, eagerLoadMask.length);
   }

   protected String getEagerLoadGroup()
   {
      return eagerLoadGroup;
   }

   protected boolean[] getEagerLoadMask()
   {
      return this.eagerLoadMask;
   }

   /**
    * Replaces the parameters in the specifiec sql with question marks, and
    * initializes the parameter setting code. Parameters are encoded in curly
    * brackets use a zero based index.
    * 
    * @param sql the sql statement that is parsed for parameters
    * @return the original sql statement with the parameters replaced with a
    *         question mark
    * @throws DeploymentException if a error occures while parsing the sql
    */
   protected String parseParameters(String sql) throws DeploymentException
   {
      StringBuffer sqlBuf = new StringBuffer();
      ArrayList params = new ArrayList();

      // Replace placeholders {0} with ?
      if(sql != null)
      {
         sql = sql.trim();

         StringTokenizer tokens = new StringTokenizer(sql, "{}", true);
         while(tokens.hasMoreTokens())
         {
            String token = tokens.nextToken();
            if(token.equals("{"))
            {
               token = tokens.nextToken();
               if(Character.isDigit(token.charAt(0)))
               {
                  QueryParameter parameter = new QueryParameter(selectManager, queryMetaData.getMethod(), token);

                  // of if we are here we can assume that we have
                  // a parameter and not a function
                  sqlBuf.append("?");
                  params.add(parameter);

                  if(!tokens.nextToken().equals("}"))
                  {
                     throw new DeploymentException("Invalid parameter - missing closing '}' : " + sql);
                  }
               }
               else
               {
                  // ok we don't have a parameter, we have a function
                  // push the tokens on the buffer and continue
                  sqlBuf.append("{").append(token);
               }
            }
            else
            {
               // not parameter... just append it
               sqlBuf.append(token);
            }
         }
      }

      parameters = params;

      return sqlBuf.toString();
   }

   // Static

   public static List getLeftJoinCMRNodes(JDBCEntityBridge entity, String path, Iterator leftJoinIter, Set declaredPaths)
      throws DeploymentException
   {
      List leftJoinCMRNodes;

      if(leftJoinIter.hasNext())
      {
         leftJoinCMRNodes = new ArrayList();
         while(leftJoinIter.hasNext())
         {
            JDBCLeftJoinMetaData leftJoin = (JDBCLeftJoinMetaData) leftJoinIter.next();
            JDBCCMRFieldBridge cmrField = entity.getCMRFieldByName(leftJoin.getCmrField());
            if(cmrField == null)
            {
               throw new DeploymentException("cmr-field in left-join was not found: cmr-field=" +
                  leftJoin.getCmrField() + ", entity=" + entity.getEntityName());
            }

            List subNodes;
            JDBCEntityBridge relatedEntity = cmrField.getRelatedJDBCEntity();
            String childPath = path + '.' + cmrField.getFieldName();
            if(declaredPaths != null)
            {
               declaredPaths.add(childPath);
            }

            subNodes = getLeftJoinCMRNodes(relatedEntity, childPath, leftJoin.getLeftJoins(), declaredPaths);

            boolean[] mask = relatedEntity.getLoadGroupMask(leftJoin.getEagerLoadGroup());
            LeftJoinCMRNode node = new LeftJoinCMRNode(childPath, cmrField, mask, subNodes);
            leftJoinCMRNodes.add(node);
         }
      }
      else
      {
         leftJoinCMRNodes = Collections.EMPTY_LIST;
      }

      return leftJoinCMRNodes;
   }

   public static final void leftJoinCMRNodes(String alias,
                                             List onFindCMRNodes,
                                             AliasManager aliasManager,
                                             StringBuffer sb)
   {
      for(int i = 0; i < onFindCMRNodes.size(); ++i)
      {
         LeftJoinCMRNode node = (LeftJoinCMRNode) onFindCMRNodes.get(i);
         JDBCCMRFieldBridge cmrField = node.cmrField;
         JDBCEntityBridge relatedEntity = cmrField.getRelatedJDBCEntity();
         String relatedAlias = aliasManager.getAlias(node.path);

         JDBCRelationMetaData relation = cmrField.getMetaData().getRelationMetaData();
         if(relation.isTableMappingStyle())
         {
            String relTableAlias = aliasManager.getRelationTableAlias(node.path);
            sb.append(" LEFT OUTER JOIN ")
               .append(cmrField.getQualifiedTableName())
               .append(' ')
               .append(relTableAlias)
               .append(" ON ");
            SQLUtil.getRelationTableJoinClause(cmrField, alias, relTableAlias, sb);

            sb.append(" LEFT OUTER JOIN ")
               .append(relatedEntity.getQualifiedTableName())
               .append(' ')
               .append(relatedAlias)
               .append(" ON ");
            SQLUtil.getRelationTableJoinClause(cmrField.getRelatedCMRField(), relatedAlias, relTableAlias, sb);
         }
         else
         {
            // foreign key mapping style
            sb.append(" LEFT OUTER JOIN ")
               .append(relatedEntity.getQualifiedTableName())
               .append(' ')
               .append(relatedAlias)
               .append(" ON ");
            SQLUtil.getJoinClause(cmrField,
               alias,
               relatedAlias,
               sb);
         }

         List subNodes = node.onFindCMRNodes;
         if(!subNodes.isEmpty())
         {
            leftJoinCMRNodes(relatedAlias, subNodes, aliasManager, sb);
         }
      }
   }

   public static final void appendLeftJoinCMRColumnNames(List onFindCMRNodes,
                                                         AliasManager aliasManager,
                                                         StringBuffer sb)
   {
      for(int i = 0; i < onFindCMRNodes.size(); ++i)
      {
         LeftJoinCMRNode node = (LeftJoinCMRNode) onFindCMRNodes.get(i);
         JDBCCMRFieldBridge cmrField = node.cmrField;
         JDBCEntityBridge relatedEntity = cmrField.getRelatedJDBCEntity();
         String childAlias = aliasManager.getAlias(node.path);

         // primary key fields
         SQLUtil.appendColumnNamesClause(relatedEntity.getPrimaryKeyFields(),
            childAlias,
            sb);

         // eager load group
         if(node.eagerLoadMask != null)
         {
            SQLUtil.appendColumnNamesClause(relatedEntity.getTableFields(),
               node.eagerLoadMask,
               childAlias,
               sb);
         }

         List subNodes = node.onFindCMRNodes;
         if(!subNodes.isEmpty())
         {
            appendLeftJoinCMRColumnNames(subNodes, aliasManager, sb);
         }
      }
   }

   private static int loadOnFindCMRFields(Object pk, List onFindCMRNodes, ResultSet rs, int index, Logger log)
   {
      Object[] ref = new Object[1];
      for(int nodeInd = 0; nodeInd < onFindCMRNodes.size(); ++nodeInd)
      {
         LeftJoinCMRNode node = (LeftJoinCMRNode) onFindCMRNodes.get(nodeInd);
         JDBCCMRFieldBridge cmrField = node.cmrField;
         ReadAheadCache myCache = cmrField.getJDBCStoreManager().getReadAheadCache();
         JDBCEntityBridge relatedEntity = cmrField.getRelatedJDBCEntity();
         ReadAheadCache relatedCache = cmrField.getRelatedManager().getReadAheadCache();

         // load related id
         ref[0] = null;
         index = relatedEntity.loadPrimaryKeyResults(rs, index, ref);
         Object relatedId = ref[0];
         boolean cacheRelatedData = relatedId != null;

         if(pk != null)
         {
            if(cmrField.getMetaData().getRelatedRole().isMultiplicityOne())
            {
               // cacheRelatedData the value
               myCache.addPreloadData(pk,
                  cmrField,
                  relatedId == null ? Collections.EMPTY_LIST : Collections.singletonList(relatedId));
            }
            else
            {
               Collection cachedValue = myCache.getCachedCMRValue(pk, cmrField);
               if(cachedValue == null)
               {
                  cachedValue = new ArrayList();
                  myCache.addPreloadData(pk, cmrField, cachedValue);
               }

               if(relatedId != null)
               {
                  if(cachedValue.contains(relatedId))
                  {
                     cacheRelatedData = false;
                  }
                  else
                  {
                     cachedValue.add(relatedId);
                  }
               }
            }
         }

         // load eager load group
         if(node.eagerLoadMask != null)
         {
            JDBCFieldBridge[] tableFields = relatedEntity.getTableFields();
            for(int fieldInd = 0; fieldInd < tableFields.length; ++fieldInd)
            {
               if(node.eagerLoadMask[fieldInd])
               {
                  JDBCFieldBridge field = tableFields[fieldInd];
                  ref[0] = null;
                  index = field.loadArgumentResults(rs, index, ref);

                  if(cacheRelatedData)
                  {
                     if(log.isTraceEnabled())
                     {
                        log.trace("Caching " +
                           relatedEntity.getEntityName() +
                           '[' +
                           relatedId +
                           "]." +
                           field.getFieldName() + "=" + ref[0]);
                     }
                     relatedCache.addPreloadData(relatedId, field, ref[0]);
                  }
               }
            }
         }

         List subNodes = node.onFindCMRNodes;
         if(!subNodes.isEmpty())
         {
            index = loadOnFindCMRFields(relatedId, subNodes, rs, index, log);
         }
      }

      return index;
   }

   public static final class LeftJoinCMRNode
   {
      public final String path;
      public final JDBCCMRFieldBridge cmrField;
      public final boolean[] eagerLoadMask;
      public final List onFindCMRNodes;

      public LeftJoinCMRNode(String path, JDBCCMRFieldBridge cmrField, boolean[] eagerLoadMask, List onFindCMRNodes)
      {
         this.path = path;
         this.cmrField = cmrField;
         this.eagerLoadMask = eagerLoadMask;
         this.onFindCMRNodes = onFindCMRNodes;
      }

      public boolean equals(Object o)
      {
         boolean result;
         if(o == this)
         {
            result = true;
         }
         else if(o instanceof LeftJoinCMRNode)
         {
            LeftJoinCMRNode other = (LeftJoinCMRNode) o;
            result = cmrField == other.cmrField;
         }
         else
         {
            result = false;
         }
         return result;
      }

      public int hashCode()
      {
         return cmrField == null ? Integer.MIN_VALUE : cmrField.hashCode();
      }

      public String toString()
      {
         return '[' + cmrField.getFieldName() + ": " + onFindCMRNodes + ']';
      }
   }


   interface QueryCollectionFactory
   {
      Collection createCollection(Connection con,
                                  PreparedStatement ps,
                                  ResultSet rs,
                                  int limit, int count,
                                  JDBCEntityBridge selectEntity,
                                  JDBCCMPFieldBridge selectField,
                                  SelectFunction selectFunction,
                                  JDBCStoreManager selectManager,
                                  List onFindCMRList,
                                  boolean[] eagerLoadMask,
                                  GenericEntityObjectFactory factory)
         throws FinderException;
   }

   class EagerCollectionFactory
      implements QueryCollectionFactory
   {
      public Collection createCollection(Connection con,
                                         PreparedStatement ps,
                                         ResultSet rs,
                                         int limit, int count,
                                         JDBCEntityBridge selectEntity,
                                         JDBCCMPFieldBridge selectField,
                                         SelectFunction selectFunction,
                                         JDBCStoreManager selectManager,
                                         List onFindCMRList,
                                         boolean[] eagerLoadMask,
                                         GenericEntityObjectFactory factory)
         throws FinderException
      {
         try
         {
            List results = new ArrayList();

            if(selectEntity != null)
            {
               ReadAheadCache selectReadAheadCache = selectManager.getReadAheadCache();
               List ids = new ArrayList();

               boolean loadOnFindCmr = !onFindCMRList.isEmpty();
               Object[] ref = new Object[1];
               Object prevPk = null;

               while((limit == 0 || count-- > 0) && rs.next())
               {
                  int index = 1;

                  // get the pk
                  index = selectEntity.loadPrimaryKeyResults(rs, index, ref);
                  Object pk = ref[0];

                  boolean addPk = (loadOnFindCmr ? !pk.equals(prevPk) : true);
                  if(addPk)
                  {
                     ids.add(pk);
                     results.add(factory.getEntityEJBObject(pk));
                     prevPk = pk;
                  }

                  // read the preload fields
                  if(eagerLoadMask != null)
                  {
                     JDBCFieldBridge[] tableFields = selectEntity.getTableFields();
                     for(int i = 0; i < eagerLoadMask.length; i++)
                     {
                        if(eagerLoadMask[i])
                        {
                           JDBCFieldBridge field = tableFields[i];
                           ref[0] = null;

                           // read the value and store it in the readahead cache
                           index = field.loadArgumentResults(rs, index, ref);

                           if(addPk)
                           {
                              selectReadAheadCache.addPreloadData(pk, field, ref[0]);
                           }
                        }
                     }

                     if(!onFindCMRList.isEmpty())
                     {
                        index = loadOnFindCMRFields(pk, onFindCMRList, rs, index, log);
                     }
                  }
               }

               // add the results list to the cache
               selectReadAheadCache.addFinderResults(ids, queryMetaData.getReadAhead());
            }
            else if(selectField != null)
            {
               // load the field
               Object[] valueRef = new Object[1];
               while((limit == 0 || count-- > 0) && rs.next())
               {
                  valueRef[0] = null;
                  selectField.loadArgumentResults(rs, 1, valueRef);
                  results.add(valueRef[0]);
               }
            }
            else
            {
               while(rs.next())
               {
                  results.add(selectFunction.readResult(rs));
               }
            }

            if(log.isDebugEnabled() && limit != 0 && count == 0)
            {
               log.debug("Query result was limited to " + limit + " row(s)");
            }

            return results;
         }
         catch(Exception e)
         {
            log.error("Find failed", e);
            throw new FinderException("Find failed: " + e);
         }
         finally
         {
            JDBCUtil.safeClose(rs);
            JDBCUtil.safeClose(ps);
            JDBCUtil.safeClose(con);
         }
      }

   }

   class LazyCollectionFactory
      implements QueryCollectionFactory
   {
      public Collection createCollection(Connection con,
                                         PreparedStatement ps,
                                         ResultSet rs,
                                         int limit, int count,
                                         JDBCEntityBridge selectEntity,
                                         JDBCCMPFieldBridge selectField,
                                         SelectFunction selectFunction,
                                         JDBCStoreManager selectManager,
                                         List onFindCMRList,
                                         boolean[] eagerLoadMask,
                                         GenericEntityObjectFactory factory)
         throws FinderException
      {
         return new LazyCollection(con,
            ps,
            rs,
            limit,
            count,
            selectEntity,
            selectField,
            selectFunction,
            selectManager,
            eagerLoadMask,
            factory);
      }

      private class LazyCollection extends AbstractCollection
      {
         private final Connection con;
         private final PreparedStatement ps;
         private final ResultSet rs;
         private final int limit;
         private int count;
         private final JDBCEntityBridge selectEntity;
         private final JDBCCMPFieldBridge selectField;
         private final SelectFunction selectFunction;
         private final JDBCStoreManager selectManager;
         private final boolean[] eagerLoadMask;
         private final GenericEntityObjectFactory factory;

         private Object prevPk;
         private Object curPk;
         private Object currentResult;

         Object[] ref = new Object[1];

         boolean loadOnFindCmr;

         private List results = null;
         private Iterator firstIterator;
         private int size;
         private boolean resourcesClosed;

         public LazyCollection(final Connection con,
                               final PreparedStatement ps,
                               final ResultSet rs,
                               int limit,
                               int count,
                               JDBCEntityBridge selectEntity,
                               JDBCCMPFieldBridge selectField,
                               SelectFunction selectFunction,
                               JDBCStoreManager selectManager,
                               boolean[] eagerLoadMask,
                               GenericEntityObjectFactory factory)
         {
            this.con = con;
            this.ps = ps;
            this.rs = rs;
            this.limit = limit;
            this.count = count;
            this.selectEntity = selectEntity;
            this.selectField = selectField;
            this.selectFunction = selectFunction;
            this.selectManager = selectManager;
            this.eagerLoadMask = eagerLoadMask;
            this.factory = factory;
            loadOnFindCmr = !onFindCMRList.isEmpty();

            firstIterator = getFirstIterator();
            if(firstIterator.hasNext())
            {
               try
               {
                  size = rs.getInt(1);
               }
               catch(SQLException e)
               {
                  throw new EJBException("Failed to read ResultSet.", e);
               }

               if(limit > 0 && size > limit)
               {
                  size = limit;
               }
            }

            if(size < 1)
            {
               firstIterator = null;
               results = new ArrayList(0);
               closeResources();
            }
            else
            {
               results = new ArrayList(size);
               try
               {
                  selectManager.getContainer().getTransactionManager().getTransaction().registerSynchronization(new Synchronization()
                  {
                     public void beforeCompletion()
                     {
                        closeResources();
                     }

                     public void afterCompletion(int status)
                     {
                        closeResources();
                     }
                  });
               }
               catch(Exception e)
               {
                  throw new EJBException("Failed to obtain current transaction", e);
               }
            }
         }

         private void closeResources()
         {
            if(!resourcesClosed)
            {
               JDBCUtil.safeClose(rs);
               JDBCUtil.safeClose(ps);
               JDBCUtil.safeClose(con);
               resourcesClosed = true;
            }
         }

         public Iterator iterator()
         {
            return firstIterator != null ? firstIterator : results.iterator();
         }

         public int size()
         {
            return firstIterator != null ? size : results.size();
         }

         public boolean add(Object o)
         {
            if(firstIterator == null)
            {
               return results.add(o);
            }
            throw new IllegalStateException("Can't modify collection while the first iterator is not exhausted.");
         }

         public boolean remove(Object o)
         {
            if(firstIterator == null)
            {
               return results.remove(o);
            }
            throw new IllegalStateException("Can't modify collection while the first iterator is not exhausted.");
         }

         private boolean hasNextResult()
         {
            try
            {
               boolean has = (limit == 0 || count-- > 0) && rs.next();
               if(!has)
               {
                  if(log.isTraceEnabled())
                  {
                     log.trace("first iterator exhausted!");
                  }
                  firstIterator = null;
                  closeResources();
               }
               return has;
            }
            catch(Exception e)
            {
               log.error("Failed to read ResultSet.", e);
               throw new EJBException("Failed to read ResultSet: " + e.getMessage());
            }
         }

         private Object readNext()
         {
            try
            {
               if(selectEntity != null)
               {
                  ReadAheadCache selectReadAheadCache = selectManager.getReadAheadCache();

                  // first one is size
                  int index = 2;

                  // get the pk
                  index = selectEntity.loadPrimaryKeyResults(rs, index, ref);
                  curPk = ref[0];

                  boolean addPk = (loadOnFindCmr ? !curPk.equals(prevPk) : true);
                  if(addPk)
                  {
                     prevPk = curPk;
                     currentResult = factory.getEntityEJBObject(curPk);
                  }

                  // read the preload fields
                  if(eagerLoadMask != null)
                  {
                     JDBCFieldBridge[] tableFields = selectEntity.getTableFields();
                     for(int i = 0; i < eagerLoadMask.length; i++)
                     {
                        if(eagerLoadMask[i])
                        {
                           JDBCFieldBridge field = tableFields[i];
                           ref[0] = null;

                           // read the value and store it in the readahead cache
                           index = field.loadArgumentResults(rs, index, ref);

                           if(addPk)
                           {
                              selectReadAheadCache.addPreloadData(curPk, field, ref[0]);
                           }
                        }
                     }

                     if(!onFindCMRList.isEmpty())
                     {
                        index = loadOnFindCMRFields(curPk, onFindCMRList, rs, index, log);
                     }
                  }
               }
               else if(selectField != null)
               {
                  // load the field
                  selectField.loadArgumentResults(rs, 2, ref);
                  currentResult = ref[0];
               }
               else
               {
                  currentResult = selectFunction.readResult(rs);
               }

               if(log.isTraceEnabled() && limit != 0 && count == 0)
               {
                  log.trace("Query result was limited to " + limit + " row(s)");
               }

               return currentResult;
            }
            catch(Exception e)
            {
               log.error("Failed to read ResultSet", e);
               throw new EJBException("Failed to read ResultSet: " + e.getMessage());
            }
         }

         private Iterator getFirstIterator()
         {
            return new Iterator()
            {
               private boolean hasNext;
               private Object cursor;

               public boolean hasNext()
               {
                  return hasNext ? hasNext : (hasNext = hasNextResult());
               }

               public Object next()
               {
                  if(!hasNext())
                  {
                     throw new NoSuchElementException();
                  }
                  hasNext = false;

                  cursor = readNext();
                  results.add(cursor);

                  return cursor;
               }

               public void remove()
               {
                  --size;
                  results.remove(cursor);
               }
            };
         }
      }
   }
}
