/*
 * The MIT License
 *
 * Copyright (c) 2009 The Broad Institute
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package htsjdk.samtools.util;

import htsjdk.utils.ValidationUtils;
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.function.BiFunction;

/**
 * A Red-Black tree with intervals for keys.
 * Not thread-safe, and cannot be made so.
 *
 * 7/24/2008: This was copied from the tedUtils package.
 * IMPORTANT!!! It has been modified to use the Reseq way of
 * handling coordinates (end-inclusive).
 *
 * @author tsharpe
 */
public class IntervalTree<V> implements Iterable<IntervalTree.Node<V>> {
    /**
     * Return the number of intervals in the tree.
     * @return The number of intervals.
     */
    public int size() {
        return mRoot == null ? 0 : mRoot.getSize();
    }

    /**
     * Remove all entries.
     */
    public void clear() {
        mRoot = null;
    }

    /**
     * Put a new interval into the tree (or update the value associated with an existing interval).
     * If the interval is novel, the special sentinel value is returned.
     * @param start The interval's start.
     * @param end The interval's end.
     * @param value The associated value.
     * @return The old value associated with that interval, or the sentinel.
     */
    public V put(final int start, final int end, final V value) {
        if (start > end)
            throw new IllegalArgumentException("Start cannot exceed end. (start=" + start + ", end=" + end + ")");

        V result = mSentinel;

        if (mRoot == null) {
            mRoot = new Node<V>(start, end, value);
        } else {
            Node<V> parent = null;
            Node<V> node = mRoot;
            int cmpVal = 0;

            while (node != null) {
                parent = node; // last non-null node
                cmpVal = node.compare(start, end);
                if (cmpVal == 0) {
                    break;
                }

                node = cmpVal < 0 ? node.getLeft() : node.getRight();
            }

            if (cmpVal == 0) {
                result = parent.setValue(value);
            } else {
                if (cmpVal < 0) {
                    mRoot = parent.insertLeft(start, end, value, mRoot);
                } else {
                    mRoot = parent.insertRight(start, end, value, mRoot);
                }
            }
        }

        return result;
    }

    /**
     * If the specified start and end positions are not already associated with a value or are
     * associated with the sentinel ( see {@link #getSentinel()}, associates it with the given (non-sentinel) value.
     * Otherwise, replaces the associated value with the results of the given
     * remapping function, or removes if the result is equal to the sentinel value. This
     * method may be of use when combining multiple values that have the same start and end position.
     *
     * @param start interval start position
     * @param end interval end position
     * @param value value to merge into the tree, must not be equal to the sentinel value
     * @param remappingFunction a function that merges the new value with the existing value for the same start and end position,
     *                          if the function returns the sentinel value then the mapping will be unset
     * @return the updated value that is stored in the tree after the completion of this merge operation, this will
     * be the sentinel value if nothing ended up being stored
     */
    public V merge(int start, int end, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
        ValidationUtils.validateArg(
                !Objects.equals(value, mSentinel), "Values equal to the sentinel value may not be merged");
        final V alreadyPresent = put(start, end, value);
        if (!Objects.equals(alreadyPresent, mSentinel)) {
            final V newComputedValue = remappingFunction.apply(value, alreadyPresent);
            if (Objects.equals(newComputedValue, mSentinel)) {
                remove(start, end);
            } else {
                put(start, end, newComputedValue);
            }
            return newComputedValue;
        }
        return value;
    }

    /**
     * Remove an interval from the tree.  If the interval does not exist in the tree the
     * special sentinel value is returned.
     * @param start The interval's start.
     * @param end The interval's end.
     * @return The value associated with that interval, or the sentinel.
     */
    public V remove(final int start, final int end) {
        V result = mSentinel;
        Node<V> node = mRoot;

        while (node != null) {
            final int cmpVal = node.compare(start, end);
            if (cmpVal == 0) {
                result = node.getValue();
                mRoot = node.remove(mRoot);
                break;
            }

            node = cmpVal < 0 ? node.getLeft() : node.getRight();
        }

        return result;
    }

