/** 
 * 
 * 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.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.util.Iterator;
import java.util.Map;

import javax.jms.JMSException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.codehaus.activemq.io.WireFormat;
import org.codehaus.activemq.io.impl.DefaultWireFormat;
import org.codehaus.activemq.journal.InvalidRecordLocationException;
import org.codehaus.activemq.journal.Journal;
import org.codehaus.activemq.journal.JournalEventListener;
import org.codehaus.activemq.journal.RecordLocation;
import org.codehaus.activemq.journal.howl.HowlJournal;
import org.codehaus.activemq.journal.impl.JournalImpl;
import org.codehaus.activemq.message.ActiveMQMessage;
import org.codehaus.activemq.message.MessageAck;
import org.codehaus.activemq.message.Packet;
import org.codehaus.activemq.service.impl.PersistenceAdapterSupport;
import org.codehaus.activemq.store.MessageStore;
import org.codehaus.activemq.store.PersistenceAdapter;
import org.codehaus.activemq.store.PreparedTransactionStore;
import org.codehaus.activemq.store.TopicMessageStore;
import org.codehaus.activemq.util.JMSExceptionHelper;
import org.codehaus.activemq.util.TransactionTemplate;
import org.objectweb.howl.log.Configuration;

import EDU.oswego.cs.dl.util.concurrent.Channel;
import EDU.oswego.cs.dl.util.concurrent.ClockDaemon;
import EDU.oswego.cs.dl.util.concurrent.ConcurrentHashMap;
import EDU.oswego.cs.dl.util.concurrent.LinkedQueue;
import EDU.oswego.cs.dl.util.concurrent.QueuedExecutor;
import EDU.oswego.cs.dl.util.concurrent.ThreadFactory;

/**
 * An implementation of {@link PersistenceAdapter} designed for
 * use with a {@link Journal} and then checkpointing asynchronously
 * on a timeout with some other long term persistent storage.
 *
 * @version $Revision: 1.4 $
 */
public class JournalPersistenceAdapter extends PersistenceAdapterSupport implements JournalEventListener {

    private static final Log log = LogFactory.getLog(JournalPersistenceAdapter.class);
    public static final String DEFAULT_JOURNAL_TYPE = "default";
    public static final String HOWL_JOURNAL_TYPE = "howl";
    
    private Journal journal;
    private String journalType = DEFAULT_JOURNAL_TYPE;
    private PersistenceAdapter longTermPersistence;
    private File directory = new File("logs");
    private WireFormat wireFormat = new DefaultWireFormat();
    private TransactionTemplate transactionTemplate;
    private boolean sync = true;
    private final ConcurrentHashMap messageStores = new ConcurrentHashMap();
    private boolean performingRecovery;
    private static final int PACKET_RECORD_TYPE = 0;
    private static final int COMMAND_RECORD_TYPE = 1;

    private Channel checkpointRequests = new LinkedQueue();
    private QueuedExecutor checkpointExecutor;
    ClockDaemon clockDaemon;
    private Object clockTicket;

    public JournalPersistenceAdapter() {
        checkpointExecutor = new QueuedExecutor(new LinkedQueue());
        checkpointExecutor.setThreadFactory(new ThreadFactory() {
            public Thread newThread(Runnable runnable) {
                Thread answer = new Thread(runnable, "Checkpoint Worker");
                answer.setDaemon(true);
                answer.setPriority(Thread.MAX_PRIORITY);
                return answer;
            }
        });
    }

    public JournalPersistenceAdapter(File directory, PersistenceAdapter longTermPersistence, DefaultWireFormat wireFormat) throws IOException {
        this();
        this.directory = directory;
        this.longTermPersistence = longTermPersistence;
        this.wireFormat = wireFormat;
    }

    public Map getInitialDestinations() {
        return longTermPersistence.getInitialDestinations();
    }

