/*
 * JBoss, Home of Professional Open Source
 * Copyright 2007, Red Hat Middleware LLC, and individual contributors
 * as indicated by the @author tags.
 * See the copyright.txt in the distribution for a full listing
 * of individual contributors.
 * This copyrighted material is made available to anyone wishing to use,
 * modify, copy, or redistribute it subject to the terms and conditions
 * of the GNU General Public License, v. 2.0.
 * This program is distributed in the hope that it will be useful, but WITHOUT A
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE.  See the GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License,
 * v. 2.0 along with this distribution; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA  02110-1301, USA.
 *
 * (C) 2007,
 * @author JBoss Inc.
 */
package org.jboss.txbridge.ba.helper;

import org.apache.log4j.Logger;
import org.jboss.txbridge.ba.annotation.*;
import org.jboss.txbridge.ba.exception.MethodIncorrectlyAnnotatedException;
import org.jboss.txbridge.ba.exception.CompensationMethodNotAccessible;
import org.jboss.txbridge.ba.service.ServiceDescription;
import org.jboss.txbridge.ba.service.ServiceDescriptionImpl;

import javax.jws.WebMethod;
import java.lang.reflect.Method;
import java.lang.annotation.Annotation;

/**
 * This component visits the service and describes it in terms of its requirements related to
 * participating in Business Activities.
 *
 * TODO: Redesign this class - it would be better to have multiple small and exchangeable classes 
 * than a big one.
 *
 * @author Maciej P. Machulak (mmachulak@redhat.com)
 * @version 0.1
 */
public class BAServiceVisitor
{
    // Logger
    private static Logger log = Logger.getLogger(BAServiceVisitor.class);