    /**
     * Find an interval.
     * @param start The interval's start.
     * @param end The interval's end.
     * @return The Node that represents that interval, or null.
     */
    public Node<V> find(final int start, final int end) {
        Node<V> node = mRoot;

        while (node != null) {
            final int cmpVal = node.compare(start, end);
            if (cmpVal == 0) {
                break;
            }

            node = cmpVal < 0 ? node.getLeft() : node.getRight();
        }

        return node;
    }

    /**
     * Find the nth interval in the tree.
     * @param idx The rank of the interval sought (from 0 to size()-1).
     * @return The Node that represents the nth interval.
     */
    public Node<V> findByIndex(final int idx) {
        return Node.findByRank(mRoot, idx + 1);
    }

    /**
     * Find the rank of the specified interval.  If the specified interval is not in the
     * tree, then -1 is returned.
     * @param start The interval's start.
     * @param end The interval's end.
     * @return The rank of that interval, or -1.
     */
    public int getIndex(final int start, final int end) {
        return Node.getRank(mRoot, start, end) - 1;
    }

    /**
     * Find the least interval in the tree.
     * @return The earliest interval, or null if the tree is empty.
     */
    public Node<V> min() {
        Node<V> result = null;
        Node<V> node = mRoot;

        while (node != null) {
            result = node;
            node = node.getLeft();
        }

        return result;
    }

    /**
     * Find the earliest interval in the tree greater than or equal to the specified interval.
     * @param start The interval's start.
     * @param end The interval's end.
     * @return The earliest >= interval, or null if there is none.
     */
    @SuppressWarnings("null")
    public Node<V> min(final int start, final int end) {
        Node<V> result = null;
        Node<V> node = mRoot;
        int cmpVal = 0;

        while (node != null) {
            result = node;
            cmpVal = node.compare(start, end);
            if (cmpVal == 0) {
                break;
            }

            node = cmpVal < 0 ? node.getLeft() : node.getRight();
        }

        if (cmpVal > 0) {
            result = result.getNext();
        }

        return result;
    }

    /**
     * Find the earliest interval in the tree that overlaps the specified interval.
     * @param start The interval's start.
     * @param end The interval's end.
     * @return The earliest overlapping interval, or null if there is none.
     */
    public Node<V> minOverlapper(final int start, final int end) {
        Node<V> result = null;
        Node<V> node = mRoot;

        if (node != null && node.getMaxEnd() >= start) {
            while (true) {
                if (node.getStart() <= end
                        && start <= node.getEnd()) { // this node overlaps.  there might be a lesser overlapper down the
                    // left sub-tree.
                    // no need to consider the right sub-tree:  even if there's an overlapper, if won't be minimal
                    result = node;
                    node = node.getLeft();
                    if (node == null || node.getMaxEnd() < start) break; // no left sub-tree or all nodes end too early
                } else { // no overlap.  if there might be a left sub-tree overlapper, consider the left sub-tree.
                    final Node<V> left = node.getLeft();
                    if (left != null && left.getMaxEnd() >= start) {
                        node = left;
                    } else { // left sub-tree cannot contain an overlapper.  consider the right sub-tree.
                        if (node.getStart() > end)
                            break; // everything in the right sub-tree is past the end of the query interval

                        node = node.getRight();
                        if (node == null || node.getMaxEnd() < start)
                            break; // no right sub-tree or all nodes end too early
                    }
                }
            }
        }

        return result;
    }

    /**
     * Find the greatest interval in the tree.
     * @return The latest interval, or null if the tree is empty.
     */
    public Node<V> max() {
        Node<V> result = null;
        Node<V> node = mRoot;

        while (node != null) {
            result = node;
            node = node.getRight();
        }

        return result;
    }

    /**
     * Find the latest interval in the tree less than or equal to the specified interval.
     * @param start The interval's start.
     * @param end The interval's end.
     * @return The latest >= interval, or null if there is none.
     */
    @SuppressWarnings("null")
    public Node<V> max(final int start, final int end) {
        Node<V> result = null;
        Node<V> node = mRoot;
        int cmpVal = 0;

        while (node != null) {
            result = node;
            cmpVal = node.compare(start, end);
            if (cmpVal == 0) {
                break;
            }

            node = cmpVal < 0 ? node.getLeft() : node.getRight();
        }

        if (cmpVal < 0) {
            result = result.getPrev();
        }

        return result;
    }

