/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 1999-2004 Bull S.A.
 * Contact: jonas-team@ow2.org
 *
 * This library 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 any later version.
 *
 * This library 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 library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 * USA
 *
 * --------------------------------------------------------------------------
 * $Id$
 * --------------------------------------------------------------------------
 */

package org.ow2.jonas.generators.genic;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Date;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Vector;

import org.ow2.jonas.Version;
import org.ow2.jonas.deployment.ejb.BeanDesc;
import org.ow2.jonas.deployment.ejb.EjbRelationshipRoleDesc;
import org.ow2.jonas.deployment.ejb.EntityCmp1Desc;
import org.ow2.jonas.deployment.ejb.EntityCmp2Desc;
import org.ow2.jonas.deployment.ejb.EntityDesc;
import org.ow2.jonas.deployment.ejb.FieldDesc;
import org.ow2.jonas.deployment.ejb.SessionStatelessDesc;
import org.ow2.jonas.lib.ejb21.JavaType;
import org.ow2.jonas.lib.util.BeanNaming;

import org.apache.velocity.VelocityContext;




import org.objectweb.jorm.metainfo.api.Manager;


/**
 * This class allows to create the Velocity Context used to build the generated
 * sources with the Velocity templates.
 *
 * @author: Helene Joanin (Bull) : Initial developer
 */

public class VContextFactory {

