/*
 * 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 org.apache.jena.tdb.sys.SystemTDB.SizeOfPointer ;
import org.apache.jena.atlas.logging.Log ;
import org.apache.jena.tdb.TDBException ;
import org.apache.jena.tdb.base.file.MetaFile ;
import org.apache.jena.tdb.base.record.RecordFactory ;
import org.apache.jena.tdb.sys.Names ;
import org.apache.jena.tdb.sys.SystemTDB ;
import org.slf4j.Logger;

/** Configuration for a B+Tree */ 
final
public class BPlusTreeParams
{
    // Global settings
    public static boolean CheckingTree = SystemTDB.Checking ;   // Check on exit of B+Tree modifiying operations
    public static boolean CheckingNode = false ;                // Check within BPTreeNode
    public static boolean CheckingConcurrency = SystemTDB.Checking ;   // Check on exit of B+Tree modifiying operations

    // Metadata
    //public static final String NS = BPlusTreeParams.class.getName() ;
    public static final String NS = Names.keyNSBPlusTree ;
    public static final String ParamOrder          = NS+".order" ;
    public static final String ParamKeyLength      = NS+".keyLength" ;
    public static final String ParamValueLength    = NS+".valueLength" ;
    public static final String ParamBlockSize      = NS+".blockSize" ;

    public static void checkAll()
    { 
        CheckingTree = true ;
        CheckingNode = true ;
    }
    
    public static boolean DumpTree = false ;                 // Dump the tree during top level logging 
    public static boolean Logging = false ;                  // Turn on/off logging the hard way
    
    public static void infoAll()
    { 
        DumpTree = true ;
        Logging = true ;
    }

    /* The gap is extra space in a node - some books have node size as 2*N 
     * (often for the classic insertion algorithm where it's easier to implement
     * by inserting then splitting).
     */ 
    private static final int Gap            = 0 ;  
    
    public static final int RootId          = 0 ;
    public static final int RootParent      = -2 ;
    public static final int NoParent        = -99 ;

    // Per instance settings
    
    /** Order of the BTree */
    final int order ;
    
    /** Record factory */
    final RecordFactory recordFactory ;

    /** Factory for key-only records */ 
    final RecordFactory keyFactory ;
    
    // ---- Derived constants.

    /** Maximum number of keys per non-leaf block */
    final int MaxRec ; 

    /** Maximum number of pointers per block per non-leaf block */
    final int MaxPtr  ;

    /** Minimum number of keys per non-leaf block */
    final int MinRec  ;

    /** Minimum number of pointers per block */
    final int MinPtr  ;
    
    /** Index of the split point */
    final int SplitIndex ;

    /** High index of keys array */
    final int HighRec ;
    
    /** High index of pointers array */
    final int HighPtr  ;
 
    /** Space in a block needed for extra information - the count
     * The parent is not stored on-disk because a block is always created by fetching from it's parent. 
     */
    static int BlockHeaderSize = 4 ;
    
    static final boolean logging(Logger log)
    {
        return Logging && log.isDebugEnabled() ;
    }
    
    @Override
    public String toString()
    {
        return String.format("Order=%d : Records [key=%d, value=%d] : records=[%d,%d] : pointers=[%d,%d] : split=%d",
                             order,
                             keyFactory.keyLength() ,
                             recordFactory.valueLength() ,
                             MinRec, MaxRec, 
                             MinPtr, MaxPtr,
                             SplitIndex
                             ) ;
    }

    public static BPlusTreeParams readMeta(MetaFile mf)
    {
        try {
            int pOrder = mf.getPropertyAsInteger(ParamOrder) ;
            int pKeyLen = mf.getPropertyAsInteger(ParamKeyLength) ;
            int pRecLen = mf.getPropertyAsInteger(ParamValueLength) ;
            return new BPlusTreeParams(pOrder, pKeyLen, pRecLen) ;
        } catch (NumberFormatException ex)
        {
            Log.error(BPlusTreeParams.class, "Badly formed metadata for B+Tree") ;
            throw new TDBException("Failed to read metadata") ;
        }
    }
    
