/*
 * 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.PreparedStatement;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
import java.util.Collections;
import javax.ejb.EJBLocalObject;
import javax.ejb.EJBObject;

import org.jboss.ejb.plugins.cmp.ejbql.Catalog;
import org.jboss.ejb.plugins.cmp.jdbc.bridge.JDBCFieldBridge;
import org.jboss.ejb.plugins.cmp.jdbc.bridge.JDBCAbstractEntityBridge;

/**
 * @author <a href="mailto:alex@jboss.org">Alex Loubyansky and others</a>
 */
import org.jboss.logging.Logger;

public final class QueryParameter
{
   public static List createParameters(int argNum, JDBCFieldBridge field)
   {
      List parameters;
      JDBCType type = field.getJDBCType();
      if(type instanceof JDBCTypeComplex)
      {
         JDBCTypeComplexProperty[] props = ((JDBCTypeComplex)type).getProperties();
         parameters = new ArrayList(props.length);
         for(int i = 0; i < props.length; i++)
         {
            QueryParameter param = new QueryParameter(
               argNum,
               false,
               null,
               props[i],
               props[i].getJDBCType());
            parameters.add(param);
         }
      }
      else
      {
         QueryParameter param = new QueryParameter(argNum, type);
         parameters = Collections.singletonList(param);
      }
      return parameters;
   }

   public static List createParameters(int argNum, JDBCAbstractEntityBridge entity)
   {
      List parameters = new ArrayList();
      JDBCFieldBridge[] pkFields = entity.getPrimaryKeyFields();
      for(int i = 0; i < pkFields.length; ++i)
      {
         JDBCFieldBridge pkField = pkFields[i];

         JDBCType type = pkField.getJDBCType();
         if(type instanceof JDBCTypeComplex)
         {
            JDBCTypeComplexProperty[] props =
               ((JDBCTypeComplex)type).getProperties();
            for(int j = 0; j < props.length; j++)
            {
               QueryParameter param = new QueryParameter(
                  argNum,
                  false,
                  pkField,
                  props[j],
                  props[j].getJDBCType());
               parameters.add(param);
            }
         }
         else
         {
            QueryParameter param = new QueryParameter(
               argNum,
               false,
               pkField,
               null,
               type.getJDBCTypes()[0]);
            param.type = type;
            parameters.add(param);
         }
      }
      return parameters;
   }

   public static List createPrimaryKeyParameters(int argNum, JDBCAbstractEntityBridge entity)
   {
      List parameters = new ArrayList();
      JDBCFieldBridge[] pkFields = entity.getPrimaryKeyFields();
      for(int i = 0; i < pkFields.length; ++i)
      {
         JDBCFieldBridge pkField = pkFields[i];

         JDBCType type = pkField.getJDBCType();
         if(type instanceof JDBCTypeComplex)
         {
            JDBCTypeComplexProperty[] props = ((JDBCTypeComplex)type).getProperties();
            for(int j = 0; j < props.length; j++)
            {
               QueryParameter param = new QueryParameter(
                  argNum,
                  true,
                  pkField,
                  props[j],
                  props[j].getJDBCType());
               parameters.add(param);
            }
         }
         else
         {
            QueryParameter param = new QueryParameter(
               argNum,
               true,
               pkField,
               null,
               type.getJDBCTypes()[0]);
            param.type = type;
            parameters.add(param);
         }
      }
      return parameters;
   }

   private int argNum;
   private final boolean isPrimaryKeyParameter;
   private JDBCFieldBridge field;
   private JDBCTypeComplexProperty property;
   private String parameterString;

   private int jdbcType;
   private JDBCType type;

