/**
 * Copyright (c) 2005-2012 EBM WebSourcing, 2012-2024 Linagora
 * 
 * This program/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 (at your
 * option) any later version.
 * 
 * This program/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 program/library; If not, see http://www.gnu.org/licenses/
 * for the GNU Lesser General Public License version 2.1.
 */
package org.ow2.petals.se.pojo;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URLClassLoader;
import java.util.Optional;
import java.util.logging.Logger;

import javax.jbi.component.ComponentContext;
import javax.jbi.messaging.DeliveryChannel;
import javax.jbi.messaging.MessagingException;

import org.ow2.petals.commons.log.Level;
import org.ow2.petals.component.framework.api.exception.PEtALSCDKException;
import org.ow2.petals.component.framework.api.message.Exchange;
import org.ow2.petals.component.framework.listener.AbstractJBIListener;
import org.ow2.petals.component.framework.process.async.AsyncContext;
import org.ow2.petals.se.pojo.exceptions.FaultException;

/**
 * @author Adrien Louis - EBM WebSourcing
 */
public class Pojo {

    public static final String COMPONENT_CONTEXT_SETTER = "setComponentContext";

    public static final String DELIVERY_CHANNEL_SETTER = "setDeliveryChannel";

    public static final String LOGGER_SETTER = "setLogger";

    public static final String ON_EXCHANGE_METHOD = "onExchange";

    public static final String ON_ASYNC_EXCHANGE_METHOD = "onAsyncExchange";

    public static final String ON_EXPIRED_ASYNC_EXCHANGE_METHOD = "onExpiredAsyncExchange";

    private DeliveryChannel channel;

    private URLClassLoader classLoader;

    private ComponentContext context;

    private Logger logger;

    private Method onExchangeMethod;

    private Method onAsyncExchangeMethod;

    private Method onExpiredAsyncExchangeMethod;

    private Object pojo;

    public Pojo(final Object object, final URLClassLoader classLoader,
            final ComponentContext context, final DeliveryChannel channel, final Logger logger) {
        this.pojo = object;
        this.context = context;
        this.channel = channel;
        this.logger = logger;
        this.classLoader = classLoader;
    }

    public Object getPojo() {
        return this.pojo;
    }

    /**
     * Call the Exchange handler method of the POJO
     * 
     * @param ex
     * @return true if the OUT message has been set on the exchange and has to
     *         be send
     * @throws MessagingException
     * @throws FaultException
     */
    public boolean callOnExchangeMethod(final Exchange ex, final Optional<Boolean> currentFlowTracingActivation,
            final AsyncContext asyncContext, final AbstractJBIListener jbiListener)
            throws MessagingException, FaultException {

        final Method onExchange = (asyncContext == null ? this.onExchangeMethod : this.onAsyncExchangeMethod);

        // First, we set the SU classloader
        final ClassLoader oldClassLoader = Thread.currentThread().getContextClassLoader();
        Thread.currentThread().setContextClassLoader(this.classLoader);

        try {
            final Object object = (asyncContext == null
                    ? onExchange.invoke(this.pojo, ex, currentFlowTracingActivation, jbiListener)
                    : onExchange.invoke(this.pojo, ex, asyncContext, jbiListener));

            return (Boolean) object;
        } catch (final IllegalArgumentException e) {
            throw new MessagingException(
                    "Java reflection exception during call on " + onExchange.getName() + "() on the POJO.", e);
        } catch (final IllegalAccessException e) {
            throw new MessagingException(
                    "Java reflection exception during call on " + onExchange.getName() + "() on the POJO.", e);
        } catch (final InvocationTargetException e) {
            // An exception is thrown during the processing of the POJO class.
            // This exception will be convert in a fault or an ERROR ack
            final Throwable orginalEx = e.getCause();
            if (orginalEx != null && orginalEx instanceof FaultException) {
                throw (FaultException) orginalEx;
            } else {
                throw new MessagingException(
                        "Processing exception during call on " + onExchange.getName() + "() on the POJO.", e);
            }
        } finally {
            // We restore the thread classloader
            Thread.currentThread().setContextClassLoader(oldClassLoader);
        }
    }

