/*
 * JBoss, Home of Professional Open Source
 * Copyright 2005, JBoss Inc., and individual contributors as indicated
 * by the @authors tag. See the copyright.txt 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.ws.core.jaxrpc;

// $Id: ParameterWrapping.java 4241 2007-08-07 19:17:32Z heiko.braun@jboss.com $

import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtField;
import javassist.CtNewMethod;
import javassist.LoaderClassPath;
import javassist.Modifier;

import javax.jws.soap.SOAPBinding.ParameterStyle;
import javax.xml.namespace.QName;

import org.jboss.logging.Logger;
import org.jboss.ws.WSException;
import org.jboss.ws.core.binding.TypeMappingImpl;
import org.jboss.ws.core.jaxrpc.binding.JBossXBDeserializerFactory;
import org.jboss.ws.core.jaxrpc.binding.JBossXBSerializerFactory;
import org.jboss.ws.core.soap.Style;
import org.jboss.ws.core.utils.HolderUtils;
import org.jboss.ws.metadata.umdm.EndpointMetaData;
import org.jboss.ws.metadata.umdm.OperationMetaData;
import org.jboss.ws.metadata.umdm.ParameterMetaData;
import org.jboss.ws.metadata.umdm.ServiceMetaData;
import org.jboss.ws.metadata.umdm.TypeMappingMetaData;
import org.jboss.ws.metadata.umdm.TypesMetaData;
import org.jboss.ws.metadata.umdm.WrappedParameter;
import org.jboss.wsf.common.JavaUtils;

/** A helper class to wrap/unwrap ducument style request/response structures.
 *
 * @author Thomas.Diesler@jboss.org
 * @author <a href="mailto:jason.greene@jboss.com">Jason T. Greene</a>
 * @since 06-Jun-2005
 */
public class ParameterWrapping
{
   // provide logging
   private static Logger log = Logger.getLogger(ParameterWrapping.class);

   // This assertion should probably be moved somewhere earlier
   private static void assertOperationMetaData(OperationMetaData opMetaData)
   {
      if (opMetaData.getStyle() != Style.DOCUMENT)
         throw new WSException("Unexpected style: " + opMetaData.getStyle());

      if (opMetaData.getParameterStyle() != ParameterStyle.WRAPPED)
         throw new WSException("Unexpected parameter style: " + opMetaData.getParameterStyle());
   }

   private static Object holderValue(Object holder)
   {
      if (holder == null)
         return null;
      if (! HolderUtils.isHolderType(holder.getClass()))
         return holder;

      return HolderUtils.getHolderValue(holder);
   }

   public static Class getWrappedType(String variable, Class wrapperType)
   {
      try
      {
         PropertyDescriptor pd = new PropertyDescriptor(variable, wrapperType);
         Method method = pd.getWriteMethod();
         return method.getParameterTypes()[0];
      }
      catch (Exception ex)
      {
         if(log.isDebugEnabled()) log.debug("Invalid request wrapper: " + ex);
         return null;
      }
   }

   public static Object wrapRequestParameters(ParameterMetaData request, Object[] methodParams)
   {
      assertOperationMetaData(request.getOperationMetaData());

      Class reqStructType = request.getJavaType();
      if(log.isDebugEnabled()) log.debug("wrapRequestParameters: " + reqStructType.getName());
      List<WrappedParameter> wrappedParameters = request.getWrappedParameters();
      try
      {
         Object reqStruct = reqStructType.newInstance();
         for (WrappedParameter param : wrappedParameters)
         {
            Object value = holderValue(methodParams[param.getIndex()]);
            param.accessor().set(reqStruct, value);
         }

         return reqStruct;
      }
      catch (RuntimeException rte)
      {
         throw rte;
      }
      catch (Exception e)
      {
         throw new WSException("Cannot wrap request structure: " + e);
      }
   }

   public static Map<Integer, Object> unwrapRequestParameters(ParameterMetaData request, Object reqStruct, Object[] methodParams)
   {
      OperationMetaData opMetaData = request.getOperationMetaData();
      assertOperationMetaData(opMetaData);

      if (reqStruct == null)
         throw new IllegalArgumentException("Request struct cannot be null");

      Class[] targetParameterTypes = opMetaData.getJavaMethod().getParameterTypes();
      Map<Integer, Object> outParameters = new HashMap<Integer, Object>(targetParameterTypes.length);
      List<WrappedParameter> wrappedParameters = request.getWrappedParameters();
      Class reqStructType = reqStruct.getClass();

      if(log.isDebugEnabled()) log.debug("unwrapRequestParameters: " + reqStructType.getName());
      try
      {
         for (WrappedParameter param : wrappedParameters)
         {
            Class targetType = targetParameterTypes[param.getIndex()];
            Object value = param.accessor().get(reqStruct);

            // INOUT Parameter
            if (HolderUtils.isHolderType(targetType))
            {
               value = HolderUtils.createHolderInstance(value, targetType);
               outParameters.put(param.getIndex(), value);
            }

            methodParams[param.getIndex()] = value;
         }
      }
      catch (RuntimeException rte)
      {
         throw rte;
      }
      catch (Exception e)
      {
         throw new IllegalArgumentException("Cannot unwrap request structure: " + e);
      }

      return outParameters;
   }

