/*
 * Copyright 2014-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.support;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.springframework.integration.IntegrationMessageHeaderAccessor;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHeaders;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.PatternMatchUtils;

/**
 * @author Gary Russell
 * @author Artem Bilan
 *
 * @since 4.0
 *
 */
public abstract class AbstractIntegrationMessageBuilder<T> {

	public AbstractIntegrationMessageBuilder<T> setExpirationDate(Long expirationDate) {
		return setHeader(IntegrationMessageHeaderAccessor.EXPIRATION_DATE, expirationDate);
	}

	public AbstractIntegrationMessageBuilder<T> setExpirationDate(Date expirationDate) {
		if (expirationDate != null) {
			return setHeader(IntegrationMessageHeaderAccessor.EXPIRATION_DATE, expirationDate.getTime());
		}
		else {
			return setHeader(IntegrationMessageHeaderAccessor.EXPIRATION_DATE, null);
		}
	}

	public AbstractIntegrationMessageBuilder<T> setCorrelationId(Object correlationId) {
		return setHeader(IntegrationMessageHeaderAccessor.CORRELATION_ID, correlationId);
	}

	public AbstractIntegrationMessageBuilder<T> pushSequenceDetails(Object correlationId, int sequenceNumber,
			int sequenceSize) {

		Object incomingCorrelationId = this.getCorrelationId();
		List<List<Object>> incomingSequenceDetails = getSequenceDetails();
		if (incomingCorrelationId != null) {
			if (incomingSequenceDetails == null) {
				incomingSequenceDetails = new ArrayList<>();
			}
			else {
				incomingSequenceDetails = new ArrayList<>(incomingSequenceDetails);
			}
			incomingSequenceDetails.add(Arrays.asList(incomingCorrelationId,
					getSequenceNumber(), getSequenceSize()));
			incomingSequenceDetails = Collections.unmodifiableList(incomingSequenceDetails);
		}
		if (incomingSequenceDetails != null) {
			setHeader(IntegrationMessageHeaderAccessor.SEQUENCE_DETAILS, incomingSequenceDetails);
		}
		return setCorrelationId(correlationId)
				.setSequenceNumber(sequenceNumber)
				.setSequenceSize(sequenceSize);
	}

	public AbstractIntegrationMessageBuilder<T> popSequenceDetails() {
		List<List<Object>> incomingSequenceDetails = getSequenceDetails();
		if (incomingSequenceDetails == null) {
			return this;
		}
		else {
			incomingSequenceDetails = new ArrayList<>(incomingSequenceDetails);
		}
		List<Object> sequenceDetails = incomingSequenceDetails.remove(incomingSequenceDetails.size() - 1);
		Assert.state(sequenceDetails.size() == 3, "Wrong sequence details (not created by MessageBuilder?): "
				+ sequenceDetails);
		setCorrelationId(sequenceDetails.get(0));
		Integer sequenceNumber = (Integer) sequenceDetails.get(1);
		Integer sequenceSize = (Integer) sequenceDetails.get(2);
		if (sequenceNumber != null) {
			setSequenceNumber(sequenceNumber);
		}
		if (sequenceSize != null) {
			setSequenceSize(sequenceSize);
		}
		if (!incomingSequenceDetails.isEmpty()) {
			setHeader(IntegrationMessageHeaderAccessor.SEQUENCE_DETAILS, incomingSequenceDetails);
		}
		else {
			removeHeader(IntegrationMessageHeaderAccessor.SEQUENCE_DETAILS);
		}
		return this;
	}

	public AbstractIntegrationMessageBuilder<T> setReplyChannel(MessageChannel replyChannel) {
		return setHeader(MessageHeaders.REPLY_CHANNEL, replyChannel);
	}

	public AbstractIntegrationMessageBuilder<T> setReplyChannelName(String replyChannelName) {
		return setHeader(MessageHeaders.REPLY_CHANNEL, replyChannelName);
	}

	public AbstractIntegrationMessageBuilder<T> setErrorChannel(MessageChannel errorChannel) {
		return setHeader(MessageHeaders.ERROR_CHANNEL, errorChannel);
	}

	public AbstractIntegrationMessageBuilder<T> setErrorChannelName(String errorChannelName) {
		return setHeader(MessageHeaders.ERROR_CHANNEL, errorChannelName);
	}