    /**
     * Return an iterator over the entire tree.
     * @return An iterator.
     */
    @Override
    public Iterator<Node<V>> iterator() {
        return new FwdIterator(min());
    }

    /**
     * Return an iterator over all intervals greater than or equal to the specified interval.
     * @param start The interval's start.
     * @param end The interval's end.
     * @return An iterator.
     */
    public Iterator<Node<V>> iterator(final int start, final int end) {
        return new FwdIterator(min(start, end));
    }

    /**
     * Return an iterator over all intervals overlapping the specified range.
     * @param start The range start.
     * @param end The range end.
     * @return An iterator.
     */
    public Iterator<Node<V>> overlappers(final int start, final int end) {
        return new OverlapIterator(start, end);
    }

    /**
     * Return an iterator over the entire tree that returns intervals in reverse order.
     * @return An iterator.
     */
    public Iterator<Node<V>> reverseIterator() {
        return new RevIterator(max());
    }

    /**
     * Return an iterator over all intervals less than or equal to the specified interval, in reverse order.
     * @param start The interval's start.
     * @param end The interval's end.
     * @return An iterator.
     */
    public Iterator<Node<V>> reverseIterator(final int start, final int end) {
        return new RevIterator(max(start, end));
    }

    /**
     * Get the special sentinel value that will be used to signal novelty when putting a new interval
     * into the tree, or to signal "not found" when removing an interval.  This is null by default.
     * @return The sentinel value.
     */
    public V getSentinel() {
        return mSentinel;
    }

    /**
     * Set the special sentinel value that will be used to signal novelty when putting a new interval
     * into the tree, or to signal "not found" when removing an interval.
     * @param sentinel The new sentinel value.
     * @return The old sentinel value.
     */
    public V setSentinel(final V sentinel) {
        final V result = mSentinel;
        mSentinel = sentinel;
        return result;
    }

    /**
     * This method is only for debugging.
     * It verifies whether the tree is internally consistent with respect to the mMaxEnd cached on each node.
     * @throws IllegalStateException If an inconsistency is detected.
     */
    public void checkMaxEnds() {
        if (mRoot != null) mRoot.checkMaxEnd();
    }

    /**
     * This method draws a nested picture of the tree on System.out.
     * Useful for debugging.
     */
    public void printTree() {
        if (mRoot != null) mRoot.printNode();
    }

    void removeNode(final Node<V> node) {
        mRoot = node.remove(mRoot);
    }

    private Node<V> mRoot;
    private V mSentinel;

    public static class Node<V1> {
        // bit-wise definitions from which the other constants are composed
        public static final int HAS_LESSER_PART = 1;
        public static final int HAS_OVERLAPPING_PART = 2;
        public static final int HAS_GREATER_PART = 4;
        public static final int IS_ADJACENT_AND_EMPTY = 0;
        public static final int IS_STRICTLY_LESS = HAS_LESSER_PART; // 1
        public static final int IS_SUBSET = HAS_OVERLAPPING_PART; // 2
        public static final int IS_LEFT_OVERHANGING_OVERLAPPER = HAS_LESSER_PART | HAS_OVERLAPPING_PART; // 3
        public static final int IS_STRICTLY_GREATER = HAS_GREATER_PART; // 4
        // there is no value that equals 5, since that would imply overhanging on left and right without overlapping
        public static final int IS_RIGHT_OVERHANGING_OVERLAPPER = HAS_GREATER_PART | HAS_OVERLAPPING_PART; // 6
        public static final int IS_SUPERSET = HAS_LESSER_PART | HAS_OVERLAPPING_PART | HAS_GREATER_PART; // 7

        Node(final int start, final int end, final V1 value) {
            mStart = start;
            mEnd = end;
            mValue = value;
            mSize = 1;
            mMaxEnd = mEnd;
            mIsBlack = true;
        }

