/*
 * Copyright 2018-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.twitter.inbound;

import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue;

import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.Lifecycle;
import org.springframework.integration.context.IntegrationContextUtils;
import org.springframework.integration.context.IntegrationObjectSupport;
import org.springframework.integration.core.MessageSource;
import org.springframework.integration.metadata.MetadataStore;
import org.springframework.integration.metadata.SimpleMetadataStore;
import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.jmx.export.annotation.ManagedOperation;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessagingException;
import org.springframework.social.twitter.api.DirectMessage;
import org.springframework.social.twitter.api.Tweet;
import org.springframework.social.twitter.api.Twitter;
import org.springframework.social.twitter.api.UserOperations;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

/**
 * Abstract class that defines common operations for receiving various types of
 * messages when using the Twitter API. This class also handles keeping track of
 * the latest inbound message it has received and avoiding, where possible,
 * redelivery of duplicate messages. This functionality is enabled using the
 * {@link org.springframework.integration.metadata.MetadataStore} strategy.
 *
 * @param <T> the target twitter object type.
 *
 * @author Josh Long
 * @author Oleg Zhurakousky
 * @author Mark Fisher
 * @author Gunnar Hillert
 * @author Artem Bilan
 */
abstract class AbstractTwitterMessageSource<T> extends IntegrationObjectSupport implements MessageSource<T>,
		Lifecycle {

	private static final int DEFAULT_PAGE_SIZE = 20;

	private final Twitter twitter;

	private final TweetComparator tweetComparator = new TweetComparator();

	private final Object lastEnqueuedIdMonitor = new Object();

	private final String metadataKey;

	private final Queue<T> tweets = new LinkedBlockingQueue<>();

	private volatile MetadataStore metadataStore;

	private volatile int prefetchThreshold = 0;

	private volatile long lastEnqueuedId = -1;

	private volatile long lastProcessedId = -1;

	private volatile int pageSize = DEFAULT_PAGE_SIZE;

	private volatile boolean running;

	protected AbstractTwitterMessageSource(Twitter twitter, String metadataKey) {
		Assert.notNull(twitter, "twitter must not be null");
		Assert.notNull(metadataKey, "metadataKey must not be null");
		this.twitter = twitter;
		if (this.twitter.isAuthorized()) {
			UserOperations userOperations = this.twitter.userOperations();
			metadataKey += "." + userOperations.getProfileId();
		}
		this.metadataKey = metadataKey;
	}


	public void setMetadataStore(MetadataStore metadataStore) {
		this.metadataStore = metadataStore;
	}

	public void setPrefetchThreshold(int prefetchThreshold) {
		this.prefetchThreshold = prefetchThreshold;
	}

	protected Twitter getTwitter() {
		return this.twitter;
	}

	protected int getPageSize() {
		return this.pageSize;
	}

	/**
	 * Set the limit for the number of results returned on each poll; default 20.
	 * @param pageSize The pageSize.
	 */
	public void setPageSize(int pageSize) {
		this.pageSize = pageSize;
	}

	@Override
	protected void onInit() {
		super.onInit();
		if (this.metadataStore == null) {
			// first try to look for a 'metadataStore' in the context
			BeanFactory beanFactory = this.getBeanFactory();
			if (beanFactory != null) {
				this.metadataStore = IntegrationContextUtils.getMetadataStore(beanFactory);
			}
			if (this.metadataStore == null) {
				this.metadataStore = new SimpleMetadataStore();
			}
		}

	}

	@Override
	public synchronized void start() {
		if (!this.running) {
			String lastId = this.metadataStore.get(this.metadataKey);
			// initialize the last status ID from the metadataStore
			if (StringUtils.hasText(lastId)) {
				this.lastProcessedId = Long.parseLong(lastId);
				synchronized (this.lastEnqueuedIdMonitor) {
					this.lastEnqueuedId = this.lastProcessedId;
				}
			}
			this.running = true;
		}
	}

	@Override
	public synchronized void stop() {
		this.running = false;
	}

	@Override
	public synchronized boolean isRunning() {
		return this.running;
	}

	@Override
	public Message<T> receive() {
		T tweet = this.tweets.poll();
		if (tweet == null) {
			this.refreshTweetQueueIfNecessary();
			tweet = this.tweets.poll();
		}

		if (tweet != null) {
			this.lastProcessedId = this.getIdForTweet(tweet);
			this.metadataStore.put(this.metadataKey, String.valueOf(this.lastProcessedId));
			return this.getMessageBuilderFactory().withPayload(tweet).build();
		}
		return null;
	}

	private void enqueueAll(List<T> tweets) {
		tweets.sort(this.tweetComparator);
		tweets.forEach(this::enqueue);
	}

	private void enqueue(T tweet) {
		synchronized (this.lastEnqueuedIdMonitor) {
			long id = this.getIdForTweet(tweet);
			if (id > this.lastEnqueuedId) {
				this.tweets.add(tweet);
				synchronized (this.lastEnqueuedIdMonitor) {
					this.lastEnqueuedId = id;
				}
			}
		}
	}

	private void refreshTweetQueueIfNecessary() {
		try {
			if (this.tweets.size() <= this.prefetchThreshold) {
				synchronized (this.lastEnqueuedIdMonitor) {
					List<T> tweets = pollForTweets(this.lastEnqueuedId);
					if (!CollectionUtils.isEmpty(tweets)) {
						enqueueAll(tweets);
					}
				}
			}
		}
		catch (RuntimeException e) {
			throw e;
		}
		catch (Exception e) {
			throw new MessagingException("failed while polling Twitter", e);
		}
	}

	/**
	 * Subclasses must implement this to return tweets.
	 * The 'sinceId' value will be negative if no last id is known.
	 *
	 * @param sinceId The id of the last reported tweet.
	 * @return The list of tweets.
	 */
	protected abstract List<T> pollForTweets(long sinceId);


	private long getIdForTweet(T twitterMessage) {
		if (twitterMessage instanceof Tweet) {
			return ((Tweet) twitterMessage).getId();
		}
		else if (twitterMessage instanceof DirectMessage) {
			return ((DirectMessage) twitterMessage).getId();
		}
		else {
			throw new IllegalArgumentException("Unsupported Twitter object: " + twitterMessage);
		}
	}


	/**
	 * Remove the metadata key and the corresponding value from the Metadata Store.
	 */
	@ManagedOperation(description = "Remove the metadata key and the corresponding value from the Metadata Store.")
	void resetMetadataStore() {
		synchronized (this) {
			this.metadataStore.remove(this.metadataKey);
			this.lastProcessedId = -1L;
			synchronized (this.lastEnqueuedIdMonitor) {
				this.lastEnqueuedId = -1L;
			}
		}
	}

	/**
	 * Obtain the last processed id.
	 * @return {@code -1} if lastProcessedId is not set, yet.
	 */
	@ManagedAttribute
	public long getLastProcessedId() {
		return this.lastProcessedId;
	}

	private class TweetComparator implements Comparator<T> {

		TweetComparator() {
			super();
		}

		@Override
		public int compare(T tweet1, T tweet2) {
			// hopefully temporary logic. Will suggest that SpringSocial use a common base class for DM and Tweet
			if (tweet1 instanceof Tweet && tweet2 instanceof Tweet) {
				Tweet t1 = (Tweet) tweet1;
				Tweet t2 = (Tweet) tweet2;
				Date t1CreatedAt = t1.getCreatedAt();
				Date t2CreatedAt = t2.getCreatedAt();
				Assert.notNull(t1CreatedAt, "Tweet is missing 'createdAt' date. Cannot compare.");
				Assert.notNull(t2CreatedAt, "Tweet is missing 'createdAt' date. Cannot compare.");
				return t1CreatedAt.compareTo(t2CreatedAt);
			}
			else if (tweet1 instanceof DirectMessage && tweet2 instanceof DirectMessage) {
				DirectMessage d1 = (DirectMessage) tweet1;
				DirectMessage d2 = (DirectMessage) tweet2;
				Date d1CreatedAt = d1.getCreatedAt();
				Date d2CreatedAt = d2.getCreatedAt();
				Assert.notNull(d1CreatedAt, "DirectMessage is missing 'createdAt' date. Cannot compare.");
				Assert.notNull(d2CreatedAt, "DirectMessage is missing 'createdAt' date. Cannot compare.");
				return d1CreatedAt.compareTo(d2CreatedAt);
			}
			else {
				throw new IllegalArgumentException("Uncomparable Twitter objects: " + tweet1 + " and " + tweet2);
			}
		}

	}

}
