/**
 * JDBM LICENSE v1.00
 *
 * Redistribution and use of this software and associated documentation
 * ("Software"), with or without modification, are permitted provided
 * that the following conditions are met:
 *
 * 1. Redistributions of source code must retain copyright
 *    statements and notices.  Redistributions must also contain a
 *    copy of this document.
 *
 * 2. Redistributions in binary form must reproduce the
 *    above copyright notice, this list of conditions and the
 *    following disclaimer in the documentation and/or other
 *    materials provided with the distribution.
 *
 * 3. The name "JDBM" must not be used to endorse or promote
 *    products derived from this Software without prior written
 *    permission of Cees de Groot.  For written permission,
 *    please contact cg@cdegroot.com.
 *
 * 4. Products derived from this Software may not be called "JDBM"
 *    nor may "JDBM" appear in their names without prior written
 *    permission of Cees de Groot.
 *
 * 5. Due credit should be given to the JDBM Project
 *    (http://jdbm.sourceforge.net/).
 *
 * THIS SOFTWARE IS PROVIDED BY THE JDBM PROJECT AND CONTRIBUTORS
 * ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
 * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL
 * CEES DE GROOT OR ANY CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * Copyright 2000 (C) Cees de Groot. All Rights Reserved.
 * Contributions are Copyright (C) 2000 by their associated contributors.
 *
 * $Id: TransactionManager.java,v 1.7 2005/06/25 23:12:32 doomdark Exp $
 */

package jdbm.recman;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.TreeSet;

import jdbm.I18n;

/**
 *  This class manages the transaction log that belongs to every
 *  {@link RecordFile}. The transaction log is either clean, or
 *  in progress. In the latter case, the transaction manager
 *  takes care of a roll forward.
 *<p>
 *  Implementation note: this is a proof-of-concept implementation
 *  which hasn't been optimized for speed. For instance, all sorts
 *  of streams are created for every transaction.
 */
// TODO: Handle the case where we are recovering lg9 and lg0, were we
// should start with lg9 instead of lg0!

public final class TransactionManager {
    private RecordFile owner;

    // streams for transaction log.
    private FileOutputStream fos;
    private ObjectOutputStream oos;

    /**
     * By default, we keep 10 transactions in the log file before
     * synchronizing it with the main database file.
     */
    static final int DEFAULT_TXNS_IN_LOG = 10;

    /**
     * Maximum number of transactions before the log file is
     * synchronized with the main database file.
     */
    private int _maxTxns = DEFAULT_TXNS_IN_LOG;

    /**
     * In-core copy of transactions. We could read everything back from
     * the log file, but the RecordFile needs to keep the dirty blocks in
     * core anyway, so we might as well point to them and spare us a lot
     * of hassle.
     */
    private List<BlockIo>[] txns = new ArrayList[DEFAULT_TXNS_IN_LOG];
    private int curTxn = -1;

    /** Extension of a log file. */
    static final String extension = ".lg";

    /**
     *  Instantiates a transaction manager instance. If recovery
     *  needs to be performed, it is done.
     *
     *  @param owner the RecordFile instance that owns this transaction mgr.
     */
    TransactionManager(RecordFile owner) throws IOException
    {
        this.owner = owner;
        recover();
        open();
    }

    
    /**
     * Synchronize log file data with the main database file.
     * <p>
     * After this call, the main database file is guaranteed to be
     * consistent and guaranteed to be the only file needed for
     * backup purposes.
     */
    public void synchronizeLog() throws IOException
    {
        synchronizeLogFromMemory();
    }

    
    /**
     * Set the maximum number of transactions to record in
     * the log (and keep in memory) before the log is
     * synchronized with the main database file.
     * <p>
     * This method must be called while there are no
     * pending transactions in the log.
     */
    public void setMaximumTransactionsInLog( int maxTxns ) throws IOException
    {
        if ( maxTxns <= 0 )
        {
            throw new IllegalArgumentException( I18n.err( I18n.ERR_563 ) );
        }
        
        if ( curTxn != -1 )
        {
            throw new IllegalStateException( I18n.err( I18n.ERR_564 ) );
        }
        
        _maxTxns = maxTxns;
        txns = new ArrayList[ maxTxns ];
    }

    
    /** Builds logfile name  */
    private String makeLogName()
    {
        return owner.getFileName() + extension;
    }


    /** Synchs in-core transactions to data file and opens a fresh log */
    private void synchronizeLogFromMemory() throws IOException
    {
        close();

        TreeSet<BlockIo> blockList = new TreeSet<BlockIo>( new BlockIoComparator() );

        for (int i = 0; i < _maxTxns; i++)
        {
            if (txns[i] == null)
            {
                continue;
            }
            
            // Add each block to the blockList, replacing the old copy of this
            // block if necessary, thus avoiding writing the same block twice
            for ( Iterator<BlockIo> k = txns[i].iterator(); k.hasNext(); )
            {
                BlockIo block = k.next();
                
                if ( blockList.contains( block ) )
                {
                    block.decrementTransactionCount();
                }
                else
                {
                    blockList.add( block );
                }
            }

            txns[i] = null;
        }
        
        // Write the blocks from the blockList to disk
        synchronizeBlocks(blockList.iterator(), true);

        owner.sync();
        open();
    }


    /** Opens the log file */
    private void open() throws IOException
    {
        fos = new FileOutputStream(makeLogName());
        oos = new ObjectOutputStream(fos);
        oos.writeShort(Magic.LOGFILE_HEADER);
        oos.flush();
        curTxn = -1;
    }

