/*
 * Copyright 2002-2022 the original author or authors.
 *
 * Licensed 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
 *
 *      https://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.springframework.batch.core.listener;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.aop.TargetSource;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.batch.support.MethodInvoker;
import org.springframework.batch.support.MethodInvokerUtils;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.Ordered;
import org.springframework.util.Assert;

import static org.springframework.batch.support.MethodInvokerUtils.getMethodInvokerByAnnotation;
import static org.springframework.batch.support.MethodInvokerUtils.getMethodInvokerForInterface;

/**
 * {@link FactoryBean} implementation that builds a listener based on the various
 * lifecycle methods or annotations that are provided. There are three possible ways of
 * having a method called as part of a listener lifecycle:
 *
 * <ul>
 * <li>Interface implementation: By implementing any of the subclasses of a listener
 * interface, methods on said interface will be called
 * <li>Annotations: Annotating a method will result in registration.
 * <li>String name of the method to be called, which is tied to a {@link ListenerMetaData}
 * value in the metaDataMap.
 * </ul>
 *
 * It should be noted that methods obtained by name or annotation that don't match the
 * listener method signatures to which they belong will cause errors. However, it is
 * acceptable to have no parameters at all. If the same method is marked in more than one
 * way. (i.e. the method name is given and it is annotated) the method will only be called
 * once. However, if the same class has multiple methods tied to a particular listener,
 * each method will be called. Also note that the same annotations cannot be applied to
 * two separate methods in a single class.
 *
 * @author Lucas Ward
 * @author Dan Garrette
 * @since 2.0
 * @see ListenerMetaData
 */
public abstract class AbstractListenerFactoryBean<T> implements FactoryBean<Object>, InitializingBean {

	private static final Log logger = LogFactory.getLog(AbstractListenerFactoryBean.class);

	private Object delegate;

	private Map<String, String> metaDataMap;

	@Override
	public Object getObject() {
		if (metaDataMap == null) {
			metaDataMap = new HashMap<>();
		}
		// Because all annotations and interfaces should be checked for, make
		// sure that each meta data
		// entry is represented.
		for (ListenerMetaData metaData : this.getMetaDataValues()) {
			if (!metaDataMap.containsKey(metaData.getPropertyName())) {
				// put null so that the annotation and interface is checked
				metaDataMap.put(metaData.getPropertyName(), null);
			}
		}

		Set<Class<?>> listenerInterfaces = new HashSet<>();

		// For every entry in the map, try and find a method by interface, name,
		// or annotation. If the same
		Map<String, Set<MethodInvoker>> invokerMap = new HashMap<>();
		boolean synthetic = false;
		for (Entry<String, String> entry : metaDataMap.entrySet()) {
			final ListenerMetaData metaData = this.getMetaDataFromPropertyName(entry.getKey());
			Set<MethodInvoker> invokers = new HashSet<>();

			MethodInvoker invoker;
			invoker = getMethodInvokerForInterface(metaData.getListenerInterface(), metaData.getMethodName(), delegate,
					metaData.getParamTypes());
			if (invoker != null) {
				invokers.add(invoker);
			}

			invoker = getMethodInvokerByName(entry.getValue(), delegate, metaData.getParamTypes());
			if (invoker != null) {
				invokers.add(invoker);
				synthetic = true;
			}

			if (metaData.getAnnotation() != null) {
				invoker = getMethodInvokerByAnnotation(metaData.getAnnotation(), delegate, metaData.getParamTypes());
				if (invoker != null) {
					invokers.add(invoker);
					synthetic = true;
				}
			}

			if (!invokers.isEmpty()) {
				invokerMap.put(metaData.getMethodName(), invokers);
				listenerInterfaces.add(metaData.getListenerInterface());
			}

		}

		if (listenerInterfaces.isEmpty()) {
			listenerInterfaces.add(this.getDefaultListenerClass());
		}

		if (!synthetic) {
			int count = 0;
			for (Class<?> listenerInterface : listenerInterfaces) {
				if (listenerInterface.isInstance(delegate)) {
					count++;
				}
			}
			// All listeners can be supplied by the delegate itself
			if (count == listenerInterfaces.size()) {
				return delegate;
			}
		}

		boolean ordered = false;
		if (delegate instanceof Ordered) {
			ordered = true;
			listenerInterfaces.add(Ordered.class);
		}

		// create a proxy listener for only the interfaces that have methods to
		// be called
		ProxyFactory proxyFactory = new ProxyFactory();
		if (delegate instanceof Advised) {
			proxyFactory.setTargetSource(((Advised) delegate).getTargetSource());
		}
		else {
			proxyFactory.setTarget(delegate);
		}
		@SuppressWarnings("rawtypes")
		Class[] a = new Class[0];

		proxyFactory.setInterfaces(listenerInterfaces.toArray(a));
		proxyFactory.addAdvisor(new DefaultPointcutAdvisor(new MethodInvokerMethodInterceptor(invokerMap, ordered)));
		return proxyFactory.getProxy();

	}

	protected abstract ListenerMetaData getMetaDataFromPropertyName(String propertyName);

	protected abstract ListenerMetaData[] getMetaDataValues();

	protected abstract Class<?> getDefaultListenerClass();

	protected MethodInvoker getMethodInvokerByName(String methodName, Object candidate, Class<?>... params) {
		if (methodName != null) {
			return MethodInvokerUtils.getMethodInvokerByName(candidate, methodName, false, params);
		}
		else {
			return null;
		}
	}

	@Override
	public boolean isSingleton() {
		return true;
	}

	public void setDelegate(Object delegate) {
		this.delegate = delegate;
	}

	public void setMetaDataMap(Map<String, String> metaDataMap) {
		this.metaDataMap = metaDataMap;
	}

	@Override
	public void afterPropertiesSet() throws Exception {
		Assert.state(delegate != null, "Delegate must not be null");
	}

	/**
	 * Convenience method to check whether the given object is or can be made into a
	 * listener.
	 * @param target the object to check
	 * @param listenerType the class of the listener.
	 * @param metaDataValues array of {@link ListenerMetaData}.
	 * @return true if the delegate is an instance of any of the listener interface, or
	 * contains the marker annotations
	 */
	public static boolean isListener(Object target, Class<?> listenerType, ListenerMetaData[] metaDataValues) {
		if (target == null) {
			return false;
		}
		if (listenerType.isInstance(target)) {
			return true;
		}
		if (target instanceof Advised) {
			TargetSource targetSource = ((Advised) target).getTargetSource();
			if (targetSource != null && targetSource.getTargetClass() != null
					&& listenerType.isAssignableFrom(targetSource.getTargetClass())) {
				return true;
			}

			if (targetSource != null && targetSource.getTargetClass() != null
					&& targetSource.getTargetClass().isInterface()) {
				logger.warn(String.format(
						"%s is an interface. The implementing class will not be queried for annotation based listener configurations. If using @StepScope on a @Bean method, be sure to return the implementing class so listener annotations can be used.",
						targetSource.getTargetClass().getName()));
			}
		}
		for (ListenerMetaData metaData : metaDataValues) {
			if (MethodInvokerUtils.getMethodInvokerByAnnotation(metaData.getAnnotation(), target) != null) {
				return true;
			}
		}
		return false;
	}

}
