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

import java.util.LinkedList;
import java.io.File;
import java.io.FileOutputStream;
import java.io.RandomAccessFile;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

/**
 * This class generates unique ids for a resource type. For example, nodes in a
 * nodes space are connected to each other via relationships. On nodes and
 * relationship one can add properties. We have three different resource types
 * here (nodes, relationships and properties) where each resource needs a unique
 * id to be able to differ resources of the same type from each other. Creating
 * three id generators (one for each resource type ) will do the trick.
 * <p>
 * <CODE>IdGenerator</CODE> makes use of so called "defragged" ids. A
 * defragged id is an id that has been in use one or many times but the resource
 * that was using it doesn't exist anymore. This makes it possible to reuse the
 * id and that in turn makes it possible to write a resource store with fixed
 * records and size (you can calculate the position of a record by knowing the
 * id without using indexes or a translation table).
 * <p>
 * The int value returned by the {@link #nextId} may not be the lowest you
 * available id but will be one of the defragged ids if such exist or the next
 * new free id that has never been used.
 * <p>
 * The {@link #freeId} will not check if the id passed in to it really is free.
 * Passing a non free id will corrupt the id generator and {@link #nextId}
 * method will eventually return that id.
 * <p>
 * The {@link #close()} method must always be invoked when done using an
 * generator (for this time). Failure to do will render the generator as
 * "sticky" and unusable next time you try to initialize a generator using the
 * same file. Also you can only have one <CODE>IdGenerator</CODE> instance per
 * id generator file at the same time.
 * <p>
 * In case of disk/file I/O failure an <CODE>IOException</CODE> is thrown.
 */
public class IdGeneratorImpl implements IdGenerator
{
    // sticky(byte), nextFreeId(long)
    private static final int HEADER_SIZE = 9;

    // if sticky the id generator wasn't closed properly so it has to be
    // rebuilt (go through the node, relationship, property, rel type etc files)
    private static final byte CLEAN_GENERATOR = (byte) 0;
    private static final byte STICKY_GENERATOR = (byte) 1;
    
    private static final long OVERFLOW_ID = 4294967294l;

    // number of defragged ids to grab form file in batch (also used for write)
    private int grabSize = -1;
    private long nextFreeId = -1;
    // total bytes read from file, used in writeIdBatch() and close()
    private long totalBytesRead = 0;
    // true if more defragged ids can be read from file
    private boolean haveMore = true;
    // marks where this sessions released ids will be written
    private long readBlocksTo = HEADER_SIZE;
    // used to calculate number of ids actually in use
    private long defraggedIdCount = -1;

    private final String fileName;
    private FileChannel fileChannel = null;
    // in memory defragged ids read from file (and from freeId)
    private final LinkedList<Long> defragedIdList = 
        new LinkedList<Long>();
    // in memory newly free defragged ids that havn't been flushed to disk yet
    private final LinkedList<Long> releasedIdList = 
        new LinkedList<Long>();
    // buffer used in readIdBatch()
    private ByteBuffer readBuffer = null;
    // buffer used in writeIdBatch() and close()
    private ByteBuffer writeBuffer = null;

    /**
     * Opens the id generator represented by <CODE>fileName</CODE>. The
     * <CODE>grabSize</CODE> means how many defragged ids we should keep in
     * memory and is also the size (x4) of the two buffers used for reading and
     * writing to the id generator file. The highest returned id will be read
     * from file and if <CODE>grabSize</CODE> number of ids exist they will be
     * read into memory (if less exist all defragged ids will be in memory).
     * <p>
     * If this id generator hasn't been closed properly since the previous
     * session (sticky) an <CODE>IOException</CODE> will be thrown. When this
     * happens one has to rebuild the id generator from the (node/rel/prop)
     * store file.
     * 
     * @param fileName
     *            The file name (and path if needed) for the id generator to be
     *            opened
     * @param grabSize
     *            The number of defragged ids to keep in memory
     * @throws IOException
     *             If no such file exist or if the id generator is sticky
     */
    public IdGeneratorImpl( String fileName, int grabSize )
    {
        if ( grabSize < 1 )
        {
            throw new IllegalArgumentException( "Illegal grabSize: " + grabSize );
        }
        this.fileName = fileName;
        this.grabSize = grabSize;
        readBuffer = ByteBuffer.allocate( grabSize * 8 );
        writeBuffer = ByteBuffer.allocate( grabSize * 8 );
        initGenerator();
    }

