/*
 * Copyright (c) 2002-2009 "Neo Technology,"
 *     Network Engine for Objects in Lund AB [http://neotechnology.com]
 *
 * This file is part of Neo4j.
 * 
 * Neo4j is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 * 
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */
package org.neo4j.kernel.impl.transaction.xaframework;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.transaction.xa.XAException;
import javax.transaction.xa.Xid;

import org.neo4j.graphdb.TransactionFailureException;
import org.neo4j.kernel.Config;
import org.neo4j.kernel.impl.transaction.XidImpl;
import org.neo4j.kernel.impl.util.ArrayMap;
import org.neo4j.kernel.impl.util.FileUtils;

/**
 * <CODE>XaLogicalLog</CODE> is a transaction and logical log combined. In
 * this log information about the transaction (such as started, prepared and
 * committed) will be written. All commands participating in the transaction
 * will also be written to the log.
 * <p>
 * Normally you don't have to do anything with this log except open it after it
 * has been instanciated (see {@link XaContainer}). The only method that may be
 * of use when implementing a XA compatible resource is the
 * {@link #getCurrentTxIdentifier}. Leave everything else be unless you know
 * what you're doing.
 * <p>
 * When the log is opened it will be scaned for uncompleted transactions and
 * those transactions will be re-created. When scan of log is complete all
 * transactions that hasn't entered prepared state will be marked as done
 * (implies rolledback) and dropped. All transactions that have been prepared
 * will be held in memory until the transaction manager tells them to commit.
 * Transaction that already started commit but didn't get flagged as done will
 * be re-committed.
 */
public class XaLogicalLog
{
    private Logger log;
    // empty record due to memory mapped file
    private static final byte EMPTY = (byte) 0;
    // tx has started
    private static final byte TX_START = (byte) 1;
    // tx has been prepared
    private static final byte TX_PREPARE = (byte) 2;
    // a XaCommand in a transaction
    private static final byte COMMAND = (byte) 3;
    // done, either a read only tx or rolledback/forget
    private static final byte DONE = (byte) 4;
    // tx one-phase commit
    private static final byte TX_1P_COMMIT = (byte) 5;
    // tx two-phase commit
    private static final byte TX_2P_COMMIT = (byte) 6;
    
    private static final char CLEAN = 'C';
    private static final char LOG1 = '1';
    private static final char LOG2 = '2';

    private FileChannel fileChannel = null;
    private final ByteBuffer buffer;
    private LogBuffer writeBuffer = null;
    private long logVersion = 0;
    private ArrayMap<Integer,StartEntry> xidIdentMap = 
        new ArrayMap<Integer,StartEntry>( 4, false, true );
    private Map<Integer,XaTransaction> recoveredTxMap = 
        new HashMap<Integer,XaTransaction>();
    private int nextIdentifier = 1;
    private boolean scanIsComplete = false;

    private String fileName = null;
    private final XaResourceManager xaRm;
    private final XaCommandFactory cf;
    private final XaTransactionFactory xaTf;
    private char currentLog = CLEAN;
    private boolean keepLogs = false;
    private boolean autoRotate = true;
    private long rotateAtSize = 10*1024*1024; // 10MB
    private boolean backupSlave = false;
    private boolean useMemoryMapped = true;

    XaLogicalLog( String fileName, XaResourceManager xaRm, XaCommandFactory cf,
        XaTransactionFactory xaTf, Map<Object,Object> config )
    {
        this.fileName = fileName;
        this.xaRm = xaRm;
        this.cf = cf;
        this.xaTf = xaTf;
        this.useMemoryMapped = getMemoryMapped( config );
        log = Logger.getLogger( this.getClass().getName() + "/" + fileName );
        buffer = ByteBuffer.allocateDirect( 9 + Xid.MAXGTRIDSIZE
            + Xid.MAXBQUALSIZE * 10 );
    }
    
    private boolean getMemoryMapped( Map<Object,Object> config )
    {
        if ( config != null )
        {
            String value = (String) config.get( Config.USE_MEMORY_MAPPED_BUFFERS );
            if ( value != null && value.toLowerCase().equals( "false" ) )
            {
                return false;
            }
        }
        return true;
    }
    
    synchronized void open() throws IOException
    {
        String activeFileName = fileName + ".active";
        if ( !new File( activeFileName ).exists() )
        {
            if ( new File( fileName ).exists() )
            {
                // old < b8 xaframework with no log rotation and we need to 
                // do recovery on it
                open( fileName );
            }
            else
            {
                open( fileName + ".1" );
                setActiveLog( LOG1 );
            }
        }
        else
        {
            FileChannel fc = new RandomAccessFile( activeFileName , 
                "rw" ).getChannel();
            byte bytes[] = new byte[256];
            ByteBuffer buf = ByteBuffer.wrap( bytes );
            int read = fc.read( buf );
            fc.close();
            if ( read != 4 )
            {
                throw new IllegalStateException( "Read " + read + 
                    " bytes from " + activeFileName + " but expected 4" ); 
            }
            buf.flip();
            char c = buf.asCharBuffer().get();
            File copy = new File( fileName + ".copy" );
            if ( copy.exists() )
            {
                if ( !copy.delete() )
                {
                    log.warning( "Unable to delete " + copy.getName() );
                }
            }
            if ( c == CLEAN )
            {
                // clean
                String newLog = fileName + ".1";
                File file = new File( newLog );
                if ( file.exists() )
                {
                    fixCleanKill( newLog );
                }
                file = new File( fileName + ".2" );
                if ( file.exists() )
                {
                    fixCleanKill( fileName + ".2" );
                }
                open( newLog );
                setActiveLog( LOG1 );
            }
            else if ( c == LOG1 )
            {
                String newLog = fileName + ".1";
                if ( !new File( newLog ).exists() )
                {
                    throw new IllegalStateException( 
                        "Active marked as 1 but no " + newLog + " exist" );
                }
                currentLog = LOG1;
                File otherLog = new File( fileName + ".2" );
                if ( otherLog.exists() )
                {
                    if ( !otherLog.delete() )
                    {
                        log.warning( "Unable to delete " + copy.getName() );
                    }
                }
                open( newLog );
            }
            else if ( c == LOG2 )
            {
                String newLog = fileName + ".2";
                if ( !new File( newLog ).exists() )
                {
                    throw new IllegalStateException( 
                        "Active marked as 2 but no " + newLog + " exist" );
                }
                File otherLog = new File( fileName + ".1" );
                if ( otherLog.exists() )
                {
                    if ( !otherLog.delete() )
                    {
                        log.warning( "Unable to delete " + copy.getName() );
                    }
                }
                currentLog = LOG2;
                open( newLog );
            }
            else
            {
                throw new IllegalStateException( "Unknown active log: " + c );
            }
        }
        if ( !useMemoryMapped )
        {
            writeBuffer = new DirectMappedLogBuffer( fileChannel );
        }
        else
        {
            writeBuffer = new MemoryMappedLogBuffer( fileChannel );
        }
    }
    
