/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.jena.tdb.base.file;

import static java.lang.String.format;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;

import static java.nio.channels.FileChannel.MapMode ;

import java.util.Arrays;

import org.apache.jena.tdb.base.block.Block ;
import org.apache.jena.tdb.sys.SystemTDB ;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/** FileAccess for a file, using memory mapped I/O */
final
public class BlockAccessMapped extends BlockAccessBase
{
    /* Blocks are addressed by positive ints - 
     * Is that a limit?
     * One billion is 2^30
     * If a block is 8K, the 2^31*2^13 =  2^44 bits or 2^14 billion = 16K Billion. = 16 trillion bytes.
     * No limit at the moment - later performance tuning will see what the cost of 48 or 63 bit addresses would be.    
     */
    
    private static Logger log = LoggerFactory.getLogger(BlockAccessMapped.class) ;
    private enum CopyContents { Overwrite, NoCopy } 
    
    // Segmentation avoids over-mapping; allows file to grow (in chunks) 
    private final int GrowthFactor = 2 ;
    private final int SegmentSize = SystemTDB.SegmentSize ;
    private final int blocksPerSegment ;                              
    
    private int initialNumSegements = 1 ;
    private MappedByteBuffer[] segments = new MappedByteBuffer[initialNumSegements] ;  
    
    // Unflushed segments.
    private int segmentDirtyCount = 0 ;
    private boolean[] segmentDirty = new boolean[initialNumSegements] ; 
    
    public BlockAccessMapped(String filename, int blockSize)
    {
        super(filename, blockSize) ;
        blocksPerSegment = SegmentSize/blockSize ;
        if ( SegmentSize%blockSize != 0 )
            getLog().warn(format("%s: Segment size(%d) not a multiple of blocksize (%d)", filename, SegmentSize, blockSize)) ;
        
        for ( int i = 0 ; i < initialNumSegements ; i++ )
            // Not strictly necessary - default value is false.
            segmentDirty[i] = false ;
        segmentDirtyCount = 0 ;
        
        if ( getLog().isDebugEnabled() )
            getLog().debug(format("Segment:%d  BlockSize=%d  blocksPerSegment=%d", SegmentSize, blockSize, blocksPerSegment)) ;
    }
    
    
    @Override
    public Block allocate(int blkSize)
    {
        if ( blkSize > 0 && blkSize != this.blockSize )
            throw new FileException("Fixed blocksize only: request= "+blkSize+"fixed size="+this.blockSize) ;
        int id = allocateId() ;
        ByteBuffer bb = getByteBuffer(id) ;
        bb.position(0) ;
        Block block = new Block(id, bb) ;
        return block ;
    }
    
    @Override
    public Block read(long id)
    {
        check(id) ;
        checkIfClosed() ;
        ByteBuffer bb = getByteBuffer(id) ;
        bb.position(0) ;
        Block block = new Block(id, bb) ;
        return block ;
    }

    @Override
    public void write(Block block)
    {
        write(block, CopyContents.NoCopy) ;
    }
    
    @Override
    public void overwrite(Block block)
    {
        overwriteNotification(block) ;
        write(block, CopyContents.Overwrite) ;
    }

    private void write(Block block, CopyContents copyContents)
    {
        check(block) ;
        checkIfClosed() ;
        int id = block.getId().intValue() ;
        
        if ( copyContents == CopyContents.Overwrite )
        {
            ByteBuffer bbDst = getByteBuffer(id) ;
            bbDst.position(0) ;
            ByteBuffer bbSrc = block.getByteBuffer() ;
            bbSrc.rewind() ;
            bbDst.put(bbSrc) ;
        }
        
        // Assumed MRSW - no need to sync as we are the only Writer
        segmentDirty[segment(id)] = true ;
        writeNotification(block) ;
    }
    
    @Override
    public void sync()
    {
        checkIfClosed() ;
        force() ;
    }