    /**
     * Returns the next "free" id. If a defragged id exist it will be returned
     * else the next free id that hasn't been used yet is returned. If no id
     * exist the capacity is exceeded (all int values >= 0 are taken) and a
     * <CODE>IOException</CODE> will be thrown.
     * 
     * @return The next free id
     * @throws IOException
     *             If the capacity is exceeded or closed generator
     */
    public synchronized long nextId()
    {
        if ( fileChannel == null )
        {
            throw new IllegalStateException( "Closed id generator " + fileName );
        }
        if ( defragedIdList.size() > 0 )
        {
            long id = defragedIdList.removeFirst();
            if ( haveMore && defragedIdList.size() == 0 )
            {
                readIdBatch();
            }
            defraggedIdCount--;
            return id;
        }
        if ( nextFreeId >= OVERFLOW_ID || nextFreeId < 0  )
        {
            throw new UnderlyingStorageException( "Id capacity exceeded" );
        }
        return nextFreeId++;
    }

    /**
     * Sets the next free "high" id. This method should be called when an id
     * generator has been rebuilt.
     * 
     * @param id
     *            The next free id
     */
    public synchronized void setHighId( long id )
    {
        nextFreeId = id;
    }

    /**
     * Returns the next "high" id that will be returned if no defragged ids
     * exist.
     * 
     * @return The next free "high" id
     */
    public synchronized long getHighId()
    {
        return nextFreeId;
    }

    /**
     * Frees the <CODE>id</CODE> making it a defragged id that will be
     * returned by next id before any new id (that hasn't been used yet) is
     * returned.
     * <p>
     * This method will throw an <CODE>IOException</CODE> if id is negative or
     * if id is greater than the highest returned id. However as stated in the
     * class documentation above the id isn't validated to see if it really is
     * free.
     * 
     * @param id
     *            The id to be made available again
     * @throws IOException
     *             If id is negative or greater than the highest returned id
     */
    public synchronized void freeId( long id )
    {
        if ( id < 0 || id >= nextFreeId )
        {
            throw new IllegalArgumentException( "Illegal id[" + id + "]" );
        }
        if ( fileChannel == null )
        {
            throw new IllegalStateException( "Generator closed " + fileName );
        }
        releasedIdList.add( id );
        defraggedIdCount++;
        if ( releasedIdList.size() >= grabSize )
        {
            writeIdBatch();
        }
    }

    /**
     * Closes the id generator flushing defragged ids in memory to file. The
     * file will be truncated to the minimal size required to hold all defragged
     * ids and it will be marked as clean (not sticky).
     * <p>
     * An invoke to the <CODE>nextId</CODE> or <CODE>freeId</CODE> after
     * this method has been invoked will result in an <CODE>IOException</CODE>
     * since the highest returned id has been set to a negative value.
     * 
     * @throws IOException
     *             If unable to close this id generator
     */
    public synchronized void close()
    {
        if ( nextFreeId == -1 )
        {
            return;
        }

        // write out lists
        if ( releasedIdList.size() > 0 )
        {
            writeIdBatch();
        }
        if ( defragedIdList.size() > 0 )
        {
            while ( defragedIdList.size() > 0 )
            {
                releasedIdList.add( defragedIdList.removeFirst() );
            }
            writeIdBatch();
        }

        // write header
        try
        {
            fileChannel.position( 0 );
            ByteBuffer buffer = ByteBuffer.allocate( HEADER_SIZE );
            buffer.put( STICKY_GENERATOR ).putLong( nextFreeId );
            buffer.flip();
            fileChannel.write( buffer );
            // move data to remove fragmentation in file
            if ( totalBytesRead > HEADER_SIZE )
            {
                long writePosition = HEADER_SIZE;
                long readPosition = readBlocksTo;
                if ( totalBytesRead < readBlocksTo )
                {
                    readPosition = totalBytesRead;
                }
                int bytesRead = -1;
                do
                {
                    writeBuffer.clear();
                    fileChannel.position( readPosition );
                    bytesRead = fileChannel.read( writeBuffer );
                    readPosition += bytesRead;
                    writeBuffer.flip();
                    fileChannel.position( writePosition );
                    writePosition += fileChannel.write( writeBuffer );
                }
                while ( bytesRead > 0 );
                // truncate
                fileChannel.truncate( writePosition );
            }
            // flush
            fileChannel.force( false );
            // remove sticky
            buffer.clear();
            buffer.put( CLEAN_GENERATOR );
            buffer.limit( 1 );
            buffer.flip();
            fileChannel.position( 0 );
            fileChannel.write( buffer );
            // flush and close
            fileChannel.force( false );
            fileChannel.close();
            fileChannel = null;
            // make this generator unusable
            nextFreeId = -1;
        }
        catch ( IOException e )
        {
            throw new UnderlyingStorageException( 
                "Unable to close id generator " + fileName, e );
        }
    }

    /**
     * Returns the file associated with this id generator.
     * 
     * @return The id generator's file name
     */
    public String getFileName()
    {
        return this.fileName;
    }

