/* -*- tab-width: 4 -*-
 *
 * Electric(tm) VLSI Design System
 *
 * File: BTree.java
 *
 * Copyright (c) 2009, Oracle and/or its affiliates. All rights reserved.
 *
 * Electric(tm) is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * Electric(tm) is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.sun.electric.database.geometry.btree;

import java.io.*;
import java.util.*;
import com.sun.electric.database.geometry.btree.unboxed.*;
import com.sun.electric.database.geometry.btree.CachingPageStorage.CachedPage;

/**
 *  A <a href=http://www.youtube.com/watch?v=coRJrcIYbF4>B+Tree</a>
 *  implemented using {@see PageStorage}.<p>
 *
 *  This is a B-Plus-Tree; values are stored only in leaf nodes.<p>
 *
 *  <h3>Usage Notes</h3>
 *
 *  Each element in a BTree is conceptually a triple
 *  &lt;ordinal,key,value&gt; where "key" is a user-supplied key
 *  (belonging to a type that is {@see Comparable}), "value" is a
 *  user-supplied value (no restrictions) and "ordinal" is an integer
 *  indicating the number of keys in the tree less than this one.
 *  Note that the ordinal is not actually stored in the tree, and
 *  inserting a new value can potentially modify the ordinals of all
 *  preexisting elements!  Each of the getXXX() methods takes one of
 *  these three coordinates (<tt>Ord</tt>, <tt>Key</tt>, or
 *  <tt>Val</tt>) and returns one of the others, or else a count
 *  (<tt>Num</tt>).  Additionally, the getXXXFromKey() methods include
 *  floor/ceiling versions that take an upper/lower bound and search
 *  for the largest/smallest key which is less/greater than the one
 *  supplied.<p>
 *
 *  The BTree supports appending a new element (that is, inserting a value
 *  with a key greater than any key in the table) in near-constant time
 *  (Actually log<sup>*</sup>(n)) and all other queries in log(n) time.
 *  All operations are done in a <i>single pass</i> down the BTree from
 *  the root to the leaves; this brings two benefits: the data structure
 *  can be made concurrent with very little lock contention and it can
 *  support copy-on-write shadow versions.<p>
 *  
 *  You must distinguish between insert() and replace() ahead of time;
 *  you can't call insert() on a key that is already in the tree or
 *  replace() on one that isn't.  In order to replace() on a BTree
 *  with a summary the summary product operation must be commutative
 *  and invertible, and you must know the old value which you are
 *  replacing.  This lets us update the interior node invariants as we
 *  walk down the tree and avoid having to walk back up afterwards.
 *  If you're not sure if a key is in the tree, just do a get() -- the
 *  net result is two passes over the tree, which is what we'd have to
 *  do anyways if the user didn't distinguish insert() from replace().
 *  We're just offering the option to double the performance in the
 *  case where the user already knows if the key is in the tree or
 *  not.<p>
 *
 *  You can associate a <i>summary</i> with each leaf node of the BTree.
 *  In order to do this, you must provide an instance of {@see
 *  com.sun.electric.database.geometry.btree.unboxed.AssociativeOperation}
 *  to the BTree when you construct it.  The AssociativeOperation knows
 *  the key and value type of the BTree, and must know two things:
 *  
 *  <ul>
 *    <li> How to calculate the summary of a single (key,value) pair.
 *    <li> How to merge two summaries.
 *  </ul>
 *  
 *  The process of merging two summaries must be associative; if we want
 *  to merge three summaries (ABC) it must not matter if we merge them as
 *  ((AB)C) or (A(BC)).  Technically this makes the merge operation
 *  a <i><a href=http://en.wikipedia.org/wiki/Semigroup>semigroup</a></i>.
 *  In exchange for providing all of this information
 *  you can ask the BTree to calculate the summary of any contiguous
 *  region of the keyspace in log(n) time.  For example, this can be used
 *  to answer "min/max over this range" queries very efficiently.<p>
 *
 *  <h3>Implementation Notes</h3>
 *
 *  We proactively split nodes as soon as they become full rather than
 *  waiting for them to become overfull.  This has a space overhead of
 *  1/NUM_KEYS_PER_PAGE, but puts an O(1) bound on the number of pages
 *  written per operation (number of pages read is still O(log n)).
 *  It also makes the walk routine tail-recursive.<p>
 *
 *  Each node of the BTree uses one page of the PageStorage; we don't
 *  yet support situations where a single page is too small for one
 *  key and one value.<p>
 *
 *  The coding style in this file is pretty unusual; it looks a lot
 *  like "Java as a better C".  This is mainly because Hotspot is
 *  remarkably good at inlining methods, but remarkably bad (still,
 *  even in Java 1.7) at figuring out when it's safe to unbox values.
 *  So I expend a lot of effort trying not to create boxed values, but
 *  don't worry at all about the overhead of method calls,
 *  particularly when made via "final" references (nearly always
 *  inlined).  I don't code this way very often -- I reserve this for
 *  the 2% of my code that bears 98% of the performance burden.  There
 *  is anecdotal evidence that using the server JVM rather than the
 *  client JVM yields a dramatic increase in performance; this coding
 *  style is especially friendly to the server JVM's optimization
 *  techniques.<p>
 *
 *  Keys, values, and summary elements are stored in <i>unboxed</i>
 *  form.  This means that each of these types must know how to
 *  serialize itself to and deserialize itself from a sequence of
 *  bytes, and <i>it must be possible to perform the important
 *  operations (comparison for keys, product for summary values)
 *  on these values in unboxed form.</i> The reasons for this are set
 *  out in the previous paragraph.  See the package btree.unboxed for
 *  further details.
 *
 *  @author Adam Megacz <adam.megacz@sun.com>
 */