    private void fixCleanKill( String fileName ) throws IOException
    {
        File file = new File( fileName );
        if ( !keepLogs )
        {
            if ( !file.delete() )
            {
                throw new IllegalStateException( 
                    "Active marked as clean and unable to delete log " + 
                    fileName );
            }
        }
        else
        {
            renameCurrentLogFileAndIncrementVersion( fileName, file.length() );
        }
    }
    
    private void open( String fileToOpen ) throws IOException
    {
        fileChannel = new RandomAccessFile( fileToOpen, "rw" ).getChannel();
        if ( fileChannel.size() != 0 )
        {
            doInternalRecovery( fileToOpen );
        }
        else
        {
            logVersion = xaTf.getCurrentVersion();
            buffer.clear();
            buffer.putLong( logVersion );
            buffer.flip();
            fileChannel.write( buffer );
            scanIsComplete = true;
        }
    }

    public boolean scanIsComplete()
    {
        return scanIsComplete;
    }

    private int getNextIdentifier()
    {
        nextIdentifier++;
        if ( nextIdentifier < 0 )
        {
            nextIdentifier = 1;
        }
        return nextIdentifier;
    }

    // returns identifier for transaction
    // [TX_START][xid[gid.length,bid.lengh,gid,bid]][identifier][format id]
    public synchronized int start( Xid xid ) throws XAException
    {
        if ( backupSlave )
        {
            throw new XAException( "Resource is configured as backup slave, " + 
                "no new transactions can be started for " + fileName + "." + 
                currentLog );
        }
        int xidIdent = getNextIdentifier();
        try
        {
            byte globalId[] = xid.getGlobalTransactionId();
            byte branchId[] = xid.getBranchQualifier();
            int formatId = xid.getFormatId();
            long position = writeBuffer.getFileChannelPosition();
            writeBuffer.put( TX_START ).put( (byte) globalId.length ).put(
                (byte) branchId.length ).put( globalId ).put( branchId )
                .putInt( xidIdent ).putInt( formatId );
            xidIdentMap.put( xidIdent, new StartEntry( xid, position ) );
        }
        catch ( IOException e )
        {
            throw new XAException( "Logical log couldn't start transaction: "
                + e );
        }
        return xidIdent;
    }

    private boolean readTxStartEntry() throws IOException
    {
        // get the global id
        long position = fileChannel.position();
        buffer.clear();
        buffer.limit( 1 );
        if ( fileChannel.read( buffer ) != buffer.limit() )
        {
            return false;
        }
        buffer.flip();
        byte globalIdLength = buffer.get();
        // get the branchId id
        buffer.clear();
        buffer.limit( 1 );
        if ( fileChannel.read( buffer ) != buffer.limit() )
        {
            return false;
        }
        buffer.flip();
        byte branchIdLength = buffer.get();
        byte globalId[] = new byte[globalIdLength];
        ByteBuffer tmpBuffer = ByteBuffer.wrap( globalId );
        if ( fileChannel.read( tmpBuffer ) != globalId.length )
        {
            return false;
        }
        byte branchId[] = new byte[branchIdLength];
        tmpBuffer = ByteBuffer.wrap( branchId );
        if ( fileChannel.read( tmpBuffer ) != branchId.length )
        {
            return false;
        }
        // get the tx identifier
        buffer.clear();
        buffer.limit( 4 );
        if ( fileChannel.read( buffer ) != buffer.limit() )
        {
            return false;
        }
        buffer.flip();
        int identifier = buffer.getInt();
        if ( identifier >= nextIdentifier )
        {
            nextIdentifier = (identifier + 1);
        }
        // get the format id
        buffer.clear();
        buffer.limit( 4 );
        if ( fileChannel.read( buffer ) != buffer.limit() )
        {
            return false;
        }
        buffer.flip();
        int formatId = buffer.getInt();
        // re-create the transaction
        Xid xid = new XidImpl( globalId, branchId, formatId );
        xidIdentMap.put( identifier, new StartEntry( xid, position ) );
        XaTransaction xaTx = xaTf.create( identifier );
        xaTx.setRecovered();
        recoveredTxMap.put( identifier, xaTx );
        xaRm.injectStart( xid, xaTx );
        return true;
    }

    // [TX_PREPARE][identifier]
    public synchronized void prepare( int identifier ) throws XAException
    {
        assert xidIdentMap.get( identifier ) != null;
        try
        {
            writeBuffer.put( TX_PREPARE ).putInt( identifier );
            writeBuffer.force();
        }
        catch ( IOException e )
        {
            throw new XAException( "Logical log unable to mark prepare ["
                + identifier + "] " + e );
        }
    }

