/*
 * 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.jdbc2;

import org.jboss.ejb.plugins.cmp.jdbc2.bridge.JDBCEntityBridge2;
import org.jboss.ejb.plugins.cmp.jdbc2.bridge.JDBCCMPFieldBridge2;
import org.jboss.ejb.plugins.cmp.jdbc2.schema.Schema;
import org.jboss.ejb.plugins.cmp.jdbc.JDBCUtil;
import org.jboss.ejb.plugins.cmp.jdbc.QueryParameter;
import org.jboss.ejb.plugins.cmp.ejbql.SelectFunction;
import org.jboss.ejb.GenericEntityObjectFactory;
import org.jboss.logging.Logger;

import javax.ejb.FinderException;
import javax.ejb.ObjectNotFoundException;
import java.util.Collection;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.HashSet;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

/**
 * @author <a href="mailto:alex@jboss.org">Alexey Loubyansky</a>
 * @version <tt>$Revision: 81030 $</tt>
 */
public abstract class AbstractQueryCommand implements QueryCommand
{
   static final CollectionFactory COLLECTION_FACTORY = new CollectionFactory()
   {
      public Collection newCollection()
      {
         return new ArrayList();
      }
   };

   static final CollectionFactory SET_FACTORY = new CollectionFactory()
   {
      public Collection newCollection()
      {
         return new HashSet();
      }
   };

   protected String sql;
   protected Logger log;
   protected JDBCEntityBridge2 entity;
   protected QueryParameter[] params = null;
   private CollectionFactory collectionFactory;
   private CollectionStrategy collectionStrategy;
   private ResultReader resultReader;
   private int offsetParam;
   private int offsetValue;
   private int limitParam;
   private int limitValue;

   // Protected

   protected void setResultType(Class clazz)
   {
      if(Set.class.isAssignableFrom(clazz))
      {
         collectionFactory = SET_FACTORY;
      }
      else if(Collection.class.isAssignableFrom(clazz))
      {
         collectionFactory = COLLECTION_FACTORY;
      }
      initCollectionStrategy();
   }

   protected void setFieldReader(JDBCCMPFieldBridge2 field)
   {
      this.resultReader = new FieldReader(field);
      initCollectionStrategy();
   }

   protected void setFunctionReader(SelectFunction func)
   {
      this.resultReader = new FunctionReader(func);
      initCollectionStrategy();
   }

   protected void setEntityReader(JDBCEntityBridge2 entity, boolean searchableOnly)
   {
      this.entity = entity;
      this.resultReader = new EntityReader(entity, searchableOnly);
      initCollectionStrategy();
   }

   private void initCollectionStrategy()
   {
      if(collectionFactory != null && resultReader != null)
      {
         collectionStrategy = new EagerCollectionStrategy(collectionFactory, resultReader, log);
      }
   }

   // QueryCommand implementation

   public JDBCStoreManager2 getStoreManager()
   {
      return (JDBCStoreManager2) entity.getManager();
   }

   public Collection fetchCollection(Schema schema, GenericEntityObjectFactory factory, Object[] args)
      throws FinderException
   {
      int offset = toInt(args, offsetParam, offsetValue);
      int limit = toInt(args, limitParam, limitValue);
      return fetchCollection(entity, sql, params, offset, limit, collectionStrategy, schema, factory, args, log);
   }

