/*
 * 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.io.IOException;
import java.io.Serializable;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;

/**
 * Manages {@link PersistenceWindow persistence windows} for a store. Each store
 * can configure how much memory it has for
 * {@link MappedPersistenceWindow memory mapped windows}. This class tries to
 * make the most efficient use of those windows by allocating them in such a way
 * that the most frequently used records/blocks (be it for read or write
 * operations) are encapsulated by a memory mapped persistence window.
 */
class PersistenceWindowPool
{
    private static final int MAX_BRICK_COUNT = 10000;

    private final String storeName;
    private final int blockSize;
    private FileChannel fileChannel;
    private Map<Integer,PersistenceRow> activeRowWindows = 
        new HashMap<Integer,PersistenceRow>();
    private long availableMem = 0;
    private long memUsed = 0;
    private int brickCount = 0;
    private int brickSize = 0;
    private BrickElement brickArray[] = new BrickElement[0];
    private int brickMiss = 0;

    private static Logger log = Logger.getLogger( PersistenceWindowPool.class
        .getName() );
    private static final int REFRESH_BRICK_COUNT = 5000;
    private final FileChannel.MapMode mapMode;

    private int hit = 0;
    private int miss = 0;
    private int switches = 0;
    private int ooe = 0;
    private boolean useMemoryMapped = true;
    
    /**
     * Create new pool for a store.
     * 
     * @param storeName
     *            Name of store that use this pool
     * @param blockSize
     *            The size of each record/block in the store
     * @param fileChannel
     *            A fileChannel to the store
     * @param mappedMem
     *            Number of bytes dedicated to memory mapped windows
     * @throws IOException
     *             If unable to create pool
     */
    PersistenceWindowPool( String storeName, int blockSize,
        FileChannel fileChannel, long mappedMem, 
        boolean useMemoryMappedBuffers, boolean readOnly )
    {
        this.storeName = storeName;
        this.blockSize = blockSize;
        this.fileChannel = fileChannel;
        this.availableMem = mappedMem;
        this.useMemoryMapped = useMemoryMappedBuffers;
        if ( readOnly )
        {
            mapMode = FileChannel.MapMode.READ_ONLY;
        }
        else
        {
            mapMode = FileChannel.MapMode.READ_WRITE;
        }
        setupBricks();
        dumpStatus();
    }

    /**
     * Acquires a windows for <CODE>position</CODE> and <CODE>operationType</CODE>
     * locking the window preventing other threads from using it.
     * 
     * @param position
     *            The position the needs to be encapsulated by the window
     * @param operationType
     *            The type of operation (READ or WRITE)
     * @return A locked window encapsulating the position
     * @throws IOException
     *             If unable to acquire the window
     */
    PersistenceWindow acquire( long position, OperationType operationType )
    {
        LockableWindow window = null;
        synchronized ( activeRowWindows )
        {
            if ( brickMiss >= REFRESH_BRICK_COUNT )
            {
                brickMiss = 0;
                refreshBricks();
            }
            if ( brickSize > 0 )
            {
                int brickIndex = (int) (position * blockSize / brickSize);
                if ( brickIndex < brickArray.length )
                {
                    window = brickArray[brickIndex].getWindow();
                    // assert window == null || window.encapsulates( position );
                    brickArray[brickIndex].setHit();
                }
                else
                {
                    expandBricks( brickIndex + 1 );
                    window = brickArray[brickIndex].getWindow();
                }
            }
            if ( window == null )
            {
                miss++;
                brickMiss++;
                
                PersistenceRow dpw = activeRowWindows.get( (int) position ); 
                
                if ( dpw == null )
                {
                    dpw = new PersistenceRow( position, blockSize, 
                        fileChannel );
                    if ( operationType == OperationType.READ )
                    {
                        dpw.readPosition();
                    }
                }
                window = dpw;
                activeRowWindows.put( (int) position, dpw );
            }
            else
            {
                hit++;
            }
            window.mark();
        }
        window.lock();
        window.setOperationType( operationType );
        return window;
    }

    void dumpStatistics()
    {
        log.finest( storeName + " hit=" + hit + " miss=" + miss + " switches="
            + switches + " ooe=" + ooe );
    }

    /**
     * Releases a window used for an operation back to the pool and unlocks it
     * so other threads may use it.
     * 
     * @param window
     *            The window to be released
     * @throws IOException
     *             If unable to release window
     */
    void release( PersistenceWindow window )
    {
        synchronized ( activeRowWindows )
        {
            if ( window instanceof PersistenceRow )
            {
                PersistenceRow dpw = (PersistenceRow) window;
                // will only write if operation was write
                dpw.writeOut();
                if ( dpw.getWaitingThreadsCount() == 0 && !dpw.isMarked() )
                {
                    int key = (int) dpw.position();
                    activeRowWindows.remove( key );
                }
            }
            ((LockableWindow) window).unLock();
        }
    }

