/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.camel.processor.aggregate;

import org.apache.camel.AggregationStrategy;
import org.apache.camel.CamelContext;
import org.apache.camel.CamelContextAware;
import org.apache.camel.Exchange;
import org.apache.camel.support.service.ServiceHelper;
import org.apache.camel.support.service.ServiceSupport;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * An {@link AggregationStrategy} that adapts to a POJO.
 * <p/>
 * This allows end users to use POJOs for the aggregation logic, instead of having to implement the
 * Camel API {@link AggregationStrategy}.
 */
public final class AggregationStrategyBeanAdapter extends ServiceSupport implements AggregationStrategy, CamelContextAware {

    private static final List<Method> EXCLUDED_METHODS = new ArrayList<>();
    private CamelContext camelContext;
    private Object pojo;
    private final Class<?> type;
    private String methodName;
    private boolean allowNullOldExchange;
    private boolean allowNullNewExchange;
    private volatile AggregationStrategyMethodInfo mi;

    static {
        // exclude all java.lang.Object methods as we dont want to invoke them
        EXCLUDED_METHODS.addAll(Arrays.asList(Object.class.getMethods()));
        // exclude all java.lang.reflect.Proxy methods as we dont want to invoke them
        EXCLUDED_METHODS.addAll(Arrays.asList(Proxy.class.getMethods()));
    }

    /**
     * Creates this adapter.
     *
     * @param pojo the pojo to use.
     */
    public AggregationStrategyBeanAdapter(Object pojo) {
        this(pojo, null);
    }

    /**
     * Creates this adapter.
     *
     * @param type the class type of the pojo
     */
    public AggregationStrategyBeanAdapter(Class<?> type) {
        this(type, null);
    }

    /**
     * Creates this adapter.
     *
     * @param pojo the pojo to use.
     * @param methodName the name of the method to call
     */
    public AggregationStrategyBeanAdapter(Object pojo, String methodName) {
        this.pojo = pojo;
        this.type = pojo.getClass();
        this.methodName = methodName;
    }

    /**
     * Creates this adapter.
     *
     * @param type the class type of the pojo
     * @param methodName the name of the method to call
     */
    public AggregationStrategyBeanAdapter(Class<?> type, String methodName) {
        this.type = type;
        this.pojo = null;
        this.methodName = methodName;
    }

    @Override
    public CamelContext getCamelContext() {
        return camelContext;
    }

    @Override
    public void setCamelContext(CamelContext camelContext) {
        this.camelContext = camelContext;
    }

    public String getMethodName() {
        return methodName;
    }

    public void setMethodName(String methodName) {
        this.methodName = methodName;
    }

    public boolean isAllowNullOldExchange() {
        return allowNullOldExchange;
    }

    public void setAllowNullOldExchange(boolean allowNullOldExchange) {
        this.allowNullOldExchange = allowNullOldExchange;
    }

    public boolean isAllowNullNewExchange() {
        return allowNullNewExchange;
    }

    public void setAllowNullNewExchange(boolean allowNullNewExchange) {
        this.allowNullNewExchange = allowNullNewExchange;
    }

    @Override
    public Exchange aggregate(Exchange oldExchange, Exchange newExchange) {
        if (!allowNullOldExchange && oldExchange == null) {
            return newExchange;
        }
        if (!allowNullNewExchange && newExchange == null) {
            return oldExchange;
        }

        try {
            Object out = mi.invoke(pojo, oldExchange, newExchange);
            if (out != null) {
                if (oldExchange != null) {
                    oldExchange.getIn().setBody(out);
                } else {
                    newExchange.getIn().setBody(out);
                }
            }
        } catch (Exception e) {
            if (oldExchange != null) {
                oldExchange.setException(e);
            } else {
                newExchange.setException(e);
            }
        }
        return oldExchange != null ? oldExchange : newExchange;
    }

    /**
     * Validates whether the given method is valid.
     *
     * @param method  the method
     * @return true if valid, false to skip the method
     */
    protected boolean isValidMethod(Method method) {
        // must not be in the excluded list
        for (Method excluded : EXCLUDED_METHODS) {
            if (method.equals(excluded)) {
                return false;
            }
        }

        // must be a public method
        if (!Modifier.isPublic(method.getModifiers())) {
            return false;
        }

        // return type must not be void and it should not be a bridge method
        if (method.getReturnType().equals(Void.TYPE) || method.isBridge()) {
            return false;
        }

        return true;
    }

    private static boolean isStaticMethod(Method method) {
        return Modifier.isStatic(method.getModifiers());
    }

    @Override
    protected void doStart() throws Exception {
        Method found = null;
        if (methodName != null) {
            for (Method method : type.getMethods()) {
                if (isValidMethod(method) && method.getName().equals(methodName)) {
                    if (found == null) {
                        found = method;
                    } else {
                        throw new IllegalArgumentException("The bean " + type + " has 2 or more methods with the name " + methodName);
                    }
                }
            }
        } else {
            for (Method method : type.getMethods()) {
                if (isValidMethod(method)) {
                    if (found == null) {
                        found = method;
                    } else {
                        throw new IllegalArgumentException("The bean " + type + " has 2 or more methods and no explicit method name was configured.");
                    }
                }
            }
        }

        if (found == null) {
            throw new UnsupportedOperationException("Cannot find a valid method with name: " + methodName + " on bean type: " + type);
        }

        // if its not a static method then we must have an instance of the pojo
        if (!isStaticMethod(found) && pojo == null) {
            pojo = camelContext.getInjector().newInstance(type);
        }

        // create the method info which has adapted to the pojo
        AggregationStrategyBeanInfo bi = new AggregationStrategyBeanInfo(type, found);
        mi = bi.createMethodInfo();

        // in case the POJO is CamelContextAware
        if (pojo instanceof CamelContextAware) {
            ((CamelContextAware) pojo).setCamelContext(getCamelContext());
        }

        // in case the pojo is a service
        ServiceHelper.startService(pojo);
    }

    @Override
    protected void doStop() throws Exception {
        ServiceHelper.stopService(pojo);
    }
}