   public static Object wrapResponseParameters(ParameterMetaData returnMetaData, Object returnValue, Map<Integer, Object> outParameters)
   {
      assertOperationMetaData(returnMetaData.getOperationMetaData());

      Class resStructType = returnMetaData.getJavaType();
      if (returnValue != null && returnValue.getClass() == resStructType)
      {
         if(log.isDebugEnabled()) log.debug("Response parameter already wrapped" + resStructType.getName());
         return returnValue;
      }

      if(log.isDebugEnabled()) log.debug("wrapResponseParameter: " + resStructType.getName());
      List<WrappedParameter> wrappedParameters = returnMetaData.getWrappedParameters();
      try
      {
         Object resStruct = resStructType.newInstance();

         for (WrappedParameter param : wrappedParameters)
         {
            Object value = (param.getIndex() < 0) ? returnValue : holderValue(outParameters.get(param.getIndex()));          
            param.accessor().set(resStruct, value);
         }
         return resStruct;
      }
      catch (RuntimeException rte)
      {
         throw rte;
      }
      catch (Exception e)
      {
         throw new WSException("Cannot wrap response structure: " + e);
      }
   }

   public static Object unwrapResponseParameters(ParameterMetaData retMetaData, Object resStruct, Object methodParams[])
   {
      OperationMetaData operationMetaData = retMetaData.getOperationMetaData();
      assertOperationMetaData(operationMetaData);

      Object retValue = null;
      if (resStruct != null)
      {
         Class resStructType = resStruct.getClass();

         if(log.isDebugEnabled()) log.debug("unwrapResponseParameter: " + resStructType.getName());
         List<WrappedParameter> wrappedParameters = retMetaData.getWrappedParameters();
         Class[] targetTypes = operationMetaData.getJavaMethod().getParameterTypes();
         try
         {
            for (WrappedParameter param : wrappedParameters)
            {
               Object value = param.accessor().get(resStruct);
               if (param.getIndex() < 0)
               {
                  retValue = value;
               }
               else
               {
                  Class targetType = targetTypes[param.getIndex()];
                  if (HolderUtils.isHolderType(targetType))
                     HolderUtils.setHolderValue(methodParams[param.getIndex()], value);
                  methodParams[param.getIndex()] = value;
               }
            }
         }
         catch (RuntimeException rte)
         {
            throw rte;
         }
         catch (Exception e)
         {
            throw new IllegalArgumentException("Cannot unwrap request structure: " + e);
         }
      }
      return retValue;
   }

   /**
    * This is just a dummy marker class used to identify a generated
    * document/literal wrapped type
    *
    * @author <a href="mailto:jason.greene@jboss.com">Jason T. Greene</a>
    * @version $Revision: 4241 $
    */
   public static class WrapperType
   {
   }

   /**
    * Generates a wrapper type and assigns it to the passed ParameterMetaData
    * object. This routine requires the pmd to contain completed wrappedTypes
    * and wrappedVariables properties of the passed ParameterMetaData object.
    *
    * @param pmd a document/literal wrapped parameter
    */
   public static void generateWrapper(ParameterMetaData pmd, boolean addTypeMapping)
   {
      List<WrappedParameter> wrappedParameters = pmd.getWrappedParameters();
      OperationMetaData operationMetaData = pmd.getOperationMetaData();
      EndpointMetaData endpointMetaData = operationMetaData.getEndpointMetaData();
      ServiceMetaData serviceMetaData = endpointMetaData.getServiceMetaData();
      ClassLoader loader = serviceMetaData.getUnifiedMetaData().getClassLoader();

      if (operationMetaData.isDocumentWrapped() == false)
         throw new WSException("Operation is not document/literal (wrapped)");

      if (wrappedParameters == null)
         throw new WSException("Cannot generate a type when their is no wrapped parameters");

      String serviceName = serviceMetaData.getServiceName().getLocalPart();
      String parameterName = pmd.getXmlName().getLocalPart();
      String endpointName = endpointMetaData.getPortName().getLocalPart();
      String packageName = endpointMetaData.getServiceEndpointInterface().getPackage().getName();

      String wrapperName = packageName + "._JBossWS_" + serviceName + "_" + endpointName + "_" + parameterName;
      if(log.isDebugEnabled()) log.debug("Generating wrapper: " + wrapperName);

      Class wrapperType;
      try
      {
         ClassPool pool = new ClassPool(true);
         pool.appendClassPath(new LoaderClassPath(loader));
         CtClass clazz = pool.makeClass(wrapperName);
         clazz.setSuperclass(pool.get(WrapperType.class.getName()));

         for (WrappedParameter param : wrappedParameters)
         {
            CtField field = new CtField(pool.get(param.getType()), param.getVariable(), clazz);
            field.setModifiers(Modifier.PRIVATE);
            clazz.addField(field);
            clazz.addMethod(CtNewMethod.getter("get" + JavaUtils.capitalize(param.getVariable()), field));
            clazz.addMethod(CtNewMethod.setter("set" + JavaUtils.capitalize(param.getVariable()), field));
         }

         wrapperType = (Class)pool.toClass(clazz, loader);
      }
      catch (Exception e)
      {
         throw new WSException("Could not generate wrapper type: " + wrapperName, e);
      }

      // Register type mapping if needed
      if (addTypeMapping)
      {
         QName xmlType = pmd.getXmlType();

         TypesMetaData typesMetaData = serviceMetaData.getTypesMetaData();
         TypeMappingMetaData tmMetaData = new TypeMappingMetaData(typesMetaData, xmlType, wrapperName);
         typesMetaData.addTypeMapping(tmMetaData);

         TypeMappingImpl typeMapping = serviceMetaData.getTypeMapping();
         typeMapping.register(wrapperType, xmlType, new JBossXBSerializerFactory(), new JBossXBDeserializerFactory());
      }

      pmd.setJavaTypeName(wrapperName);
   }
}