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

import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.nio.ByteBuffer;
import org.apache.jena.atlas.lib.ByteBufferLib;

public final class Block
{
    // XXX Note development code added to enforce read-only use. Marked [RO]

    // 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;

    // These are information carrying flags -- not enforced
    // although .write checks the readOnly flags.
    private boolean readOnly = false;
    private boolean modified = false;

    private /*[[Dev-RO]] 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;
        // Default - writable, unmodified block.
        this.readOnly = false;
        this.modified = false;
        this.underlyingByteBuffer = null;
    }

    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");
        // Hard to force a read-only byte buffer because higher levels
        // may slice this and have their own byte buffers or even
        // intBuffers etc.
        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 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.
     */
    private Block replicate(ByteBuffer dstBuffer) {
        replicateByteBuffer(getByteBuffer(), dstBuffer);
        Block b = new Block(getId(), dstBuffer);
        b.setModified(modified);
        b.setReadOnly(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);
    }
}