        Node(final Node<V1> parent, final int start, final int end, final V1 value) {
            mParent = parent;
            mStart = start;
            mEnd = end;
            mValue = value;
            mMaxEnd = mEnd;
            mSize = 1;
        }

        public int getStart() {
            return mStart;
        }

        public int getEnd() {
            return mEnd;
        }

        public int getLength() {
            return mEnd - mStart + 1;
        }

        public int getRelationship(final Node<V1> interval) {
            int result = 0;
            if (mStart < interval.getStart()) result = HAS_LESSER_PART;
            if (mEnd > interval.getEnd()) result |= HAS_GREATER_PART;
            if (mStart <= interval.getEnd() && interval.getStart() <= mEnd) result |= HAS_OVERLAPPING_PART;
            return result;
        }

        public boolean isAdjacent(final Node<V1> interval) {
            return mStart == interval.getEnd() + 1 || mEnd + 1 == interval.getStart();
        }

        public V1 getValue() {
            return mValue;
        }

        public V1 setValue(final V1 value) {
            final V1 result = mValue;
            mValue = value;
            return result;
        }

        int getSize() {
            return mSize;
        }

        int getMaxEnd() {
            return mMaxEnd;
        }

        Node<V1> getLeft() {
            return mLeft;
        }

        Node<V1> insertLeft(final int start, final int end, final V1 value, final Node<V1> root) {
            mLeft = new Node<V1>(this, start, end, value);
            return insertFixup(mLeft, root);
        }

        Node<V1> getRight() {
            return mRight;
        }

        Node<V1> insertRight(final int start, final int end, final V1 value, final Node<V1> root) {
            mRight = new Node<V1>(this, start, end, value);
            return insertFixup(mRight, root);
        }

        Node<V1> getNext() {
            Node<V1> result;

            if (mRight != null) {
                result = mRight;
                while (result.mLeft != null) {
                    result = result.mLeft;
                }
            } else {
                Node<V1> node = this;
                result = mParent;
                while (result != null && node == result.mRight) {
                    node = result;
                    result = result.mParent;
                }
            }

            return result;
        }

        Node<V1> getPrev() {
            Node<V1> result;

            if (mLeft != null) {
                result = mLeft;
                while (result.mRight != null) {
                    result = result.mRight;
                }
            } else {
                Node<V1> node = this;
                result = mParent;
                while (result != null && node == result.mLeft) {
                    node = result;
                    result = result.mParent;
                }
            }

            return result;
        }

        boolean wasRemoved() {
            return mSize == 0;
        }

        Node<V1> remove(Node<V1> root) {
            if (mSize == 0) {
                throw new IllegalStateException("Entry was already removed.");
            }

            if (mLeft == null) {
                if (mRight == null) { // no children
                    if (mParent == null) {
                        root = null;
                    } else if (mParent.mLeft == this) {
                        mParent.mLeft = null;
                        fixup(mParent);

                        if (mIsBlack) root = removeFixup(mParent, null, root);
                    } else {
                        mParent.mRight = null;
                        fixup(mParent);

                        if (mIsBlack) root = removeFixup(mParent, null, root);
                    }
                } else { // single child on right
                    root = spliceOut(mRight, root);
                }
            } else if (mRight == null) { // single child on left
                root = spliceOut(mLeft, root);
            } else { // two children
                final Node<V1> next = getNext();
                root = next.remove(root);

                // put next into tree in same position as this, effectively removing this
                if ((next.mParent = mParent) == null) root = next;
                else if (mParent.mLeft == this) mParent.mLeft = next;
                else mParent.mRight = next;

                if ((next.mLeft = mLeft) != null) {
                    mLeft.mParent = next;
                }

                if ((next.mRight = mRight) != null) {
                    mRight.mParent = next;
                }

                next.mIsBlack = mIsBlack;
                next.mSize = mSize;

                // PIC-123 fix
                fixup(next);
            }

            mSize = 0;
            return root;
        }

        // backwards comparison!  compares start+end to this.
        int compare(final int start, final int end) {
            int result = 0;

            if (start > mStart) result = 1;
            else if (start < mStart) result = -1;
            else if (end > mEnd) result = 1;
            else if (end < mEnd) result = -1;

            return result;
        }