    private boolean readTxPrepareEntry() throws IOException
    {
        // get the tx identifier
        buffer.clear();
        buffer.limit( 4 );
        if ( fileChannel.read( buffer ) != buffer.limit() )
        {
            return false;
        }
        buffer.flip();
        int identifier = buffer.getInt();
        StartEntry entry = xidIdentMap.get( identifier );
        if ( entry == null )
        {
            return false;
        }
        Xid xid = entry.getXid();
        if ( xaRm.injectPrepare( xid ) )
        {
            // read only we can remove
            xidIdentMap.remove( identifier );
            recoveredTxMap.remove( identifier );
        }
        return true;
    }

    // [TX_1P_COMMIT][identifier]
    public synchronized void commitOnePhase( int identifier )
        throws XAException
    {
        assert xidIdentMap.get( identifier ) != null;
        try
        {
            writeBuffer.put( TX_1P_COMMIT ).putInt( identifier );
            writeBuffer.force();
        }
        catch ( IOException e )
        {
            throw new XAException( "Logical log unable to mark 1P-commit ["
                + identifier + "] " + e );
        }
    }

    private boolean readTxOnePhaseCommit() throws IOException
    {
        // get the tx identifier
        buffer.clear();
        buffer.limit( 4 );
        if ( fileChannel.read( buffer ) != buffer.limit() )
        {
            return false;
        }
        buffer.flip();
        int identifier = buffer.getInt();
        StartEntry entry = xidIdentMap.get( identifier );
        if ( entry == null )
        {
            return false;
        }
        Xid xid = entry.getXid();
        try
        {
            xaRm.injectOnePhaseCommit( xid );
        }
        catch ( XAException e )
        {
            e.printStackTrace();
            throw new IOException( e.getMessage() );
        }
        return true;
    }

    // [DONE][identifier]
    public synchronized void done( int identifier ) throws XAException
    {
        if ( backupSlave )
        {
            return;
        }
        assert xidIdentMap.get( identifier ) != null;
        try
        {
            writeBuffer.put( DONE ).putInt( identifier );
            xidIdentMap.remove( identifier );
        }
        catch ( IOException e )
        {
            throw new XAException( "Logical log unable to mark as done ["
                + identifier + "] " + e );
        }
    }

    // [DONE][identifier] called from XaResourceManager during internal recovery
    synchronized void doneInternal( int identifier ) throws IOException
    {
        buffer.clear();
        buffer.put( DONE ).putInt( identifier );
        buffer.flip();
        fileChannel.write( buffer );
        xidIdentMap.remove( identifier );
    }

    private boolean readDoneEntry() throws IOException
    {
        // get the tx identifier
        buffer.clear();
        buffer.limit( 4 );
        if ( fileChannel.read( buffer ) != buffer.limit() )
        {
            return false;
        }
        buffer.flip();
        int identifier = buffer.getInt();
        StartEntry entry = xidIdentMap.get( identifier );
        if ( entry == null )
        {
            return false;
        }
        Xid xid = entry.getXid();
        xaRm.pruneXid( xid );
        xidIdentMap.remove( identifier );
        recoveredTxMap.remove( identifier );
        return true;
    }

    // [TX_2P_COMMIT][identifier]
    public synchronized void commitTwoPhase( int identifier ) throws XAException
    {
        assert xidIdentMap.get( identifier ) != null;
        try
        {
            writeBuffer.put( TX_2P_COMMIT ).putInt( identifier );
            writeBuffer.force();
        }
        catch ( IOException e )
        {
            throw new XAException( "Logical log unable to mark 2PC ["
                + identifier + "] " + e );
        }
    }
    
    private boolean readTxTwoPhaseCommit() throws IOException
    {
        // get the tx identifier
        buffer.clear();
        buffer.limit( 4 );
        if ( fileChannel.read( buffer ) != buffer.limit() )
        {
            return false;
        }
        buffer.flip();
        int identifier = buffer.getInt();
        StartEntry entry = xidIdentMap.get( identifier );
        if ( entry == null )
        {
            return false;
        }
        Xid xid = entry.getXid();
        if ( xid == null )
        {
            return false;
        }
        try
        {
            xaRm.injectTwoPhaseCommit( xid );
        }
        catch ( XAException e )
        {
            e.printStackTrace();
            throw new IOException( e.getMessage() );
        }
        return true;
    }
    
    // [COMMAND][identifier][COMMAND_DATA]
    public synchronized void writeCommand( XaCommand command, int identifier )
        throws IOException
    {
        checkLogRotation();
        assert xidIdentMap.get( identifier ) != null;
        writeBuffer.put( COMMAND ).putInt( identifier );
        command.writeToFile( writeBuffer ); // fileChannel, buffer );
    }

    private boolean readCommandEntry() throws IOException
    {
        buffer.clear();
        buffer.limit( 4 );
        if ( fileChannel.read( buffer ) != buffer.limit() )
        {
            return false;
        }
        buffer.flip();
        int identifier = buffer.getInt();
        XaCommand command = cf.readCommand( fileChannel, buffer );
        if ( command == null )
        {
            // readCommand returns null if full command couldn't be loaded
            return false;
        }
        command.setRecovered();
        XaTransaction xaTx = recoveredTxMap.get( identifier );
        xaTx.injectCommand( command );
        return true;
    }

    private void checkLogRotation() throws IOException
    {
        if ( autoRotate && 
            writeBuffer.getFileChannelPosition() >= rotateAtSize )
        {
            long currentPos = writeBuffer.getFileChannelPosition();
            long firstStartEntry = getFirstStartEntry( currentPos ); 
            // only rotate if no huge tx is running
            if ( ( currentPos - firstStartEntry ) < rotateAtSize / 2 )
            {
                rotate();
            }
        }
    }