    /**
     * This method processes a single method that
     *
     * @param method is the method to be processed.
     * @return the service description.
     * @throws org.jboss.txbridge.ba.exception.MethodIncorrectlyAnnotatedException if the method is not annotated correctly.
     * @throws org.jboss.txbridge.ba.exception.CompensationMethodNotAccessible if the specified compensatiom method is not accessible.
     */
    public static synchronized ServiceDescription processMethod(Method method)
            throws MethodIncorrectlyAnnotatedException, CompensationMethodNotAccessible
    {
        log.info("processMethod()");

        // Create new ServiceDescription
        ServiceDescription serviceDescription = new ServiceDescriptionImpl();

        // Check what is the name of the method...
        log.info("Method name: " + method.getName());
        String methodName = method.getName();
        // ...by default the web method name is the same as the method's name.
        String webMethodName = methodName;

        // Set the class of the method
        Class methodClass = method.getDeclaringClass();
        serviceDescription.setOriginalClass(methodClass);

        // Check if there is a business activity annotation
        Class[] baClass = null;
        Class[] baInterface = null;
        String baJndiName = null;
        String baProviderURL = null;
        BACompensation BACompensationAnnotation = (BACompensation) methodClass.getAnnotation(BACompensation.class);
        if (BACompensationAnnotation != null)
        {
            log.info("BACompensation annotation found.");
            baClass = BACompensationAnnotation.serviceClass();
            log.info("Compensation class: " + baClass);
            baInterface = BACompensationAnnotation.ejbInterface();
            log.info("EJB Interface: " + baInterface);
            baJndiName = BACompensationAnnotation.jndiName();
            log.info("JNDI name: " + baJndiName);
            baProviderURL = BACompensationAnnotation.providerURL();
            log.info("Provider URL: " + baProviderURL);

        }

        // Check if there is a WebMethod annotation
        WebMethod webMethodAnnotation = method.getAnnotation(WebMethod.class);
        if (webMethodAnnotation != null)
        {
            log.info("WebMethod annotation present!");

            // Check if the operation name has been specified
            String name = webMethodAnnotation.operationName();
            if (name != null && !name.equals(""))
            {
                log.info("WebMethod operationName: " + name);
                // If yes... change the name of the web method
                webMethodName = name;
            }
        }
        else
        {
            throw new MethodIncorrectlyAnnotatedException("Method does not have @WebMethod annotation.");
        }

        // Remember names of the method
        serviceDescription.setOriginalMethodName(methodName);
        serviceDescription.setOriginalWebMethodName(webMethodName);

        // Set the parameter types of the original method
        serviceDescription.setOriginalParameterTypes(method.getParameterTypes());



        // Check if this method is a BA service
        BAService baService = method.getAnnotation(BAService.class);
        BAServiceType baServiceType;
        if (baService != null)
        {
            log.info("Method " + methodName + " is a BA service.");

            // Get the type of the service
            baServiceType = baService.type();
            log.info("BAServiceType: " + baServiceType);
            serviceDescription.setServiceType(baServiceType);

            // Get the agreement type
            BAAgreementType baAgreeementType = baService.agreement();
            log.info("BAAgreementType: " + baAgreeementType);
            serviceDescription.setAgreementType(baAgreeementType);
        }
        else
        {
            throw new MethodIncorrectlyAnnotatedException("Method does not have @BAService annotation.");
        }

        // Check the compensation method
        BACompensatedBy baComp = method.getAnnotation(BACompensatedBy.class);
        if (baComp != null)
        {
            log.info("BACompensatedBy annotation present!");

            String compensationMethodName = baComp.value();
            log.info("Compensation method: " + compensationMethodName);
            serviceDescription.setCompensationMethodName(compensationMethodName);

            // Check the compensation type
            log.info("Compensation type: " + baComp.type());
            serviceDescription.setCompensationType(baComp.type());

            // Set parameter match
            log.info("Parameters match: " + baComp.match());
            serviceDescription.setParameterMatch(baComp.match());

            // Check if this is a single compensation
            log.info("Compensation single: " + baComp.single());
            serviceDescription.setCompensationSingle(baComp.single());

            // Get compensation mode
            BACompensationMode compensationMode = baComp.mode();
            serviceDescription.setCompensationMode(baComp.mode());
            log.info("Compensation mode: " + compensationMode);

            // Get information according to the required compensation
            if (compensationMode == BACompensationMode.POJO)
            {
                // We need following information
                // 1) Method name (already obtained)
                // 2) Method's class

                // Getting method's class
                Class compensationMethodClass = baComp.serviceClass()[0];
                if (compensationMethodClass == null)
                {
                    // It can be either local class or a class specified in BACompensation annotation
                    if (baClass != null)
                    {
                        log.info("Compensation method class specified in BA");
                        compensationMethodClass = baClass[0];
                    }
                    else
                    {
                        log.info("Compensation method is in the same class: ");
                        compensationMethodClass = method.getDeclaringClass();
                    }
                }
                else
                {
                    log.info("Compensation class: " + compensationMethodClass);
                }
                serviceDescription.setCompensationClass(compensationMethodClass);

                // Checking if the compensation method exists
                log.info("Checking if compensation method exists");
                Method[] methodList = compensationMethodClass.getMethods();
                boolean methodFound = false;
                for (Method singleMethod : methodList)
                {
                    log.info("Comparing " + compensationMethodName + " against " + singleMethod.getName() + ".");
                    if (singleMethod.getName().equals(compensationMethodName))
                    {
                        // Remember types of parameters
                        log.info("Getting parameter types");
                        Class[] parameterTypes = singleMethod.getParameterTypes();

                        if (parameterTypes != null)
                        {
                            log.info("Compensation method has parameters");

                            log.info("Setting compensation type");
                            serviceDescription.setCompensationParameterTypes(singleMethod.getParameterTypes());

                            // Remember annotations of the parameters of the compensation method
                            log.info("Checking annotations");
                            Object[] parameterAnnotation = new Object[singleMethod.getParameterTypes().length];
                            Annotation[][] parameterAnnotations = singleMethod.getParameterAnnotations();
                            int i = 0;
                            for (Annotation[] annotationArray : parameterAnnotations)
                            {
                                for (Annotation annotation : annotationArray)
                                {
                                    if (annotation instanceof BAParam)
                                    {
                                        log.info("BAParam annotation present for parameter: " + i);
                                        String valueS = ((BAParam)annotation).value();
                                        try
                                        {
                                            Integer valueI = new Integer(valueS);
                                            parameterAnnotation[i] = valueI;
                                        }
                                        catch (NumberFormatException nfe)
                                        {
                                            parameterAnnotation[i] = valueS;
                                        }
                                    }
                                    else
                                    {
                                        log.info("No BAParam annotation for parameter: " + i);
                                    }
                                }
                                i++;
                            }
                            serviceDescription.setCompensationParameterAnnotations(parameterAnnotation);
                        }
                        else
                        {
                            log.info("Compensation method does not take any parameters");
                        }
                        methodFound = true;
                        break;
                    }
                }
                if (!methodFound)
                {
                    throw new CompensationMethodNotAccessible("Compensation method not found.");
                }

            }
            else if (compensationMode == BACompensationMode.EJB)
            {

                // We need following information
                // 1) Method name (already obtained)
                // 2) Method's class (optional)
                // 3) Interface of the EJB
                // 4) JNDI name of the EJB
                // 5) Provider URL (optional)

                // Method's class
                log.info("Getting method's class");
                Class[] compensationMethodClass = baComp.serviceClass();
                boolean baParamAnnotationFound = true;
                if (compensationMethodClass.length == 0)
                {
                    log.info("Method's class not specified");

                    // Class can be either specified in the Business Activity annotation or compensation is remote
                    log.info("Checking if method's class has been specified in BACompensation");
                    if (baClass != null && baClass.length > 0 && baClass[0] != null)
                    {
                        log.info("Compensation class specified in the BACompensation annotation");
                        compensationMethodClass = new Class[1];
                        compensationMethodClass[0] = baClass[0];
                    }
                    else
                    {
                        log.info("Compensation class not specified - remote compensation.");
                    }
                }
                else
                {
                    log.info("Method's class specified");
                }

                if (compensationMethodClass.length > 0)
                {
                    // Compensation class specified
                    log.info("Compensation class: " + compensationMethodClass[0]);

                    // Checking if the compensation method exists
                    log.info("Checking if compensation method exists");
                    Method[] methodList = compensationMethodClass[0].getMethods();
                    boolean methodFound = false;
                    for (Method singleMethod : methodList)
                    {
                        log.info("Comparing " + compensationMethodName + " against " + singleMethod.getName() + ".");
                        if (singleMethod.getName().equals(compensationMethodName))
                        {
                            log.info("Method found!");

                            // Remember types of parameters
                            log.info("Getting parameter types");
                            Class[] parameterTypes = singleMethod.getParameterTypes();

                            if (parameterTypes != null)
                            {
                                log.info("Compensation method has parameters");

                                log.info("Setting compensation type");
                                serviceDescription.setCompensationParameterTypes(singleMethod.getParameterTypes());

                                // Remember annotations of the parameters of the compensation method
                                log.info("Checking annotations");
                                Object[] parameterAnnotation = new Object[singleMethod.getParameterTypes().length];
                                Annotation[][] parameterAnnotations = singleMethod.getParameterAnnotations();
                                int i = 0;
                                for (Annotation[] annotationArray : parameterAnnotations)
                                {
                                    for (Annotation annotation : annotationArray)
                                    {
                                        if (annotation instanceof BAParam)
                                        {
                                            baParamAnnotationFound = true;
                                            log.info("BAParam annotation present for parameter: " + i);
                                            String valueS = ((BAParam)annotation).value();
                                            try
                                            {
                                                Integer valueI = new Integer(valueS);
                                                parameterAnnotation[i] = valueI;
                                            }
                                            catch (NumberFormatException nfe)
                                            {
                                                parameterAnnotation[i] = valueS;
                                            }
                                        }
                                        else
                                        {
                                            log.info("No BAParam annotation for parameter: " + i);
                                        }
                                    }
                                    i++;
                                }
                                serviceDescription.setCompensationParameterAnnotations(parameterAnnotation);
                            }
                            else
                            {
                                log.info("Compensation method does not take any parameters");
                            }
                            methodFound = true;
                            break;
                        }
                    }
                    if (!methodFound)
                    {
                        throw new CompensationMethodNotAccessible("Compensation method not found.");
                    }

                }
                serviceDescription.setCompensationClass(compensationMethodClass[0]);

                // Get the interface
                Class[] ejbInterface = baComp.ejbInterface();
                if (ejbInterface.length == 0)
                {
                    log.info("EJB interface not specified");
                    if (baInterface != null && baInterface.length > 0 && !baInterface[0].equals(""))
                    {
                        log.info("EJB interface specified in BACompensation - storing value");
                        ejbInterface = new Class[1];
                        ejbInterface[0] = baInterface[0];
                    }
                    else
                    {
                        throw new MethodIncorrectlyAnnotatedException("Interface to EJB not specified.");
                    }
                }
                else
                {
                    log.info("EJB Interface specified.");
                }
                if (ejbInterface.length > 0 )
                {
                    // Interface specified
                    log.info("EJB Interface: " + ejbInterface[0]);

                    // Checking if the compensation method exists
                    log.info("Checking if compensation method exists");
                    Method[] methodList = ejbInterface[0].getMethods();
                    boolean methodFound = false;
                    for (Method singleMethod : methodList)
                    {
                        log.info("Comparing " + compensationMethodName + " against " + singleMethod.getName() + ".");
                        if (singleMethod.getName().equals(compensationMethodName))
                        {
                            log.info("Method found!");

                            // Remember types of parameters
                            log.info("Getting parameter types");
                            Class[] parameterTypes = singleMethod.getParameterTypes();

                            if (parameterTypes != null)
                            {
                                log.info("Compensation method has parameters");

                                log.info("Setting compensation type");
                                serviceDescription.setCompensationParameterTypes(singleMethod.getParameterTypes());

                                // Remember annotations of the parameters of the compensation method
                                if (!baParamAnnotationFound)
                                {

                                    log.info("Checking annotations");
                                    Object[] parameterAnnotation = new Object[singleMethod.getParameterTypes().length];
                                    Annotation[][] parameterAnnotations = singleMethod.getParameterAnnotations();
                                    int i = 0;
                                    for (Annotation[] annotationArray : parameterAnnotations)
                                    {
                                        for (Annotation annotation : annotationArray)
                                        {
                                            if (annotation instanceof BAParam)
                                            {
                                                log.info("BAParam annotation present for parameter: " + i);
                                                String valueS = ((BAParam)annotation).value();
                                                try
                                                {
                                                    Integer valueI = new Integer(valueS);
                                                    parameterAnnotation[i] = valueI;
                                                }
                                                catch (NumberFormatException nfe)
                                                {
                                                    parameterAnnotation[i] = valueS;
                                                }
                                            }
                                            else
                                            {
                                                log.info("No BAParam annotation for parameter: " + i);
                                            }
                                        }
                                        i++;
                                    }
                                    serviceDescription.setCompensationParameterAnnotations(parameterAnnotation);
                                }
                            }
                            else
                            {
                                log.info("Compensation method does not take any parameters");
                            }
                            methodFound = true;
                            break;
                        }
                    }
                    if (!methodFound)
                    {
                        throw new CompensationMethodNotAccessible("Compensation method not found.");
                    }

                }
                serviceDescription.setCompensationEjbInterface(ejbInterface[0]);

                // Get the JNDI name of the EJB
                log.info("Getting JNDI name of the EJB");
                String jndiName = baComp.jndiName();
                if (!jndiName.equals(""))
                {
                    log.info("JNDI name: " + jndiName);
                }
                else
                {
                    // Check if the JNDI name has been specified
                    if (baJndiName != null && !baJndiName.equals(""))
                    {
                        jndiName = baJndiName;
                    }
                    else
                    {
                        throw new MethodIncorrectlyAnnotatedException("JNDI name not specified");
                    }
                }
                serviceDescription.setCompensationJNDIName(jndiName);

                // Check if the URL provider has been specified
                String providerURL = baComp.providerURL();
                if (!providerURL.equals(""))
                {
                    log.info("Provider URL: " + providerURL);
                }
                else
                {
                    // Check if the provider URL has been specified
                    if (baProviderURL != null && !baProviderURL.equals(""))
                    {
                        providerURL = baProviderURL;
                    }
                    else
                    {
                        providerURL = null;
                    }
                }
                serviceDescription.setCompensationProviderURL(providerURL);
            }

        }
        else
        {
            log.info("BACompensatedBy annotation missing.");
            log.info("Checking the type of the service.");
            if (baServiceType == BAServiceType.READONLY)
            {
                log.info("Service is read only. BACompensatedBy is not necessary.");
            }
            else
            {
                throw new MethodIncorrectlyAnnotatedException("BACompensatedBy annotation missing.");
            }
        }


        // Remember @BAParam annotation values
        Object[] parameterAnnotation = new Object[method.getParameterTypes().length];
        Annotation[][] parameterAnnotations = method.getParameterAnnotations();
        int i = 0;
        for (Annotation[] annotationArray : parameterAnnotations)
        {
            for (Annotation annotation : annotationArray)
            {
                if (annotation instanceof BAParam)
                {
                    log.info("BAParam annotation present for parameter: " + i);
                    String valueS = ((BAParam)annotation).value();
                    try
                    {
                        Integer valueI = new Integer(valueS);
                        parameterAnnotation[i] = valueI;
                    }
                    catch (NumberFormatException nfe)
                    {
                        parameterAnnotation[i] = valueS;
                    }
                }
            }
            i++;
        }
        serviceDescription.setOriginalParameterAnnotations(parameterAnnotation);

        // Remember @BAResult annotation value
        BAResult baResult = method.getAnnotation(BAResult.class);
        Integer returnId;
        if (baResult != null)
        {
            log.info("BAResult annotation present!");
            String returnName = baResult.value();
            if (!returnName.equals(""))
            {
                // Check if this is a numerical identifier or not
                try
                {
                    returnId = new Integer(returnName);
                    log.info("Return identifier: " + returnId);
                    serviceDescription.setOriginalReturnId(returnId);
                }
                catch (NumberFormatException nfe)
                {
                    log.info("Return identifier: " + returnName);
                    serviceDescription.setOriginalReturnId(returnName);
                }
            }
            else
            {
                throw new MethodIncorrectlyAnnotatedException("BAResult annotation has a wrong identifier.");
            }

        }

        // Return the service description
        return serviceDescription;
    }

}
