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

import java.io.IOException;

import javax.jms.JMSException;

import jdbm.btree.BTree;
import jdbm.helper.Tuple;
import jdbm.helper.TupleBrowser;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.activemq.AlreadyClosedException;
import org.activemq.message.ActiveMQMessage;
import org.activemq.message.MessageAck;
import org.activemq.service.MessageIdentity;
import org.activemq.service.impl.MessageEntry;
import org.activemq.store.MessageStore;
import org.activemq.store.RecoveryListener;
import org.activemq.util.JMSExceptionHelper;

/**
 * @version $Revision: 1.1 $
 */
public class JdbmMessageStore implements MessageStore {
    private static final Log log = LogFactory.getLog(JdbmMessageStore.class);

    private BTree messageTable;
    private BTree orderedIndex;
    private long lastSequenceNumber = 0;

    public JdbmMessageStore(BTree messageTable, BTree orderedIndex) {
        this.messageTable = messageTable;
        this.orderedIndex = orderedIndex;        
    }

    public synchronized void addMessage(ActiveMQMessage message) throws JMSException {
        if (log.isDebugEnabled()) {
            log.debug("Adding message to container: " + message);
        }
        MessageEntry entry = new MessageEntry(message);
        Object sequenceNumber = null;
        synchronized (this) {
            sequenceNumber = new Long(++lastSequenceNumber);
        }
        try {
            String messageID = message.getJMSMessageID();
            getMessageTable().insert(messageID, entry, true);
            getOrderedIndex().insert(sequenceNumber, messageID, true);
            MessageIdentity answer = message.getJMSMessageIdentity();
            answer.setSequenceNumber(sequenceNumber);
        }
        catch (IOException e) {
            throw JMSExceptionHelper.newJMSException("Failed to add message: " + message + " in container: " + e, e);
        }
    }

    public synchronized ActiveMQMessage getMessage(MessageIdentity identity) throws JMSException {
        String messageID = identity.getMessageID();
        ActiveMQMessage message = null;
        try {
            MessageEntry entry = (MessageEntry) getMessageTable().find(messageID);
            if (entry != null) {
                message = entry.getMessage();
                message.getJMSMessageIdentity().setSequenceNumber(identity.getSequenceNumber());
            }
        }
        catch (IOException e) {
            throw JMSExceptionHelper.newJMSException("Failed to get message for messageID: " + messageID + " " + e, e);
        }
        return message;
    }

    public synchronized void removeMessage(MessageAck ack) throws JMSException {
        removeMessage(ack.getMessageIdentity());
    }

    public synchronized void removeMessage(MessageIdentity identity) throws JMSException {
        if (identity.getMessageID() == null) {
            throw new JMSException("Cannot remove message with null messageID for sequence number: " + identity.getSequenceNumber());
        }
        try {
            if( identity.getSequenceNumber()==null )
                identity.setSequenceNumber(findSequenceNumber(identity.getMessageID()));
            getMessageTable().remove(identity.getMessageID());
            getOrderedIndex().remove(identity.getSequenceNumber());
        }
        catch (IOException e) {
            throw JMSExceptionHelper.newJMSException("Failed to delete message for messageID: " + identity.getMessageID() + " " + e, e);
        }
        catch (IllegalArgumentException e) {
            log.warn("Could not find sequence number: " + identity.getSequenceNumber() + " in queue. " + e);
        }
    }

    public synchronized void recover(RecoveryListener listener) throws JMSException {
        try {
            Tuple tuple = new Tuple();
            TupleBrowser iter = getOrderedIndex().browse();
            while (iter.getNext(tuple)) {
                Long key = (Long) tuple.getKey();
                MessageIdentity messageIdentity = null;
                if (key != null) {
                    String messageID = (String) tuple.getValue();
                    if (messageID != null) {
                        messageIdentity = new MessageIdentity(messageID, key);
                    }
                }
                if (messageIdentity != null) {
                    listener.recoverMessage(messageIdentity);
                }
                else {
                    log.warn("Could not find message for sequenceNumber: " + key);
                }
            }
        }
        catch (IOException e) {
            throw JMSExceptionHelper.newJMSException("Failed to recover the durable queue store. Reason: " + e, e);
        }
    }

    public synchronized void start() throws JMSException {
        try {
            // lets iterate through all IDs from the
            Tuple tuple = new Tuple();
            Long lastSequenceNumber = null;
            TupleBrowser iter = getOrderedIndex().browse();
            while (iter.getNext(tuple)) {
                lastSequenceNumber = (Long) tuple.getKey();
            }
            if (lastSequenceNumber != null) {
                this.lastSequenceNumber = lastSequenceNumber.longValue();
                if (log.isDebugEnabled()) {
                    log.debug("Last sequence number is: " + lastSequenceNumber + " for: " + this);
                }
            }
            else {
                if (log.isDebugEnabled()) {
                    log.debug("Started empty database for: " + this);
                }
            }
        }
        catch (IOException e) {
            throw JMSExceptionHelper.newJMSException("Failed to find the last sequence number. Reason: " + e, e);
        }
    }

    public synchronized void stop() throws JMSException {
        JMSException firstException = closeTable(orderedIndex, null);
        firstException = closeTable(messageTable, firstException);
        orderedIndex = null;
        messageTable = null;
        if (firstException != null) {
            throw firstException;
        }
    }


    // Implementation methods
    //-------------------------------------------------------------------------

    protected long getLastSequenceNumber() {
        return lastSequenceNumber;
    }

    protected BTree getMessageTable() throws AlreadyClosedException {
        if (messageTable == null) {
            throw new AlreadyClosedException("JDBM MessageStore");
        }
        return messageTable;
    }

    protected BTree getOrderedIndex() throws AlreadyClosedException {
        if (orderedIndex == null) {
            throw new AlreadyClosedException("JDBM MessageStore");
        }
        return orderedIndex;
    }


    /**
     * Looks up the message using the given sequence number
     */
    protected ActiveMQMessage getMessageBySequenceNumber(Long sequenceNumber) throws IOException, JMSException {
        ActiveMQMessage message = null;
        String messageID = (String) getOrderedIndex().find(sequenceNumber);
        if (messageID != null) {
            message = getMessage(new MessageIdentity(messageID, sequenceNumber));
        }
        return message;
    }

    /**
     * Finds the sequence number for the given messageID
     *
     * @param messageID
     * @return
     */
    protected Object findSequenceNumber(String messageID) throws IOException, AlreadyClosedException {
        log.warn("Having to table scan to find the sequence number for messageID: " + messageID);

        Tuple tuple = new Tuple();
        TupleBrowser iter = getOrderedIndex().browse();
        while (iter.getNext(tuple)) {
            Object value = tuple.getValue();
            if (messageID.equals(value)) {
                return tuple.getKey();
            }
        }
        return null;
    }

    protected JMSException closeTable(BTree table, JMSException firstException) {
        table = null;
        return null;
    }

    public void removeAllMessages() throws JMSException {
        try {
            Tuple tuple = new Tuple();
            TupleBrowser iter = getOrderedIndex().browse();
            while (iter.getNext(tuple)) {                
                Object sequenceID = tuple.getKey();
                MessageIdentity messageIdentity = null;
                if (sequenceID != null) {
                    String messageID = (String) tuple.getValue();
                    if (messageID != null) {
                        getMessageTable().remove(messageID);
                    }
                    getOrderedIndex().remove(sequenceID);
                }
            }
        }
        catch (IOException e) {
            throw JMSExceptionHelper.newJMSException("Failed to delete all messages in table: " + e, e);
        }
    }
}
