/*
 * Copyright 2002-2019 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.integration.endpoint;

import java.util.Collection;
import java.util.HashSet;

import org.aopalliance.aop.Advice;

import org.springframework.aop.framework.Advised;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.AopUtils;
import org.springframework.aop.support.NameMatchMethodPointcutAdvisor;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.context.Lifecycle;
import org.springframework.integration.StaticMessageHeaderAccessor;
import org.springframework.integration.acks.AckUtils;
import org.springframework.integration.acks.AcknowledgmentCallback;
import org.springframework.integration.aop.MessageSourceMutator;
import org.springframework.integration.channel.ReactiveStreamsSubscribableChannel;
import org.springframework.integration.context.ExpressionCapable;
import org.springframework.integration.core.MessageSource;
import org.springframework.integration.core.MessagingTemplate;
import org.springframework.integration.history.MessageHistory;
import org.springframework.integration.support.context.NamedComponent;
import org.springframework.integration.support.management.TrackableComponent;
import org.springframework.integration.transaction.IntegrationResourceHolder;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessagingException;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;

/**
 * A Channel Adapter implementation for connecting a
 * {@link MessageSource} to a {@link MessageChannel}.
 *
 * @author Mark Fisher
 * @author Oleg Zhurakousky
 * @author Gary Russell
 * @author Artem Bilan
 */