    public MessageStore createQueueMessageStore(String destinationName) throws JMSException {
        MessageStore checkpointStore = longTermPersistence.createQueueMessageStore(destinationName);
        JournalMessageStore store = new JournalMessageStore(this, checkpointStore, destinationName, sync);
        messageStores.put(destinationName, store);
        return store;
    }

    public TopicMessageStore createTopicMessageStore(String destinationName) throws JMSException {
        return longTermPersistence.createTopicMessageStore(destinationName);
    }

    public PreparedTransactionStore createPreparedTransactionStore() throws JMSException {
        return longTermPersistence.createPreparedTransactionStore();
    }

    public void beginTransaction() throws JMSException {
        longTermPersistence.beginTransaction();
    }

    public void commitTransaction() throws JMSException {
        longTermPersistence.commitTransaction();
    }

    public void rollbackTransaction() {
        longTermPersistence.rollbackTransaction();
    }

    public synchronized void start() throws JMSException {
        longTermPersistence.start();
        if (journal == null) {
            try {
                log.info("Opening journal.");
                journal = createJournal();
                log.info("Opened journal: " + journal);
                journal.setJournalEventListener(this);
            }
            catch (Exception e) {
                throw JMSExceptionHelper.newJMSException("Failed to open transaction journal: " + e, e);
            }
            try {
                recover();
            }
            catch (Exception e) {
                throw JMSExceptionHelper.newJMSException("Failed to recover transactions from journal: " + e, e);
            }
        }

        // Do a checkpoint periodically.
        clockTicket = getClockDaemon().executePeriodically(1000 * 60, new Runnable() {
            public void run() {
                checkpoint();
            }
        }, false);

    }