    /**
     * Call the Exchange handler method of the POJO
     * 
     * @param expiredExchange
     * @param asyncContext
     * @param jbiListener
     * @throws PEtALSCDKException
     * @throws FaultException
     */
    public void callOnExpiredAsyncJBIMessage(final Exchange expiredExchange, final AsyncContext asyncContext,
            final AbstractJBIListener jbiListener) throws PEtALSCDKException, FaultException {

        // First, we set the SU classloader
        final ClassLoader oldClassLoader = Thread.currentThread().getContextClassLoader();
        Thread.currentThread().setContextClassLoader(this.classLoader);

        try {
            this.onExpiredAsyncExchangeMethod.invoke(this.pojo, expiredExchange, asyncContext, jbiListener);
        } catch (final IllegalArgumentException e) {
            throw new PEtALSCDKException(
                    "Java reflection exception during call on " + onAsyncExchangeMethod.getName() + "() on the POJO.",
                    e);
        } catch (final IllegalAccessException e) {
            throw new PEtALSCDKException(
                    "Java reflection exception during call on " + onAsyncExchangeMethod.getName() + "() on the POJO.",
                    e);
        } catch (final InvocationTargetException e) {
            // An exception is thrown during the processing of the POJO class.
            // This exception will be convert in a fault or an ERROR ack
            final Throwable orginalEx = e.getCause();
            if (orginalEx != null && orginalEx instanceof FaultException) {
                throw (FaultException) orginalEx;
            } else {
                throw new PEtALSCDKException(
                        "Processing exception during call on " + onAsyncExchangeMethod.getName() + "() on the POJO.",
                        e);
            }
        } finally {
            // We restore the thread classloader
            Thread.currentThread().setContextClassLoader(oldClassLoader);
        }
    }

    public ClassLoader getClassLoader() {
        return this.classLoader;
    }

    public void init() throws Exception {
        setLoggerOnPojo();
        setContextOnPojo();
        setChannelOnPojo();

        call("init");

        setupOnExchangeMethod();
        setupOnAsyncExchangeMethod();
        setupOnExpiredAsyncExchangeMethod();
    }

    /**
     * TODO: Use when the POJO would be bound to the SU life-cycle.
     * 
     * @throws Exception
     */
    public void shutdown() throws Exception {
        try {
            this.classLoader.close();
        } catch (Exception e) {
            if (logger != null && logger.isLoggable(Level.FINEST)) {
                String msg = e.getMessage();
                if (msg == null || msg.isEmpty())
                    msg = "An URL class loader for POJO SE unsuccessfully tried to release JAR resources.";
                logger.finest(msg);
            }
        }
        call("shutdown");
    }

    /**
     * TODO: Use when the POJO would be bound to the SU life-cycle.
     * 
     * @throws Exception
     */
    public void start() throws Exception {
        call("start");
    }

    /**
     * TODO: Use when the POJO would be bound to the SU life-cycle.
     * 
     * @throws Exception
     */
    public void stop() throws Exception {
        call("stop");
    }

    /**
     * Call the method on the pojo. If the method is not found, nothing happens.
     * The method can not have parameters
     * 
     * @throws MessagingException
     *             invocation failed or the method has thrown an exception
     */
    protected void call(final String methodName) throws MessagingException {

        // First, we set the SU classloader
        final ClassLoader oldClassLoader = Thread.currentThread().getContextClassLoader();
        Thread.currentThread().setContextClassLoader(this.classLoader);

        try {
            final Method m = findMethod(this.pojo.getClass(), methodName, null);

            if (m != null) {
                try {
                    m.invoke(this.pojo);
                } catch (final IllegalArgumentException e) {
                    throw new MessagingException("Java reflection exception during call on "
                            + methodName + "() on the POJO.", e);
                } catch (final IllegalAccessException e) {
                    throw new MessagingException("Java reflection exception during call on "
                            + methodName + "() on the POJO.", e);
                } catch (final InvocationTargetException e) {
                    // An exception is thrown calling the POJO class method.
                    throw new MessagingException("Processing exception during call on "
                            + methodName + "() on the POJO.", e);
                }
            }
        } finally {
            // We restore the thread classloader
            Thread.currentThread().setContextClassLoader(oldClassLoader);
        }
    }