    void close()
    {
        flushAll();
        synchronized ( activeRowWindows )
        {
            for ( BrickElement element : brickArray )
            {
                if ( element.getWindow() != null )
                {
                    element.getWindow().close();
                    element.setWindow( null );
                }
            }
            fileChannel = null;
            activeRowWindows.clear();
        }
        activeRowWindows = null;
        dumpStatistics();
    }

    void flushAll()
    {
        synchronized ( activeRowWindows )
        {
            for ( BrickElement element : brickArray )
            {
                if ( element.getWindow() != null )
                {
                    element.getWindow().force();
                }
            }
        }
        try
        {
            fileChannel.force( false );
        }
        catch ( IOException e )
        {
            throw new UnderlyingStorageException( 
                "Failed to flush file channel " + storeName, e );
        }
    }

    private static class BrickElement
    {
        private int index;
        private int hitCount;
        private LockableWindow window = null;

        BrickElement( int index )
        {
            this.index = index;
        }

        void setWindow( LockableWindow window )
        {
            this.window = window;
        }

        LockableWindow getWindow()
        {
            return window;
        }

        int index()
        {
            return index;
        }

        void setHit()
        {
            hitCount += 10;
            if ( hitCount < 0 )
            {
                hitCount -= 10;
            }
        }

        int getHit()
        {
            return hitCount;
        }

        void refresh()
        {
            if ( window == null )
            {
                hitCount /= 1.25;
            }
            else
            {
                hitCount /= 1.15;
            }
        }

        public String toString()
        {
            return "" + hitCount + (window == null ? "x" : "o");
        }
    }

    private void setupBricks()
    {
        long fileSize = -1;
        try
        {
            fileSize = fileChannel.size();
        }
        catch ( IOException e )
        {
            throw new UnderlyingStorageException( 
                "Unable to get file size for " + storeName, e );
        }
        if ( blockSize == 0 )
        {
            return;
        }
        if ( availableMem > 0 && availableMem < blockSize * 10l )
        {
            logWarn( "Unable to use " + availableMem
                + "b as memory mapped windows, need at least " + blockSize * 10
                + "b (block size * 10)" );
            logWarn( "Memory mapped windows have been turned off" );
            availableMem = 0;
            brickCount = 0;
            brickSize = 0;
            return;
        }
        if ( availableMem > 0 && fileSize > 0 )
        {
            double ratio = (availableMem + 0.0d) / fileSize;
            if ( ratio >= 1 )
            {
                brickSize = (int) (availableMem / 100 );
                if ( brickSize < 0 )
                {
                    brickSize = Integer.MAX_VALUE;
                }
                brickSize = (brickSize / blockSize) * blockSize;
                brickCount = (int) (fileSize / brickSize);
            }
            else
            {
                brickCount = (int) (100.0d / ratio);
                if ( brickCount > MAX_BRICK_COUNT )
                {
                    brickCount = MAX_BRICK_COUNT;
                }
                if ( fileSize / brickCount > availableMem )
                {
                    logWarn( "Unable to use " + (availableMem / 1024)
                        + "kb as memory mapped windows, need at least "
                        + (fileSize / brickCount / 1024) + "kb" );
                    logWarn( "Memory mapped windows have been turned off" );
                    availableMem = 0;
                    brickCount = 0;
                    brickSize = 0;
                    return;
                }
                brickSize = (int) (fileSize / brickCount);
                if ( brickSize < 0 )
                {
                    brickSize = Integer.MAX_VALUE;
                    brickSize = (brickSize / blockSize) * blockSize;
                    brickCount = (int) (fileSize / brickSize);
                }
                else
                {
                    brickSize = (brickSize / blockSize) * blockSize;
                }
                assert brickSize > blockSize;
            }
        }
        else if ( availableMem > 0 )
        {
            brickSize = (int) (availableMem / 100);
            if ( brickSize < 0 )
            {
                brickSize = Integer.MAX_VALUE;
            }
            brickSize = (brickSize / blockSize) * blockSize;
        }
        brickArray = new BrickElement[brickCount];
        for ( int i = 0; i < brickCount; i++ )
        {
            BrickElement element = new BrickElement( i );
            brickArray[i] = element;
        }
    }