   public Object fetchOne(Schema schema, GenericEntityObjectFactory factory, Object[] args) throws FinderException
   {
      schema.flush();
      return executeFetchOne(args, factory);
   }

   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;
   }

   // Protected

   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 Object executeFetchOne(Object[] args, GenericEntityObjectFactory factory) throws FinderException
   {
      return fetchOne(entity, sql, params, resultReader, args, factory, log);
   }

   static Collection fetchCollection(JDBCEntityBridge2 entity,
                                     String sql,
                                     QueryParameter[] params,
                                     int offset,
                                     int limit,
                                     CollectionStrategy collectionStrategy,
                                     Schema schema,
                                     GenericEntityObjectFactory factory,
                                     Object[] args,
                                     Logger log)
      throws FinderException
   {
      schema.flush();

      int count = offset;
      Collection result;

      Connection con = null;
      PreparedStatement ps = null;
      ResultSet rs = null;
      boolean throwRuntimeExceptions = entity.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 = entity.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
      {
         if(log.isDebugEnabled())
         {
            log.debug("executing: " + sql);
         }

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

         if(params != null)
         {
            for(int i = 0; i < params.length; i++)
            {
               params[i].set(log, ps, i + 1, args);
            }
         }

         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("Finder failed: " + e.getMessage(), e);
         FinderException fe = new FinderException(e.getMessage());
         fe.initCause(e);
         throw fe;
      }

      result = collectionStrategy.readResultSet(con, ps, rs, limit, count, factory);

      return result;
   }
   
   static Object fetchOne(JDBCEntityBridge2 entity,
                          String sql,
                          QueryParameter[] params,
                          ResultReader resultReader,
                          Object[] args,
                          GenericEntityObjectFactory factory,
                          Logger log)
      throws FinderException
   {
      Object pk;
      Connection con = null;
      PreparedStatement ps = null;
      ResultSet rs = null;
      boolean throwRuntimeExceptions = entity.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 = entity.getDataSource().getConnection();
          } 
          catch (SQLException sqle) 
          {
              javax.ejb.EJBException ejbe = new javax.ejb.EJBException("Could not get a connection; " + sqle);
              //ejbe.initCause(sqle); only for JBoss 4 and +
              throw ejbe;
          } 
      }
      try
      {
         if(log.isDebugEnabled())
         {
            log.debug("executing: " + sql);
         }

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

         if(params != null)
         {
            for(int i = 0; i < params.length; i++)
            {
               params[i].set(log, ps, i + 1, args);
            }
         }

         rs = ps.executeQuery();
         if(rs.next())
         {
            pk = resultReader.readRow(rs, factory);
            if(rs.next())
            {
               List list = new ArrayList();
               list.add(pk);
               list.add(resultReader.readRow(rs, factory));
               while(rs.next())
               {
                  list.add(resultReader.readRow(rs, factory));
               }
               throw new FinderException("More than one instance matches the single-object finder criteria: " + list);
            }
         }
         else
         {
            throw new ObjectNotFoundException();
         }
      }
      catch(FinderException e)
      {
         throw e;
      }
      catch(Exception e)
      {
          FinderException fe = new FinderException(e.getMessage());
          fe.initCause(e);
          throw fe;
      }
      finally
      {
         JDBCUtil.safeClose(rs);
         JDBCUtil.safeClose(ps);
         JDBCUtil.safeClose(con);
      }

      return pk;
   }
   
   protected void setParameters(List p)
   {
      if(p.size() > 0)
      {
         params = new QueryParameter[p.size()];
         for(int i = 0; i < p.size(); i++)
         {
            Object pi = p.get(i);
            if(!(pi instanceof QueryParameter))
            {
               throw new IllegalArgumentException("Element "
                  +
                  i
                  +
                  " of list is not an instance of QueryParameter, but " +
                  p.get(i).getClass().getName());
            }
            params[i] = (QueryParameter) pi;
         }
      }
   }

   // Inner

   static interface CollectionFactory
   {
      Collection newCollection();
   }

   static interface ResultReader
   {
      Object readRow(ResultSet rs, GenericEntityObjectFactory factory) throws SQLException;
   }

   static class EntityReader implements ResultReader
   {
      private final JDBCEntityBridge2 entity;
      private final boolean searchableOnly;

      public EntityReader(JDBCEntityBridge2 entity, boolean searchableOnly)
      {
         this.entity = entity;
         this.searchableOnly = searchableOnly;
      }

      public Object readRow(ResultSet rs, GenericEntityObjectFactory factory)
      {
         final Object pk = entity.getTable().loadRow(rs, searchableOnly);
         return pk == null ? null : factory.getEntityEJBObject(pk);
      }
   };

   static class FieldReader implements ResultReader
   {
      private final JDBCCMPFieldBridge2 field;

      public FieldReader(JDBCCMPFieldBridge2 field)
      {
         this.field = field;
      }

      public Object readRow(ResultSet rs, GenericEntityObjectFactory factory) throws SQLException
      {
         return field.loadArgumentResults(rs, 1);
      }
   }

   static class FunctionReader implements ResultReader
   {
      private final SelectFunction function;

      public FunctionReader(SelectFunction function)
      {
         this.function = function;
      }

      public Object readRow(ResultSet rs, GenericEntityObjectFactory factory) throws SQLException
      {
         return function.readResult(rs);
      }
   }

   interface CollectionStrategy
   {
      Collection readResultSet(Connection con, PreparedStatement ps, ResultSet rs, int limit, int count, GenericEntityObjectFactory factory)
         throws FinderException;
   }

   static class EagerCollectionStrategy
      implements CollectionStrategy
   {
      private final CollectionFactory collectionFactory;
      private final ResultReader resultReader;
      private final Logger log;

      public EagerCollectionStrategy(CollectionFactory collectionFactory,
                     ResultReader resultReader, Logger log)
      {
         this.collectionFactory = collectionFactory;
         this.resultReader = resultReader;
         this.log = log;
      }

      public Collection readResultSet(Connection con,
                                      PreparedStatement ps,
                                      ResultSet rs,
                                      int limit, int count,
                                      GenericEntityObjectFactory factory)
         throws FinderException
      {
         Collection result;
         try
         {
            if((limit == 0 || count-- > 0) && rs.next())
            {
               result = collectionFactory.newCollection();
               Object instance = resultReader.readRow(rs, factory);
               result.add(instance);
               while((limit == 0 || count-- > 0) && rs.next())
               {
                  instance = resultReader.readRow(rs, factory);
                  result.add(instance);
               }
            }
            else
            {
               result = Collections.EMPTY_SET;
            }
         }
         catch(Exception e)
         {
            log.error("Finder failed: " + e.getMessage(), e);
            throw new FinderException(e.getMessage());
         }
         finally
         {
            JDBCUtil.safeClose(rs);
            JDBCUtil.safeClose(ps);
            JDBCUtil.safeClose(con);
         }
         return result;
      }
   }
}
