/*
 * 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.channel;

import java.util.ArrayDeque;
import java.util.Collections;
import java.util.Comparator;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;

import org.apache.commons.logging.Log;

import org.springframework.beans.factory.BeanFactory;
import org.springframework.core.OrderComparator;
import org.springframework.integration.IntegrationPattern;
import org.springframework.integration.IntegrationPatternType;
import org.springframework.integration.context.IntegrationContextUtils;
import org.springframework.integration.context.IntegrationObjectSupport;
import org.springframework.integration.history.MessageHistory;
import org.springframework.integration.support.management.AbstractMessageChannelMetrics;
import org.springframework.integration.support.management.ConfigurableMetricsAware;
import org.springframework.integration.support.management.DefaultMessageChannelMetrics;
import org.springframework.integration.support.management.IntegrationManagedResource;
import org.springframework.integration.support.management.TrackableComponent;
import org.springframework.integration.support.management.metrics.MeterFacade;
import org.springframework.integration.support.management.metrics.MetricsCaptor;
import org.springframework.integration.support.management.metrics.SampleFacade;
import org.springframework.integration.support.management.metrics.TimerFacade;
import org.springframework.integration.support.utils.IntegrationUtils;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageDeliveryException;
import org.springframework.messaging.converter.MessageConverter;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

/**
 * Base class for {@link MessageChannel} implementations providing common
 * properties such as the channel name. Also provides the common functionality
 * for sending and receiving {@link Message Messages} including the invocation
 * of any {@link org.springframework.messaging.support.ChannelInterceptor ChannelInterceptors}.
 *
 * @author Mark Fisher
 * @author Oleg Zhurakousky
 * @author Gary Russell
 * @author Artem Bilan
 */
