/** 
 * 
 * Copyright 2004 Hiram Chirino
 * Copyright 2004 Protique Ltd
 * 
 * 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 
 * 
 * http://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.codehaus.activemq.store.journal;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;

import javax.jms.JMSException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.codehaus.activemq.journal.RecordLocation;
import org.codehaus.activemq.message.ActiveMQMessage;
import org.codehaus.activemq.message.MessageAck;
import org.codehaus.activemq.service.MessageIdentity;
import org.codehaus.activemq.service.QueueMessageContainer;
import org.codehaus.activemq.store.MessageStore;
import org.codehaus.activemq.store.cache.CacheMessageStore;
import org.codehaus.activemq.store.cache.CacheMessageStoreAware;
import org.codehaus.activemq.util.Callback;
import org.codehaus.activemq.util.TransactionTemplate;

/**
 * A MessageStore that uses a Journal to store it's messages.
 * 
 * @version $Revision: 1.2 $
 */
public class JournalMessageStore implements MessageStore, CacheMessageStoreAware {

	private static final Log log = LogFactory.getLog(JournalMessageStore.class);
	
	private final static class AckData {
		private final RecordLocation location;
		private final MessageAck ack;
		AckData(MessageAck ack, RecordLocation location) {
			this.ack = ack;
			this.location = location;			
		}
	}
	
	private final JournalPersistenceAdapter peristenceAdapter;
	private final MessageStore longTermStore;
	private final String destinationName;
	private final TransactionTemplate transactionTemplate;
	
	private HashMap addedMessageLocations = new HashMap();
	private ArrayList removedMessageLocations = new ArrayList();
	
	/** A MessageStore that we can use to retreive messages quickly. */
	private MessageStore cacheMessageStore = this;
	
	private boolean sync = true;

	public JournalMessageStore(JournalPersistenceAdapter adapter, MessageStore checkpointStore, String destinationName, boolean sync) {
		this.peristenceAdapter = adapter;
		this.longTermStore = checkpointStore;
		this.destinationName = destinationName;
		this.sync=sync;
		this.transactionTemplate = new TransactionTemplate(adapter);
	}

	/**
	 * Not synchronized since the Journal has better throughput if you increase
	 * the number of conncurrent writes that it is doing.
	 */
	public MessageIdentity addMessage(ActiveMQMessage message) throws JMSException {
		boolean sync = message.isReceiptRequired();
		RecordLocation location = peristenceAdapter.writePacket(destinationName, message, sync);
		synchronized(this) {
			addedMessageLocations.put(message.getJMSMessageIdentity(), location);
		}

		// Update the messageIdentity sequence number so that we can reteive the message
		// from the journal at a later time.
		MessageIdentity messageIdentity = message.getJMSMessageIdentity();
		messageIdentity.setSequenceNumber(location);
		return messageIdentity;
	}	
	
	/**
	 */
	public void removeMessage(MessageIdentity identity, MessageAck ack)
			throws JMSException {

		RecordLocation ackLocation = peristenceAdapter.writePacket(destinationName, ack, sync);

		synchronized(this) {
			RecordLocation addLocation = (RecordLocation) addedMessageLocations.remove(identity);			
			if( addLocation==null ) {
				removedMessageLocations.add(new AckData(ack, ackLocation));
			}
		}		
	}

	/**
	 * @return
	 * @throws JMSException
	 */
	public RecordLocation checkpoint() throws JMSException {
		final RecordLocation rc[] = new RecordLocation[]{null};
		
		// swap out the message hashmaps..
		final ArrayList addedMessageIdentitys;
		final ArrayList removedMessageLocations;
		synchronized(this) {
			addedMessageIdentitys = new ArrayList(this.addedMessageLocations.keySet());
			removedMessageLocations = this.removedMessageLocations;
			this.removedMessageLocations = new ArrayList();
		}
		
		transactionTemplate.run(new Callback() {
			public void execute() throws Throwable {
				
				// Checkpoint the added messages.
				Iterator iterator = addedMessageIdentitys.iterator();
				while (iterator.hasNext()) {					
					MessageIdentity identity = (MessageIdentity) iterator.next();
					
					ActiveMQMessage msg = getCacheMessage(identity);
					longTermStore.addMessage(msg);
					synchronized(this) {
						RecordLocation location = (RecordLocation)addedMessageLocations.remove(identity);
						if( rc[0]==null || rc[0].compareTo(location)<0 ) {
							rc[0] = location;
						}
					}
					
				}				
				
				// Checkpoint the removed messages.
				iterator = removedMessageLocations.iterator();
				while (iterator.hasNext()) {					
					AckData data = (AckData)iterator.next();
					longTermStore.removeMessage(data.ack.getMessageIdentity(),data.ack);

					if( rc[0]==null || rc[0].compareTo(data.location)<0 ) {
						rc[0] = data.location;
					}
				}				
				
			}

		});
		
		return rc[0];
	}
	
	private ActiveMQMessage getCacheMessage(MessageIdentity identity) throws JMSException {
		return cacheMessageStore.getMessage(identity);
	}

	/**
	 * 
	 */
	public ActiveMQMessage getMessage(MessageIdentity identity)	throws JMSException {
		ActiveMQMessage answer=null;
		
		Object location = identity.getSequenceNumber();
		if( location==null ) {
			// The sequence number may not have been set but it may still be in the journal.
			synchronized(this) {
				location = addedMessageLocations.get(identity);
			}
		}
		// Do we have a Journal sequence number?
		if(location!=null && location instanceof RecordLocation) {
			answer = (ActiveMQMessage) peristenceAdapter.readPacket((RecordLocation)location);
			if( answer !=null )
				return answer;
		}
		
		// If all else fails try the long term message store.
		return longTermStore.getMessage(identity);
	}

	/**
	 * Replays the checkpointStore first as those messages are the oldest ones,
	 * then messages are replayed from the transaction log and then the cache is
	 * updated.
	 * 
	 * @param container
	 * @throws JMSException
	 */
	public synchronized void recover(final QueueMessageContainer container)
			throws JMSException {
		longTermStore.recover(container);
	}

	public void start() throws JMSException {
		longTermStore.start();
	}

	public void stop() throws JMSException {
		longTermStore.stop();
	}

	/**
	 * @return Returns the longTermStore.
	 */
	public MessageStore getLongTermStore() {
		return longTermStore;
	}

	/**
	 * @see org.codehaus.activemq.store.cache.CacheMessageStoreAware#setCacheMessageStore(org.codehaus.activemq.store.cache.CacheMessageStore)
	 */
	public void setCacheMessageStore(CacheMessageStore store) {
		cacheMessageStore = store;
		// Propagate the setCacheMessageStore method call to the longTermStore if possible.
		if( longTermStore instanceof CacheMessageStoreAware ) {
			((CacheMessageStoreAware)longTermStore).setCacheMessageStore(store);
		}
	}

	/**
     * @see org.codehaus.activemq.store.MessageStore#removeAllMessages()
     */
    public void removeAllMessages() throws JMSException {
        peristenceAdapter.checkpoint();
        longTermStore.removeAllMessages();        
    }

}