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

import com.sleepycat.je.*;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.codehaus.activemq.AlreadyClosedException;
import org.codehaus.activemq.io.WireFormat;
import org.codehaus.activemq.message.ActiveMQMessage;
import org.codehaus.activemq.message.MessageAck;
import org.codehaus.activemq.service.MessageContainer;
import org.codehaus.activemq.service.MessageIdentity;
import org.codehaus.activemq.service.QueueMessageContainer;
import org.codehaus.activemq.store.MessageStore;
import org.codehaus.activemq.util.JMSExceptionHelper;

import javax.jms.JMSException;
import java.io.IOException;

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

    private Database database;
    private WireFormat wireFormat;
    private SecondaryDatabase secondaryDatabase;
    private SecondaryConfig secondaryConfig;
    private SequenceNumberCreator sequenceNumberCreator;
    private MessageContainer container;
    private CursorConfig cursorConfig;


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

    public void setMessageContainer(MessageContainer container) {
        this.container = container;
    }

    public MessageIdentity addMessage(ActiveMQMessage message) throws JMSException {
        checkClosed();
        String messageID = message.getJMSMessageID();
        try {
            Transaction transaction = BDbHelper.getTransaction();
            DatabaseEntry key = createKey(messageID);
            DatabaseEntry value = new DatabaseEntry(asBytes(message));
            database.put(transaction, key, value);

            MessageIdentity answer = message.getJMSMessageIdentity();
            answer.setSequenceNumber(sequenceNumberCreator.getLastKey());
            return answer;
        }
        catch (DatabaseException e) {
            throw JMSExceptionHelper.newJMSException("Failed to broker message: " + messageID + " in container: " + e, e);
        }
        catch (IOException e) {
            throw JMSExceptionHelper.newJMSException("Failed to broker message: " + messageID + " in container: " + e, e);
        }
    }

    public ActiveMQMessage getMessage(MessageIdentity identity) throws JMSException {
        checkClosed();
        ActiveMQMessage answer = null;
        String messageID = identity.getMessageID();
        try {
            DatabaseEntry key = createKey(messageID);
            DatabaseEntry value = new DatabaseEntry();
            if (database.get(null, key, value, null) == OperationStatus.SUCCESS) {
                answer = extractMessage(value);
            }
            return answer;
        }
        catch (DatabaseException e) {
            throw JMSExceptionHelper.newJMSException("Failed to peek next message after: " + messageID + " from container: " + e, e);
        }
        catch (IOException e) {
            throw JMSExceptionHelper.newJMSException("Failed to broker message: " + messageID + " in container: " + e, e);
        }
    }

    public void removeMessage(MessageIdentity identity, MessageAck ack) throws JMSException {
        checkClosed();
        String messageID = identity.getMessageID();
        try {
            Transaction transaction = BDbHelper.getTransaction();

            // we need to find the alternative primary key for the given messageID
            DatabaseEntry sequenceNumber = getSequenceNumberKey(identity);

            //System.out.println("Deleting sequenceNumber: " + BDbHelper.longFromBytes(sequenceNumber.getData()));

            sequenceNumberCreator.setDeleteKey(sequenceNumber);

            OperationStatus status = secondaryDatabase.delete(transaction, sequenceNumber);
            if (status != OperationStatus.SUCCESS) {
                log.error("Could not delete sequenece number for: " + identity + " status: " + status);
            }
        }
        catch (DatabaseException e) {
            throw JMSExceptionHelper.newJMSException("Failed to delete message: " + messageID + " from container: " + e, e);
        }
    }

    public void recover(QueueMessageContainer container) throws JMSException {
        checkClosed();
        SecondaryCursor cursor = null;
        try {
            cursor = secondaryDatabase.openSecondaryCursor(BDbHelper.getTransaction(), cursorConfig);
            DatabaseEntry sequenceNumberEntry = new DatabaseEntry();
            DatabaseEntry keyEntry = new DatabaseEntry();
            DatabaseEntry valueEntry = new DatabaseEntry();
            OperationStatus status = cursor.getFirst(sequenceNumberEntry, keyEntry, valueEntry, LockMode.DEFAULT);
            while (status == OperationStatus.SUCCESS) {
                String messageID = extractString(keyEntry);
                container.recoverMessageToBeDelivered(new MessageIdentity(messageID, sequenceNumberEntry));
                status = cursor.getNext(sequenceNumberEntry, keyEntry, valueEntry, LockMode.DEFAULT);
            }
            if (status != OperationStatus.NOTFOUND) {
                log.warn("Unexpected status code while recovering: " + status);
            }
        }
        catch (DatabaseException e) {
            throw JMSExceptionHelper.newJMSException("Failed to recover container. Reason: " + e, e);
        }
        finally {
            if (cursor != null) {
                try {
                    cursor.close();
                }
                catch (DatabaseException e) {
                    log.warn("Caught exception closing cursor: " + e, e);
                }
            }
        }

    }


    public void start() throws JMSException {
    }

    public void stop() throws JMSException {
        JMSException firstException = BDbPersistenceAdapter.closeDatabase(secondaryDatabase, null);
        firstException = BDbPersistenceAdapter.closeDatabase(database, firstException);

        secondaryDatabase = null;
        database = null;

        if (firstException != null) {
            throw firstException;
        }
    }

    // Implementation methods
    //-------------------------------------------------------------------------
    protected SecondaryDatabase getSecondaryDatabase() {
        return secondaryDatabase;
    }

    protected Database getDatabase() {
        return database;
    }

    public CursorConfig getCursorConfig() {
        return cursorConfig;
    }

    public MessageContainer getContainer() {
        return container;
    }


    protected void checkClosed() throws AlreadyClosedException {
        if (database == null) {
            throw new AlreadyClosedException("Berkeley DB MessageStore");
        }
    }

    /**
     * Returns the sequence number key for the given message identity. If the
     * sequence number is not available it will be queried (which is slow & will generate a warning
     * as it is not recommended) and then it'll be cached inside the MessageIdentity
     *
     * @param identity
     * @return
     * @throws DatabaseException
     */
    protected DatabaseEntry getSequenceNumberKey(MessageIdentity identity) throws DatabaseException {
        DatabaseEntry sequenceNumber = (DatabaseEntry) identity.getSequenceNumber();
        if (sequenceNumber == null) {
            sequenceNumber = findSequenceNumber(identity.getMessageID());
        }
        return sequenceNumber;
    }

    protected DatabaseEntry createKey(String messageID) {
        DatabaseEntry key = new DatabaseEntry(asBytes(messageID));
        return key;
    }


    /**
     * Iterates through from the start of the collection until the given message ID is found
     *
     * @param messageID
     * @return
     */
    protected DatabaseEntry findSequenceNumber(String messageID) throws DatabaseException {
        log.warn("Having to table scan to find the sequence number for messageID: " + messageID);

        SecondaryCursor cursor = null;
        try {
            cursor = secondaryDatabase.openSecondaryCursor(BDbHelper.getTransaction(), cursorConfig);
            DatabaseEntry sequenceNumberEntry = new DatabaseEntry();
            DatabaseEntry keyEntry = new DatabaseEntry();
            DatabaseEntry valueEntry = new DatabaseEntry();
            OperationStatus status = cursor.getFirst(sequenceNumberEntry, keyEntry, valueEntry, LockMode.DEFAULT);
            while (status == OperationStatus.SUCCESS) {
                String value = extractString(keyEntry);
                if (messageID.equals(value)) {
                    return sequenceNumberEntry;
                }
                status = cursor.getNext(sequenceNumberEntry, keyEntry, valueEntry, LockMode.DEFAULT);
            }
        }
        finally {
            if (cursor != null) {
                try {
                    cursor.close();
                }
                catch (DatabaseException e) {
                    log.warn("Caught exception closing cursor: " + e, e);
                }
            }
        }
        return null;
    }

    protected String extractString(DatabaseEntry entry) {
        return new String(entry.getData(), entry.getOffset(), entry.getSize());
    }

    protected ActiveMQMessage extractMessage(DatabaseEntry value) throws IOException {
        // we must synchronize access to wireFormat
        synchronized (wireFormat) {
            return (ActiveMQMessage) wireFormat.fromBytes(value.getData(), value.getOffset(), value.getSize());
        }
    }

    protected byte[] asBytes(ActiveMQMessage message) throws IOException, JMSException {
        // we must synchronize access to wireFormat
        synchronized (wireFormat) {
            return wireFormat.toBytes(message);
        }
    }

    protected byte[] asBytes(String messageID) {
        return messageID.getBytes();
    }

    /**
     * @see org.codehaus.activemq.store.MessageStore#removeAllMessages()
     */
    public void removeAllMessages() throws JMSException {
        checkClosed();
        SecondaryCursor cursor = null;
        Transaction transaction = BDbHelper.getTransaction();
        try {
            cursor = secondaryDatabase.openSecondaryCursor(BDbHelper.getTransaction(), cursorConfig);
            DatabaseEntry sequenceNumberEntry = new DatabaseEntry();
            DatabaseEntry keyEntry = new DatabaseEntry();
            DatabaseEntry valueEntry = new DatabaseEntry();
            OperationStatus status = cursor.getFirst(sequenceNumberEntry, keyEntry, valueEntry, LockMode.DEFAULT);
            while (status == OperationStatus.SUCCESS) {
                sequenceNumberCreator.setDeleteKey(sequenceNumberEntry);
                OperationStatus deleteStatus = secondaryDatabase.delete(transaction, sequenceNumberEntry);
                if (deleteStatus != OperationStatus.SUCCESS) {
                    String messageID = extractString(keyEntry);
                    log.error("Could not delete sequenece number for: " + messageID + " status: " + status);                }

                status = cursor.getNext(sequenceNumberEntry, keyEntry, valueEntry, LockMode.DEFAULT);
            }
            if (status != OperationStatus.NOTFOUND) {
                log.warn("Unexpected status code while recovering: " + status);
            }
        }
        catch (DatabaseException e) {
            throw JMSExceptionHelper.newJMSException("Failed to recover container. Reason: " + e, e);
        }
        finally {
            if (cursor != null) {
                try {
                    cursor.close();
                }
                catch (DatabaseException e) {
                    log.warn("Caught exception closing cursor: " + e, e);
                }
            }
        }
    }

}