   public QueryParameter(
      JDBCEntityPersistenceStore manager,
      Method method,
      String parameterString)
   {

      // Method parameter will never be a primary key object, but always
      // a complete entity.
      this.isPrimaryKeyParameter = false;

      this.parameterString = parameterString;

      if(parameterString == null || parameterString.length() == 0)
      {
         throw new IllegalArgumentException("Parameter string is empty");
      }

      StringTokenizer tok = new StringTokenizer(parameterString, ".");

      // get the argument number
      try
      {
         argNum = Integer.parseInt(tok.nextToken());
      }
      catch(NumberFormatException e)
      {
         throw new IllegalArgumentException("The parameter must begin with a number");
      }

      // get the argument type
      if(argNum > method.getParameterTypes().length)
      {
         throw new IllegalArgumentException("The parameter index is " + argNum +
            " but the query method only has " +
            method.getParameterTypes().length + "parameter(s)");
      }
      Class argType = method.getParameterTypes()[argNum];

      // get the jdbc type object
      JDBCType type;

      // if this is an entity parameter
      if(EJBObject.class.isAssignableFrom(argType) ||
         EJBLocalObject.class.isAssignableFrom(argType))
      {
         // get the field name
         // check more tokens
         if(!tok.hasMoreTokens())
         {
            throw new IllegalArgumentException("When the parameter is an ejb a field name must be supplied.");
         }
         String fieldName = tok.nextToken();

         // get the field from the entity
         field = getCMPField(manager, argType, fieldName);
         if(!field.isPrimaryKeyMember())
         {
            throw new IllegalArgumentException("The specified field must be a primay key field");
         }

         // get the jdbc type object
         type = field.getJDBCType();
      }
      else
      {
         // get jdbc type from type manager
         type = manager.getJDBCTypeFactory().getJDBCType(argType);
      }

      if(type instanceof JDBCTypeSimple)
      {
         if(tok.hasMoreTokens())
         {
            throw new IllegalArgumentException("Parameter is NOT a known " +
               "dependent value class, so a properties cannot supplied.");
         }
         jdbcType = type.getJDBCTypes()[0];
         this.type = type;
      }
      else
      {
         if(!tok.hasMoreTokens())
         {
            throw new IllegalArgumentException("Parmeter is a known " +
               "dependent value class, so a property must be supplied");
         }

         // build the propertyName
         StringBuffer propertyName = new StringBuffer(parameterString.length());
         propertyName.append(tok.nextToken());
         while(tok.hasMoreTokens())
         {
            propertyName.append('.').append(tok.nextToken());
         }
         property = ((JDBCTypeComplex)type).getProperty(propertyName.toString());
         jdbcType = property.getJDBCType();
      }
   }

   public QueryParameter(int argNum, JDBCType type)
   {
      this.argNum = argNum;
      this.type = type;
      this.jdbcType = type.getJDBCTypes()[0];
      this.isPrimaryKeyParameter = false;
      initToString();
   }

   public QueryParameter(
      int argNum,
      boolean isPrimaryKeyParameter,
      JDBCFieldBridge field,
      JDBCTypeComplexProperty property,
      int jdbcType)
   {

      this.argNum = argNum;
      this.isPrimaryKeyParameter = isPrimaryKeyParameter;
      this.field = field;
      this.property = property;
      this.jdbcType = jdbcType;

      initToString();
   }

   private void initToString()
   {
      StringBuffer parameterBuf = new StringBuffer();
      parameterBuf.append(argNum);
      if(field != null)
      {
         parameterBuf.append('.').append(field.getFieldName());
      }
      if(property != null)
      {
         parameterBuf.append('.').append(property.getPropertyName());
      }
      parameterString = parameterBuf.toString();
   }

   public void set(Logger log, PreparedStatement ps, int index, Object[] args)
      throws Exception
   {
      Object arg = args[argNum];
      JDBCParameterSetter param;
      if(field != null)
      {
         if(!isPrimaryKeyParameter)
         {
            if(arg instanceof EJBObject)
            {
               arg = ((EJBObject)arg).getPrimaryKey();
            }
            else if(arg instanceof EJBLocalObject)
            {
               arg = ((EJBLocalObject)arg).getPrimaryKey();
            }
            else
            {
               throw new IllegalArgumentException("Expected an instanc of " +
                  "EJBObject or EJBLocalObject, but got an instance of " +
                  arg.getClass().getName());
            }
         }
         arg = field.getPrimaryKeyValue(arg);

         // use mapper
         final JDBCType jdbcType = field.getJDBCType();
         arg = jdbcType.getColumnValue(0, arg);
         param = jdbcType.getParameterSetter()[0];
      }
      else if(property != null)
      {
         arg = property.getColumnValue(arg);
         param = property.getParameterSetter();
      }
      else
      {
         if(type != null)
         {
            arg = type.getColumnValue(0, arg);
            param = type.getParameterSetter()[0];
         }
         else
         {
            param = JDBCUtil.getParameterSetter(jdbcType, arg == null ? null : arg.getClass());
         }
      }

      param.set(ps, index, jdbcType, arg, log);
      //JDBCUtil.setParameter(log, ps, index, jdbcType, arg);
   }

   private static JDBCFieldBridge getCMPField(
      JDBCEntityPersistenceStore manager,
      Class intf,
      String fieldName)
   {
      Catalog catalog = manager.getCatalog();
      JDBCAbstractEntityBridge entityBridge = (JDBCAbstractEntityBridge)catalog.getEntityByInterface(intf);
      if(entityBridge == null)
      {
         throw new IllegalArgumentException("Entity not found in application " +
            "catalog with interface=" + intf.getName());
      }

      JDBCFieldBridge cmpField = (JDBCFieldBridge)entityBridge.getFieldByName(fieldName);
      if(cmpField == null)
      {
         throw new IllegalArgumentException("cmpField not found:" +
            " cmpFieldName=" + fieldName +
            " entityName=" + entityBridge.getEntityName());
      }
      return cmpField;
   }

   public String toString()
   {
      return parameterString;
   }
}