        @SuppressWarnings("null")
        static <V1> Node<V1> getNextOverlapper(Node<V1> node, final int start, final int end) {
            do {
                Node<V1> nextNode = node.mRight;
                if (nextNode != null && nextNode.mMaxEnd >= start) {
                    node = nextNode;
                    while ((nextNode = node.mLeft) != null && nextNode.mMaxEnd >= start) node = nextNode;
                } else {
                    nextNode = node;
                    while ((node = nextNode.mParent) != null && node.mRight == nextNode) nextNode = node;
                }

                if (node != null && node.mStart > end) node = null;
            } while (node != null && !(node.mStart <= end && start <= node.mEnd));

            return node;
        }

        static <V1> Node<V1> findByRank(Node<V1> node, int rank) {
            while (node != null) {
                final int nodeRank = node.getRank();
                if (rank == nodeRank) break;

                if (rank < nodeRank) {
                    node = node.mLeft;
                } else {
                    node = node.mRight;
                    rank -= nodeRank;
                }
            }

            return node;
        }

        static <V1> int getRank(Node<V1> node, final int start, final int end) {
            int rank = 0;

            while (node != null) {
                final int cmpVal = node.compare(start, end);
                if (cmpVal < 0) {
                    node = node.mLeft;
                } else {
                    rank += node.getRank();
                    if (cmpVal == 0) return rank; // EARLY RETURN!!!

                    node = node.mRight;
                }
            }

            return 0;
        }

        private int getRank() {
            int result = 1;
            if (mLeft != null) result = mLeft.mSize + 1;
            return result;
        }

        private Node<V1> spliceOut(final Node<V1> child, Node<V1> root) {
            if ((child.mParent = mParent) == null) {
                root = child;
                child.mIsBlack = true;
            } else {
                if (mParent.mLeft == this) mParent.mLeft = child;
                else mParent.mRight = child;
                fixup(mParent);

                if (mIsBlack) root = removeFixup(mParent, child, root);
            }

            return root;
        }

        private Node<V1> rotateLeft(Node<V1> root) {
            final Node<V1> child = mRight;

            final int childSize = child.mSize;
            child.mSize = mSize;
            mSize -= childSize;

            if ((mRight = child.mLeft) != null) {
                mRight.mParent = this;
                mSize += mRight.mSize;
            }

            if ((child.mParent = mParent) == null) root = child;
            else if (this == mParent.mLeft) mParent.mLeft = child;
            else mParent.mRight = child;

            child.mLeft = this;
            mParent = child;

            setMaxEnd();
            child.setMaxEnd();

            return root;
        }

        private Node<V1> rotateRight(Node<V1> root) {
            final Node<V1> child = mLeft;

            final int childSize = child.mSize;
            child.mSize = mSize;
            mSize -= childSize;

            if ((mLeft = child.mRight) != null) {
                mLeft.mParent = this;
                mSize += mLeft.mSize;
            }

            if ((child.mParent = mParent) == null) root = child;
            else if (this == mParent.mLeft) mParent.mLeft = child;
            else mParent.mRight = child;

            child.mRight = this;
            mParent = child;

            setMaxEnd();
            child.setMaxEnd();

            return root;
        }

        private void setMaxEnd() {
            mMaxEnd = mEnd;
            if (mLeft != null) mMaxEnd = Math.max(mMaxEnd, mLeft.mMaxEnd);
            if (mRight != null) mMaxEnd = Math.max(mMaxEnd, mRight.mMaxEnd);
        }

        private static <V1> void fixup(Node<V1> node) {
            do {
                node.mSize = 1;
                node.mMaxEnd = node.mEnd;
                if (node.mLeft != null) {
                    node.mSize += node.mLeft.mSize;
                    node.mMaxEnd = Math.max(node.mMaxEnd, node.mLeft.mMaxEnd);
                }
                if (node.mRight != null) {
                    node.mSize += node.mRight.mSize;
                    node.mMaxEnd = Math.max(node.mMaxEnd, node.mRight.mMaxEnd);
                }
            } while ((node = node.mParent) != null);
        }