@IntegrationManagedResource
@SuppressWarnings("deprecation")
public abstract class AbstractMessageChannel extends IntegrationObjectSupport
		implements MessageChannel, TrackableComponent, ChannelInterceptorAware,
		org.springframework.integration.support.management.MessageChannelMetrics,
		ConfigurableMetricsAware<AbstractMessageChannelMetrics>,
		IntegrationPattern {

	protected final ChannelInterceptorList interceptors; // NOSONAR

	private final Comparator<Object> orderComparator = new OrderComparator();

	private final ManagementOverrides managementOverrides = new ManagementOverrides();

	protected final Set<MeterFacade> meters = ConcurrentHashMap.newKeySet(); // NOSONAR

	private volatile boolean shouldTrack = false;

	private volatile Class<?>[] datatypes = new Class<?>[0];

	private volatile String fullChannelName;

	private volatile MessageConverter messageConverter;

	private volatile boolean countsEnabled;

	private volatile boolean statsEnabled;

	private volatile boolean loggingEnabled = true;

	private volatile AbstractMessageChannelMetrics channelMetrics = new DefaultMessageChannelMetrics();

	private MetricsCaptor metricsCaptor;

	private TimerFacade successTimer;

	private TimerFacade failureTimer;

	public AbstractMessageChannel() {
		this.interceptors = new ChannelInterceptorList(logger);
	}

	@Override
	public String getComponentType() {
		return "channel";
	}

	@Override
	public IntegrationPatternType getIntegrationPatternType() {
		return IntegrationPatternType.message_channel;
	}

	@Override
	public void setShouldTrack(boolean shouldTrack) {
		this.shouldTrack = shouldTrack;
	}

	@Override
	public void registerMetricsCaptor(MetricsCaptor metricsCaptorToRegister) {
		this.metricsCaptor = metricsCaptorToRegister;
	}

	@Nullable
	protected MetricsCaptor getMetricsCaptor() {
		return this.metricsCaptor;
	}

	@Override
	public void setCountsEnabled(boolean countsEnabled) {
		this.countsEnabled = countsEnabled;
		this.managementOverrides.countsConfigured = true;
		if (!countsEnabled) {
			this.statsEnabled = false;
			this.managementOverrides.statsConfigured = true;
		}
	}

	@Override
	public boolean isCountsEnabled() {
		return this.countsEnabled;
	}

	@Override
	public void setStatsEnabled(boolean statsEnabled) {
		if (statsEnabled) {
			this.countsEnabled = true;
			this.managementOverrides.countsConfigured = true;
		}
		this.statsEnabled = statsEnabled;
		this.channelMetrics.setFullStatsEnabled(statsEnabled);
		this.managementOverrides.statsConfigured = true;
	}

	@Override
	public boolean isStatsEnabled() {
		return this.statsEnabled;
	}

	@Override
	public boolean isLoggingEnabled() {
		return this.loggingEnabled;
	}

	@Override
	public void setLoggingEnabled(boolean loggingEnabled) {
		this.loggingEnabled = loggingEnabled;
		this.managementOverrides.loggingConfigured = true;
	}

	protected AbstractMessageChannelMetrics getMetrics() {
		return this.channelMetrics;
	}

	@Override
	public void configureMetrics(AbstractMessageChannelMetrics metrics) {
		Assert.notNull(metrics, "'metrics' must not be null");
		this.channelMetrics = metrics;
		this.managementOverrides.metricsConfigured = true;
	}

	/**
	 * Specify the Message payload datatype(s) supported by this channel. If a
	 * payload type does not match directly, but the 'conversionService' is
	 * available, then type conversion will be attempted in the order of the
	 * elements provided in this array.
	 * <p> If this property is not set explicitly, any Message payload type will be
	 * accepted.
	 * @param datatypes The supported data types.
	 * @see #setMessageConverter(MessageConverter)
	 */
	public void setDatatypes(Class<?>... datatypes) {
		this.datatypes = (datatypes != null && datatypes.length > 0)
				? datatypes : new Class<?>[0];
	}

	/**
	 * Set the list of channel interceptors. This will clear any existing
	 * interceptors.
	 * @param interceptors The list of interceptors.
	 */
	@Override
	public void setInterceptors(List<ChannelInterceptor> interceptors) {
		Collections.sort(interceptors, this.orderComparator);
		this.interceptors.set(interceptors);
	}

	/**
	 * Add a channel interceptor to the end of the list.
	 * @param interceptor The interceptor.
	 */
	@Override
	public void addInterceptor(ChannelInterceptor interceptor) {
		this.interceptors.add(interceptor);
	}

	/**
	 * Add a channel interceptor to the specified index of the list.
	 * @param index The index to add interceptor.
	 * @param interceptor The interceptor.
	 */
	@Override
	public void addInterceptor(int index, ChannelInterceptor interceptor) {
		this.interceptors.add(index, interceptor);
	}

	/**
	 * Specify the {@link MessageConverter} to use when trying to convert to
	 * one of this channel's supported datatypes (in order) for a Message whose payload
	 * does not already match.
	 * <p> <b>Note:</b> only the {@link MessageConverter#fromMessage(Message, Class)}
	 * method is used. If the returned object is not a {@link Message}, the inbound
	 * headers will be copied; if the returned object is a {@code Message}, it is
	 * expected that the converter will have fully populated the headers; no
	 * further action is performed by the channel. If {@code null} is returned,
	 * conversion to the next datatype (if any) will be attempted.
	 * Defaults to a
	 * {@link org.springframework.integration.support.converter.DefaultDatatypeChannelMessageConverter}.
	 * @param messageConverter The message converter.
	 */
	public void setMessageConverter(MessageConverter messageConverter) {
		this.messageConverter = messageConverter;
	}

	/**
	 * Return a read-only list of the configured interceptors.
	 */
	@Override
	public List<ChannelInterceptor> getInterceptors() {
		return this.interceptors.getInterceptors();
	}

	@Override
	public boolean removeInterceptor(ChannelInterceptor interceptor) {
		return this.interceptors.remove(interceptor);
	}

	@Override
	@Nullable
	public ChannelInterceptor removeInterceptor(int index) {
		return this.interceptors.remove(index);
	}

	/**
	 * Exposes the interceptor list instance for subclasses.
	 * @return The channel interceptor list.
	 */
	protected ChannelInterceptorList getIChannelInterceptorList() {
		return this.interceptors;
	}

	@Override
	public void reset() {
		this.channelMetrics.reset();
	}

	@Override
	public int getSendCount() {
		return this.channelMetrics.getSendCount();
	}

	@Override
	public long getSendCountLong() {
		return this.channelMetrics.getSendCountLong();
	}

	@Override
	public int getSendErrorCount() {
		return this.channelMetrics.getSendErrorCount();
	}

	@Override
	public long getSendErrorCountLong() {
		return this.channelMetrics.getSendErrorCountLong();
	}

	@Override
	public double getTimeSinceLastSend() {
		return this.channelMetrics.getTimeSinceLastSend();
	}

	@Override
	public double getMeanSendRate() {
		return this.channelMetrics.getMeanSendRate();
	}

	@Override
	public double getMeanErrorRate() {
		return this.channelMetrics.getMeanErrorRate();
	}

	@Override
	public double getMeanErrorRatio() {
		return this.channelMetrics.getMeanErrorRatio();
	}

	@Override
	public double getMeanSendDuration() {
		return this.channelMetrics.getMeanSendDuration();
	}

	@Override
	public double getMinSendDuration() {
		return this.channelMetrics.getMinSendDuration();
	}

	@Override
	public double getMaxSendDuration() {
		return this.channelMetrics.getMaxSendDuration();
	}

	@Override
	public double getStandardDeviationSendDuration() {
		return this.channelMetrics.getStandardDeviationSendDuration();
	}

	@Override
	public org.springframework.integration.support.management.Statistics getSendDuration() {
		return this.channelMetrics.getSendDuration();
	}

	@Override
	public org.springframework.integration.support.management.Statistics getSendRate() {
		return this.channelMetrics.getSendRate();
	}

	@Override
	public org.springframework.integration.support.management.Statistics getErrorRate() {
		return this.channelMetrics.getErrorRate();
	}

	@Override
	public ManagementOverrides getOverrides() {
		return this.managementOverrides;
	}

	@Override
	protected void onInit() {
		super.onInit();
		if (this.messageConverter == null) {
			BeanFactory beanFactory = getBeanFactory();
			if (beanFactory != null &&
					beanFactory.containsBean(
							IntegrationContextUtils.INTEGRATION_DATATYPE_CHANNEL_MESSAGE_CONVERTER_BEAN_NAME)) {

				this.messageConverter =
						beanFactory.getBean(
								IntegrationContextUtils.INTEGRATION_DATATYPE_CHANNEL_MESSAGE_CONVERTER_BEAN_NAME,
								MessageConverter.class);
			}
		}
		if (this.statsEnabled) {
			this.channelMetrics.setFullStatsEnabled(true);
		}
		this.fullChannelName = null;
	}

	/**
	 * Returns the fully qualified channel name including the application context
	 * id, if available.
	 *
	 * @return The name.
	 */
	public String getFullChannelName() {
		if (this.fullChannelName == null) {
			String contextId = getApplicationContextId();
			String componentName = getComponentName();
			componentName = (StringUtils.hasText(contextId) ? contextId + "." : "") +
					(StringUtils.hasText(componentName) ? componentName : "unknown.channel.name");
			this.fullChannelName = componentName;
		}
		return this.fullChannelName;
	}

	/**
	 * Send a message on this channel. If the channel is at capacity, this
	 * method will block until either space becomes available or the sending
	 * thread is interrupted.
	 * @param message the Message to send
	 * @return <code>true</code> if the message is sent successfully or
	 * <code>false</code> if the sending thread is interrupted.
	 */
	@Override
	public boolean send(Message<?> message) {
		return this.send(message, -1);
	}

	/**
	 * Send a message on this channel. If the channel is at capacity, this
	 * method will block until either the timeout occurs or the sending thread
	 * is interrupted. If the specified timeout is 0, the method will return
	 * immediately. If less than zero, it will block indefinitely (see
	 * {@link #send(Message)}).
	 * @param messageArg the Message to send
	 * @param timeout the timeout in milliseconds
	 * @return <code>true</code> if the message is sent successfully,
	 * <code>false</code> if the message cannot be sent within the allotted
	 * time or the sending thread is interrupted.
	 */
	@Override // NOSONAR complexity
	public boolean send(Message<?> messageArg, long timeout) {
		Assert.notNull(messageArg, "message must not be null");
		Assert.notNull(messageArg.getPayload(), "message payload must not be null");
		Message<?> message = messageArg;
		if (this.shouldTrack) {
			message = MessageHistory.write(message, this, getMessageBuilderFactory());
		}

		Deque<ChannelInterceptor> interceptorStack = null;
		boolean sent = false;
		boolean metricsProcessed = false;
		org.springframework.integration.support.management.MetricsContext metricsContext = null;
		boolean countsAreEnabled = this.countsEnabled;
		ChannelInterceptorList interceptorList = this.interceptors;
		AbstractMessageChannelMetrics metrics = this.channelMetrics;
		SampleFacade sample = null;
		try {
			message = convertPayloadIfNecessary(message);
			boolean debugEnabled = this.loggingEnabled && logger.isDebugEnabled();
			if (debugEnabled) {
				logger.debug("preSend on channel '" + this + "', message: " + message);
			}
			if (interceptorList.getSize() > 0) {
				interceptorStack = new ArrayDeque<>();
				message = interceptorList.preSend(message, this, interceptorStack);
				if (message == null) {
					return false;
				}
			}
			if (countsAreEnabled) {
				metricsContext = metrics.beforeSend();
				if (this.metricsCaptor != null) {
					sample = this.metricsCaptor.start();
				}
				sent = doSend(message, timeout);
				if (sample != null) {
					sample.stop(sendTimer(sent));
				}
				metrics.afterSend(metricsContext, sent);
				metricsProcessed = true;
			}
			else {
				sent = doSend(message, timeout);
			}

			if (debugEnabled) {
				logger.debug("postSend (sent=" + sent + ") on channel '" + this + "', message: " + message);
			}
			if (interceptorStack != null) {
				interceptorList.postSend(message, this, sent);
				interceptorList.afterSendCompletion(message, this, sent, null, interceptorStack);
			}
			return sent;
		}
		catch (Exception ex) {
			if (countsAreEnabled && !metricsProcessed) {
				if (sample != null) {
					sample.stop(buildSendTimer(false, ex.getClass().getSimpleName()));
				}
				metrics.afterSend(metricsContext, false);
			}
			if (interceptorStack != null) {
				interceptorList.afterSendCompletion(message, this, sent, ex, interceptorStack);
			}
			throw IntegrationUtils.wrapInDeliveryExceptionIfNecessary(message,
					() -> "failed to send Message to channel '" + this.getComponentName() + "'", ex);
		}
	}

	private TimerFacade sendTimer(boolean sent) {
		if (sent) {
			if (this.successTimer == null) {
				this.successTimer = buildSendTimer(true, "none");
			}
			return this.successTimer;
		}
		else {
			if (this.failureTimer == null) {
				this.failureTimer = buildSendTimer(false, "none");
			}
			return this.failureTimer;
		}
	}

	private TimerFacade buildSendTimer(boolean success, String exception) {
		TimerFacade timer = this.metricsCaptor.timerBuilder(SEND_TIMER_NAME)
				.tag("type", "channel")
				.tag("name", getComponentName() == null ? "unknown" : getComponentName())
				.tag("result", success ? "success" : "failure")
				.tag("exception", exception)
				.description("Send processing time")
				.build();
		this.meters.add(timer);
		return timer;
	}

	private Message<?> convertPayloadIfNecessary(Message<?> message) {
		if (this.datatypes.length > 0) {
			// first pass checks if the payload type already matches any of the datatypes
			for (Class<?> datatype : this.datatypes) {
				if (datatype.isAssignableFrom(message.getPayload().getClass())) {
					return message;
				}
			}
			if (this.messageConverter != null) {
				// second pass applies conversion if possible, attempting datatypes in order
				for (Class<?> datatype : this.datatypes) {
					Object converted = this.messageConverter.fromMessage(message, datatype);
					if (converted != null) {
						if (converted instanceof Message) {
							return (Message<?>) converted;
						}
						else {
							return getMessageBuilderFactory()
									.withPayload(converted)
									.copyHeaders(message.getHeaders())
									.build();
						}
					}
				}
			}
			throw new MessageDeliveryException(message, "Channel '" + this.getComponentName() +
					"' expected one of the following data types [" +
					StringUtils.arrayToCommaDelimitedString(this.datatypes) +
					"], but received [" + message.getPayload().getClass() + "]");
		}
		else {
			return message;
		}
	}

	/**
	 * Subclasses must implement this method. A non-negative timeout indicates
	 * how long to wait if the channel is at capacity (if the value is 0, it
	 * must return immediately with or without success). A negative timeout
	 * value indicates that the method should block until either the message is
	 * accepted or the blocking thread is interrupted.
	 * @param message The message.
	 * @param timeout The timeout.
	 * @return true if the send was successful.
	 */
	protected abstract boolean doSend(Message<?> message, long timeout);

	@Override
	public void destroy() {
		this.meters.forEach(MeterFacade::remove);
		this.meters.clear();
	}

	/**
	 * A convenience wrapper class for the list of ChannelInterceptors.
	 */
	protected static class ChannelInterceptorList {

		protected final List<ChannelInterceptor> interceptors = new CopyOnWriteArrayList<>(); // NOSONAR

		private final Log logger;

		private int size;

		public ChannelInterceptorList(Log logger) {
			this.logger = logger;
		}

		public boolean set(List<ChannelInterceptor> interceptors) {
			synchronized (this.interceptors) {
				this.interceptors.clear();
				this.size = interceptors.size();
				return this.interceptors.addAll(interceptors);
			}
		}

		public int getSize() {
			return this.size;
		}

		public boolean add(ChannelInterceptor interceptor) {
			this.size++;
			return this.interceptors.add(interceptor);
		}

		public void add(int index, ChannelInterceptor interceptor) {
			this.size++;
			this.interceptors.add(index, interceptor);
		}

		@Nullable
		public Message<?> preSend(Message<?> messageArg, MessageChannel channel,
				Deque<ChannelInterceptor> interceptorStack) {

			Message<?> message = messageArg;
			if (this.size > 0) {
				for (ChannelInterceptor interceptor : this.interceptors) {
					Message<?> previous = message;
					message = interceptor.preSend(message, channel);
					if (message == null) {
						if (this.logger.isDebugEnabled()) {
							this.logger.debug(interceptor.getClass().getSimpleName()
									+ " returned null from preSend, i.e. precluding the send.");
						}
						afterSendCompletion(previous, channel, false, null, interceptorStack);
						return null;
					}
					interceptorStack.add(interceptor);
				}
			}
			return message;
		}

		public void postSend(Message<?> message, MessageChannel channel, boolean sent) {
			if (this.size > 0) {
				for (ChannelInterceptor interceptor : this.interceptors) {
					interceptor.postSend(message, channel, sent);
				}
			}
		}

		public void afterSendCompletion(Message<?> message, MessageChannel channel, boolean sent,
				@Nullable Exception ex, Deque<ChannelInterceptor> interceptorStack) {

			for (Iterator<ChannelInterceptor> iterator = interceptorStack.descendingIterator(); iterator.hasNext(); ) {
				ChannelInterceptor interceptor = iterator.next();
				try {
					interceptor.afterSendCompletion(message, channel, sent, ex);
				}
				catch (Exception ex2) {
					this.logger.error("Exception from afterSendCompletion in " + interceptor, ex2);
				}
			}
		}

		public boolean preReceive(MessageChannel channel, Deque<ChannelInterceptor> interceptorStack) {
			if (this.size > 0) {
				for (ChannelInterceptor interceptor : this.interceptors) {
					if (!interceptor.preReceive(channel)) {
						afterReceiveCompletion(null, channel, null, interceptorStack);
						return false;
					}
					interceptorStack.add(interceptor);
				}
			}
			return true;
		}

		@Nullable
		public Message<?> postReceive(Message<?> messageArg, MessageChannel channel) {
			Message<?> message = messageArg;
			if (this.size > 0) {
				for (ChannelInterceptor interceptor : this.interceptors) {
					message = interceptor.postReceive(message, channel);
					if (message == null) {
						return null;
					}
				}
			}
			return message;
		}

		public void afterReceiveCompletion(@Nullable Message<?> message, MessageChannel channel,
				@Nullable Exception ex, @Nullable Deque<ChannelInterceptor> interceptorStack) {

			if (interceptorStack != null) {
				for (Iterator<ChannelInterceptor> iterator = interceptorStack.descendingIterator(); iterator.hasNext(); ) {
					ChannelInterceptor interceptor = iterator.next();
					try {
						interceptor.afterReceiveCompletion(message, channel, ex);
					}
					catch (Exception ex2) {
						this.logger.error("Exception from afterReceiveCompletion in " + interceptor, ex2);
					}
				}
			}
		}

		public List<ChannelInterceptor> getInterceptors() {
			return Collections.unmodifiableList(this.interceptors);
		}

		public boolean remove(ChannelInterceptor interceptor) {
			if (this.interceptors.remove(interceptor)) {
				this.size--;
				return true;
			}
			else {
				return false;
			}
		}

		@Nullable
		public ChannelInterceptor remove(int index) {
			ChannelInterceptor removed = this.interceptors.remove(index);
			if (removed != null) {
				this.size--;
			}
			return removed;
		}

	}

}
