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

import static java.lang.String.format ;
import static org.apache.jena.atlas.lib.Alg.decodeIndex ;
import static org.apache.jena.tdb.index.bplustree.BPlusTreeParams.CheckingNode ;
import org.apache.jena.atlas.io.IndentedWriter ;
import org.apache.jena.tdb.base.StorageException ;
import org.apache.jena.tdb.base.block.Block ;
import org.apache.jena.tdb.base.buffer.RecordBuffer ;
import org.apache.jena.tdb.base.record.Record ;
import org.apache.jena.tdb.base.recordbuffer.RecordBufferPage ;
import org.slf4j.Logger ;
import org.slf4j.LoggerFactory ;

/** B+Tree wrapper over a block of records in a RecordBufferPage.
 * This class adds no persistent state to a RecordBufferPage */
public final class BPTreeRecords extends BPTreePage
{
    private static Logger log = LoggerFactory.getLogger(BPTreeRecords.class) ;
    private final RecordBufferPage  rBuffPage ;
    private RecordBuffer            rBuff ;         // Used heavily.
    
    BPTreeRecords(BPlusTree bpTree, RecordBufferPage rbp)
    {
        super(bpTree) ;
        rBuffPage = rbp ;
        rBuff = rBuffPage.getRecordBuffer() ;
    }
    
    RecordBufferPage getRecordBufferPage()
    { return rBuffPage ; }
    
    /*TEMP*/ public RecordBuffer getRecordBuffer()
    { return rBuff ; }

    @Override
    public final Block getBackingBlock()
    {
        return rBuffPage.getBackingBlock() ;
    }
    
    @Override
    public void reset(Block block)
    { 
        rBuffPage.reset(block) ;
        rBuff = rBuffPage.getRecordBuffer() ;
    }

    int getLink()
    { return rBuffPage.getLink() ; }

    @Override
    public boolean isFull()
    {
        return ( rBuff.size() >= rBuff.maxSize() ) ;
    }
    
    @Override
    public boolean hasAnyKeys()
    {
        return rBuff.size() > 0 ;
    }

    @Override
    public boolean isMinSize()
    {
        // 50% packing minimum.
        // If of max length 5 (i.e. odd), min size is 2.  Integer division works.  
        return ( rBuff.size() <= rBuff.maxSize()/2 ) ;
   }

    @Override
    public Record internalSearch(Record rec)
    {
        int i = rBuff.find(rec) ;
        if ( i < 0 )
            return null ;
        return rBuff.get(i) ;
    }

//    @Override
//    public BPTreeRecords findPage(Record record)
//    {
//        if ( rBuff.size() == 0 )
//            return this ;
//        
//        // Not true if above the last record.
//        if ( this.getLink() != RecordBufferPage.NO_ID && Record.keyGT(record, maxRecord()) ) 
//            error("Record [%s] not in this page: %s", record , this) ;
//        return this ;
//    }
//    
//    @Override
//    public BPTreeRecords findFirstPage() { return this ; }

    @Override final
    public void write()     { bpTree.getRecordsMgr().write(this) ; } 
    
    @Override final
    public void promote()   { bpTree.getRecordsMgr().promote(this) ; } 
    
    @Override final
    public void release()   { bpTree.getRecordsMgr().release(this) ; }
    
    @Override final
    public void free()      { bpTree.getRecordsMgr().free(this) ; }

    @Override
    public Record internalInsert(Record record)
    {
        promote() ;
        int i = rBuff.find(record) ;
        Record r2 = null ;
        if ( i < 0 )
        {
            i = decodeIndex(i) ;
            if ( rBuff.size() >= rBuff.maxSize())  
                throw new StorageException("RecordBlock.put overflow") ; 
            rBuff.add(i, record) ;
        }
        else
        {
            r2 = rBuff.get(i) ;
            if ( Record.compareByKeyValue(record, r2) != 0 )
                // Replace : return old
                rBuff.set(i, record) ;
        }
        write() ;
        return r2 ;
    }

    @Override
    public Record internalDelete(Record record)
    {
        promote() ;
        int i = rBuff.find(record) ;
        if ( i < 0 )
            return null ;
        Record r2 = rBuff.get(i) ;
        rBuff.remove(i) ;
        write() ;
        return r2 ;       
    }
    
    @Override final
    public Record getSplitKey()
    {
        int splitIdx = rBuff.size()/2-1 ;
        Record r = rBuff.get(splitIdx) ;
        return r ;
    }