    private void refreshBricks()
    {
        if ( brickSize <= 0 )
        {
            // memory mapped turned off
            return;
        }
        ArrayList<BrickElement> nonMappedBricks = new ArrayList<BrickElement>();
        ArrayList<BrickElement> mappedBricks = new ArrayList<BrickElement>();
        for ( int i = 0; i < brickCount; i++ )
        {
            BrickElement be = brickArray[i];
            if ( be.getWindow() != null )
            {
                mappedBricks.add( be );
            }
            else
            {
                nonMappedBricks.add( be );
            }
            be.refresh();
        }
        Collections.sort( nonMappedBricks, new BrickSorter() );
        Collections.sort( mappedBricks, new BrickSorter() );
        int mappedIndex = 0;
        int nonMappedIndex = nonMappedBricks.size() - 1;
        // fill up unused memory
        while ( memUsed + brickSize <= availableMem && nonMappedIndex >= 0 )
        {
            BrickElement nonMappedBrick = nonMappedBricks.get( 
                nonMappedIndex-- );
            if ( nonMappedBrick.getHit() == 0 )
            {
                return;
            }
            try
            {
                nonMappedBrick.setWindow( 
                    allocateNewWindow( nonMappedBrick.index() ) ); 
                memUsed += brickSize;
            }
            catch ( MappedMemException e )
            {
                e.printStackTrace();
                ooe++;
                logWarn( "Unable to memory map" );
            }
            catch ( OutOfMemoryError e )
            {
                e.printStackTrace();
                ooe++;
                logWarn( "Unable to allocate direct buffer" );
            }
        }
        
        // switch bad mappings
        while ( nonMappedIndex >= 0 && mappedIndex < mappedBricks.size() )
        {
            BrickElement mappedBrick = mappedBricks.get( mappedIndex++ );
            BrickElement nonMappedBrick = nonMappedBricks
                .get( nonMappedIndex-- );
            if ( mappedBrick.getHit() >= nonMappedBrick.getHit() )
            {
                break;
            }
            LockableWindow window = mappedBrick.getWindow();
            if ( window.getWaitingThreadsCount() == 0 && !window.isMarked() )
            {
                if ( window instanceof MappedPersistenceWindow )
                {
                    ((MappedPersistenceWindow) window).unmap();
                }
                else if ( window instanceof PlainPersistenceWindow )
                {
                    ((PlainPersistenceWindow) window).writeOut();
                }
                mappedBrick.setWindow( null );
                memUsed -= brickSize;
                try
                {
                    nonMappedBrick.setWindow( 
                        allocateNewWindow( nonMappedBrick.index() ) );
                    memUsed += brickSize;
                    switches++;
                }
                catch ( MappedMemException e )
                {
                    ooe++;
                    logWarn( "Unable to memory map" );
                }
                catch ( OutOfMemoryError e )
                {
                    ooe++;
                    logWarn( "Unable to allocate direct buffer" );
                }
            }
        }
    }

    private void expandBricks( int newBrickCount )
    {
        if ( newBrickCount > brickCount )
        {
            BrickElement tmpArray[] = new BrickElement[newBrickCount];
            System.arraycopy( brickArray, 0, tmpArray, 0, brickArray.length );
            for ( int i = brickArray.length; i < tmpArray.length; i++ )
            {
                BrickElement be = new BrickElement( i );
                tmpArray[i] = be;
                if ( memUsed + brickSize <= availableMem )
                {
                    try
                    {
                        be.setWindow( allocateNewWindow( i ) );
                        memUsed += brickSize;
                    }
                    catch ( MappedMemException e )
                    {
                        ooe++;
                        logWarn( "Unable to memory map" );
                    }
                    catch ( OutOfMemoryError e )
                    {
                        ooe++;
                        logWarn( "Unable to allocate direct buffer" );
                    }
                }
            }
            brickArray = tmpArray;
            brickCount = tmpArray.length;
        }
    }

    private LockableWindow allocateNewWindow( long brick )
    {
        if ( useMemoryMapped )
        {
             return new MappedPersistenceWindow( 
                brick * brickSize / blockSize, blockSize, 
                brickSize, fileChannel, mapMode );
        }
        PlainPersistenceWindow dpw = 
            new PlainPersistenceWindow( 
                brick * brickSize / blockSize, 
                blockSize, brickSize, fileChannel );
        dpw.readPosition();
        return dpw;
    }
    static class BrickSorter implements Comparator<BrickElement>, Serializable
    {
        public int compare( BrickElement o1, BrickElement o2 )
        {
            return o1.getHit() - o2.getHit();
        }

        public boolean equals( Object o )
        {
            if ( o instanceof BrickSorter )
            {
                return true;
            }
            return false;
        }

        public int hashCode()
        {
            return 7371;
        }
    }

    private void dumpStatus()
    {
        try
        {
            log.fine( "[" + storeName + "] brickCount=" + brickCount
                + " brickSize=" + brickSize + "b mappedMem=" + availableMem
                + "b (storeSize=" + fileChannel.size() + "b)" );
        }
        catch ( IOException e )
        {
            throw new UnderlyingStorageException( 
                "Unable to get file size for " + storeName, e );
        }
    }

    private void logWarn( String logMessage )
    {
        log.warning( "[" + storeName + "] " + logMessage );
    }
    
    WindowPoolStats getStats()
    {
        return new WindowPoolStats( storeName, availableMem, memUsed, brickCount, 
                brickSize, hit, miss, ooe );
    }
}