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

import java.io.ByteArrayOutputStream ;
import java.io.PrintStream ;
import java.nio.ByteBuffer ;

import org.apache.jena.atlas.lib.ByteBufferLib ;

// import tx.base.BlockRef ;

import org.apache.jena.tdb.base.page.Page ;

public final class Block
{
    // While the general mechanisms support long block id,
    // some uses make restrictions.
    // BlockMgrs:
    //   Blocks for indexes (B+Trees) are addressed by int - one int is 2G of 8K units = 16T
    // Blocks for objects are addressed by long - this is file offset so 2G is not enough. 
    
    private final Long id ;          // Keep as object.  It's the cache key.
    //private BlockRef blockRef ;
    

    // Information carrying these are not enforced. 
    private boolean readOnly = false ;
    private boolean modified = false ;
    
    private final ByteBuffer byteBuffer ;
    // If the byteBuffer is, say, a slice of another one,
    // this can be used to carry a ref to the real ByteBuffer.  
    private ByteBuffer underlyingByteBuffer ;

    public Block(long id, ByteBuffer byteBuffer)
    {
        this(Long.valueOf(id), byteBuffer) ; 
    }
    
    public Block(Long id, ByteBuffer byteBuffer)
    {
        // ByteBuffer is whole disk space from byte 0 for this disk unit. 
        this.id = id ; 
        this.byteBuffer = byteBuffer ;
        //this.blockRef = null ;
        
        this.readOnly = false ;
        this.modified = false ;
        this.underlyingByteBuffer = null ;
    }
    
    
    public <T extends Page> T convert(BlockConverter<T> converter)
    {
        // converter.checkType(type) ; 
        return converter.fromBlock(this) ;
    }
        
    
    public final Long getId()   { return id ; }
    
    public final ByteBuffer getByteBuffer()
    {
        return byteBuffer ;
    }
 
    public boolean isReadOnly()     { return readOnly ; }
    public void setReadOnly(boolean readonly)
    {
        if ( readonly && modified )
            throw new BlockException("Attempt to mark a modified block as read-only") ;
        this.readOnly = readonly ;
    }
    
    public boolean isModified()     { return modified ; }

    public void setModified(boolean modified)
    {
        if ( readOnly && modified )
            throw new BlockException("Attempt to mark a readonly block as modified") ;
        this.modified = modified ;
    }

//    public BlockRef getBlockRef()   { return blockRef ; }

    public ByteBuffer getUnderlyingByteBuffer()
    { return underlyingByteBuffer ; }


    public void setUnderlyingByteBuffer(ByteBuffer underlyingByteBuffer)
    { this.underlyingByteBuffer = underlyingByteBuffer ; }
    
    @Override
    public String toString()
    {
        ByteBuffer bb = getByteBuffer() ;
        if ( true )
            // Short form.
            return String.format("Block: %d (posn=%d, limit=%d, cap=%d)", id, bb.position(), bb.limit(), bb.capacity()) ;
        // Long form - with some bytes from the ByteBuffer.
        ByteArrayOutputStream out = new ByteArrayOutputStream() ;
        PrintStream x = new PrintStream(out) ;
        ByteBufferLib.print(x, bb) ;
        x.flush() ;
        String str = out.toString() ;
        return String.format("Block: %d %s", id, str) ;
    }
    
    /** Deep copy, including ByteBuffer contents into a HeapByteBuffer. */
    public Block replicate()
    {
        ByteBuffer dstBuffer = ByteBuffer.allocate(getByteBuffer().capacity());
        return replicate(dstBuffer);
    }
    
    /**
     * Deep copy, including ByteBuffer contents, using the supplied ByteBuffer to hold the contents and
     * to be used when constructing the new Block.  The capacity of the supplied ByteBuffer must be equal
     * to or greater than this block's capacity.
     */
    public Block replicate(ByteBuffer dstBuffer)
    {
        replicateByteBuffer(getByteBuffer(), dstBuffer) ;
        Block b = new Block(getId(), dstBuffer) ;
        b.modified = modified ;
        b.readOnly = readOnly ;
//        b.blockRef = null ;
        return b ;
    }

    public static void replicate(Block srcBlock, Block dstBlock)
    {
        if ( ! srcBlock.getId().equals(dstBlock.getId()) )
            throw new BlockException("FileAccessMem: Attempt to copy across blocks: "+srcBlock.getId()+" => "+dstBlock.getId()) ;
        replicate(srcBlock.getByteBuffer(), dstBlock.getByteBuffer()) ;
    }  

    private static ByteBuffer replicateByteBuffer(ByteBuffer srcBlk, ByteBuffer dstBlk)
    {
        int x = srcBlk.position() ;
        int y = srcBlk.limit() ;
        srcBlk.clear() ;
        
        if ( srcBlk.hasArray() && dstBlk.hasArray() )
            System.arraycopy(srcBlk.array(), 0, dstBlk.array(), 0, srcBlk.capacity()) ;
        else
            dstBlk.put(srcBlk) ;
        
        srcBlk.position(x);
        dstBlk.position(x);
        srcBlk.limit(y);
        dstBlk.limit(y);
        return dstBlk ; 
    }  

    private static void replicate(ByteBuffer srcBlk, ByteBuffer dstBlk)
    {
        srcBlk.position(0) ;
        dstBlk.position(0) ;
        dstBlk.put(srcBlk) ;
    }  
}