    /**
     * Creates the Velocity Context used to build the generated sources with the Velocity templates.
     * There are different types of Velocity Context :
     *   HOME, LOCAL_HOME, REMOTE, LOCAL, ENTITY_HANDLE, ENTITY_CMP_JDBC, ...
     * @see Source.java for type definitions.
     * @param dd The bean Deployment Descriptor
     * @param srcType the type of Velocity Context
     * @return Return the Velocity Context associated to the gievn bean for the given source to generate
     * @throws GenICException in error case
     */
    public static VelocityContext create(BeanDesc dd, int srcType, Manager mgr) throws GenICException {

        String value = null;
        VelocityContext vc = new VelocityContext();

        vc.put("dd", dd);
        vc.put("EntityCMP2", (dd instanceof EntityCmp2Desc ? Boolean.TRUE : Boolean.FALSE));

        vc.put("jVersion", Version.getNumber());

        if (dd.getHomeClass() != null) {
            value = dd.getHomeClass().getName();
            vc.put("home", value);
        }

        if (dd.getLocalHomeClass() != null) {
            value = dd.getLocalHomeClass().getName();
            vc.put("localhome", value);
        }

        if (dd.getRemoteClass() != null) {
            value = dd.getRemoteClass().getName();
            vc.put("remote", value);
        }

        if (dd.getLocalClass() != null) {
            value = dd.getLocalClass().getName();
            vc.put("local", value);
        }

        if (dd instanceof SessionStatelessDesc) {
            SessionStatelessDesc ssd = (SessionStatelessDesc) dd;
            if (ssd.getServiceEndpointClass() != null) {
                value = ssd.getServiceEndpointClass().getName();
                vc.put("serviceEndpoint", value);
            }
            if (ssd.getFullWrpServiceEndpointName() != null) {
                value = ssd.getFullWrpServiceEndpointName();
                vc.put("service-endpoint_wrp", value);
            }
            if (ssd.getFullWrpSEHomeName() != null) {
                value = ssd.getFullWrpSEHomeName();
                vc.put("service-endpoint-home_wrp", value);
            }
        }

        if (dd.getFullWrpHomeName() != null) {
            value = dd.getFullWrpHomeName();
            vc.put("home_wrp", value);
        }

        if (dd.getFullWrpLocalHomeName() != null) {
            value = dd.getFullWrpLocalHomeName();
            vc.put("localhome_wrp", value);
        }

        if (dd.getFullWrpRemoteName() != null) {
            value = dd.getFullWrpRemoteName();
            vc.put("remote_wrp", value);
        }

        if (dd.getFullWrpLocalName() != null) {
            value = dd.getFullWrpLocalName();
            vc.put("local_wrp", value);
        }

        value = dd.getEjbClass().getName();
        vc.put("bean", value);

        value = dd.getFullDerivedBeanName();
        vc.put("bean_wrp", value);    // Same as "bean" if not a Entity CMP

        if (dd.getFullWrpHandleName() != null) {
            value = dd.getFullWrpHandleName();
            vc.put("handle", value);
        }

        vc.put("isClusterReplicated", Boolean.valueOf(dd.isClusterReplicated()));

        if (dd instanceof EntityDesc) {
            Class c = ((EntityDesc) dd).getPrimaryKeyClass();
            if (c.equals(Object.class)) {
                // Auto key generated so convert into Integer
                c = Integer.class;
            }
            value = c.getName();
            vc.put("pk", value);
            vc.put("pkIsUserClass", pkIsUserClass(value));
            if (dd instanceof EntityCmp2Desc) {
                if (c.equals(String.class)) {
                    vc.put("pkEncodeMethod", "encodeString");
                    vc.put("pkDecodeMethod", "decodeString((java.lang.String) pk)");
                    vc.put("pkMappingToMemoryMethod", "");
                } else if (c.equals(Character.class)) {
                    vc.put("pkEncodeMethod", "encodeOchar");
                    vc.put("pkDecodeMethod", "decodeOchar((java.lang.Character) pk)");
                    vc.put("pkMappingToMemoryMethod", "");
                } else if (c.equals(Byte.class)) {
                    vc.put("pkEncodeMethod", "encodeObyte");
                    vc.put("pkDecodeMethod", "decodeObyte((java.lang.Byte) pk)");
                    vc.put("pkMappingToMemoryMethod", "");
                } else if (c.equals(Short.class)) {
                    vc.put("pkEncodeMethod", "encodeOshort");
                    vc.put("pkDecodeMethod", "decodeOshort((java.lang.Short) pk)");
                    vc.put("pkMappingToMemoryMethod", "");
                } else if (c.equals(Integer.class)) {
                    vc.put("pkEncodeMethod", "encodeOint");
                    vc.put("pkDecodeMethod", "decodeOint((java.lang.Integer) pk)");
                    vc.put("pkMappingToMemoryMethod", "");
                } else if (c.equals(Long.class)) {
                    vc.put("pkEncodeMethod", "encodeOlong");
                    vc.put("pkDecodeMethod", "decodeOlong((java.lang.Long) pk)");
                    vc.put("pkMappingToMemoryMethod", "");
                } else if (c.equals(Date.class)) {
                    vc.put("pkEncodeMethod", "encodeDate");
                    vc.put("pkDecodeMethod", "decodeDate((java.util.Date) pk)");
                    vc.put("pkMappingToMemoryMethod", "");
                } else if (c.equals(Float.class)) {
                    vc.put("pkEncodeMethod", "encodeString");
                    vc.put("pkDecodeMethod", "decodeString((java.lang.String) org.ow2.jonas.lib.ejb21.jorm.FloatPkFieldMapping.toStorage(pk))");
                    vc.put("pkMappingToMemoryMethod", "org.ow2.jonas.lib.ejb21.jorm.FloatPkFieldMapping.toMemory");
                }
            }
        }

        switch (srcType) {
        case Source.HOME:
            value = BeanNaming.getPackageName(dd.getFullWrpHomeName());
            vc.put("package", value);
            value = dd.getWrpHomeName();
            vc.put("class", value);
            break;
        case Source.LOCAL_HOME:
            value = BeanNaming.getPackageName(dd.getFullWrpLocalHomeName());
            vc.put("package", value);
            value = dd.getWrpLocalHomeName();
            vc.put("class", value);
            break;
        case Source.REMOTE:
            value = BeanNaming.getPackageName(dd.getFullWrpRemoteName());
            vc.put("package", value);
            value = dd.getWrpRemoteName();
            vc.put("class", value);
            break;
        case Source.LOCAL:
            value = BeanNaming.getPackageName(dd.getFullWrpLocalName());
            vc.put("package", value);
            value = dd.getWrpLocalName();
            vc.put("class", value);
            break;
        case Source.SERVICE_ENDPOINT:
            SessionStatelessDesc ssd = (SessionStatelessDesc) dd;
            value = BeanNaming.getPackageName(ssd.getFullWrpServiceEndpointName());
            vc.put("package", value);
            value = ssd.getWrpServiceEndpointName();
            vc.put("class", value);
            break;
        case Source.SERVICE_ENDPOINT_HOME:
            SessionStatelessDesc ssd1 = (SessionStatelessDesc) dd;
            value = BeanNaming.getPackageName(ssd1.getFullWrpSEHomeName());
            vc.put("package", value);
            value = ssd1.getWrpSEHomeName();
            vc.put("class", value);
            break;
        case Source.ENTITY_HANDLE:
            value = BeanNaming.getPackageName(dd.getFullWrpHandleName());
            vc.put("package", value);
            value = dd.getWrpHandleName();
            vc.put("class", value);
            break;
        case Source.ENTITY_CMP_JDBC:
            value = BeanNaming.getPackageName(dd.getFullDerivedBeanName());
            vc.put("package", value);
            value = dd.getDerivedBeanName();
            vc.put("class", value);
            break;
        default:
            break;
        }

        ArrayList prototypeMethodList = new ArrayList();
        ArrayList vcMethodList = new ArrayList();
        Method [] methods = null;
        switch (srcType) {
        case Source.HOME:
            /*
             * Add in the methods list, the Home interface methods, except
             * - methods (other than remove) defined in javax.ejb.EJBHome,
             * - and overriding methods.
             */
            methods = dd.getHomeClass().getMethods();
            for (int i = 0; i < methods.length; i++) {
                Method method = methods[i];
                if (!method.getDeclaringClass().equals(javax.ejb.EJBHome.class)
                    || "remove".equals(method.getName())) {
                    String pMeth = convertMethod2String(method);
                    if (!prototypeMethodList.contains(pMeth)) {
                        VcMethod vm = new VcMethod(method, dd.getMethodDesc(method), dd, mgr);
                        vcMethodList.add(vm);
                        prototypeMethodList.add(pMeth);
                    }
                }
            }
            break;
        case Source.LOCAL_HOME:
            /*
             * Add in the methods list, the LocalHome interface methods, except
             * - methods (other than remove) defined in javax.ejb.EJBLocalHome,
             * - and overriding methods.
             */
            methods = dd.getLocalHomeClass().getMethods();
            for (int i = 0; i < methods.length; i++) {
                Method method = methods[i];
                if (!method.getDeclaringClass().equals(javax.ejb.EJBLocalHome.class)
                    || "remove".equals(method.getName())) {
                    String pMeth = convertMethod2String(method);
                    if (!prototypeMethodList.contains(pMeth)) {
                        VcMethod vm = new VcMethod(method, dd.getMethodDesc(method), dd, mgr);
                        vcMethodList.add(vm);
                        prototypeMethodList.add(pMeth);
                    }
                }
            }
            break;
        case Source.REMOTE:
            /*
             * Add in the methods list, the Remote interface methods, except
             * - methods (other than remove) defined in javax.ejb.EJBObject,
             * - and overriding methods.
             */
            methods = dd.getRemoteClass().getMethods();
            for (int i = 0; i < methods.length; i++) {
                Method method = methods[i];
                if (!method.getDeclaringClass().equals(javax.ejb.EJBObject.class)
                    || "remove".equals(method.getName())) {
                    String pMeth = convertMethod2String(method);
                    if (!prototypeMethodList.contains(pMeth)) {
                        VcMethod vm = new VcMethod(method, dd.getMethodDesc(method), dd, mgr);
                        vcMethodList.add(vm);
                        prototypeMethodList.add(pMeth);
                    }
                }
            }
            break;
        case Source.LOCAL:
            /*
             * Add in the methods list, the Local interface methods, except
             * - methods (other than remove) defined in javax.ejb.EJBLocalObject,
             * - and overriding methods.
             */
            methods = dd.getLocalClass().getMethods();
            for (int i = 0; i < methods.length; i++) {
                Method method = methods[i];
                if (!method.getDeclaringClass().equals(javax.ejb.EJBLocalObject.class)
                    || "remove".equals(method.getName())) {
                    String pMeth = convertMethod2String(method);
                    if (!prototypeMethodList.contains(pMeth)) {
                        VcMethod vm = new VcMethod(method, dd.getMethodDesc(method), dd, mgr);
                        vcMethodList.add(vm);
                        prototypeMethodList.add(pMeth);
                    }
                }
            }
            break;
        case Source.SERVICE_ENDPOINT:
            /*
             * Add in the methods list, the ServiceEndpoint interface methods
             */
            SessionStatelessDesc ssd = (SessionStatelessDesc) dd;
            methods = ssd.getServiceEndpointClass().getMethods();
            for (int i = 0; i < methods.length; i++) {
                Method method = methods[i];
                String pMeth = convertMethod2String(method);
                if (!prototypeMethodList.contains(pMeth)) {
                    VcMethod vm = new VcMethod(method, ssd.getMethodDesc(method), dd, mgr);
                    vcMethodList.add(vm);
                    prototypeMethodList.add(pMeth);
                }
            }
            break;
        case Source.ENTITY_CMP_JDBC:
            /*
             * Add in the methods list, the Home and LocalHome interfaces methods, except
             * - methods defined in javax.ejb.EJBHome and javax.ejb.EJBLocalHome and
             * - and overriding methods.
             * Same methods may be defined both in the Home interface and in the
             * LocalHome interface. Don't add twice this method in the vcMethodList !!!
             *
             * Futhermore, in case of create() methods, methods added in the vcMethodList
             * are the ejbCreate() associated bean's methods.
             */
            if (dd.getHomeClass() != null) {
                methods = dd.getHomeClass().getMethods();
                for (int i = 0; i < methods.length; i++) {
                    Method method = methods[i];
                    if (!method.getDeclaringClass().equals(javax.ejb.EJBHome.class)) {
                        String pMeth = convertMethod2String(method);
                        if (!prototypeMethodList.contains(pMeth)) {
                            VcMethod vm = null;
                            if (method.getName().startsWith("create")) {
                                Method beanMethod = getBeanMethod(method, dd.getEjbClass());
                                vm = new VcMethod(beanMethod, dd.getMethodDesc(method), dd, mgr);
                                // add the ejbPostCreate method
                                Method m = getEjbPostCreateMethod(method, dd.getEjbClass());
                                VcMethod vm2 = new VcMethod(m, dd.getMethodDesc(method), dd, mgr);
                                vcMethodList.add(vm2);
                            } else {
                                vm = new VcMethod(method, dd.getMethodDesc(method), dd, mgr);
                            }
                            vcMethodList.add(vm);
                            prototypeMethodList.add(pMeth);
                        }
                    }
                }
            }
            if (dd.getLocalHomeClass() != null) {
                methods = dd.getLocalHomeClass().getMethods();
                for (int i = 0; i < methods.length; i++) {
                    Method method = methods[i];
                    if (!method.getDeclaringClass().equals(javax.ejb.EJBLocalHome.class)) {
                        String pMeth = convertMethod2String(method);
                        if (!prototypeMethodList.contains(pMeth)) {
                            VcMethod vm = null;
                            if (method.getName().startsWith("create")) {
                                Method beanMethod = getBeanMethod(method, dd.getEjbClass());
                                vm = new VcMethod(beanMethod, dd.getMethodDesc(method), dd, mgr);
                                // add the ejbPostCreate method
                                Method m = getEjbPostCreateMethod(method, dd.getEjbClass());
                                VcMethod vm2 = new VcMethod(m, dd.getMethodDesc(method), dd, mgr);
                                vcMethodList.add(vm2);
                            } else {
                                vm = new VcMethod(method, dd.getMethodDesc(method), dd, mgr);
                            }
                            vcMethodList.add(vm);
                            prototypeMethodList.add(pMeth);
                        }
                    }
                }
            }

            /*
             * Add the
             * - setEntityContext(javax.ejb.EntityContext),
             * - ejbActivate(),
             * - ejbLoad(),
             * - ejbStore(),
             * - ejbRemove()
             * bean's methods.
             */
            try {
                Class[] params = {javax.ejb.EntityContext.class};
                Method beanMethod = dd.getEjbClass().getMethod("setEntityContext",
                                                               params);
                VcMethod vm = new VcMethod(beanMethod, null, dd, mgr);
                vcMethodList.add(vm);
            } catch (Exception e) {
                throw new Error("setEntityContext(javax.ejb.EntityContext) method not defined in "
                                + dd.getEjbClass().getName());
            }
            try {
                Method beanMethod = dd.getEjbClass().getMethod("ejbActivate", new Class[0]);
                VcMethod vm = new VcMethod(beanMethod, null, dd, mgr);
                vcMethodList.add(vm);
            } catch (Exception e) {
                throw new Error("ejbActivate() method not defined in "
                                + dd.getEjbClass().getName());
            }
            try {
                Method beanMethod = dd.getEjbClass().getMethod("ejbLoad", new Class[0]);
                VcMethod vm = new VcMethod(beanMethod, null, dd, mgr);
                vcMethodList.add(vm);
            } catch (Exception e) {
                throw new Error("ejbLoad() method not defined in "
                                + dd.getEjbClass().getName());
            }
            try {
                Method beanMethod = dd.getEjbClass().getMethod("ejbStore", new Class[0]);
                VcMethod vm = new VcMethod(beanMethod, null, dd, mgr);
                vcMethodList.add(vm);
            } catch (Exception e) {
                throw new Error("ejbStore() method not defined in "
                                + dd.getEjbClass().getName());
            }
            try {
                Method beanMethod = dd.getEjbClass().getMethod("ejbRemove", new Class[0]);
                VcMethod vm = new VcMethod(beanMethod, null, dd, mgr);
                vcMethodList.add(vm);
            } catch (Exception e) {
                throw new Error("ejbRemove() method not defined in "
                                + dd.getEjbClass().getName());
            }

            if (dd instanceof EntityCmp2Desc) {
                /*
                 * Add the ejbSelect() methods defined in the bean.
                 */
                Method[] bMeths = dd.getEjbClass().getMethods();
                for (int i = 0; i < bMeths.length; i++) {
                    if (bMeths[i].getName().startsWith("ejbSelect")) {
                        VcMethod vm = new VcMethod(bMeths[i], dd.getMethodDesc(bMeths[i]), dd, mgr);
                        vcMethodList.add(vm);
                    }
                }
            }
            break;
        default:
            break;
        }
        vc.put("methodList", new Vector(vcMethodList));

        ArrayList vcFieldList = new ArrayList();
        ArrayList vcFieldPkList = new ArrayList();
        ArrayList vcFieldNoPkList = new ArrayList();
        if (dd instanceof EntityCmp1Desc) {
            EntityCmp1Desc edd = (EntityCmp1Desc) dd;
            for (Iterator i = edd.getCmpFieldDescIterator(); i.hasNext();) {
                 FieldDesc fd = (FieldDesc) i.next();
                 VcField vcf = new VcField(fd.getName(), fd.getFieldType(), fd, true);
                 vcFieldList.add(vcf);
                 if (fd.isPrimaryKey()) {
                     vcFieldPkList.add(vcf);
                 } else {
                     vcFieldNoPkList.add(vcf);
                 }
            }
        } else if (dd instanceof EntityCmp2Desc) {
            EntityCmp2Desc edd = (EntityCmp2Desc) dd;
            // Define the CMP fields
            Iterator it = edd.getCmpFieldDescIterator();
            while (it.hasNext()) {
                FieldDesc fd = (FieldDesc) it.next();
                VcField vcf = new VcField(fd);
                vcFieldList.add(vcf);
                if (fd.isPrimaryKey()) {
                    vcFieldPkList.add(vcf);
                } else {
                    vcFieldNoPkList.add(vcf);
                }
            }
            // Define the CMR fields
            Hashtable cmrFields = new Hashtable();
            for (Iterator i = edd.getEjbRelationshipRoleDescIterator(); i.hasNext();) {
                EjbRelationshipRoleDesc rsr = (EjbRelationshipRoleDesc) i.next();
                String cmrname = rsr.getCmrFieldName();
                if (!cmrFields.containsKey(cmrname)) {
                    cmrFields.put(cmrname, new VcCMRField(rsr));
                }
            }
            vc.put("cmrList", cmrFields.values());
        }

        vc.put("fieldList", new Vector(vcFieldList));
        vc.put("fieldPkList", new Vector(vcFieldPkList));
        vc.put("fieldNoPkList", new Vector(vcFieldNoPkList));

        return vc;
    }

