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

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

import javax.jms.JMSException;

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

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

    private static final Log log = LogFactory.getLog(JournalMessageStore.class);

    protected final JournalPersistenceAdapter peristenceAdapter;

    protected final MessageStore longTermStore;

    protected final String destinationName;

    protected final TransactionTemplate transactionTemplate;

    private RecordLocation nextMark;

    private LinkedHashMap addedMessageIds = new LinkedHashMap();

    private ArrayList removedMessageLocations = new ArrayList();

    /** A MessageStore that we can use to retreive messages quickly. */
    private MessageStore cacheMessageStore = this;

    public JournalMessageStore(JournalPersistenceAdapter adapter, MessageStore checkpointStore, String destinationName) {
        this.peristenceAdapter = adapter;
        this.longTermStore = checkpointStore;
        this.destinationName = destinationName;
        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 void addMessage(final ActiveMQMessage message) throws JMSException {
        final RecordLocation location = peristenceAdapter.writePacket(destinationName, message, message.isReceiptRequired());
        TransactionManager.getContexTransaction().addPostCommitTask(new TransactionTask(){
            public void execute() throws Throwable {
                synchronized (JournalMessageStore.this) {
                    nextMark = location;
                    MessageIdentity id = message.getJMSMessageIdentity();
                    addedMessageIds.put(id, location);
                }
            }
        });
    }

    /**
     */
    public void removeMessage(final MessageAck ack) throws JMSException {

        final RecordLocation ackLocation = peristenceAdapter.writePacket(destinationName, ack, ack.isReceiptRequired());        
        TransactionManager.getContexTransaction().addPostCommitTask(new TransactionTask(){
            public void execute() throws Throwable {
                synchronized (JournalMessageStore.this) {
                    nextMark = ackLocation;
                    MessageIdentity id = ack.getMessageIdentity();
                    if (addedMessageIds.remove(id) == null) {
                        removedMessageLocations.add(ack);
                    }
                }
            }
        });
    }

    /**
     * @return
     * @throws JMSException
     */
    public RecordLocation checkpoint() throws JMSException {

        RecordLocation rc;
        final HashMap cpAddedMessageIds;
        final ArrayList cpRemovedMessageLocations;

        // swap out the message hashmaps..
        synchronized (this) {
            rc = nextMark;
            cpAddedMessageIds = this.addedMessageIds;
            cpRemovedMessageLocations = this.removedMessageLocations;

            this.nextMark = null;
            this.addedMessageIds = new LinkedHashMap();
            this.removedMessageLocations = new ArrayList();
        }

        transactionTemplate.run(new Callback() {
            public void execute() throws Throwable {

                // Checkpoint the added messages.
                Iterator iterator = cpAddedMessageIds.keySet().iterator();
                while (iterator.hasNext()) {
                    MessageIdentity identity = (MessageIdentity) iterator.next();
                    ActiveMQMessage msg = getCacheMessage(identity);
                    // Pull it out of the journal if we have to.
                    if (msg == null) {
                        RecordLocation location = (RecordLocation) cpAddedMessageIds.get(identity);
                        msg = (ActiveMQMessage) peristenceAdapter.readPacket((RecordLocation) location);
                    }
                    if( msg != null ) {
                        try {
                            longTermStore.addMessage(msg);
                        } catch (Throwable e) {
                            log.warn("Message could not be added to long term store: " + e.getMessage(), e);
                        }
                    } else {
                        log.warn("Journal could not reload message: " + identity);                        
                    }
                }

                // Checkpoint the removed messages.
                iterator = cpRemovedMessageLocations.iterator();
                while (iterator.hasNext()) {
                    try {
                        MessageAck ack = (MessageAck) iterator.next();
                        longTermStore.removeMessage(ack);
                    } catch (Throwable e) {
                        log.debug("Message could not be removed from long term store: " + e.getMessage(), e);
                    }
                }
            }

        });

        return rc;
    }

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

    /**
     * 
     */
    public ActiveMQMessage getMessage(MessageIdentity identity) throws JMSException {
        ActiveMQMessage answer = null;

        Object location;
        synchronized (this) {
            location = addedMessageIds.get(identity);
        }
        
        // Do we have a still have it in the journal?
        if (location != null ) {
            try {
                answer = (ActiveMQMessage)peristenceAdapter.readPacket((RecordLocation) location);
                if (answer != null)
                    return answer;
            } catch (Throwable e) {
                // We could have had an async checkpoint and thus we cannot read that location anymore,
                // but now the message should be in the long term store.
            }
        }

        // 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 listener
     * @throws JMSException
     */
    public void recover(final RecoveryListener listener) throws JMSException {
        peristenceAdapter.checkpoint(true);
        longTermStore.recover(listener);
    }

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

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

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

    /**
     * @see org.activemq.store.cache.CacheMessageStoreAware#setCacheMessageStore(org.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.activemq.store.MessageStore#removeAllMessages()
     */
    public void removeAllMessages() throws JMSException {
        peristenceAdapter.checkpoint(true);
        longTermStore.removeAllMessages();
    }

}