    private void renameCurrentLogFileAndIncrementVersion( String logFileName, 
        long endPosition ) throws IOException
    {
        File file = new File( logFileName );
        if ( !file.exists() )
        {
            throw new IOException( "Logical log[" + logFileName + 
                "] not found" );
        }
        String newName = fileName + ".v" + xaTf.getAndSetNewVersion();
        File newFile = new File( newName );
        boolean renamed = FileUtils.renameFile( file, newFile );
        
        if ( !renamed )
        {
            throw new IOException( "Failed to rename log to: " + newName );
        }
        else
        {
            try
            {
                FileChannel channel = new RandomAccessFile( newName, 
                    "rw" ).getChannel();
                FileUtils.truncateFile( channel, endPosition );
            }
            catch ( IOException e )
            {
                log.log( Level.WARNING, 
                    "Failed to truncate log at correct size", e );
            }
        }
    }
    
    private void deleteCurrentLogFile( String logFileName ) throws IOException
    {
        File file = new File( logFileName );
        if ( !file.exists() )
        {
            throw new IOException( "Logical log[" + logFileName + 
                "] not found" );
        }
        boolean deleted = FileUtils.deleteFile( file );
        if ( !deleted )
        {
            log.warning( "Unable to delete clean logical log[" + logFileName +
                "]" );
        }
    }
    
    private void releaseCurrentLogFile() throws IOException
    {
        if ( writeBuffer != null )
        {
            writeBuffer.force();
            writeBuffer = null;
        }
        fileChannel.close();
        fileChannel = null;
    }

    public synchronized void close() throws IOException
    {
        if ( fileChannel == null || !fileChannel.isOpen() )
        {
            log.fine( "Logical log: " + fileName + " already closed" );
            return;
        }
        long endPosition = writeBuffer.getFileChannelPosition();
        if ( xidIdentMap.size() > 0 )
        {
            log.info( "Close invoked with " + xidIdentMap.size() + 
                " running transaction(s). " );
            writeBuffer.force();
            writeBuffer = null;
            fileChannel.close();
            log.info( "Dirty log: " + fileName + "." + currentLog + 
                " now closed. Recovery will be started automatically next " + 
                "time it is opened." );
            return;
        }
        releaseCurrentLogFile();
        char logWas = currentLog;
        if ( currentLog != CLEAN ) // again special case, see above
        {
            setActiveLog( CLEAN );
        }
        if ( !keepLogs || backupSlave )
        {
            if ( logWas == CLEAN )
            {
                // special case going from old xa version with no log rotation
                // and we started with a recovery
                deleteCurrentLogFile( fileName );
            }
            else
            {
                deleteCurrentLogFile( fileName + "." + logWas );
            }
        }
        else
        {
            renameCurrentLogFileAndIncrementVersion( fileName + "." + 
                logWas, endPosition );
        }
    }

    private void doInternalRecovery( String logFileName ) throws IOException
    {
        log.info( "Non clean shutdown detected on log [" + logFileName + 
            "]. Recovery started ..." );
        // get log creation time
        buffer.clear();
        buffer.limit( 8 );
        if ( fileChannel.read( buffer ) != 8 )
        {
            log.info( "Unable to read timestamp information, "
                + "no records in logical log." );
            fileChannel.close();
            boolean success = FileUtils.renameFile( new File( logFileName ), 
                new File( logFileName + "_unknown_timestamp_" + 
                    System.currentTimeMillis() + ".log" ) );
            assert success;
            fileChannel = new RandomAccessFile( logFileName, 
                "rw" ).getChannel();
            return;
        }
        buffer.flip();
        logVersion = buffer.getLong();
        log.fine( "Logical log version: " + logVersion );
        long logEntriesFound = 0;
        long lastEntryPos = fileChannel.position();
        while ( readEntry() )
        {
            logEntriesFound++;
            lastEntryPos = fileChannel.position();
        }
        // make sure we overwrite any broken records
        fileChannel.position( lastEntryPos );
        // zero out the slow way since windows don't support truncate very well
        buffer.clear();
        while ( buffer.hasRemaining() )
        {
            buffer.put( (byte)0 );
        }
        buffer.flip();
        long endPosition = fileChannel.size();
        do
        {
            long bytesLeft = fileChannel.size() - fileChannel.position();
            if ( bytesLeft < buffer.capacity() )
            {
                buffer.limit( (int) bytesLeft );
            }
            fileChannel.write( buffer );
            buffer.flip();
        } while ( fileChannel.position() < endPosition );
        fileChannel.position( lastEntryPos );
        scanIsComplete = true;
        log.fine( "Internal recovery completed, scanned " + logEntriesFound
            + " log entries." );
        xaRm.checkXids();
        if ( xidIdentMap.size() == 0 )
        {
            log.fine( "Recovery completed." );
        }
        else
        {
            log.fine( "[" + logFileName + "] Found " + xidIdentMap.size()
                + " prepared 2PC transactions." );
            for ( StartEntry entry : xidIdentMap.values() )
            {
                log.fine( "[" + logFileName + "] 2PC xid[" + 
                    entry.getXid() + "]" );
            }
        }
        recoveredTxMap.clear();
    }

    // for testing, do not use!
    void reset()
    {
        xidIdentMap.clear();
        recoveredTxMap.clear();
    }