    /** Split: place old high half in 'other'. Return the new (upper) BPTreeRecords(BPTreePage).
     * Split is the high end of the low page.
     */
    @Override final
    public BPTreePage split() 
    {
        // LinkIn
        // Create a new BPTreeRecords.
        
        BPTreeRecords other = create(rBuffPage.getLink()) ;
        rBuffPage.setLink(other.getId()) ;
        
        int splitIdx = rBuff.size()/2-1 ;
        Record r = rBuff.get(splitIdx) ;                // Only need key for checking later.
        
        int moveLen =  rBuff.size()-(splitIdx+1) ;      // Number to move.
        // Copy high end to new.  
        rBuff.copy(splitIdx+1, other.getRecordBufferPage().getRecordBuffer(), 0, moveLen) ;
        rBuff.clear(splitIdx+1, moveLen) ;
        rBuff.setSize(splitIdx+1) ;
        
        if ( CheckingNode )
        {
            if ( !Record.keyEQ(r, maxRecord()) )
                error("BPTreeRecords.split: Not returning expected record") ;
        }
        return other ;
    }

    private BPTreeRecords create(int linkId)
    {
        BPTreeRecords newPage = bpTree.getRecordsMgr().create() ;
        newPage.getRecordBufferPage().setLink(linkId) ;
        return newPage ;
    }

    @Override
    public Record shiftRight(BPTreePage other, Record splitKey)
    {
        // Error checking by RecordBuffer
        BPTreeRecords page = cast(other) ;
        rBuff.shiftRight(page.rBuff) ;
        if ( rBuff.size() == 0 )
            return null ;
        return rBuff.getHigh() ;
    }
    
    @Override
    public Record shiftLeft(BPTreePage other, Record splitKey)
    {
        // Error checking by RecordBuffer
        BPTreeRecords page = cast(other) ;
        rBuff.shiftLeft(page.rBuff) ;
        if ( rBuff.size() == 0 )
            return null ;
        return rBuff.getHigh() ;
    }

    @Override
    public BPTreePage merge(BPTreePage right, Record splitKey)
    {
        // Split key ignored - it's for the B+Tree case of pushing down a key
        // Records blocks have all the key/values in them anyway.
        return merge(this, cast(right)) ;
    }

    private static BPTreeRecords merge(BPTreeRecords left, BPTreeRecords right)
    {
        // Copy right to top of left.
        // The other way round needs a shift as well.
        right.rBuff.copyToTop(left.rBuff) ;
        // Same as: right.rBuff.copy(0, left.rBuff, left.rBuff.size(), right.rBuff.size()) ;
        right.rBuff.clear() ;
        
        //The right page is released by the caller.  left is still in use.
        // So the test code can poke around in the right block after merge. 
        //left.bpTree.getRecordsMgr().release(left.getId()) ;
        
        // Fix up the link chain.
        left.rBuffPage.setLink(right.rBuffPage.getLink()) ;
        return left ;
    }
    
    private static BPTreeRecords cast(BPTreePage page)
    {
        try { return (BPTreeRecords)page  ; }
        catch (ClassCastException ex) { error("Wrong type: "+page) ; return null ; }
    }
    
    @Override final
    public Record minRecord()
    {
        return getLowRecord() ;
    }

    @Override final
    public Record maxRecord()
    {
        return getHighRecord() ;
    }

    private static void error(String msg, Object... args)
    {
        msg = format(msg, args) ;
        System.out.println(msg) ;
        System.out.flush();
        throw new BPTreeException(msg) ;
    }

    @Override
    public final Record getLowRecord()
    {
        if ( rBuff.size() == 0 )
            return null ;
        return rBuff.getLow() ;
    }

    @Override
    public final Record getHighRecord()
    {
        if ( rBuff.size() == 0 )
            return null ;
        return rBuff.getHigh() ;
    }

    @Override
    public final int getMaxSize()             { return rBuff.maxSize() ; }
    
    @Override
    public final int getCount()             { return rBuff.size() ; }
 
    @Override
    public final void setCount(int count)   { rBuff.setSize(count) ; }
    
    @Override
    public String toString()
    { return String.format("BPTreeRecords[id=%d, link=%d]: %s", getId(), getLink(), rBuff.toString()); }
    
    @Override
    public final void checkNode()
    {
        if ( ! CheckingNode ) return ;
        if ( rBuff.size() < 0 || rBuff.size() > rBuff.maxSize() )
            error("Misized: %s", this) ;

        for ( int i = 1 ; i < getCount() ; i++ )
        {
            Record r1 = rBuff.get(i-1) ;
            Record r2 = rBuff.get(i) ;
            if ( Record.keyGT(r1, r2) )
                error("Not sorted: %s", this) ;
        }
    }
    
    @Override
    public final void checkNodeDeep()
    { checkNode() ; }

//    @Override
//    public ByteBuffer getBackingByteBuffer()   { return rBuffPage.getBackingByteBuffer() ; }

    @Override
    public int getId()                  { return rBuffPage.getId() ; } 

//    @Override
//    public void setId(int id)           { rBuffPage.setId(id) ; }

    @Override
    public void output(IndentedWriter out)
    {
        out.print(toString()) ;
    }
}