    public synchronized void stop() throws JMSException {

        if (clockTicket != null) {
            // Stop the periodical checkpoint.
            ClockDaemon.cancel(clockTicket);
        }

        // Take one final checkpoint and stop checkpoint processing.
        checkpoint();
        checkpointExecutor.shutdownAfterProcessingCurrentlyQueuedTasks();

        JMSException firstException = null;
        if (journal != null) {
            try {
                journal.close();
                journal = null;
            }
            catch (Exception e) {
                firstException = JMSExceptionHelper.newJMSException("Failed to close Howl transaction log due to: " + e, e);
            }
        }
        longTermPersistence.stop();

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

    // Properties
    //-------------------------------------------------------------------------
    public PersistenceAdapter getLongTermPersistence() {
        return longTermPersistence;
    }

    public void setLongTermPersistence(PersistenceAdapter longTermPersistence) {
        this.longTermPersistence = longTermPersistence;
    }

    /**
     * @return Returns the directory.
     */
    public File getDirectory() {
        return directory;
    }

    /**
     * @param directory The directory to set.
     */
    public void setDirectory(File directory) {
        this.directory = directory;
    }

    /**
     * @return Returns the sync.
     */
    public boolean isSync() {
        return sync;
    }

    /**
     * @param sync The sync to set.
     */
    public void setSync(boolean sync) {
        this.sync = sync;
    }

    /**
     * @return Returns the wireFormat.
     */
    public WireFormat getWireFormat() {
        return wireFormat;
    }

    /**
     * @param wireFormat The wireFormat to set.
     */
    public void setWireFormat(WireFormat wireFormat) {
        this.wireFormat = wireFormat;
    }

    public String getJournalType() {
        return journalType;
    }

    public void setJournalType(String journalType) {
        this.journalType = journalType;
    }

    protected Journal createJournal() throws IOException {
        if( DEFAULT_JOURNAL_TYPE.equals(journalType) ) {
            return new JournalImpl(directory);
        }
        if( HOWL_JOURNAL_TYPE.equals(journalType) ) {
            try {
                Configuration config = new Configuration();
                config.setLogFileDir(directory.getCanonicalPath());
                return new HowlJournal(config);
            } catch (IOException e) {
                throw e;
            } catch (Exception e) {
                throw (IOException)new IOException("Could not open HOWL journal: "+e.getMessage()).initCause(e);
            }
        }
        throw new IllegalStateException("Unsupported valued for journalType attribute: "+journalType);
    }

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

    /**
     * The Journal give us a call back so that we can move old data out of the journal.
     * Taking a checkpoint does this for us.
     *
     * @see org.codehaus.activemq.journal.JournalEventListener#overflowNotification(org.codehaus.activemq.journal.RecordLocation)
     */
    public void overflowNotification(RecordLocation safeLocation) {
        checkpoint();
    }

    /**
     * When we checkpoint we move all the journaled data to long term storage.
     */
    public void checkpoint() {
        try {
            // Do the checkpoint asynchronously.
            checkpointRequests.put(Boolean.TRUE);
            checkpointExecutor.execute(new Runnable() {
                public void run() {

                    // Avoid running a checkpoint too many times in a row.
                    // Consume any queued up checkpoint requests.
                    try {
                        boolean requested = false;
                        while (checkpointRequests.poll(0) != null) {
                            requested = true;
                        }
                        if (!requested) {
                            return;
                        }
                    }
                    catch (InterruptedException e1) {
                        return;
                    }

                    log.info("Checkpoint started.");
                    Iterator iterator = messageStores.values().iterator();
                    RecordLocation newMark = null;
                    while (iterator.hasNext()) {
                        try {
                            JournalMessageStore ms = (JournalMessageStore) iterator.next();
                            RecordLocation mark = ms.checkpoint();
                            if (mark != null && (newMark == null || newMark.compareTo(mark) < 0)) {
                                newMark = mark;
                            }
                        }
                        catch (Exception e) {
                            log.error("Failed to checkpoint a message store: " + e, e);
                        }
                    }
                    try {
                        if (newMark != null) {
                            journal.setMark(newMark, true);
                        }
                    }
                    catch (Exception e) {
                        log.error("Failed to mark the Journal: " + e, e);
                    }
                    log.info("Checkpoint done.");
                }
            });
        }
        catch (InterruptedException e) {
            log.warn("Request to start checkpoint failed: " + e, e);
        }
    }

    /**
     * @param destinationName
     * @param message
     * @param sync
     * @throws JMSException
     */
    public RecordLocation writePacket(String destination, Packet packet, boolean sync) throws JMSException {
        try {

            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            DataOutputStream os = new DataOutputStream(baos);
            os.writeByte(PACKET_RECORD_TYPE);
            os.writeUTF(destination);
            wireFormat.writePacket(packet, os);
            os.close();
            byte[] data = baos.toByteArray();
            return journal.write(data, sync);

        }
        catch (IOException e) {
            throw createWriteException(packet, e);
        }
    }

    /**
     * @param destinationName
     * @param message
     * @param sync
     * @throws JMSException
     */
    public RecordLocation writeCommand(String command, boolean sync) throws JMSException {
        try {

            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            DataOutputStream os = new DataOutputStream(baos);
            os.writeByte(COMMAND_RECORD_TYPE);
            os.writeUTF(command);
            os.close();
            byte[] data = baos.toByteArray();
            return journal.write(data, sync);

        }
        catch (IOException e) {
            throw createWriteException(command, e);
        }
    }

    /**
     * @param location
     * @return
     * @throws JMSException
     */
    public Packet readPacket(RecordLocation location) throws JMSException {
        try {
            byte[] data = journal.read(location);

            DataInputStream is = new DataInputStream(new ByteArrayInputStream(data));
            byte type = is.readByte();
            if (type != PACKET_RECORD_TYPE) {
                throw new IOException("Record is not a packet type.");
            }
            String destination = is.readUTF();
            Packet packet = wireFormat.readPacket(is);
            is.close();
            return packet;

        }
        catch (InvalidRecordLocationException e) {
            throw createReadException(location, e);
        }
        catch (IOException e) {
            throw createReadException(location, e);
        }
    }


    /**
     * Move all the messages that were in the journal into long term storeage.  We just replay and do a checkpoint.
     *
     * @throws JMSException
     * @throws IOException
     * @throws InvalidRecordLocationException
     * @throws IllegalStateException
     */
    private void recover() throws IllegalStateException, InvalidRecordLocationException, IOException, JMSException {

        RecordLocation pos = null;
        int transactionCounter = 0;

        log.info("Journal Recovery Started.");

        // While we have records in the journal.
        while ((pos = journal.getNextRecordLocation(pos)) != null) {
            byte[] data = journal.read(pos);

            // Read the destination and packate from the record.
            String destination = null;
            Packet packet = null;
            DataInputStream is = new DataInputStream(new ByteArrayInputStream(data));
            try {
                byte type = is.readByte();
                switch (type) {
                    case PACKET_RECORD_TYPE:

                        // Is the current packet part of the destination?
                        destination = is.readUTF();
                        packet = wireFormat.readPacket(is);

                        // Try to replay the packet.
                        JournalMessageStore store = (JournalMessageStore) createQueueMessageStore(destination);
                        if (packet instanceof ActiveMQMessage) {
                            ActiveMQMessage msg = (ActiveMQMessage) packet;
                            try {
                                store.getLongTermStore().addMessage(msg);
                                transactionCounter++;
                            }
                            catch (Throwable e) {
                                log.error("Recovery Failure: Could not add message: " + msg.getJMSMessageIdentity().getMessageID() + ", reason: " + e, e);
                            }
                        }
                        else if (packet instanceof MessageAck) {
                            MessageAck ack = (MessageAck) packet;
                            try {
                                store.getLongTermStore().removeMessage(ack.getMessageIdentity(), ack);
                                transactionCounter++;
                            }
                            catch (Throwable e) {
                                log.error("Recovery Failure: Could not remove message: " + ack.getMessageIdentity().getMessageID() + ", reason: " + e, e);
                            }
                        }
                        else {
                            log.error("Unknown type of packet in transaction log which will be discarded: " + packet);
                        }

                        break;
                    case COMMAND_RECORD_TYPE:

                        break;
                    default:
                        log.error("Unknown type of record in transaction log which will be discarded: " + type);
                        break;
                }
            }
            finally {
                is.close();
            }
        }

        RecordLocation location = writeCommand("RECOVERED", true);
        journal.setMark(location, true);

        log.info("Journal Recovered: " + transactionCounter + " message(s) in transactions recovered.");
    }

    private JMSException createReadException(RecordLocation location, Exception e) {
        return JMSExceptionHelper.newJMSException("Failed to read to journal for: " + location + ". Reason: " + e, e);
    }

    protected JMSException createWriteException(Packet packet, Exception e) {
        return JMSExceptionHelper.newJMSException("Failed to write to journal for: " + packet + ". Reason: " + e, e);
    }

    protected JMSException createWriteException(String command, Exception e) {
        return JMSExceptionHelper.newJMSException("Failed to write to journal for command: " + command + ". Reason: " + e, e);
    }

    protected JMSException createRecoveryFailedException(Exception e) {
        return JMSExceptionHelper.newJMSException("Failed to recover from journal. Reason: " + e, e);
    }

    public ClockDaemon getClockDaemon() {
        if (clockDaemon == null) {
            clockDaemon = new ClockDaemon();
            clockDaemon.setThreadFactory(new ThreadFactory() {
                public Thread newThread(Runnable runnable) {
                    Thread thread = new Thread(runnable, "Checkpoint Timmer");
                    thread.setDaemon(true);
                    return thread;
                }
            });
        }
        return clockDaemon;
    }

    public void setClockDaemon(ClockDaemon clockDaemon) {
        this.clockDaemon = clockDaemon;
    }

}