        private static <V1> Node<V1> insertFixup(Node<V1> daughter, Node<V1> root) {
            Node<V1> mom = daughter.mParent;
            fixup(mom);

            while (mom != null && !mom.mIsBlack) {
                final Node<V1> gramma = mom.mParent;
                Node<V1> auntie = gramma.mLeft;
                if (auntie == mom) {
                    auntie = gramma.mRight;
                    if (auntie != null && !auntie.mIsBlack) {
                        mom.mIsBlack = true;
                        auntie.mIsBlack = true;
                        gramma.mIsBlack = false;
                        daughter = gramma;
                    } else {
                        if (daughter == mom.mRight) {
                            root = mom.rotateLeft(root);
                            mom = daughter;
                        }
                        mom.mIsBlack = true;
                        gramma.mIsBlack = false;
                        root = gramma.rotateRight(root);
                        break;
                    }
                } else {
                    if (auntie != null && !auntie.mIsBlack) {
                        mom.mIsBlack = true;
                        auntie.mIsBlack = true;
                        gramma.mIsBlack = false;
                        daughter = gramma;
                    } else {
                        if (daughter == mom.mLeft) {
                            root = mom.rotateRight(root);
                            mom = daughter;
                        }
                        mom.mIsBlack = true;
                        gramma.mIsBlack = false;
                        root = gramma.rotateLeft(root);
                        break;
                    }
                }
                mom = daughter.mParent;
            }
            root.mIsBlack = true;
            return root;
        }

        private static <V1> Node<V1> removeFixup(Node<V1> parent, Node<V1> node, Node<V1> root) {
            do {
                if (node == parent.mLeft) {
                    Node<V1> sister = parent.mRight;
                    if (!sister.mIsBlack) {
                        sister.mIsBlack = true;
                        parent.mIsBlack = false;
                        root = parent.rotateLeft(root);
                        sister = parent.mRight;
                    }
                    if ((sister.mLeft == null || sister.mLeft.mIsBlack)
                            && (sister.mRight == null || sister.mRight.mIsBlack)) {
                        sister.mIsBlack = false;
                        node = parent;
                    } else {
                        if (sister.mRight == null || sister.mRight.mIsBlack) {
                            sister.mLeft.mIsBlack = true;
                            sister.mIsBlack = false;
                            root = sister.rotateRight(root);
                            sister = parent.mRight;
                        }
                        sister.mIsBlack = parent.mIsBlack;
                        parent.mIsBlack = true;
                        sister.mRight.mIsBlack = true;
                        root = parent.rotateLeft(root);
                        node = root;
                    }
                } else {
                    Node<V1> sister = parent.mLeft;
                    if (!sister.mIsBlack) {
                        sister.mIsBlack = true;
                        parent.mIsBlack = false;
                        root = parent.rotateRight(root);
                        sister = parent.mLeft;
                    }
                    if ((sister.mLeft == null || sister.mLeft.mIsBlack)
                            && (sister.mRight == null || sister.mRight.mIsBlack)) {
                        sister.mIsBlack = false;
                        node = parent;
                    } else {
                        if (sister.mLeft == null || sister.mLeft.mIsBlack) {
                            sister.mRight.mIsBlack = true;
                            sister.mIsBlack = false;
                            root = sister.rotateLeft(root);
                            sister = parent.mLeft;
                        }
                        sister.mIsBlack = parent.mIsBlack;
                        parent.mIsBlack = true;
                        sister.mLeft.mIsBlack = true;
                        root = parent.rotateRight(root);
                        node = root;
                    }
                }
                parent = node.mParent;
            } while (parent != null && node.mIsBlack);

            node.mIsBlack = true;
            return root;
        }

        public void checkMaxEnd() {
            if (mMaxEnd != calcMaxEnd()) {
                throw new IllegalStateException("Max end mismatch " + mMaxEnd + " vs " + calcMaxEnd() + ": " + this);
            }
            if (mLeft != null) mLeft.checkMaxEnd();
            if (mRight != null) mRight.checkMaxEnd();
        }