public class SourcePollingChannelAdapter extends AbstractPollingEndpoint
		implements TrackableComponent {

	private final MessagingTemplate messagingTemplate = new MessagingTemplate();

	private final Collection<Advice> appliedAdvices = new HashSet<>();

	private volatile MessageSource<?> originalSource;

	private volatile MessageSource<?> source;

	private volatile MessageChannel outputChannel;

	private volatile String outputChannelName;

	private volatile boolean shouldTrack;

	/**
	 * Specify the source to be polled for Messages.
	 *
	 * @param source The message source.
	 */
	public void setSource(MessageSource<?> source) {
		this.source = source;

		Object target = extractProxyTarget(source);
		this.originalSource = target != null ? (MessageSource<?>) target : source;

		if (source instanceof ExpressionCapable) {
			setPrimaryExpression(((ExpressionCapable) source).getExpression());
		}
	}

	/**
	 * Specify the {@link MessageChannel} where Messages should be sent.
	 *
	 * @param outputChannel The output channel.
	 */
	public void setOutputChannel(MessageChannel outputChannel) {
		this.outputChannel = outputChannel;
	}

	/**
	 * Return this endpoint's source.
	 * @return the source.
	 * @since 4.3
	 */
	public MessageSource<?> getMessageSource() {
		return this.source;
	}

	public void setOutputChannelName(String outputChannelName) {
		Assert.hasText(outputChannelName, "'outputChannelName' must not be empty");
		this.outputChannelName = outputChannelName;
	}

	/**
	 * Specify the maximum time to wait for a Message to be sent to the
	 * output channel.
	 *
	 * @param sendTimeout The send timeout.
	 */
	public void setSendTimeout(long sendTimeout) {
		this.messagingTemplate.setSendTimeout(sendTimeout);
	}

	/**
	 * Specify whether this component should be tracked in the Message History.
	 *
	 * @param shouldTrack true if the component should be tracked.
	 */
	@Override
	public void setShouldTrack(boolean shouldTrack) {
		this.shouldTrack = shouldTrack;
	}

	@Override
	public String getComponentType() {
		return (this.source instanceof NamedComponent) ?
				((NamedComponent) this.source).getComponentType() : "inbound-channel-adapter";
	}

	@Override
	protected boolean isReceiveOnlyAdvice(Advice advice) {
		return advice instanceof MessageSourceMutator;
	}

	@Override
	protected void applyReceiveOnlyAdviceChain(Collection<Advice> chain) {
		if (!CollectionUtils.isEmpty(chain)) {
			if (AopUtils.isAopProxy(this.source)) {
				Advised advised = (Advised) this.source;
				this.appliedAdvices.forEach(advised::removeAdvice);
				chain.forEach(advice -> advised.addAdvisor(adviceToReceiveAdvisor(advice)));
			}
			else {
				ProxyFactory proxyFactory = new ProxyFactory(this.source);
				chain.forEach(advice -> proxyFactory.addAdvisor(adviceToReceiveAdvisor(advice)));
				this.source = (MessageSource<?>) proxyFactory.getProxy(getBeanClassLoader());
			}
			this.appliedAdvices.clear();
			this.appliedAdvices.addAll(chain);
			if (!(isSyncExecutor()) && logger.isWarnEnabled()) {
				logger.warn(getComponentName() + ": A task executor is supplied and " + chain.size()
						+ "MessageSourceMutator(s) is/are provided. If an advice mutates the source, such "
						+ "mutations are not thread safe and could cause unexpected results, especially with "
						+ "high frequency pollers. Consider using a downstream ExecutorChannel instead of "
						+ "adding an executor to the poller");
			}
		}
	}

	@Override
	protected boolean isReactive() {
		return getOutputChannel() instanceof ReactiveStreamsSubscribableChannel;
	}

	private NameMatchMethodPointcutAdvisor adviceToReceiveAdvisor(Advice advice) {
		NameMatchMethodPointcutAdvisor sourceAdvisor = new NameMatchMethodPointcutAdvisor(advice);
		sourceAdvisor.addMethodName("receive");
		return sourceAdvisor;
	}

	@Override
	protected void doStart() {
		if (this.source instanceof Lifecycle) {
			((Lifecycle) this.source).start();
		}
		super.doStart();

		if (isReactive()) {
			((ReactiveStreamsSubscribableChannel) this.outputChannel).subscribeTo(getPollingFlux());
		}
	}


	@Override
	protected void doStop() {
		super.doStop();
		if (this.source instanceof Lifecycle) {
			((Lifecycle) this.source).stop();
		}
	}


	@Override
	protected void onInit() {
		Assert.notNull(this.source, "source must not be null");
		Assert.state((this.outputChannelName == null && this.outputChannel != null)
						|| (this.outputChannelName != null && this.outputChannel == null),
				"One and only one of 'outputChannelName' or 'outputChannel' is required.");
		super.onInit();
		if (this.getBeanFactory() != null) {
			this.messagingTemplate.setBeanFactory(this.getBeanFactory());
		}
	}

	public MessageChannel getOutputChannel() {
		if (this.outputChannelName != null) {
			synchronized (this) {
				if (this.outputChannelName != null) {
					this.outputChannel = getChannelResolver().resolveDestination(this.outputChannelName);
					this.outputChannelName = null;
				}
			}
		}
		return this.outputChannel;
	}

	@Override
	protected void handleMessage(Message<?> messageArg) {
		Message<?> message = messageArg;
		if (this.shouldTrack) {
			message = MessageHistory.write(message, this, getMessageBuilderFactory());
		}
		AcknowledgmentCallback ackCallback = StaticMessageHeaderAccessor.getAcknowledgmentCallback(message);
		try {
			this.messagingTemplate.send(getOutputChannel(), message);
			AckUtils.autoAck(ackCallback);
		}
		catch (Exception e) {
			AckUtils.autoNack(ackCallback);
			if (e instanceof MessagingException) {
				throw (MessagingException) e;
			}
			else {
				throw new MessagingException(message, "Failed to send Message", e);
			}
		}
	}

	@Override
	protected Message<?> receiveMessage() {
		return this.source.receive();
	}

	@Override
	protected Object getResourceToBind() {
		return this.originalSource;
	}

	@Override
	protected String getResourceKey() {
		return IntegrationResourceHolder.MESSAGE_SOURCE;
	}

	private static Object extractProxyTarget(Object target) {
		if (!(target instanceof Advised)) {
			return target;
		}
		Advised advised = (Advised) target;
		try {
			return extractProxyTarget(advised.getTargetSource().getTarget());
		}
		catch (Exception e) {
			throw new BeanCreationException("Could not extract target", e);
		}
	}


}