    private boolean readEntry() throws IOException
    {
        buffer.clear();
        buffer.limit( 1 );
        if ( fileChannel.read( buffer ) != buffer.limit() )
        {
            // ok no more entries we're done
            return false;
        }
        buffer.flip();
        byte entry = buffer.get();
        switch ( entry )
        {
            case TX_START:
                return readTxStartEntry();
            case TX_PREPARE:
                return readTxPrepareEntry();
            case TX_1P_COMMIT:
                return readTxOnePhaseCommit();
            case TX_2P_COMMIT:
                return readTxTwoPhaseCommit();
            case COMMAND:
                return readCommandEntry();
            case DONE:
                return readDoneEntry();
            case EMPTY:
                fileChannel.position( fileChannel.position() - 1 );
                return false;
            default:
                throw new IOException( "Internal recovery failed, "
                    + "unknown log entry[" + entry + "]" );
        }
    }

    private ArrayMap<Thread,Integer> txIdentMap = 
        new ArrayMap<Thread,Integer>( 5, true, true );

    void registerTxIdentifier( int identifier )
    {
        txIdentMap.put( Thread.currentThread(), identifier );
    }

    void unregisterTxIdentifier()
    {
        txIdentMap.remove( Thread.currentThread() );
    }

    /**
     * If the current thread is committing a transaction the identifier of that
     * {@link XaTransaction} can be obtained invoking this method.
     * 
     * @return the identifier of the transaction committing or <CODE>-1</CODE>
     *         if current thread isn't committing any transaction
     */
    public int getCurrentTxIdentifier()
    {
        Integer intValue = txIdentMap.get( Thread.currentThread() );
        if ( intValue != null )
        {
            return intValue;
        }
        return -1;
    }

    public ReadableByteChannel getLogicalLog( long version ) throws IOException
    {
        String name = fileName + ".v" + version;
        if ( !new File( name ).exists() )
        {
            throw new IOException( "No such log version:" + version );
        }
        return new RandomAccessFile( name, "r" ).getChannel();
    }
    
    public long getLogicalLogLength( long version )
    {
        String name = fileName + ".v" + version;
        File file = new File( name );
        if ( !file.exists() )
        {
            return -1;
        }
        return file.length();
    }
    
    public boolean hasLogicalLog( long version )
    {
        String name = fileName + ".v" + version;
        return new File( name ).exists();
    }
    
    public boolean deleteLogicalLog( long version )
    {
        String name = fileName + ".v" + version;
        File file = new File(name );
        if ( file.exists() )
        {
            return FileUtils.deleteFile( file );
        }
        return false;
    }
    
    public void makeBackupSlave()
    {
        if ( xidIdentMap.size() > 0 )
        {
            throw new IllegalStateException( "There are active transactions" );
        }
        backupSlave = true;
    }
    
    private static class LogApplier
    {
        private final ReadableByteChannel byteChannel;
        private final ByteBuffer buffer;
        private final XaTransactionFactory xaTf;
        private final XaResourceManager xaRm;
        private final XaCommandFactory xaCf;
        private final ArrayMap<Integer,StartEntry> xidIdentMap;
        private final Map<Integer,XaTransaction> recoveredTxMap;
        
        LogApplier( ReadableByteChannel byteChannel, ByteBuffer buffer, 
            XaTransactionFactory xaTf, XaResourceManager xaRm, 
            XaCommandFactory xaCf, ArrayMap<Integer,StartEntry> xidIdentMap, 
            Map<Integer,XaTransaction> recoveredTxMap )
        {
            this.byteChannel = byteChannel;
            this.buffer = buffer;
            this.xaTf = xaTf;
            this.xaRm = xaRm;
            this.xaCf = xaCf;
            this.xidIdentMap = xidIdentMap;
            this.recoveredTxMap = recoveredTxMap;
        }
        
        boolean readAndApplyEntry() throws IOException
        {
            buffer.clear();
            buffer.limit( 1 );
            if ( byteChannel.read( buffer ) != buffer.limit() )
            {
                // ok no more entries we're done
                return false;
            }
            buffer.flip();
            byte entry = buffer.get();
            switch ( entry )
            {
                case TX_START:
                    readTxStartEntry();
                    return true;
                case TX_PREPARE:
                    readTxPrepareEntry();
                    return true;
                case TX_1P_COMMIT:
                    readAndApplyTxOnePhaseCommit();
                    return true;
                case TX_2P_COMMIT:
                    readAndApplyTxTwoPhaseCommit();
                    return true;
                case COMMAND:
                    readCommandEntry();
                    return true;
                case DONE:
                    readDoneEntry();
                    return true;
                case EMPTY:
                    return false;
                default:
                    throw new IOException( "Internal recovery failed, "
                        + "unknown log entry[" + entry + "]" );
            }
        }

        private void readTxStartEntry() throws IOException
        {
            // get the global id
            buffer.clear();
            buffer.limit( 1 );
            if ( byteChannel.read( buffer ) != buffer.limit() )
            {
                throw new IOException( "Unable to read tx start entry" );
            }
            buffer.flip();
            byte globalIdLength = buffer.get();
            // get the branchId id
            buffer.clear();
            buffer.limit( 1 );
            if ( byteChannel.read( buffer ) != buffer.limit() )
            {
                throw new IOException( "Unable to read tx start entry" );
            }
            buffer.flip();
            byte branchIdLength = buffer.get();
            byte globalId[] = new byte[globalIdLength];
            ByteBuffer tmpBuffer = ByteBuffer.wrap( globalId );
            if ( byteChannel.read( tmpBuffer ) != globalId.length )
            {
                throw new IOException( "Unable to read tx start entry" );
            }
            byte branchId[] = new byte[branchIdLength];
            tmpBuffer = ByteBuffer.wrap( branchId );
            if ( byteChannel.read( tmpBuffer ) != branchId.length )
            {
                throw new IOException( "Unable to read tx start entry" );
            }
            // get the tx identifier
            buffer.clear();
            buffer.limit( 4 );
            if ( byteChannel.read( buffer ) != buffer.limit() )
            {
                throw new IOException( "Unable to read tx start entry" );
            }
            buffer.flip();
            int identifier = buffer.getInt();
            // get the format id
            buffer.clear();
            buffer.limit( 4 );
            if ( byteChannel.read( buffer ) != buffer.limit() )
            {
                throw new IOException( "Unable to read tx start entry" );
            }
            buffer.flip();
            int formatId = buffer.getInt();
            // re-create the transaction
            Xid xid = new XidImpl( globalId, branchId, formatId );
            xidIdentMap.put( identifier, new StartEntry( xid, -1 ) );
            XaTransaction xaTx = xaTf.create( identifier );
            xaTx.setRecovered();
            recoveredTxMap.put( identifier, xaTx );
            xaRm.injectStart( xid, xaTx );
        }
    
