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

import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Set;

import org.springframework.integration.IntegrationMessageHeaderAccessor;
import org.springframework.messaging.Message;
import org.springframework.util.Assert;

/**
 * Represents a mutable group of correlated messages that is bound to a certain {@link MessageStore} and group id.
 * The group will grow during its lifetime, when messages are <code>add</code>ed to it.
 * This MessageGroup is thread safe.
 *
 * @author Iwein Fuld
 * @author Oleg Zhurakousky
 * @author Dave Syer
 * @author Gary Russell
 * @author Artem Bilan
 *
 * @since 2.0
 */
public class SimpleMessageGroup implements MessageGroup {

	private final Object groupId;

	private final Collection<Message<?>> messages;

	private final Set<Integer> sequences = new HashSet<>();

	private final long timestamp;

	private volatile int lastReleasedMessageSequence;

	private volatile long lastModified;

	private volatile boolean complete;

	public SimpleMessageGroup(Object groupId) {
		this(Collections.<Message<?>>emptyList(), groupId);
	}

	public SimpleMessageGroup(Collection<? extends Message<?>> messages, Object groupId) {
		this(messages, groupId, System.currentTimeMillis(), false);
	}

	public SimpleMessageGroup(MessageGroup messageGroup) {
		this(messageGroup.getMessages(), messageGroup.getGroupId(), messageGroup.getTimestamp(),
				messageGroup.isComplete());
	}

	public SimpleMessageGroup(Collection<? extends Message<?>> messages, Object groupId, long timestamp,
			boolean complete) {
		this(new LinkedHashSet<>(), messages, groupId, timestamp, complete, false);
	}

	public SimpleMessageGroup(Collection<Message<?>> internalStore, Collection<? extends Message<?>> messages,
			Object groupId, long timestamp, boolean complete, boolean storePreLoaded) {

		Assert.notNull(internalStore, "'internalStore' must not be null");
		this.messages = internalStore;
		this.groupId = groupId;
		this.timestamp = timestamp;
		this.complete = complete;
		if (!storePreLoaded) {
			Assert.notNull(messages, "'messages' must not be null");
			for (Message<?> message : messages) {
				if (message != null) { //see INT-2666
					addMessage(message);
				}
			}
		}
	}

	@Override
	public long getTimestamp() {
		return this.timestamp;
	}

	@Override
	public void setLastModified(long lastModified) {
		this.lastModified = lastModified;
	}

	@Override
	public long getLastModified() {
		return this.lastModified;
	}

	@Override
	public boolean canAdd(Message<?> message) {
		return true;
	}

	@Override
	public void add(Message<?> messageToAdd) {
		addMessage(messageToAdd);
	}

	@Override
	public boolean remove(Message<?> message) {
		this.sequences.remove(message.getHeaders().get(IntegrationMessageHeaderAccessor.SEQUENCE_NUMBER));
		return this.messages.remove(message);
	}

	@Override
	public int getLastReleasedMessageSequenceNumber() {
		return this.lastReleasedMessageSequence;
	}

	private boolean addMessage(Message<?> message) {
		Integer sequence = message.getHeaders().get(IntegrationMessageHeaderAccessor.SEQUENCE_NUMBER, Integer.class);
		this.sequences.add(sequence != null ? sequence : 0);
		return this.messages.add(message);
	}

	@Override
	public Collection<Message<?>> getMessages() {
		return Collections.unmodifiableCollection(this.messages);
	}

	@Override
	public void setLastReleasedMessageSequenceNumber(int sequenceNumber) {
		this.lastReleasedMessageSequence = sequenceNumber;
	}

	@Override
	public Object getGroupId() {
		return this.groupId;
	}

	@Override
	public boolean isComplete() {
		return this.complete;
	}

	@Override
	public void complete() {
		this.complete = true;
	}

	@Override
	public int getSequenceSize() {
		if (size() == 0) {
			return 0;
		}
		return new IntegrationMessageHeaderAccessor(getOne()).getSequenceSize();
	}

	@Override
	public int size() {
		return this.messages.size();
	}

	@Override
	public Message<?> getOne() {
		synchronized (this.messages) {
			Iterator<Message<?>> iterator = this.messages.iterator();
			return iterator.hasNext() ? iterator.next() : null;
		}
	}

	@Override
	public void clear() {
		this.messages.clear();
		this.sequences.clear();
	}

	/**
	 * Return true if a message with this sequence number header exists in
	 * the group.
	 * @param sequence the sequence number.
	 * @return true if it exists.
	 * @since 4.3.7
	 */
	public boolean containsSequence(Integer sequence) {
		return this.sequences.contains(sequence);
	}

	@Override
	public String toString() {
		return "SimpleMessageGroup{" +
				"groupId=" + this.groupId +
				", messages=" + this.messages +
				", timestamp=" + this.timestamp +
				", lastModified=" + this.lastModified +
				'}';
	}

}