        private int calcMaxEnd() {
            int end = mEnd;
            if (mLeft != null) end = Math.max(end, mLeft.mMaxEnd);
            if (mRight != null) end = Math.max(end, mRight.mMaxEnd);
            return end;
        }

        public void printNode() {
            this.printNodeInternal("", "root: ");
        }

        private void printNodeInternal(final String padding, final String tag) {
            System.out.println(padding + tag + " " + this);
            if (mLeft != null) mLeft.printNodeInternal(padding + "  ", "left: ");
            if (mRight != null) mRight.printNodeInternal(padding + "  ", "right:");
        }

        public String toString() {
            return "Node(" + mStart + "," + mEnd + "," + mValue + "," + mSize + "," + mMaxEnd + "," + mIsBlack + ")";
        }

        private Node<V1> mParent;
        private Node<V1> mLeft;
        private Node<V1> mRight;
        private final int mStart;
        private final int mEnd;
        private V1 mValue;
        private int mSize;
        private int mMaxEnd;
        private boolean mIsBlack;
    }

    public class FwdIterator implements Iterator<Node<V>> {
        public FwdIterator(final Node<V> node) {
            mNext = node;
        }

        @Override
        public boolean hasNext() {
            return mNext != null;
        }

        @Override
        public Node<V> next() {
            if (mNext == null) {
                throw new NoSuchElementException("No next element.");
            }

            if (mNext.wasRemoved()) {
                mNext = min(mNext.getStart(), mNext.getEnd());
                if (mNext == null)
                    throw new ConcurrentModificationException(
                            "Current element was removed, and there are no more elements.");
            }
            mLast = mNext;
            mNext = mNext.getNext();
            return mLast;
        }

        @Override
        public void remove() {
            if (mLast == null) {
                throw new IllegalStateException("No entry to remove.");
            }

            removeNode(mLast);
            mLast = null;
        }

        private Node<V> mNext;
        private Node<V> mLast;
    }

    public class RevIterator implements Iterator<Node<V>> {
        public RevIterator(final Node<V> node) {
            mNext = node;
        }

        @Override
        public boolean hasNext() {
            return mNext != null;
        }

        @Override
        public Node<V> next() {
            if (mNext == null) throw new NoSuchElementException("No next element.");
            if (mNext.wasRemoved()) {
                mNext = max(mNext.getStart(), mNext.getEnd());
                if (mNext == null)
                    throw new ConcurrentModificationException(
                            "Current element was removed, and there are no more elements.");
            }
            mLast = mNext;
            mNext = mNext.getPrev();
            return mLast;
        }

        @Override
        public void remove() {
            if (mLast == null) {
                throw new IllegalStateException("No entry to remove.");
            }

            removeNode(mLast);
            mLast = null;
        }

        private Node<V> mNext;
        private Node<V> mLast;
    }

    public class OverlapIterator implements Iterator<Node<V>> {
        public OverlapIterator(final int start, final int end) {
            mNext = minOverlapper(start, end);
            mStart = start;
            mEnd = end;
        }

        @Override
        public boolean hasNext() {
            return mNext != null;
        }

        @Override
        public Node<V> next() {
            if (mNext == null) {
                throw new NoSuchElementException("No next element.");
            }

            if (mNext.wasRemoved()) {
                throw new ConcurrentModificationException("Current element was removed.");
            }

            mLast = mNext;
            mNext = Node.getNextOverlapper(mNext, mStart, mEnd);
            return mLast;
        }

        @Override
        public void remove() {
            if (mLast == null) {
                throw new IllegalStateException("No entry to remove.");
            }

            removeNode(mLast);
            mLast = null;
        }

        private Node<V> mNext;
        private Node<V> mLast;
        private final int mStart;
        private final int mEnd;
    }

    public static class ValuesIterator<V1> implements Iterator<V1> {
        public ValuesIterator(final Iterator<Node<V1>> itr) {
            mItr = itr;
        }

        @Override
        public boolean hasNext() {
            return mItr.hasNext();
        }

        @Override
        public V1 next() {
            return mItr.next().getValue();
        }

        @Override
        public void remove() {
            mItr.remove();
        }

        private final Iterator<Node<V1>> mItr;
    }
}