    /** Startup recovery on all files */
    private void recover() throws IOException
    {
        String logName = makeLogName();
        File logFile = new File(logName);
        
        if (!logFile.exists())
        {
            return;
        }
        
        if (logFile.length() == 0)
        {
            logFile.delete();
            
            return;
        }

        FileInputStream fis = new FileInputStream(logFile);
        ObjectInputStream ois = new ObjectInputStream(fis);

        try {
            if (ois.readShort() != Magic.LOGFILE_HEADER)
            {
                ois.close();
                throw new Error( I18n.err( I18n.ERR_565 ) );
            }
        }
        catch (IOException e)
        {
            // corrupted/empty logfile
            ois.close();
            logFile.delete();
            return;
        }

        while (true)
        {
            List<BlockIo> blocks = null;
            
            try
            {
                blocks = (List<BlockIo>) ois.readObject();
            }
            catch (ClassNotFoundException e)
            {
                ois.close();
                throw new Error( I18n.err( I18n.ERR_566, e ) );
            }
            catch (IOException e)
            {
                // corrupted logfile, ignore rest of transactions
                break;
            }
            
            synchronizeBlocks(blocks.iterator(), false);

            // ObjectInputStream must match exactly each
            // ObjectOutputStream created during writes
            try
            {
                ois = new ObjectInputStream(fis);
            }
            catch (IOException e)
            {
                // corrupted logfile, ignore rest of transactions
                break;
            }
        }
        
        owner.sync();
        ois.close();
        logFile.delete();
    }

    /** Synchronizes the indicated blocks with the owner. */
    private void synchronizeBlocks(Iterator<BlockIo> blockIterator, boolean fromCore)
        throws IOException
    {
        // write block vector elements to the data file.
        while ( blockIterator.hasNext() )
        {
            BlockIo cur = blockIterator.next();
            owner.sync(cur);
            
            if (fromCore)
            {
                cur.decrementTransactionCount();
                
                if (!cur.isInTransaction())
                {
                    owner.releaseFromTransaction(cur, true);
                }
            }
        }
    }


    /** Set clean flag on the blocks. */
    private void setClean(List<BlockIo> blocks) throws IOException
    {
        for (Iterator<BlockIo> k = blocks.iterator(); k.hasNext(); )
        {
            BlockIo cur = k.next();
            cur.setClean();
        }
    }

    /** Discards the indicated blocks and notify the owner. */
    private void discardBlocks(List<BlockIo> blocks) throws IOException
    {
        for (Iterator<BlockIo> k = blocks.iterator(); k.hasNext(); )
        {
            BlockIo cur = k.next();
            cur.decrementTransactionCount();
            
            if (!cur.isInTransaction())
            {
                owner.releaseFromTransaction(cur, false);
            }
        }
    }

    /**
     *  Starts a transaction. This can block if all slots have been filled
     *  with full transactions, waiting for the synchronization thread to
     *  clean out slots.
     */
    void start() throws IOException
    {
        curTxn++;
        
        if (curTxn == _maxTxns)
        {
            synchronizeLogFromMemory();
            curTxn = 0;
        }
        
        txns[curTxn] = new ArrayList();
    }

    /**
     *  Indicates the block is part of the transaction.
     */
    void add(BlockIo block) throws IOException
    {
        block.incrementTransactionCount();
        txns[curTxn].add(block);
    }

    /**
     *  Commits the transaction to the log file.
     */
    void commit() throws IOException
    {
        oos.writeObject(txns[curTxn]);
        sync();

        // set clean flag to indicate blocks have been written to log
        setClean(txns[curTxn]);

        // reset ObjectOutputStream in order to store
        // newer states of BlockIo
        oos = new ObjectOutputStream(fos);
        oos.reset();
    }

    /** Flushes and syncs */
    private void sync() throws IOException
    {
        oos.flush();
        fos.flush();
        fos.getFD().sync();
    }

    /**
     *  Shutdowns the transaction manager. Resynchronizes outstanding
     *  logs.
     */
    void shutdown() throws IOException
    {
        synchronizeLogFromMemory();
        close();
    }

    /**
     *  Closes open files.
     */
    private void close() throws IOException
    {
        sync();
        oos.close();
        fos.close();
        oos = null;
        fos = null;
    }

    /**
     * Force closing the file without synchronizing pending transaction data.
     * Used for testing purposes only.
     */
    void forceClose() throws IOException
    {
        oos.close();
        fos.close();
        oos = null;
        fos = null;
    }

    /**
     * Use the disk-based transaction log to synchronize the data file.
     * Outstanding memory logs are discarded because they are believed
     * to be inconsistent.
     */
    void synchronizeLogFromDisk() throws IOException
    {
        close();

        for ( int i=0; i < _maxTxns; i++ )
        {
            if (txns[i] == null)
            {
                continue;
            }
            
            discardBlocks(txns[i]);
            txns[i] = null;
        }

        recover();
        open();
    }


    /** INNER CLASS.
     *  Comparator class for use by the tree set used to store the blocks
     *  to write for this transaction.  The BlockIo objects are ordered by
     *  their blockIds.
     */
    public static class BlockIoComparator implements Comparator<BlockIo>
    {
        public int compare( BlockIo block1, BlockIo block2 )
        {
            int result = 0;
            
            if ( block1.getBlockId() == block2.getBlockId() )
            {
                result = 0;
            }
            else if ( block1.getBlockId() < block2.getBlockId() )
            {
                result = -1;
            }
            else
            {
                result = 1;
            }
            
            return result;
        }

        public boolean equals(Object obj)
        {
            return super.equals(obj);
        }
    } // class BlockIOComparator
}