        private void readTxPrepareEntry() throws IOException
        {
            // get the tx identifier
            buffer.clear();
            buffer.limit( 4 );
            if ( byteChannel.read( buffer ) != buffer.limit() )
            {
                throw new IOException( "Unable to read tx prepare entry" );
            }
            buffer.flip();
            int identifier = buffer.getInt();
            StartEntry entry = xidIdentMap.get( identifier );
            if ( entry == null )
            {
                throw new IOException( "Unable to read tx prepeare entry" );
            }
            Xid xid = entry.getXid();
            if ( xaRm.injectPrepare( xid ) )
            {
                // read only, we can remove
                xidIdentMap.remove( identifier );
                recoveredTxMap.remove( identifier );
            }
        }

        private void readAndApplyTxOnePhaseCommit() throws IOException
        {
            // get the tx identifier
            buffer.clear();
            buffer.limit( 4 );
            if ( byteChannel.read( buffer ) != buffer.limit() )
            {
                throw new IOException( "Unable to read tx 1PC entry" );
            }
            buffer.flip();
            int identifier = buffer.getInt();
            StartEntry entry = xidIdentMap.get( identifier );
            if ( entry == null )
            {
                throw new IOException( "Unable to read tx prepeare entry" );
            }
            Xid xid = entry.getXid();
            try
            {
                xaRm.commit( xid, true );
            }
            catch ( XAException e )
            {
                e.printStackTrace();
                throw new IOException( e.getMessage() );
            }
        }

        private void readAndApplyTxTwoPhaseCommit() throws IOException
        {
            // get the tx identifier
            buffer.clear();
            buffer.limit( 4 );
            if ( byteChannel.read( buffer ) != buffer.limit() )
            {
                throw new IOException( "Unable to read tx 2PC entry" );
            }
            buffer.flip();
            int identifier = buffer.getInt();
            StartEntry entry = xidIdentMap.get( identifier );
            if ( entry == null )
            {
                throw new IOException( "Unable to read tx prepeare entry" );
            }
            Xid xid = entry.getXid();
            try
            {
                xaRm.commit( xid, true );
            }
            catch ( XAException e )
            {
                e.printStackTrace();
                throw new IOException( e.getMessage() );
            }
        }

        private void readCommandEntry() throws IOException
        {
            buffer.clear();
            buffer.limit( 4 );
            if ( byteChannel.read( buffer ) != buffer.limit() )
            {
                throw new IOException( "Unable to read tx command entry" );
            }
            buffer.flip();
            int identifier = buffer.getInt();
            XaCommand command = xaCf.readCommand( byteChannel, buffer );
            if ( command == null )
            {
                throw new IOException( "Unable to read command entry" );
            }
            command.setRecovered();
            XaTransaction xaTx = recoveredTxMap.get( identifier );
            xaTx.injectCommand( command );
        }

        private boolean readDoneEntry() throws IOException
        {
            // get the tx identifier
            buffer.clear();
            buffer.limit( 4 );
            if ( byteChannel.read( buffer ) != buffer.limit() )
            {
                return false;
            }
            buffer.flip();
            int identifier = buffer.getInt();
            StartEntry entry = xidIdentMap.get( identifier );
            if ( entry == null )
            {
                throw new IOException( "Unable to read tx done entry" );
            }
            Xid xid = entry.getXid();
            xaRm.pruneXidIfExist( xid );
            xidIdentMap.remove( identifier );
            recoveredTxMap.remove( identifier );
            return true;
        }
    }
    
    public synchronized void applyLog( ReadableByteChannel byteChannel ) 
        throws IOException
    {
        if ( !backupSlave )
        {
            throw new IllegalStateException( "This is not a backup slave" );
        }
        if ( xidIdentMap.size() > 0 )
        {
            throw new IllegalStateException( "There are active transactions" );
        }
        buffer.clear();
        buffer.limit( 8 );
        if ( byteChannel.read( buffer ) != 8 )
        {
            throw new IOException( "Unable to read log version" );
        }
        buffer.flip();
        logVersion = buffer.getLong();
        if ( logVersion != xaTf.getCurrentVersion() )
        {
            throw new IllegalStateException( "Tried to apply version " + 
                logVersion + " but expected version " + 
                xaTf.getCurrentVersion() );
        }
        log.fine( "Logical log version: " + logVersion );
        long logEntriesFound = 0;
        LogApplier logApplier = new LogApplier( byteChannel, buffer, xaTf, xaRm,
            cf, xidIdentMap, recoveredTxMap );
        while ( logApplier.readAndApplyEntry() )
        {
            logEntriesFound++;
        }
        byteChannel.close();
        xaTf.flushAll();
        xaTf.getAndSetNewVersion();
        xaRm.reset();
        log.info( "Log[" + fileName + "] version " + logVersion + 
                " applied successfully." );
    }
    