    public void addToMetaData(MetaFile mf)
    {
        mf.setProperty(ParamOrder, order) ;
        mf.setProperty(ParamKeyLength, recordFactory.keyLength()) ;
        mf.setProperty(ParamValueLength, recordFactory.valueLength()) ;
        mf.flush() ;
    }

    public BPlusTreeParams(int order, int keyLen, int valLen)
    { 
        this(order, new RecordFactory(keyLen, valLen)) ;
    }
    
    public BPlusTreeParams(int order, RecordFactory factory)
    {
        // BTrees of order one aren't strictly BTrees, where the order is >= 2
        // Order 1 => Min size = 0 and max size = 2*N-1 = 1.
        // If there is a gap, then the code may be defensive enough
        // and something will work.  The B+Trees may have empty nodes
        // (i.e. no keys, single child).

        if ( order < 2 )
            throw new IllegalArgumentException("BPTree: illegal order (min 2): "+order);

        this.order = order ;
        recordFactory = factory ;
        keyFactory = factory.keyFactory() ;

        // Derived constants.
        MaxRec  = 2*order-1 + Gap ;
        MaxPtr  = 2*order + Gap ; 
        MinRec = order-1 ;
        MinPtr  = order ;
        
        SplitIndex = order-1+Gap;
        
        HighPtr  = MaxPtr - 1 ;
        HighRec  = HighPtr-1 ;
    }

    public int getOrder()
    {
        return order ;
    }

    public int getPtrLength()
    {
        return SizeOfPointer ;
    }

    public int getRecordLength()
    {
        return recordFactory.recordLength() ;
    }

    public RecordFactory getRecordFactory()
    {
        return recordFactory ;
    }
    
    public int getKeyLength()
    {
        return keyFactory.recordLength() ;
    }

    public RecordFactory getKeyFactory()
    {
        return keyFactory ;
    }
    
    
    public int getCalcBlockSize()
    {
        return calcBlockSize(order, recordFactory) ;
    }
    
    /** Return the best fit for the blocksize and the record length.
     * Knows about block header space. 
     */ 
    public static int calcOrder(int blockSize, RecordFactory factory) 
    {
        return calcOrder(blockSize, factory.recordLength()) ; 
    }
    
    /** Return the best fit for the blocksize and the record length.
     * Knows about block header space. 
     */ 
    public static int calcOrder(int blockSize, int recordLength) 
    {
        // Length = X*recordLength+(X+1)*PtrLength
        // => X = (Length-PtrLength)/(recordLength+PtrLength)
        // BTree order N
        // MaxRec = 2N-1+Gap = X
        // N = (X+1-Gap)/2
        blockSize -= BlockHeaderSize ;
        
        int X = (blockSize-recordLength)/(recordLength+SizeOfPointer) ;
        int order = (X+1-Gap)/2 ;
        return order ;
    }

    /** return the size of a block */
    public static int calcBlockSize(int bpTreeOrder, RecordFactory factory) 
    {
        BPlusTreeParams p = new BPlusTreeParams(bpTreeOrder, factory) ;
        int x = p.getMaxRec()*factory.recordLength() + p.getMaxPtr()*SizeOfPointer ;
        x += BlockHeaderSize ;
        return x ;
    }


    public int getMaxRec()
    {
        return MaxRec ;
    }


    public int getMaxPtr()
    {
        return MaxPtr ;
    }


    public int getMinRec()
    {
        return MinRec;
    }

    public int getMinPtr()
    {
        return MinPtr ;
    }

//    /** return the size of a block */
//    public static int calcBlockSize(int bTreeOrder, int recordLength)
//    {
//        BTreeParams p = new BTreeParams(bTreeOrder, recordLength, 0) ;
//        int x = p.getMaxRec()*recordLength + p.getMaxPtr()*PtrLength ;
//        x += BlockHeaderSize ;
//        return x ;
//    }
}