    /**
     * Convert a method to a string depending on its name and its parameters list.
     * @param meth method to convert
     * @return Return a string representation of the given method
     */
    private static String convertMethod2String(Method meth) {
        String value = new String(meth.getName());
        Class[] params = meth.getParameterTypes();
        value = value + "_" + params.length;
        for (int p = 0; p < params.length; p++) {
            value = value + "_" + JavaType.getName(params[p]);
        }
        return (value);
    }

    /**
     * @param method home.create() method
     * @param bean class of the bean
     * @return Return the associated bean's method ejbPostCreate matching the create method of the interface
     */
    private static Method getEjbPostCreateMethod(Method method, Class bean) {
        Method beanMethod;
        String beanMethodName = "ejbPost" + BeanNaming.firstToUpperCase(method.getName());
        try {
            beanMethod = bean.getMethod(beanMethodName, method.getParameterTypes());
        } catch (Exception e) {
            throw new Error("No associated ejbPostCreate method for the interface method " + method.toString());
        }
        return beanMethod;
    }

    /**
     * Return the associated bean's method of the given interface's method, ie
     * - ejbRemove(..) for [home|object]remove(..)
     * - ejbCreateXxx(..) for home.createXxx(..)
     * - ejbFindXxx(..) for home.findXxx(..)
     * - ejbHomeXxx(..) for home.xxx(..)
     * - xxx(..) for object.xxx(..)
     * @param method interface method
     * @param bean class of the bean
     * @return Return the associated bean's method to the interface method
     */
    private static Method getBeanMethod(Method method, Class bean) {

        String methodName = method.getName();
        Method beanMethod;
        boolean isMethodHome =
            javax.ejb.EJBHome.class.isAssignableFrom(method.getDeclaringClass())
            || javax.ejb.EJBLocalHome.class.isAssignableFrom(method.getDeclaringClass());

        String beanMethodName = methodName;
        if ("remove".equals(methodName)) {
            beanMethodName = "ejbRemove";
        } else if (isMethodHome) {
            if (methodName.startsWith("create") || methodName.startsWith("find")) {
                beanMethodName = "ejb" + BeanNaming.firstToUpperCase(methodName);
            } else {
                beanMethodName = "ejbHome" + BeanNaming.firstToUpperCase(methodName);
            }
        }

        try {
            beanMethod = bean.getMethod(beanMethodName, method.getParameterTypes());
        } catch (Exception e) {
            throw new Error("No associated bean's method for the interface method " + method.toString());
        }
        return beanMethod;
    }

    /**
     * @param classname name of a class
     * @return true if the class is a 'user' class, false if it's a java class
     */
    public static Boolean pkIsUserClass(String classname) {
        if (classname.startsWith("java.")) {
            return Boolean.FALSE;
        } else {
            return Boolean.TRUE;
        }
    }

    /**
     * @param vc VelocityContext to trace
     * @return Return a string representation of the given VelocityContext for debug use
     */
    public static String toString(VelocityContext vc) {
        StringBuffer ret = new StringBuffer();
        Object [] keys = vc.internalGetKeys();
        for (int i = 0; i < keys.length; i++) {
            String key = (String) keys[i];
            Object value = vc.internalGet(key);
            if (i > 0) {
                ret.append("\n");
            }
            if (!"dd".equals(key)) {
                // Do not trace the BeanDesc (variable named "dd")
                ret.append("  key = " + key + "; value = " + value);
            }
        }
        return (ret.toString());
    }

}
