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

import java.lang.reflect.Method;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.namespace.QName;
import javax.xml.rpc.ParameterMode;
import javax.xml.rpc.holders.Holder;

import org.jboss.ws.Constants;
import org.jboss.ws.WSException;
import org.jboss.ws.core.jaxrpc.ParameterWrapping;
import org.jboss.ws.core.utils.HolderUtils;
import org.jboss.ws.metadata.umdm.FaultMetaData;
import org.jboss.ws.metadata.umdm.OperationMetaData;
import org.jboss.ws.metadata.umdm.ParameterMetaData;
import org.jboss.ws.metadata.umdm.WrappedParameter;
import org.jboss.ws.metadata.wsdl.WSDLUtils;
import org.jboss.ws.tools.ToolsUtils;
import org.jboss.ws.tools.Configuration.OperationConfig;
import org.jboss.wsf.common.JavaUtils;

/**
 * Builds the Tools Endpoint Meta Data using Java Reflection
 *
 * @author <mailto:Anil.Saldhana@jboss.org>Anil Saldhana
 * @since Oct 19, 2005
 */
public class ReflectiveMetaDataBuilder
{
   private Class seiClass = null;

   private ToolsEndpointMetaData tmd = null;

   private String targetNamespace = null;

   private Map<String, List<OperationConfig>> operationMap = null;

   private Map<String, Integer> operationNameCount = new HashMap<String, Integer>();

   public ReflectiveMetaDataBuilder(ToolsEndpointMetaData tmd)
   {
      this.seiClass = tmd.getServiceEndpointInterface();
      checkServiceEndpointInterface();
      this.targetNamespace = tmd.getPortName().getNamespaceURI();
      this.tmd = tmd;
   }

   public void setOperationMap(Map<String, List<OperationConfig>> operationMap)
   {
      this.operationMap = operationMap;
   }

   public ToolsEndpointMetaData generate()
   {
      generateOperationMetaData(seiClass.getMethods());
      return tmd;
   }

   // PRIVATE METHODS
   private void checkServiceEndpointInterface()
   {
      if (seiClass == null)
         throw new IllegalArgumentException("Illegal Null Argument: seiClass");
      if (seiClass.isInterface() == false)
         throw new IllegalArgumentException("Illegal seiClass : not an interface");
      if (! Remote.class.isAssignableFrom(seiClass))
         throw new WSException("A service endpoint interface MUST extend java.rmi.Remote: " + seiClass.getName());
   }

   private FaultMetaData getFaultMetaData(Class exType, OperationMetaData om)
   {
      String exname = WSDLUtils.getInstance().getJustClassName(exType);
      QName xmlName = new QName(tmd.typeNamespace, exname);

      FaultMetaData fm = new FaultMetaData(om, xmlName, xmlName, exType.getName());
      return fm;
   }

   private OperationConfig getOperationConfig(String name, Class<?>[] types)
   {
      if (operationMap == null)
         return null;

      List<OperationConfig> configs = operationMap.get(name);
      if (configs == null)
         return null;

      for (OperationConfig config : configs)
      {
         if (config.params.size() == types.length)
         {
            int i;
            for (i = 0; i < types.length; i++)
            {
               String typeName = config.params.get(i).javaType;
               if (! JavaUtils.getSourceName(types[i]).equals(typeName))
                  break;
            }

            if (i == types.length)
               return config;
         }
      }

      return null;
   }

   private void generateOperationMetaData(Method[] marr)
   {
      if (marr == null)
         throw new WSException("Number of methods in the seiClass is zero");

      for (Method m : marr)
      {
         String methodname = m.getName();
         Class[] paramTypes = m.getParameterTypes();
         int len = paramTypes.length;

         OperationMetaData om = getOperationMetaData(m);
         OperationConfig opc = getOperationConfig(methodname, m.getParameterTypes());
         if (opc != null)
            om.setOneWay(opc.isOneWay);

         ParameterMetaData wrappedParameter = null;
         List<WrappedParameter> wrappedParameters = null;

         if (om.isDocumentWrapped())
         {
            QName xmlName = new QName(tmd.typeNamespace, om.getQName().getLocalPart());
            QName xmlType = xmlName;

            wrappedParameter = new ParameterMetaData(om, xmlName, xmlType, null);
            wrappedParameters = new ArrayList<WrappedParameter>(len);
            wrappedParameter.setWrappedParameters(wrappedParameters);

            om.addParameter(wrappedParameter);

            if (! om.isOneWay())
            {
               xmlType = xmlName = new QName(tmd.typeNamespace, om.getResponseName().getLocalPart());
               ParameterMetaData retMetaData = new ParameterMetaData(om, xmlName, xmlType, null);
               retMetaData.setWrappedParameters(new ArrayList<WrappedParameter>(0));
               om.setReturnParameter(retMetaData);
            }
         }

         for (int i = 0; i < len; i++)
         {
            Class paramType = paramTypes[i];
            /**
             * Test if method param extends java.rmi.Remote
             */
            if (Remote.class.isAssignableFrom(paramType))
               throw new WSException("Param Type " + paramType.getName() + " should not extend java.rmi.Remote");

            if (om.isDocumentWrapped() && !isHeaderParameter(opc, i))
            {
               QName xmlName = getXmlName(paramType, opc, i, null);
               wrappedParameters.add(new WrappedParameter(xmlName, paramType.getName(), convertToProperty(xmlName.getLocalPart()), i));
            }
            else
            {
               om.addParameter(getParameterMetaData(paramType, om, opc, i));
            }
         }

         // Handle return type
         Class returnType = m.getReturnType();
         if (void.class != returnType)
         {
            if (Remote.class.isAssignableFrom(returnType))
               throw new WSException("Return Type " + returnType.getName() + " should not extend java.rmi.Remote");

            if (om.isDocumentWrapped())
            {
               QName name = getReturnXmlName(opc, null);

               WrappedParameter wrapped = new WrappedParameter(name, returnType.getName(), convertToProperty(name.getLocalPart()), -1);
               ParameterMetaData retMetaData = om.getReturnParameter();
               retMetaData.getWrappedParameters().add(wrapped);
            }
            else
            {
               om.setReturnParameter(getParameterMetaDataForReturnType(returnType, om, opc));
            }
         }

         if (om.isDocumentWrapped())
         {
            ParameterWrapping.generateWrapper(wrappedParameter, false);
            if (! om.isOneWay())
               ParameterWrapping.generateWrapper(om.getReturnParameter(), false);
         }

         Class[] exceptionTypes = m.getExceptionTypes();
         boolean remoteExceptionFound = false;
         if (exceptionTypes != null)
         {
            for (int i = 0; i < exceptionTypes.length; i++)
            {
               if (RemoteException.class.isAssignableFrom(exceptionTypes[i]))
                  remoteExceptionFound = true;
               else
                  om.addFault(getFaultMetaData(exceptionTypes[i], om));
            }
         }

         if (! remoteExceptionFound)
            throw new WSException(m.getName() + " does not throw RemoteException.");

         om.assertDocumentBare();
         tmd.addOperation(om);
      }
   }