    /**
     * Find a "set...(DeliveryChannel channel)" on the pojo and set the
     * DeliveryChannel on it. The method is not mandatory.
     * 
     * @throws PEtALSCDKException
     *             invocation failed
     */
    protected void setChannelOnPojo() throws PEtALSCDKException {

        // First, we set the SU classloader
        final ClassLoader oldClassLoader = Thread.currentThread().getContextClassLoader();
        Thread.currentThread().setContextClassLoader(this.classLoader);

        try {
            final Method setChannelMethod = findMethod(pojo.getClass(), DELIVERY_CHANNEL_SETTER,
                    DeliveryChannel.class);

            if (setChannelMethod != null) {
                try {
                    setChannelMethod.invoke(pojo, channel);
                } catch (final Exception e) {
                    throw new PEtALSCDKException("Can not set the DeliveryChannel on the POJO", e);
                }
            }
        } finally {
            // We restore the thread classloader
            Thread.currentThread().setContextClassLoader(oldClassLoader);
        }
    }

    /**
     * Find a "setComponentContext(ComponentContext context)" on the POJO and
     * set the ComponentContext on it. The method is not mandatory.
     * 
     * @throws PEtALSCDKException
     *             invocation failed
     */
    protected void setContextOnPojo() throws PEtALSCDKException {

        // First, we set the SU classloader
        final ClassLoader oldClassLoader = Thread.currentThread().getContextClassLoader();
        Thread.currentThread().setContextClassLoader(this.classLoader);

        try {
            final Method setContextMethod = findMethod(pojo.getClass(), COMPONENT_CONTEXT_SETTER,
                    ComponentContext.class);

            if (setContextMethod != null) {
                try {
                    setContextMethod.invoke(pojo, context);

                } catch (final Exception e) {
                    throw new PEtALSCDKException("Can not set the ComponentContext on the POJO", e);
                }
            }
        } finally {
            // We restore the thread classloader
            Thread.currentThread().setContextClassLoader(oldClassLoader);
        }
    }

    /**
     * Find a "set...(Logger logger)" on the pojo and set the logger on it. The
     * method is not mandatory.
     * 
     * @throws PEtALSCDKException
     *             invocation failed
     */
    protected void setLoggerOnPojo() throws PEtALSCDKException {

        // First, we set the SU classloader
        final ClassLoader oldClassLoader = Thread.currentThread().getContextClassLoader();
        Thread.currentThread().setContextClassLoader(this.classLoader);

        try {
            final Method setLoggerMethod = findMethod(pojo.getClass(), LOGGER_SETTER, Logger.class);

            if (setLoggerMethod != null) {
                try {
                    setLoggerMethod.invoke(this.pojo, logger);

                } catch (final Exception e) {
                    throw new PEtALSCDKException("Can not set the Logger on the POJO", e);
                }
            }
        } finally {
            // We restore the thread classloader
            Thread.currentThread().setContextClassLoader(oldClassLoader);
        }
    }

    /**
     * Find a method {@code onExchange(Exchange, Optional<Boolean>, AbstractJBIListener)} returning {@code boolean} on
     * the POJO to be used when message exchanges are accepted.
     * 
     * @throws PEtALSCDKException
     *             method not found or invocation failed
     */
    protected void setupOnExchangeMethod() throws PEtALSCDKException {

        this.onExchangeMethod = findMethod(this.pojo.getClass(), ON_EXCHANGE_METHOD, Exchange.class,
                Optional.class, AbstractJBIListener.class);

        if (this.onExchangeMethod == null) {
            throw new PEtALSCDKException("The '" + ON_EXCHANGE_METHOD + "' method is not found in "
                    + this.pojo.getClass());

        }
        if (this.onExchangeMethod.getReturnType() != Boolean.TYPE) {
            throw new PEtALSCDKException("The '" + ON_EXCHANGE_METHOD
                    + "' method signature is not correct, it must return a boolean");
        }

    }