    public synchronized void rotate() throws IOException
    {
        xaTf.flushAll();
        String newLogFile = fileName + ".2";
        String currentLogFile = fileName + ".1";
        char newActiveLog = LOG2;
        long currentVersion = xaTf.getCurrentVersion();
        String oldCopy = fileName + ".v" + currentVersion;
        if ( currentLog == CLEAN || currentLog == LOG2 )
        {
            newActiveLog = LOG1;
            newLogFile = fileName + ".1";
            currentLogFile = fileName + ".2";
        }
        else
        {
            assert currentLog == LOG1;
        }
        if ( new File( newLogFile ).exists() )
        {
            throw new IOException( "New log file: " + newLogFile + 
                " already exist" );
        }
        if ( new File( oldCopy ).exists() )
        {
            throw new IOException( "Copy log file: " + oldCopy + 
                " already exist" );
        }
        long endPosition = writeBuffer.getFileChannelPosition();
        writeBuffer.force();
        FileChannel newLog = new RandomAccessFile( 
            newLogFile, "rw" ).getChannel();
        buffer.clear();
        buffer.putLong( currentVersion + 1 ).flip();
        if ( newLog.write( buffer ) != 8 )
        {
            throw new IOException( "Unable to write log version to new" );
        }
        fileChannel.position( 0 );
        buffer.clear();
        buffer.limit( 8 );
        if( fileChannel.read( buffer ) != 8 )
        {
            throw new IOException( "Verification of log version failed" );
        }
        buffer.flip();
        long verification = buffer.getLong();
        if ( verification != currentVersion )
        {
            throw new IOException( "Verification of log version failed, " + 
                " expected " + currentVersion + " got " + verification );
        }
        if ( xidIdentMap.size() > 0 )
        {
            fileChannel.position( getFirstStartEntry( endPosition ) );
        }
        buffer.clear();
        buffer.limit( 1 );
        boolean emptyHit = false;
        while ( fileChannel.read( buffer ) == 1 && !emptyHit )
        {
            buffer.flip();
            byte entry = buffer.get();
            switch ( entry )
            {
                case TX_START:
                    readAndWriteTxStartEntry( newLog );
                    break;
                case TX_PREPARE:
                    readAndWriteTxPrepareEntry( newLog );
                    break;
                case TX_1P_COMMIT:
                    readAndWriteTxOnePhaseCommit( newLog );
                    break;
                case TX_2P_COMMIT:
                    readAndWriteTxTwoPhaseCommit( newLog );
                    break;
                case COMMAND:
                    readAndWriteCommandEntry( newLog );
                    break;
                case DONE:
                    readAndVerifyDoneEntry();
                    break;
                case EMPTY:
                    emptyHit = true;
                    break;
                default:
                    throw new IOException( "Log rotation failed, "
                        + "unknown log entry[" + entry + "]" );
            }
            buffer.clear();
            buffer.limit( 1 );
        }
        newLog.force( false );
        releaseCurrentLogFile();
        setActiveLog( newActiveLog );
        if ( keepLogs )
        {
            renameCurrentLogFileAndIncrementVersion( currentLogFile, 
                endPosition );
        }
        else
        {
            deleteCurrentLogFile( currentLogFile );
            xaTf.getAndSetNewVersion();
        }
        if ( xaTf.getCurrentVersion() != ( currentVersion + 1 ) )
        {
            throw new IOException( "version change failed" );
        }
        fileChannel = newLog;
        if ( !useMemoryMapped )
        {
            writeBuffer = new DirectMappedLogBuffer( fileChannel );
        }
        else
        {
            writeBuffer = new MemoryMappedLogBuffer( fileChannel );
        }
    }
    
    private long getFirstStartEntry( long endPosition )
    {
        long firstEntryPosition = endPosition;
        for ( StartEntry entry : xidIdentMap.values() )
        {
            if ( entry.getStartPosition() < firstEntryPosition )
            {
                assert entry.getStartPosition() > 0; 
                firstEntryPosition = entry.getStartPosition();
            }
        }
        return firstEntryPosition;
    }

    private void setActiveLog( char c ) throws IOException
    {
        if ( c != CLEAN && c != LOG1 && c != LOG2 )
        {
            throw new IllegalArgumentException( "Log must be either clean, " + 
                "1 or 2" );
        }
        if ( c == currentLog )
        {
            throw new IllegalStateException( "Log should not be equal to " + 
                "current " + currentLog );
        }
        ByteBuffer bb = ByteBuffer.wrap( new byte[4] );
        bb.asCharBuffer().put( c ).flip();
        FileChannel fc = new RandomAccessFile( fileName + ".active" , 
            "rw" ).getChannel();
        int wrote = fc.write( bb );
        if ( wrote != 4 )
        {
            throw new IllegalStateException( "Expected to write 4 -> " + wrote );
        }
        fc.force( false );
        fc.close();
        currentLog = c;
    }

    // [COMMAND][identifier][COMMAND_DATA]
    private void readAndWriteCommandEntry( FileChannel newLog ) 
        throws IOException
    {
        buffer.clear();
        buffer.put( COMMAND );
        buffer.limit( 1 + 4 );
        if ( fileChannel.read( buffer ) != 4 )
        {
            throw new IllegalStateException( "Unable to read command header" );
        }
        buffer.flip();
        buffer.position( 1 );
        int identifier = buffer.getInt();
        FileChannel writeToLog = null;
        if ( xidIdentMap.get( identifier ) != null )
        {
            writeToLog = newLog;
        }
        if ( writeToLog != null )
        {
            buffer.position( 0 );
            if ( writeToLog.write( buffer ) != 5 )
            {
                throw new TransactionFailureException( 
                    "Unable to write command header" );
            }
        }
        XaCommand command = cf.readCommand( fileChannel, buffer );
        if ( writeToLog != null )
        {
            command.writeToFile( new DirectLogBuffer( writeToLog, buffer ) );
        }
    }