    /**
     * Creates a new id generator.
     * 
     * @param fileName
     *            The name of the id generator
     * @throws IOException
     *             If unable to create the id generator
     */
    public static void createGenerator( String fileName )
    {
        // sanity checks
        if ( fileName == null )
        {
            throw new IllegalArgumentException( "Null filename" );
        }
        File file = new File( fileName );
        if ( file.exists() )
        {
            throw new IllegalStateException( "Can't create IdGeneratorFile["
                + fileName + "], file already exists" );
        }
        try
        {
            FileChannel channel = new FileOutputStream( fileName ).getChannel();
            // write the header
            ByteBuffer buffer = ByteBuffer.allocate( HEADER_SIZE );
            buffer.put( CLEAN_GENERATOR ).putLong( 0 ).flip();
            channel.write( buffer );
            channel.force( false );
            channel.close();
        }
        catch ( IOException e )
        {
            throw new UnderlyingStorageException( 
                "Unable to create id generator" + fileName, e );
        }
    }

    // initialize the id generator and performs a simple validation
    private synchronized void initGenerator()
    {
        try
        {
            fileChannel = new RandomAccessFile( fileName, "rw" ).getChannel();
            ByteBuffer buffer = ByteBuffer.allocate( HEADER_SIZE );
            totalBytesRead = fileChannel.read( buffer );
            if ( totalBytesRead != HEADER_SIZE )
            {
                fileChannel.close();
                throw new InvalidIdGeneratorException(
                    "Unable to read header, bytes read: " + totalBytesRead );
            }
            buffer.flip();
            byte storageStatus = buffer.get();
            if ( storageStatus != CLEAN_GENERATOR )
            {
                fileChannel.close();
                throw new InvalidIdGeneratorException( "Sticky generator[ "
                    + fileName
                    + "] delete this id generator and build a new one" );
            }
            this.nextFreeId = buffer.getLong();
            buffer.flip();
            buffer.put( STICKY_GENERATOR ).limit( 1 ).flip();
            fileChannel.position( 0 );
            fileChannel.write( buffer );
            fileChannel.position( HEADER_SIZE );
            readBlocksTo = fileChannel.size();
            defraggedIdCount = (int) (readBlocksTo - HEADER_SIZE) / 8;
            readIdBatch();
        }
        catch ( IOException e )
        {
            throw new UnderlyingStorageException( 
                "Unable to init id generator " + fileName, e );
        }
    }

    private void readIdBatch()
    {
        if ( !haveMore )
        {
            return;
        }
        if ( totalBytesRead >= readBlocksTo )
        {
            haveMore = false;
            return;
        }
        try
        {
            if ( totalBytesRead + readBuffer.capacity() > readBlocksTo )
            {
                readBuffer.clear();
                readBuffer
                    .limit( (int) (readBlocksTo - fileChannel.position()) );
            }
            else
            {
                readBuffer.clear();
            }
            fileChannel.position( totalBytesRead );
            int bytesRead = fileChannel.read( readBuffer );
            assert fileChannel.position() <= readBlocksTo;
            totalBytesRead += bytesRead;
            readBuffer.flip();
            assert (bytesRead % 8) == 0;
            int idsRead = bytesRead / 8;
            defraggedIdCount -= idsRead;
            for ( int i = 0; i < idsRead; i++ )
            {
                long id = readBuffer.getLong();
                defragedIdList.add( id );
            }
        }
        catch ( IOException e )
        {
            throw new UnderlyingStorageException(
                "Failed reading defragged id batch", e );
        }
    }

    // writes a batch of defragged ids to file
    private void writeIdBatch()
    {
        // position at end
        try
        {
            fileChannel.position( fileChannel.size() );
            writeBuffer.clear();
            while ( releasedIdList.size() > 0 )
            {
                writeBuffer.putLong( releasedIdList.removeFirst() );
                if ( writeBuffer.position() == writeBuffer.capacity() )
                {
                    writeBuffer.flip();
                    fileChannel.write( writeBuffer );
                    writeBuffer.clear();
                }
            }
            writeBuffer.flip();
            fileChannel.write( writeBuffer );
            // position for next readIdBatch
            fileChannel.position( totalBytesRead );
        }
        catch ( IOException e )
        {
            throw new UnderlyingStorageException( 
                "Unable to write defragged id " + " batch", e );
        }
    }

    /**
     * Utility method that will dump all defragged id's and the "high id" to
     * console. Do not call while running store using this id generator since it
     * could corrupt the id generator (not thread safe). This method will close
     * the id generator after being invoked.
     * 
     * @throws IOException
     *             If problem dumping free ids
     */
    public synchronized void dumpFreeIds()
    {
        while ( haveMore )
        {
            readIdBatch();
        }
        java.util.Iterator<Long> itr = defragedIdList.iterator();
        while ( itr.hasNext() )
        {
            System.out.print( " " + itr.next() );
        }
        System.out.println( "\nNext free id: " + nextFreeId );
        close();
    }

    public synchronized long getNumberOfIdsInUse()
    {
        return nextFreeId - defraggedIdCount;
    }
}