    /**
     * Find a method {@code onAsyncExchange(Exchange, AsyncContext, AbstractJBIListener)} returning {@code boolean} the
     * POJO to be used when message exchanges are accepted.
     * 
     * @throws PEtALSCDKException
     *             method not found or invocation failed
     */
    protected void setupOnAsyncExchangeMethod() throws PEtALSCDKException {

        final Class<?>[] args = new Class[] { Exchange.class, AsyncContext.class, AbstractJBIListener.class };
        try {
            this.onAsyncExchangeMethod = this.pojo.getClass().getMethod(ON_ASYNC_EXCHANGE_METHOD, args);
        } catch (final NoSuchMethodException e) {
            this.onAsyncExchangeMethod = null; // Async method is optional
        }

        if (this.onAsyncExchangeMethod != null && this.onAsyncExchangeMethod.getReturnType() != Boolean.TYPE) {
            throw new PEtALSCDKException(
                    "The '" + ON_ASYNC_EXCHANGE_METHOD + "' method signature is not correct, it must return a boolean");
        }

    }

    /**
     * Find a method {@code onExpiredAsyncExchange(Exchange, AsyncContext, AbstractJBIListener)} returning {@code void}
     * on the POJO to be used when async message exchanges are expired.
     * 
     * @throws PEtALSCDKException
     *             method not found or invocation failed
     */
    protected void setupOnExpiredAsyncExchangeMethod() throws PEtALSCDKException {

        final Class<?>[] args = new Class[] { Exchange.class, AsyncContext.class, AbstractJBIListener.class };
        try {
            this.onExpiredAsyncExchangeMethod = this.pojo.getClass().getMethod(ON_EXPIRED_ASYNC_EXCHANGE_METHOD, args);
        } catch (NoSuchMethodException e) {
            this.onExpiredAsyncExchangeMethod = null; // Async method is optional
        }

        if (this.onExpiredAsyncExchangeMethod != null
                && !this.onExpiredAsyncExchangeMethod.getReturnType().equals(Void.TYPE)) {
            throw new PEtALSCDKException("The '" + ON_EXPIRED_ASYNC_EXCHANGE_METHOD
                    + "' method signature is not correct, it must return a boolean");
        }

    }

    /**
     * Find a method on the specified class (including those inherited from superclasses) with the specified name, and
     * having the optional specified class or super-class as parameter.
     * 
     * @param clazz
     *            class on which the method has to be found
     * @param name
     *            the method name
     * @param parameters
     *            parameters of the method (only specific parameters are allowed, but is not mandatory)
     * @return the matching method, or null if no one match
     */
    private static final Method findMethod(final Class<?> clazz, final String name, final Class<?>... parameters) {

        Method method = null;
        final Method[] classMethods = clazz.getMethods();

        for (int i = 0; i < classMethods.length && method == null; i++) {
            final Method currentMethod = classMethods[i];
            if (currentMethod.getName().equals(name) && Modifier.isPublic(currentMethod.getModifiers())) {
                if (parameters != null && parameters.length > 0) {
                    boolean isParametersValid = true;
                    for (int j = 0; j < parameters.length; j++) {
                        if (!parameters[j].isAssignableFrom(currentMethod.getParameterTypes()[j])) {
                            isParametersValid = false;
                            break;
                        }
                    }
                    if (isParametersValid) {
                        method = currentMethod;
                    }
                } else if (parameters == null && currentMethod.getParameterTypes().length == 0) {
                    method = currentMethod;
                }
            }
        }

        return method;
    }
}