    private void readAndVerifyDoneEntry() 
        throws IOException
    {
        buffer.clear();
        buffer.limit( 4 );
        if ( fileChannel.read( buffer ) != 4 )
        {
            throw new IllegalStateException( "Unable to read done entry" );
        }
        buffer.flip();
        int identifier = buffer.getInt();
        if ( xidIdentMap.get( identifier ) != null )
        {
            throw new IllegalStateException( identifier + 
                " done entry found but still active" );
        }
    }

    // [TX_1P_COMMIT][identifier]
    private void readAndWriteTxOnePhaseCommit( FileChannel newLog ) 
        throws IOException
    {
        buffer.clear();
        buffer.limit( 1 + 4 );
        buffer.put( TX_1P_COMMIT );
        if ( fileChannel.read( buffer ) != 4 )
        {
            throw new IllegalStateException( "Unable to read 1P commit entry" );
        }
        buffer.flip();
        buffer.position( 1 );
        int identifier = buffer.getInt();
        FileChannel writeToLog = null;
        if ( xidIdentMap.get( identifier ) != null )
        {
            writeToLog = newLog;
        }
        buffer.position( 0 );
        if ( writeToLog != null && writeToLog.write( buffer ) != 5 )
        {
            throw new TransactionFailureException( 
                "Unable to write 1P commit entry" );
        }
    }

    private void readAndWriteTxTwoPhaseCommit( FileChannel newLog ) 
        throws IOException
    {
        buffer.clear();
        buffer.limit( 1 + 4 );
        buffer.put( TX_2P_COMMIT );
        if ( fileChannel.read( buffer ) != 4 )
        {
            throw new IllegalStateException( "Unable to read 2P commit entry" );
        }
        buffer.flip();
        buffer.position( 1 );
        int identifier = buffer.getInt();
        FileChannel writeToLog = null;
        if ( xidIdentMap.get( identifier ) != null )
        {
//            throw new IllegalStateException( identifier + 
//                " 2PC found but still active" );
            writeToLog = newLog;
        }
        buffer.position( 0 );
        if ( writeToLog != null && writeToLog.write( buffer ) != 5 )
        {
            throw new TransactionFailureException( 
                "Unable to write 2P commit entry" );
        }
    }
    
    private void readAndWriteTxPrepareEntry( FileChannel newLog ) 
        throws IOException
    {
        // get the tx identifier
        buffer.clear();
        buffer.limit( 1 + 4 );
        buffer.put( TX_PREPARE );
        if ( fileChannel.read( buffer ) != 4 )
        {
            throw new IllegalStateException( "Unable to read prepare entry" );
        }
        buffer.flip();
        buffer.position( 1 );
        int identifier = buffer.getInt();
        FileChannel writeToLog = null;
        if ( xidIdentMap.get( identifier ) != null )
        {
            writeToLog = newLog;
        }
        buffer.position( 0 );
        if ( writeToLog != null && writeToLog.write( buffer ) != 5 )
        {
            throw new TransactionFailureException( 
                "Unable to write prepare entry" );
        }
    }

    // [TX_START][xid[gid.length,bid.lengh,gid,bid]][identifier][format id]
    private void readAndWriteTxStartEntry( FileChannel newLog ) 
        throws IOException
    {
        // get the global id
        buffer.clear();
        buffer.put( TX_START );
        buffer.limit( 3 );
        if ( fileChannel.read( buffer ) != 2 )
        {
            throw new IllegalStateException( 
                "Unable to read tx start entry xid id lengths" );
        }
        buffer.flip();
        buffer.position( 1 );
        byte globalIdLength = buffer.get();
        byte branchIdLength = buffer.get();
        int xidLength = globalIdLength + branchIdLength;
        buffer.limit( 3 + xidLength + 8 );
        buffer.position( 3 );
        if ( fileChannel.read( buffer ) != 8 + xidLength )
        {
            throw new IllegalStateException( "Unable to read xid" );
        }
        buffer.flip();
        buffer.position( 3 + xidLength );
        int identifier = buffer.getInt();
        FileChannel writeToLog = null;
        StartEntry entry = xidIdentMap.get( identifier );
        if ( entry != null )
        {
            writeToLog = newLog;
            entry.setStartPosition( newLog.position() );
        }
        buffer.position( 0 );
        if ( writeToLog != null && 
            writeToLog.write( buffer ) != 3 + 8 + xidLength )
        {
            throw new TransactionFailureException( 
                "Unable to write tx start xid" );
        }
    }

    public void setKeepLogs( boolean keep )
    {
        this.keepLogs = keep;
    }
    
    public boolean isLogsKept()
    {
        return this.keepLogs;
    }
    
    public void setAutoRotateLogs( boolean autoRotate )
    {
        this.autoRotate = autoRotate;
    }
    
    public boolean isLogsAutoRotated()
    {
        return this.autoRotate;
    }
    
    public void setLogicalLogTargetSize( long size )
    {
        this.rotateAtSize = size;
    }
    
    public long getLogicalLogTargetSize()
    {
        return this.rotateAtSize;
    }
    
    private static class StartEntry
    {
        private final Xid xid;
        private long startEntryPosition;
        
        StartEntry( Xid xid, long startPosition )
        {
            this.xid = xid;
            this.startEntryPosition = startPosition;
        }
        
        Xid getXid()
        {
            return xid;
        }
        
        long getStartPosition()
        {
            return startEntryPosition;
        }
        
        void setStartPosition( long newPosition )
        {
            startEntryPosition = newPosition;
        }
    }

    public String getFileName( long version ) 
    {
        return fileName + ".v" + version;
    }
}