    private ByteBuffer getByteBuffer(long _id)
    {
        // Limitation: ids must be integers.
        // ids are used to index into []-arrays.
        int id = (int)_id ;
        
        int seg = segment(id) ;                 // Segment.
        int segOff = byteOffset(id) ;           // Byte offset in segment

        if ( getLog().isTraceEnabled() ) 
            getLog().trace(format("%d => [%d, %d]", id, seg, segOff)) ;

        synchronized (this) {
            try {
                // Need to put the alloc AND the slice/reset inside a sync.
                ByteBuffer segBuffer = allocSegment(seg) ;
                // Now slice the buffer to get the ByteBuffer to return
                segBuffer.position(segOff) ;
                segBuffer.limit(segOff+blockSize) ;
                ByteBuffer dst = segBuffer.slice() ;
                
                // And then reset limit to max for segment.
                segBuffer.limit(segBuffer.capacity()) ;
                // Extend block count when we allocate above end. 
                numFileBlocks = Math.max(numFileBlocks, id+1) ;
                return dst ;
            } catch (IllegalArgumentException ex) {
                // Shouldn't (ha!) happen because the second "limit" resets 
                log.error("Id: "+id) ;
                log.error("Seg="+seg) ;
                log.error("Segoff="+segOff) ;
                log.error(ex.getMessage(), ex) ;
                throw ex ;
            }
        }
    }
    
    private final int segment(int id)                   { return id/blocksPerSegment ; }
    private final int byteOffset(int id)                { return (id%blocksPerSegment)*blockSize ; }
    private final long fileLocation(long segmentNumber) { return segmentNumber*SegmentSize ; }
    
    // Even for MultipleReader this needs to be sync'ed.
    private MappedByteBuffer allocSegment(int seg)
    {
        // Auxiliary function for get - which holds the lock needed here.
        // The MappedByteBuffer must be sliced and reset once found/allocated
        // so as not to mess up the underlying MappedByteBuffer in segments[].
        
        // Only allocSegment(seg) and flushDirtySegements() and close()
        // directly access segments[] 

        if ( seg < 0 )
        {
            getLog().error("Segment negative: "+seg) ;
            throw new FileException("Negative segment: "+seg) ;
        }

        while ( seg >= segments.length )
        {
            // More space needed.
            MappedByteBuffer[] segments2 = new MappedByteBuffer[GrowthFactor*segments.length] ;
            System.arraycopy(segments, 0, segments2, 0, segments.length) ;
            boolean[] segmentDirty2 = new boolean[GrowthFactor*segmentDirty.length] ;
            System.arraycopy(segmentDirty, 0, segmentDirty2, 0, segmentDirty.length) ;

            segmentDirty = segmentDirty2 ;
            segments = segments2 ;
        }
        
        long offset = fileLocation(seg) ;
        
        if ( offset < 0 )
        {
            getLog().error("Segment offset gone negative: "+seg) ;
            throw new FileException("Negative segment offset: "+seg) ;
        }
        
        MappedByteBuffer segBuffer = segments[seg] ;
        if ( segBuffer == null )
        {
            try {
                segBuffer = file.channel().map(MapMode.READ_WRITE, offset, SegmentSize) ;
                if ( getLog().isDebugEnabled() )
                    getLog().debug(format("Segment: %d", seg)) ;
                segments[seg] = segBuffer ;
            } catch (IOException ex)
            {
                if ( ex.getCause() instanceof java.lang.OutOfMemoryError )
                    throw new FileException("BlockMgrMapped.segmentAllocate: Segment = "+seg+" : Offset = "+offset) ;
                throw new FileException("BlockMgrMapped.segmentAllocate: Segment = "+seg, ex) ;
            }
        }
        return segBuffer ;
    }

    private synchronized void flushDirtySegments()
    {
        // This does not force dirty segments to disk.
        super.force() ;
        
        // A linked list (with uniqueness) of dirty segments may be better.
        for ( int i = 0 ; i < segments.length ; i++ )
        {
            if ( segments[i] != null && segmentDirty[i] )
            {
                // Can we "flush" them all at once?
                segments[i].force() ;
                segmentDirty[i] = false ;
                segmentDirtyCount-- ;
            }
        }
    }

    @Override
    protected void _close()
    {
        force() ;
        // There is no unmap operation for MappedByteBuffers.
        // Sun Bug id bug_id=4724038
        // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4724038
       Arrays.fill(segments, null) ;
       Arrays.fill(segmentDirty, false) ;
       segmentDirtyCount = 0 ;
    }
    
    @Override
    protected void force()
    {
        flushDirtySegments() ;
        super.force() ;
    }
    
    @Override
    protected Logger getLog()
    {
        return log ;
    }
    
    @Override
    public String toString()
    {
        return super.getLabel() ;
    }
}
