/**
 * 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.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 ArrayList[] 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 blockList = new TreeSet( 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 k = txns[i].iterator(); k.hasNext(); )
            {
                BlockIo block = ( BlockIo ) 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 )
        {
            ArrayList blocks = null;
            try
            {
                blocks = ( ArrayList ) 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 blockIterator, boolean fromCore )
        throws IOException
    {
        // write block vector elements to the data file.
        while ( blockIterator.hasNext() )
        {
            BlockIo cur = ( BlockIo ) blockIterator.next();
            owner.synch( cur );
            if ( fromCore )
            {
                cur.decrementTransactionCount();
                if ( !cur.isInTransaction() )
                {
                    owner.releaseFromTransaction( cur, true );
                }
            }
        }
    }


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


    /** Discards the indicated blocks and notify the owner. */
    private void discardBlocks( ArrayList blocks )
        throws IOException
    {
        for ( Iterator k = blocks.iterator(); k.hasNext(); )
        {
            BlockIo cur = ( BlockIo ) 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
    {

        public int compare( Object o1, Object o2 )
        {
            BlockIo block1 = ( BlockIo ) o1;
            BlockIo block2 = ( BlockIo ) o2;
            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

}