	public AbstractIntegrationMessageBuilder<T> setSequenceNumber(Integer sequenceNumber) {
		return setHeader(IntegrationMessageHeaderAccessor.SEQUENCE_NUMBER, sequenceNumber);
	}

	public AbstractIntegrationMessageBuilder<T> setSequenceSize(Integer sequenceSize) {
		return setHeader(IntegrationMessageHeaderAccessor.SEQUENCE_SIZE, sequenceSize);
	}

	public AbstractIntegrationMessageBuilder<T> setPriority(Integer priority) {
		return setHeader(IntegrationMessageHeaderAccessor.PRIORITY, priority);
	}

	/**
	 * Remove headers from the provided map matching to the provided pattens
	 * and only after that copy the result into the target message headers.
	 * @param headersToCopy a map of headers to copy.
	 * @param headerPatternsToFilter an arrays of header patterns to filter before copying.
	 * @return the current {@link AbstractIntegrationMessageBuilder}.
	 * @since 5.1
	 * @see #copyHeadersIfAbsent(Map)
	 */
	public AbstractIntegrationMessageBuilder<T> filterAndCopyHeadersIfAbsent(Map<String, ?> headersToCopy,
			@Nullable String... headerPatternsToFilter) {

		Map<String, ?> headers = headersToCopy;

		if (!ObjectUtils.isEmpty(headerPatternsToFilter)) {
			headers = new HashMap<>(headersToCopy);
			headers.entrySet()
					.removeIf(entry -> PatternMatchUtils.simpleMatch(headerPatternsToFilter, entry.getKey()));
		}

		return copyHeadersIfAbsent(headers);
	}

	@Nullable
	protected abstract List<List<Object>> getSequenceDetails();

	protected abstract Object getCorrelationId();

	protected abstract Object getSequenceNumber();

	protected abstract Object getSequenceSize();

	public abstract T getPayload();

	public abstract Map<String, Object> getHeaders();

	@Nullable
	public abstract <V> V getHeader(String key, Class<V> type);

	/**
	 * Set the value for the given header name. If the provided value is <code>null</code>, the header will be removed.
	 * @param headerName The header name.
	 * @param headerValue The header value.
	 * @return this.
	 */
	public abstract AbstractIntegrationMessageBuilder<T> setHeader(String headerName, @Nullable Object headerValue);

	/**
	 * Set the value for the given header name only if the header name is not already associated with a value.
	 * @param headerName The header name.
	 * @param headerValue The header value.
	 * @return this.
	 */
	public abstract AbstractIntegrationMessageBuilder<T> setHeaderIfAbsent(String headerName, Object headerValue);

	/**
	 * Removes all headers provided via array of 'headerPatterns'. As the name suggests the array
	 * may contain simple matching patterns for header names. Supported pattern styles are:
	 * "xxx*", "*xxx", "*xxx*" and "xxx*yyy".
	 * @param headerPatterns The header patterns.
	 * @return this.
	 */
	public abstract AbstractIntegrationMessageBuilder<T> removeHeaders(String... headerPatterns);

	/**
	 * Remove the value for the given header name.
	 * @param headerName The header name.
	 * @return this.
	 */
	public abstract AbstractIntegrationMessageBuilder<T> removeHeader(String headerName);

	/**
	 * Copy the name-value pairs from the provided Map. This operation will overwrite any existing values. Use {
	 * {@link #copyHeadersIfAbsent(Map)} to avoid overwriting values. Note that the 'id' and 'timestamp' header values
	 * will never be overwritten.
	 * @param headersToCopy The headers to copy.
	 * @return this.
	 * @see MessageHeaders#ID
	 * @see MessageHeaders#TIMESTAMP
	 */
	public abstract AbstractIntegrationMessageBuilder<T> copyHeaders(@Nullable Map<String, ?> headersToCopy);

	/**
	 * Copy the name-value pairs from the provided Map. This operation will <em>not</em> overwrite any existing values.
	 * @param headersToCopy The headers to copy.
	 * @return this.
	 */
	public abstract AbstractIntegrationMessageBuilder<T> copyHeadersIfAbsent(@Nullable Map<String, ?> headersToCopy);

	public abstract Message<T> build();

}
