/*
 * 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.jdbc.metadata.JDBCDeclaredQueryMetaData;
import org.jboss.ejb.plugins.cmp.jdbc.SQLUtil;
import org.jboss.ejb.plugins.cmp.jdbc.QueryParameter;
import org.jboss.ejb.plugins.cmp.ejbql.Catalog;
import org.jboss.deployment.DeploymentException;
import org.jboss.logging.Logger;

import java.util.ArrayList;
import java.util.StringTokenizer;

/**
 * @author <a href="mailto:alex@jboss.org">Alexey Loubyansky</a>
 * @version <tt>$Revision: 81030 $</tt>
 */
public class DeclaredSQLQueryCommand
   extends AbstractQueryCommand
{
   private JDBCCMPFieldBridge2 selectedField;

   public DeclaredSQLQueryCommand(JDBCEntityBridge2 entity, JDBCDeclaredQueryMetaData metadata)
      throws DeploymentException
   {
      initResultReader(entity, metadata);

      this.sql = buildSQL(metadata);
      this.sql = parseParameters(this.sql, metadata);

      setResultType(metadata.getMethod().getReturnType());

      log =
         Logger.getLogger(getClass().getName() + "." + entity.getEntityName() + "#" + metadata.getMethod().getName());
      log.debug("sql: " + sql);
   }

   private void initResultReader(JDBCEntityBridge2 entity, JDBCDeclaredQueryMetaData metadata)
      throws DeploymentException
   {
      String entityName = metadata.getEJBName();
      if(entityName != null)
      {
         Catalog catalog = entity.getManager().getCatalog();
         JDBCEntityBridge2 otherEntity = (JDBCEntityBridge2) catalog.getEntityByEJBName(entityName);
         if(otherEntity == null)
         {
            throw new DeploymentException("Unknown entity: " + entityName);
         }
         this.entity = otherEntity;
      }
      else
      {
         this.entity = entity;
      }

      String fieldName = metadata.getFieldName();
      if(fieldName == null)
      {
         setEntityReader(this.entity, metadata.isSelectDistinct());
      }
      else
      {
         selectedField = (JDBCCMPFieldBridge2) entity.getFieldByName(fieldName);
         if(selectedField == null)
         {
            throw new DeploymentException("Unknown cmp field: " + fieldName);
         }

         setFieldReader(selectedField);
      }
   }

   private String buildSQL(JDBCDeclaredQueryMetaData metadata)
   {
      StringBuffer sql = new StringBuffer(300);

      sql.append(SQLUtil.SELECT);
      if(metadata.isSelectDistinct())
      {
         sql.append(SQLUtil.DISTINCT);
      }

      String alias = metadata.getAlias();
      String from = metadata.getFrom();
      String table;
      String selectList;
      if(metadata.getFieldName() == null)
      {
         // we are selecting a full entity
         table = this.entity.getQualifiedTableName();

         // get a list of all fields to be loaded
         // put pk fields in front
         String tableAlias = getTableAlias(alias, from, this.entity.getTableName());
         selectList = SQLUtil.getColumnNamesClause(this.entity.getPrimaryKeyFields(),
            tableAlias,
            new StringBuffer(35)).toString();
      }
      else
      {
         // we are just selecting one field
         JDBCStoreManager2 manager = (JDBCStoreManager2) selectedField.getManager();
         table = manager.getEntityBridge().getQualifiedTableName();
         selectList = SQLUtil.getColumnNamesClause(selectedField,
            getTableAlias(alias, from, manager.getEntityBridge().getTableName()),
            new StringBuffer()).toString();
      }
      sql.append(selectList);
      String additionalColumns = metadata.getAdditionalColumns();
      if(additionalColumns != null)
      {
         sql.append(additionalColumns);
      }
      sql.append(SQLUtil.FROM).append(table);
      if(alias != null)
      {
         sql.append(' ').append(alias);
      }
      if(from != null)
      {
         sql.append(' ').append(from);
      }

      String where = metadata.getWhere();
      if(where != null && where.trim().length() > 0)
      {
         sql.append(SQLUtil.WHERE).append(where);
      }

      String order = metadata.getOrder();
      if(order != null && order.trim().length() > 0)
      {
         sql.append(SQLUtil.ORDERBY).append(order);
      }

      String other = metadata.getOther();
      if(other != null && other.trim().length() > 0)
      {
         sql.append(' ').append(other);
      }
      return sql.toString();
   }

   private static String getTableAlias(String alias, String from, String table)
   {
      String tableAlias;
      if(alias != null)
      {
         tableAlias = alias;
      }
      else if(from != null)
      {
         tableAlias = table;
      }
      else
      {
         tableAlias = SQLUtil.EMPTY_STRING;
      }
      return tableAlias;
   }

   /**
    * Replaces the parameters in the specific 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, JDBCDeclaredQueryMetaData metadata) 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(entity.getManager(), metadata.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);
            }
         }
      }

      setParameters(params);

      return sqlBuf.toString();
   }
}