public class BTree
    <K extends Serializable & Comparable,
     V extends Serializable,
     S extends Serializable> {

    public static interface Summary
        <K extends Serializable & Comparable,
        V extends Serializable,
        S extends Serializable> extends
        AssociativeOperation<S>,
        UnboxedFunction<Pair<K,V>,S> { };

    final CachingPageStorage   ps;
    final UnboxedComparable<K> uk;
    final Summary<K,V,S>       summary;
    final Unboxed<V>           uv;
    final UnboxedInt           ui = UnboxedInt.instance;

    private LeafNodeCursor<K,V,S>       leafNodeCursor;
    private InteriorNodeCursor<K,V,S>   interiorNodeCursor1;
    private InteriorNodeCursor<K,V,S>   interiorNodeCursor2;

    int                  rootpage;
    private final byte[] keybuf;
    private final byte[] keybuf2;
    private final byte[] sbuf;

    private final byte[] largestKey;
    private       int    largestKeyPage = -1;  // or -1 if unknown

    private       int    size = 0;
    private final byte[] monbuf;  // scratch buffer

    /**
     *  Create a BTree.
     *  @param ps the PageStorage to hold the underlying bytes
     *  @param uk the unboxed type for keys (must be comparable)
     *  @param uv the unboxed type for values
     *  @param summary the function which summarizes a single (key,value) pair
     */
    public BTree(CachingPageStorage ps,
                 UnboxedComparable<K> uk,
                 Unboxed<V> uv,
                 Summary<K,V,S> summary) {
        if (summary!=null && !(summary instanceof AssociativeCommutativeOperation))
                throw new RuntimeException("Only commutative summary operations are supported (allows one-pass insertion)");
        this.summary = summary;
        this.ps = ps;
        this.uk = uk;
        this.uv = uv;
        this.leafNodeCursor = new LeafNodeCursor<K,V,S>(this);
        this.interiorNodeCursor1 = new InteriorNodeCursor<K,V,S>(this);
        this.interiorNodeCursor2 = new InteriorNodeCursor<K,V,S>(this);
        this.rootpage = ps.createPage();
        this.keybuf = new byte[uk.getSize()];
        this.keybuf2 = new byte[uk.getSize()];
        this.sbuf = summary==null ? null : new byte[summary.getSize()];
        this.largestKey = new byte[uk.getSize()];
        leafNodeCursor.initBuf(ps.getPage(rootpage, false), rootpage, true);
        leafNodeCursor.writeBack();
        this.monbuf = this.summary == null ? null : new byte[this.summary.getSize()];
    }

    /**
     *  Returns the number of entries in the tree with a key between
     *  min and max inclusive; if either min or max is null it is treated
     *  as negative or positive infinity (respectively)
     */
    public int getNumFromKeys(K min, K max) {
        if (min==null && max==null) return size;
        throw new RuntimeException("not implemented");
    }

    /** same as getNumFromKeys(null,null) */
    public int  size() { return getNumFromKeys(null,null); }

    /** returns the value in the tree, or null if not found */
    public V getValFromKey(K key) {
        uk.serialize(key, keybuf, 0);
        return (V)walk(keybuf, 0, null, null, Op.GET_VAL_FROM_KEY, 0);
    }

    /** returns the value of the largest key less than or equal to the one supplied */
    public V getValFromKeyFloor(K key) {
        uk.serialize(key, keybuf, 0);
        return (V)walk(keybuf, 0, null, null, Op.GET_VAL_FROM_KEY_FLOOR, 0);
    }

    /** returns the value of the smallest key greater than or equal to the one supplied */
    public V getValFromKeyCeiling(K key) {
        uk.serialize(key, keybuf, 0);
        return (V)walk(keybuf, 0, null, null, Op.GET_VAL_FROM_KEY_CEIL, 0);
    }

    /** returns the ordinal of the given key, or -1 if not found */
    public int getOrdFromKey(K key) {
        uk.serialize(key, keybuf, 0);
        return ((Integer)walk(keybuf, 0, null, null, Op.GET_ORD_FROM_KEY, 0)).intValue();
    }

    /** returns the ordinal of the largest key less than or equal to the one supplied */
    public int getOrdFromKeyFloor(K key) {
        uk.serialize(key, keybuf, 0);
        return ((Integer)walk(keybuf, 0, null, null, Op.GET_ORD_FROM_KEY_FLOOR, 0)).intValue();
    }

    /** returns the ordinal of the smallest key greater than or equal to the one supplied */
    public int getOrdFromKeyCeiling(K key) {
        uk.serialize(key, keybuf, 0);
        return ((Integer)walk(keybuf, 0, null, null, Op.GET_ORD_FROM_KEY_CEIL, 0)).intValue();
    }

    /** returns the least key <i>strictly</i> greater than the argument */
    public V getKeyFromKeyNext(K key) {
        throw new RuntimeException("not implemented");
    }

    /** returns the greatest key <i>strictly</i> less than the argument */
    public V getKeyFromKeyPrev(K key) {
        throw new RuntimeException("not implemented");
    }

    /** returns the i^th value in the tree */
    public V getValFromOrd(int ord) {
        return (V)walk(null, 0, null, null, Op.GET_VAL_FROM_ORD, ord);
    }

    /** returns the i^th key in the tree */
    public K getKeyFromOrd(int ord) {
        return (K)walk(null, 0, null, null, Op.GET_KEY_FROM_ORD, ord);
    }

    /** will throw an exception if the key is already in the tree */
    public void insert(K key, V val) {
        uk.serialize(key, keybuf, 0);
        walk(keybuf, 0, null, val, Op.INSERT, 0);
        size++;
    }

    /** 
     *  Returns value previously in the tree; to support one-pass
     *  replacement you must know the value which was previously in the
     *  tree; if that turns out to be incorrect an exception will be
     *  thrown
     */
    public V replace(K key, V newval) {
        uk.serialize(key, keybuf, 0);
        return (V)walk(keybuf, 0, null, newval, Op.REPLACE, 0);
    }

    /** removes key from the tree; throws an exception if key not found; may not be used if there is a summary */
    public V remove(K key) { return remove(key, null); }

    /** removes (key,val) from the tree; throws an exception if key not found or get(key)!=val */
    public V remove(K key, V val) {
        if (val==null && summary!=null)
            throw new RuntimeException("remove(key) and remove(key,null) may not be used on BTrees with summaries");
        if (summary!=null && !(summary instanceof InvertibleOperation))
            throw new RuntimeException("BTrees with non-InvertibleOperation summaries are insert-only");
        uk.serialize(key, keybuf, 0);
        V ret = (V)walk(keybuf, 0, val, null, Op.REMOVE, 0);
        if (ret!=null) size--;
        return ret;
    }

    /** removes (key,val) from the tree; throws an exception if key not found or get(key)!=val */
    public V removeRange(K key1, K key2) {
        if (summary!=null && !(summary instanceof InvertibleOperation))
            throw new RuntimeException("BTrees with non-InvertibleOperation summaries are insert-only");
        throw new RuntimeException("not implemented");
    }

    /** remove all entries */
    public void clear() {
        size = 0;
        largestKeyPage = -1;
        // free all pages except rootpage
        // make rootpage a leaf page, set its size to zero
        //leafNodeCursor.initBuf(ps.getPage(rootpage, false), true);
        //leafNodeCursor.writeBack();
        throw new RuntimeException("not implemented");
    }

    /** compute the summary of all (key,value) pairs between min and max, inclusive; null means "no limit" */
    public S getSummaryFromKeys(K min, K max) {
        if (min!=null) uk.serialize(min, keybuf, 0);
        if (max!=null) uk.serialize(max, keybuf2, 0);
        int ret = 0;
        ret = (Integer)walk(min==null?null:keybuf, 0, null, null, Op.SUMMARIZE_LEFT,  ret, max==null?null:keybuf2, 0, sbuf, 0);
        ret = (Integer)walk(min==null?null:keybuf, 0, null, null, Op.SUMMARIZE_RIGHT, ret, max==null?null:keybuf2, 0, sbuf, 0);
        return ret==0 ? null : (S)summary.deserialize(sbuf, 0);
    }
    
    private static enum Op {
        GET_VAL_FROM_KEY,
        GET_VAL_FROM_KEY_FLOOR,
        GET_VAL_FROM_KEY_CEIL,
        GET_ORD_FROM_KEY,
        GET_ORD_FROM_KEY_FLOOR,
        GET_ORD_FROM_KEY_CEIL,
        GET_VAL_FROM_ORD,
        GET_KEY_FROM_ORD,
        GET_NEXT,
        GET_PREV,
        REMOVE,
        INSERT,
        REPLACE,
        SUMMARIZE_LEFT,
        SUMMARIZE_RIGHT,
            ;
        public boolean isGetFromOrd() {
            switch(this) {
                case GET_VAL_FROM_ORD:
                case GET_KEY_FROM_ORD:
                    return true;
                default:
                    return false;
            }
        }
        public boolean isGetOrd() {
            switch(this) {
                case GET_ORD_FROM_KEY:
                case GET_ORD_FROM_KEY_FLOOR:
                case GET_ORD_FROM_KEY_CEIL:
                    return true;
                default:
                    return false;
            }
        }
        public boolean isGetFromKey() {
            switch(this) {
                case GET_VAL_FROM_KEY:
                case GET_VAL_FROM_KEY_FLOOR:
                case GET_VAL_FROM_KEY_CEIL:
                case GET_ORD_FROM_KEY:
                case GET_ORD_FROM_KEY_FLOOR:
                case GET_ORD_FROM_KEY_CEIL:
                    return true;
                default:
                    return false;
            }
        }
        public boolean isGetFromKeyFloor() {
            switch(this) {
                case GET_VAL_FROM_KEY_FLOOR:
                case GET_ORD_FROM_KEY_FLOOR:
                    return true;
                default:
                    return false;
            }
        }
        public boolean isGetFromKeyCeil() {
            switch(this) {
                case GET_VAL_FROM_KEY_CEIL:
                case GET_ORD_FROM_KEY_CEIL:
                    return true;
                default:
                    return false;
            }
        }
    }
    

    private Object walk(byte[] key, int key_ofs, V oldval, V newval, Op op, int ord) {
        return walk(key, key_ofs, oldval, newval, op, ord, null, 0, null, 0);
    }

    /**
     *  B+Tree walking routine.
     *
     *  This is the hairiest part, so I arranged things to share a single
     *  codepath across all four operations (insert/replace/delete/find).
     *
     *  The routine is implemented using a loop rather than recursive
     *  calls because the JVM does not support tail recursion (and
     *  probably never will, because its lame security model is based
     *  on stack inspection).
     *
     *  On writes/deletes, this returns the previous value.
     *
     */
    private Object walk(byte[] key, int key_ofs,
                        V oldval,
                        V newval,
                        Op op,
                        int ord,
                        byte[] key2, int key2_ofs,
                        byte[] ret, int ret_ofs) {
        Object return_val = null;
        int pageid = rootpage;
        int idx = -1;
        int global_ord = 0;

        LeafNodeCursor<K,V,S>       leafNodeCursor = this.leafNodeCursor;
        InteriorNodeCursor<K,V,S>   interiorNodeCursor = this.interiorNodeCursor1;
        InteriorNodeCursor<K,V,S>   parentNodeCursor = this.interiorNodeCursor2;
        NodeCursor cur = null;

        boolean summaryInitialized = ord!=0;
        boolean rightEdge = true;
        boolean cheat = false;
        int comp = 0;

        if (largestKeyPage != -1 && op==Op.INSERT) {
            leafNodeCursor.setBuf(ps.getPage(largestKeyPage, true));
            comp = uk.compare(key, key_ofs, largestKey, 0);
            if (comp >= 0 && !leafNodeCursor.isFull()) {
                pageid = largestKeyPage;
                parentNodeCursor.forgetCachedPage();
                cheat = true;
                cur = leafNodeCursor;
            }
        }

        OUT: while(true) {
            if (cur==null || cur.getCachedPage()==null || cur.getPageId() != pageid) {
                CachedPage cp = ps.getPage(pageid, true);
                cur = LeafNodeCursor.isLeafNode(cp) ? leafNodeCursor : interiorNodeCursor;
                cur.setBuf(cp);
            }
            assert cheat || pageid==rootpage || cur.getParent()==parentNodeCursor.getPageId();

            if ((op==Op.INSERT || op==Op.REPLACE) && cur.isFull()) {
                assert cur!=parentNodeCursor;
                int old;

                // is the node we're splitting the last child of its parent or the root node?
                boolean splitting_last_or_root = false;
                if (pageid == rootpage) {
                    parentNodeCursor.initRoot();
                    parentNodeCursor.setBucketPageId(0, pageid);
                    cur.setParent(parentNodeCursor.getPageId());
                    idx = 0;
                    old = size;
                    splitting_last_or_root = true;
                } else {
                    assert !parentNodeCursor.isFull();
                    splitting_last_or_root = idx>=parentNodeCursor.getNumBuckets()-1;
                    old = splitting_last_or_root ? -1 : parentNodeCursor.getNumValsBelowBucket(idx);
                }
                if (op==Op.INSERT && old!=-1) old -= 1;
                int ofs = parentNodeCursor.insertNewBucketAt(idx+1);
                int oldpage = cur.getPageId();

                // optimization: if we're splitting a node on the
                // "right edge" of the tree, make the split uneven --
                // put everything on the left side.

                int splitPoint = rightEdge ? cur.getNumBuckets()-1 : cur.getMaxBuckets()/2;
                if (rightEdge) splitUnEven++; else splitEven++;

                if (summary!=null) {
                    cur.getSummary(0, monbuf, 0);
                    parentNodeCursor.setSummary(idx, monbuf, 0);
                    for(int i=1; i<splitPoint; i++) {
                        cur.getSummary(i, monbuf, 0);
                        parentNodeCursor.mergeSummaryCommutative(idx, monbuf, 0);
                    }
                }
                int num = cur.split(parentNodeCursor.getBuf(), ofs, splitPoint);
                parentNodeCursor.setNumValsBelowBucket(idx, num);
                int newpage = cur.getPageId();
                if (largestKeyPage==oldpage) largestKeyPage = newpage;
                parentNodeCursor.setBucketPageId(idx+1, newpage);
                cur.setParent(parentNodeCursor.getPageId());
                if (!splitting_last_or_root)
                    parentNodeCursor.setNumValsBelowBucket(idx+1, old-num);
                if (summary!=null && (!parentNodeCursor.isRightMost() || idx+1<parentNodeCursor.getNumBuckets()-1)) {
                    cur.getSummary(0, monbuf, 0);
                    parentNodeCursor.setSummary(idx+1, monbuf, 0);
                    for(int i=1; i<cur.getNumBuckets() - (cur.isRightMost() ? 1 : 0); i++) {
                        cur.getSummary(i, monbuf, 0);
                        parentNodeCursor.mergeSummaryCommutative(idx+1, monbuf, 0);
                    }
                }

                cur.writeBack();
                parentNodeCursor.writeBack();
                pageid = rootpage;
                cheat = false;
                continue;
            }

            if (op==Op.SUMMARIZE_LEFT || op==Op.SUMMARIZE_RIGHT) {
                int start = key==null ? 0 : cur.search(key, key_ofs);
                start--;
                start = Math.max(0,start);
                int next = op==Op.SUMMARIZE_RIGHT ? -1 : start;
                if (!cur.isLeafNode()) start = Math.max(1,start);
                for(int i=start; i<cur.getNumBuckets(); i++) {
                    int cmpLeft  = key==null  ? -1 : cur.compare(key, key_ofs, i);
                    int cmpRight = key2==null ? 1 : cur.isLeafNode()
                        ? (cur.compare(key2, key2_ofs, i+1) < 0 ? -1 : 1)
                        : (i==cur.getNumBuckets()-1 ? -1 : cur.compare(key2, key2_ofs, i+1));
                    if (cmpLeft <= 0 && cmpRight > 0 && (cur.isLeafNode() || i<cur.getNumBuckets()-1)) {
                        if (summaryInitialized) {
                            cur.getSummary(i, monbuf, 0);
                            summary.multiply(ret, ret_ofs, monbuf, 0, ret, ret_ofs);
                        } else {
                            cur.getSummary(i, ret, ret_ofs);
                            summaryInitialized = true;
                        }
                    }
                    if (cmpLeft > 0 && op==Op.SUMMARIZE_LEFT) next = i;
                    if (cmpRight < 0) {
                        if (op==Op.SUMMARIZE_RIGHT) next = i;
                        break;
                    }
                }
                if (next==-1) next = cur.getNumBuckets()-1;
                if (cur.isLeafNode()) return summaryInitialized ? 1 : 0;
                pageid = ((InteriorNodeCursor)cur).getBucketPageId(next);
                InteriorNodeCursor<K,V,S> ic = (InteriorNodeCursor)cur;
                interiorNodeCursor = parentNodeCursor;
                parentNodeCursor = ic;
                continue;
            }

            if (cheat) {
                idx = leafNodeCursor.getNumBuckets()-1;
                comp = 1;
            } else if (!op.isGetFromOrd()) {
                idx = cur.search(key, key_ofs);
                comp = cur.compare(key, key_ofs, idx);
            } else if (!cur.isLeafNode()) {
                // FIXME: linear scan => bad
                for(idx = 0; idx < interiorNodeCursor.getNumBuckets()-1; idx++) {
                    int k = interiorNodeCursor.getNumValsBelowBucket(idx);
                    if (ord < k) break;
                    ord -= k;
                }
            }
            if (cur.isLeafNode()) {
                switch(op) {
                    case GET_VAL_FROM_ORD:       return ord >= leafNodeCursor.getNumBuckets() ? null : leafNodeCursor.getVal(ord);
                    case GET_KEY_FROM_ORD:       return ord >= leafNodeCursor.getNumBuckets() ? null : leafNodeCursor.getKey(ord);
                    case GET_VAL_FROM_KEY:       return comp==0 ? leafNodeCursor.getVal(idx) : null;
                    case GET_VAL_FROM_KEY_FLOOR: return leafNodeCursor.getVal(idx);
                    case GET_VAL_FROM_KEY_CEIL:  /*might need to backtrack one step*/ throw new RuntimeException("not implemented");
                    case GET_ORD_FROM_KEY:       return comp==0 ? new Integer(idx+global_ord) : new Integer(-1);
                    case GET_ORD_FROM_KEY_FLOOR: return new Integer(idx+global_ord /*FIXME: off the end?*/);
                    case GET_ORD_FROM_KEY_CEIL:
                        return comp==0 ? new Integer(idx+global_ord) : new Integer(idx+global_ord+1 /*FIXME: off the end?*/);
                    case REMOVE:
                        if (comp!=0) throw new RuntimeException("attempt to remove key which did not exist");
                        return_val = leafNodeCursor.getVal(idx);
                        leafNodeCursor.deleteVal(idx);
                        // FIXME: REMOVE needs to update largestKeyPage
                        break OUT;
                    case INSERT:
                        if (comp==0) throw new RuntimeException("attempt to re-insert a value at key "+leafNodeCursor.getKey(idx));
                        if (cheat) insertionFastPath++; else insertionSlowPath++;
                        if (largestKeyPage==-1 || cheat) System.arraycopy(key, key_ofs, largestKey, 0, largestKey.length);
                        if (largestKeyPage==-1) largestKeyPage = pageid;
                        leafNodeCursor.insertVal(idx+1, key, key_ofs, newval);
                        break OUT;
                    case REPLACE:
                        if (comp!=0) throw new RuntimeException("attempt to replace a key that wasn't there");
                        if (cheat) insertionFastPath++; else insertionSlowPath++;
                        if (largestKeyPage==-1 || cheat) System.arraycopy(key, key_ofs, largestKey, 0, largestKey.length);
                        if (largestKeyPage==-1) largestKeyPage = pageid;
                        return_val = leafNodeCursor.setVal(idx, newval);
                        break OUT;
                }
            } else {
                switch(op) {
                    case REPLACE:
                        break;
                    case REMOVE:
                        if (idx < interiorNodeCursor.getNumBuckets()-1) {
                            interiorNodeCursor.setNumValsBelowBucket(idx, interiorNodeCursor.getNumValsBelowBucket(idx)-1);
                            interiorNodeCursor.writeBack();
                        }
                        break;
                    case INSERT:
                        if (idx < interiorNodeCursor.getNumBuckets()-1) {
                            interiorNodeCursor.setNumValsBelowBucket(idx, interiorNodeCursor.getNumValsBelowBucket(idx)+1);
                            interiorNodeCursor.writeBack();
                        }
                        break;
                }
                if (op.isGetOrd())
                    for(int i = 0; i < idx; i++)
                        global_ord += interiorNodeCursor.getNumValsBelowBucket(i);
                rightEdge &= idx==interiorNodeCursor.getNumBuckets()-1;
                pageid = interiorNodeCursor.getBucketPageId(idx);
                InteriorNodeCursor<K,V,S> ic = interiorNodeCursor; interiorNodeCursor = parentNodeCursor; parentNodeCursor = ic;
                assert interiorNodeCursor!=parentNodeCursor;
                continue;
            }
        }
        byte[] vbuf = new byte[uk.getSize()+uv.getSize()];
        while(cur.getParent() != cur.getPageId()) {
            parentNodeCursor.setBuf(ps.getPage(cur.getParent(), true));
            int slot = parentNodeCursor.getSlotByChildPageId(cur.getPageId());
            assert slot!=-1;
            if (summary!=null && slot < parentNodeCursor.getNumBuckets()-1) {
                switch(op) {
                    case REMOVE:
                        // TO DO: we can do much better for REPLACE with an InvertibleOperation
                    case REPLACE:
                        cur.getSummary(monbuf, 0);
                        parentNodeCursor.setSummary(slot, monbuf, 0);
                        parentNodeCursor.writeBack();
                        break;
                    case INSERT:
                        System.arraycopy(key, 0, vbuf, 0, uk.getSize());
                        uv.serialize(newval, vbuf, uk.getSize());
                        summary.call(vbuf, 0, monbuf, 0);
                        parentNodeCursor.mergeSummaryCommutative(slot, monbuf, 0);
                        parentNodeCursor.writeBack();
                        break;
                }
            }
            InteriorNodeCursor<K,V,S> ic = interiorNodeCursor; interiorNodeCursor = parentNodeCursor; parentNodeCursor = ic;
            cur = interiorNodeCursor;
        }
        return return_val;
    }



    // Performance Statistics //////////////////////////////////////////////////////////////////////////////

    static long insertionFastPath = 0;
    static long insertionSlowPath = 0;
    static long splitEven = 0;
    static long splitUnEven = 0;

    /** debugging method; may go away in future releases */
    public static void clearStats() {
        BTree.splitUnEven = 0;
        BTree.splitEven = 0;
        BTree.insertionFastPath = 0;
        BTree.insertionSlowPath = 0;
    }

    /** debugging method; may go away in future releases */
    public static void dumpStats(PrintStream pw) {
        pw.println("BTree stats: insertion fastpath = " +
                   BTree.insertionFastPath + "/" + (BTree.insertionFastPath+BTree.insertionSlowPath) + " = " +
                   (int)(( BTree.insertionFastPath * 100 )/(float)(BTree.insertionFastPath+BTree.insertionSlowPath)) + "%");
        pw.println("             intelligent splits = " +
                    BTree.splitUnEven + "/" + (BTree.splitUnEven+BTree.splitEven) + " = " +
                    (int)(( BTree.splitUnEven * 100 )/(float)(BTree.splitUnEven+BTree.splitEven)) + "%");
    }

}
