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

import java.io.IOException;

import javax.jms.JMSException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.activemq.io.WireFormat;
import org.activemq.message.ActiveMQMessage;
import org.activemq.message.ConsumerInfo;
import org.activemq.service.MessageIdentity;
import org.activemq.service.SubscriberEntry;
import org.activemq.store.RecoveryListener;
import org.activemq.store.TopicMessageStore;
import org.activemq.util.JMSExceptionHelper;

import com.sleepycat.je.Database;
import com.sleepycat.je.DatabaseEntry;
import com.sleepycat.je.DatabaseException;
import com.sleepycat.je.LockMode;
import com.sleepycat.je.OperationStatus;
import com.sleepycat.je.SecondaryConfig;
import com.sleepycat.je.SecondaryCursor;
import com.sleepycat.je.SecondaryDatabase;
import com.sleepycat.je.Transaction;

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

    private Database subscriptionDatabase;

    public BDbTopicMessageStore(Database database, SecondaryDatabase secondaryDatabase, SecondaryConfig secondaryConfig, SequenceNumberCreator sequenceNumberCreator, WireFormat wireFormat, Database subscriptionDatabase) {
        super(database, secondaryDatabase, secondaryConfig, sequenceNumberCreator, wireFormat);
        this.subscriptionDatabase = subscriptionDatabase;
    }

    public void incrementMessageCount(MessageIdentity messageId) {
        /** TODO */
    }

    public void decrementMessageCountAndMaybeDelete(MessageIdentity messageIdentity) {
        /** TODO */
    }

    public void setLastAcknowledgedMessageIdentity(String subscription, MessageIdentity messageIdentity) throws JMSException {
        checkClosed();
        try {
            doSetLastAcknowledgedMessageIdentity(subscription, messageIdentity);
        }
        catch (DatabaseException e) {
            throw JMSExceptionHelper.newJMSException("Failed to update last acknowledge messageID for : "
                    + messageIdentity + ". Reason: " + e, e);
        }
    }

    /**
     * 
     */
    public void recoverSubscription(String subscriptionId, MessageIdentity lastDispatchedMessage, final RecoveryListener listener) throws JMSException {
        checkClosed();
        SecondaryCursor cursor = null;
        try {
            DatabaseEntry lastAckKey = getLastAcknowledgedMessageID(subscriptionId, lastDispatchedMessage);
            if (lastAckKey != null) {
                cursor = getSecondaryDatabase().openSecondaryCursor(BDbHelper.getTransaction(), getCursorConfig());
                DatabaseEntry valueEntry = new DatabaseEntry();
                OperationStatus status = cursor.getSearchKey(lastAckKey, valueEntry, LockMode.DEFAULT);
                if (status != OperationStatus.SUCCESS) {
                    log.error("Could not find the last acknowledged record for: " + subscriptionId + ". Status: " + status);
                }
                else {
                    while (true) {
                        // lets pass straight over the first entry, which we've already ack'd
                        status = cursor.getNext(lastAckKey, valueEntry, LockMode.DEFAULT);
                        if (status != OperationStatus.SUCCESS) {
                            if (status != OperationStatus.NOTFOUND) {
                                log.warn("Strange result when iterating to end of collection: " + status);
                            }
                            break;
                        }

                        ActiveMQMessage message = extractMessage(valueEntry);
                        listener.recoverMessage(message.getJMSMessageIdentity());
                    }
                }
            }
        }
        catch (DatabaseException e) {
            throw JMSExceptionHelper.newJMSException("Unable to recover topic subscription for: "
                    + subscriptionId + ". Reason: " + e, e);
        }
        catch (IOException e) {
            throw JMSExceptionHelper.newJMSException("Unable to recover topic subscription for: "
                    + subscriptionId + ". Reason: " + e, e);
        }
        finally {
            if (cursor != null) {
                try {
                    cursor.close();
                }
                catch (DatabaseException e) {
                    log.warn("Caught exception closing cursor: " + e, e);
                }
            }
        }
    }

    public MessageIdentity getLastestMessageIdentity() throws JMSException {
        checkClosed();
        SecondaryCursor cursor = null;
        try {
            cursor = getSecondaryDatabase().openSecondaryCursor(BDbHelper.getTransaction(), getCursorConfig());
            DatabaseEntry keyEntry = new DatabaseEntry();
            DatabaseEntry valueEntry = new DatabaseEntry();
            OperationStatus status = cursor.getLast(keyEntry, valueEntry, LockMode.DEFAULT);
            if (status == OperationStatus.SUCCESS) {
                if (log.isDebugEnabled()) {
                    log.debug("Loaded last sequence number of: " + BDbHelper.longFromBytes(keyEntry.getData()));
                }
                return new MessageIdentity(null, keyEntry);
            }
            else if (status != OperationStatus.NOTFOUND) {
                log.error("Could not find the last sequence number. Status: " + status);
            }
            return null;
        }
        catch (DatabaseException e) {
            throw JMSExceptionHelper.newJMSException("Unable to load the last sequence number. Reason: " + e, e);
        }
        finally {
            if (cursor != null) {
                try {
                    cursor.close();
                }
                catch (DatabaseException e) {
                    log.warn("Caught exception closing cursor: " + e, e);
                }
            }
        }
    }

    public SubscriberEntry getSubscriberEntry(ConsumerInfo info) throws JMSException {
        return null;  /** TODO */
    }

    public void setSubscriberEntry(ConsumerInfo info, SubscriberEntry subscriberEntry) throws JMSException {
        /** TODO */
    }

    public synchronized void stop() throws JMSException {
        JMSException firstException = BDbPersistenceAdapter.closeDatabase(subscriptionDatabase, null);
        subscriptionDatabase = null;
        super.stop();
        if (firstException != null) {
            throw JMSExceptionHelper.newJMSException("Unable to close the subscription database: " + firstException, firstException);
        }
    }

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

    protected DatabaseEntry getLastAcknowledgedMessageID(String subscriptionId, MessageIdentity lastDispatchedMessage) throws DatabaseException {
        DatabaseEntry key = createKey(subscriptionId);
        DatabaseEntry value = new DatabaseEntry();
        OperationStatus status = subscriptionDatabase.get(null, key, value, null);
        if (status == OperationStatus.SUCCESS) {
            return value;
        }
        else if (status == OperationStatus.NOTFOUND) {
            // we need to insert the new entry
            if (lastDispatchedMessage != null) {
                return doSetLastAcknowledgedMessageIdentity(subscriptionId, lastDispatchedMessage);
            }
        }
        else {
            log.warn("Unexpected status return from querying lastAcknowledgeSequenceNumber for: " + subscriptionId + " status: " + status);
        }
        return null;
    }

    protected DatabaseEntry doSetLastAcknowledgedMessageIdentity(String subscription, MessageIdentity messageIdentity) throws DatabaseException {
        Transaction transaction = BDbHelper.getTransaction();
        DatabaseEntry key = createKey(subscription);
        DatabaseEntry value = getSequenceNumberKey(messageIdentity);
        subscriptionDatabase.put(transaction, key, value);
        return value;
    }

    public void deleteSubscription(String subscription) throws JMSException {
        checkClosed();
        try {
            doDeleteSubscription(subscription);
        }
        catch (DatabaseException e) {
            throw JMSExceptionHelper.newJMSException("Failed to remove subscription for : "+ subscription + ". Reason: " + e, e);
        }
    }

    protected void doDeleteSubscription(String subscription) throws DatabaseException {
        Transaction transaction = BDbHelper.getTransaction();
        DatabaseEntry key = createKey(subscription);
        subscriptionDatabase.delete(transaction, key);
    }
}