   private OperationMetaData getOperationMetaData(Method m)
   {
      String methodName = m.getName();
      int count = 0;
      if (operationNameCount.containsKey(methodName))
         count = operationNameCount.get(methodName);

      operationNameCount.put(methodName, ++count);

      String localName = (count > 1) ? methodName + count : methodName;
      OperationMetaData om = new OperationMetaData(tmd, new QName(targetNamespace, localName), methodName);
      om.setSOAPAction("");// FIXME:Default SOAP Action
      return om;
   }

   private boolean isHeaderParameter(OperationConfig config, int index)
   {
      if (config == null)
         return false;

      return config.params.get(index).header;
   }

   private ParameterMetaData getParameterMetaData(Class type, OperationMetaData om, OperationConfig config, int index)
   {
      QName xmlType = ToolsUtils.getXMLType(type, tmd.typeNamespace);
      QName xmlName = getXmlName(type, config, index, om.isDocumentBare() ? om.getQName().getLocalPart() : null);
      ParameterMetaData pm = new ParameterMetaData(om, xmlName, xmlType, type.getName());

      pm.setInHeader(isHeaderParameter(config, index));

      String mode = null;
      if (config != null)
         mode = config.params.get(index).mode;

      boolean holder = Holder.class.isAssignableFrom(type);

      if (holder)
      {
         pm.setJavaTypeName(HolderUtils.getValueType(type).getName());
         if (mode != null)
         {
            if (mode.equals("OUT"))
               pm.setMode(ParameterMode.OUT);
            else if (mode.equals("INOUT"))
               pm.setMode(ParameterMode.INOUT);
         }
         else
         {
            pm.setMode(ParameterMode.INOUT);
         }
      }

      return pm;
   }

   private QName getXmlName(Class type, OperationConfig config, int index, String defaultName)
   {
      if (config != null)
      {
         QName name = config.params.get(index).xmlName;
         if (name != null)
         {
            if ("".equals(name.getNamespaceURI()))
               name = new QName(tmd.typeNamespace, name.getLocalPart());

            return name;
         }
      }

      if (defaultName == null)
    	  defaultName = getDefaultName(type) + "_" + (index + 1);

      QName xmlName = new QName(tmd.typeNamespace, defaultName);
      return xmlName;
   }

   private QName getReturnXmlName(OperationConfig config, String defaultName)
   {
      if (config != null)
      {
         QName name = config.returnXmlName;
         if (name != null)
         {
            if ("".equals(name.getNamespaceURI()))
               name = new QName(tmd.typeNamespace, name.getLocalPart());

            return name;
         }
      }

      if (defaultName == null)
    	  defaultName = Constants.DEFAULT_RPC_RETURN_NAME;

      return new QName(tmd.typeNamespace, defaultName);
   }


   private ParameterMetaData getParameterMetaDataForReturnType(Class type, OperationMetaData om, OperationConfig config)
   {
      QName xmlType = ToolsUtils.getXMLType(type, tmd.typeNamespace);
      QName xmlName = getReturnXmlName(config, om.isDocumentBare() ? om.getResponseName().getLocalPart() : null);
      ParameterMetaData pm = new ParameterMetaData(om, xmlName, xmlType, type.getName());

      return pm;
   }

   private String getDefaultName(Class javaClass)
   {
      String name = "";
      WSDLUtils utils = WSDLUtils.getInstance();
      if (Holder.class.isAssignableFrom(javaClass))
         javaClass = utils.getJavaTypeForHolder(javaClass);
      if (javaClass.isArray())
      {
         name = utils.getMessagePartForArray(javaClass);
      }
      else
         name = utils.getJustClassName(javaClass);
      return name;
   }

   private String convertToProperty(String variable)
   {
      if (Character.isUpperCase(variable.charAt(0)))
      {
         char c = Character.toLowerCase(variable.charAt(0));
         StringBuilder builder = new StringBuilder(variable);
         builder.setCharAt(0, c);
         variable = builder.toString();
      }

      return variable;
   }
}
