/**
 * 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.activemq.kaha.impl.index.tree;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.apache.activemq.kaha.Marshaller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Page in a BTree
 * 
 * 
 */
class TreePage {

    static final int PAGE_HEADER_SIZE = 18;
    private static final transient Logger LOG = LoggerFactory.getLogger(TreePage.class);

    static enum Flavour {
        LESS, MORE
    }

    private TreeIndex tree;
    private int maximumEntries;
    private long id;
    private long parentId = TreeEntry.NOT_SET;
    private boolean leaf = true;
    private List<TreeEntry> treeEntries;
    /*
     * for persistence only
     */
    private long nextFreePageId = TreeEntry.NOT_SET;
    private boolean active = true;

    /**
     * Constructor
     * 
     * @param tree
     * @param id
     * @param parentId
     * @param maximumEntries
     */
    TreePage(TreeIndex tree, long id, long parentId, int maximumEntries) {
        this(maximumEntries);
        this.tree = tree;
        this.id = id;
        this.parentId = parentId;
    }

    /**
     * Constructor
     * 
     * @param maximumEntries
     */
    public TreePage(int maximumEntries) {
        this.maximumEntries = maximumEntries;
        this.treeEntries = new ArrayList<TreeEntry>(maximumEntries);
    }

    public String toString() {
        return "TreePage[" + getId() + "]parent=" + getParentId();
    }

    public boolean equals(Object o) {
        boolean result = false;
        if (o instanceof TreePage) {
            TreePage other = (TreePage)o;
            result = other.id == id;
        }
        return result;
    }

    public int hashCode() {
        return (int)id;
    }

    boolean isActive() {
        return this.active;
    }

    void setActive(boolean active) {
        this.active = active;
    }

    long getNextFreePageId() {
        return this.nextFreePageId;
    }

    void setNextFreePageId(long nextPageId) {
        this.nextFreePageId = nextPageId;
    }

    long getId() {
        return id;
    }

    void setId(long id) {
        this.id = id;
    }

    void write(Marshaller keyMarshaller, DataOutput dataOut) throws IOException {
        writeHeader(dataOut);
        dataOut.writeInt(treeEntries.size());
        for (TreeEntry entry : treeEntries) {
            entry.write(keyMarshaller, dataOut);
        }
    }

    void read(Marshaller keyMarshaller, DataInput dataIn) throws IOException {
        readHeader(dataIn);
        int size = dataIn.readInt();
        treeEntries.clear();
        for (int i = 0; i < size; i++) {
            TreeEntry entry = new TreeEntry();
            entry.read(keyMarshaller, dataIn);
            treeEntries.add(entry);
        }
    }

    void readHeader(DataInput dataIn) throws IOException {
        active = dataIn.readBoolean();
        leaf = dataIn.readBoolean();
        setParentId(dataIn.readLong());
        nextFreePageId = dataIn.readLong();
    }

    void writeHeader(DataOutput dataOut) throws IOException {
        dataOut.writeBoolean(isActive());
        dataOut.writeBoolean(isLeaf());
        dataOut.writeLong(getParentId());
        dataOut.writeLong(nextFreePageId);
    }

    boolean isEmpty() {
        return treeEntries.isEmpty();
    }

    boolean isFull() {
        return treeEntries.size() >= maximumEntries;
    }

    boolean isRoot() {
        return getParentId() < 0;
    }

    boolean isLeaf() {
        if (treeEntries.isEmpty()) {
            leaf = true;
        }
        return leaf;
    }

    boolean isUnderflowed() {
        return treeEntries.size() < (maximumEntries / 2);
    }

    boolean isOverflowed() {
        return treeEntries.size() > maximumEntries;
    }

    void setLeaf(boolean newValue) {
        this.leaf = newValue;
    }

    TreePage getParent() throws IOException {
        return tree.lookupPage(parentId);
    }

    long getParentId() {
        return parentId;
    }

    void setParentId(long newId) throws IOException {
        if (newId == this.id) {
            throw new IllegalStateException("Cannot set page as a child of itself " + this
                                            + " trying to set parentId = " + newId);
        }
        this.parentId = newId;
        tree.writePage(this);
    }

    List<TreeEntry> getEntries() {
        return treeEntries;
    }

    void setEntries(List<TreeEntry> newEntries) {
        this.treeEntries = newEntries;
    }

    int getMaximumEntries() {
        return this.maximumEntries;
    }

    void setMaximumEntries(int maximumEntries) {
        this.maximumEntries = maximumEntries;
    }

    int size() {
        return treeEntries.size();
    }

    TreeIndex getTree() {
        return this.tree;
    }

    void setTree(TreeIndex tree) {
        this.tree = tree;
    }

    void reset() throws IOException {
        treeEntries.clear();
        setParentId(TreeEntry.NOT_SET);
        setNextFreePageId(TreeEntry.NOT_SET);
        setLeaf(true);
    }

    public TreeEntry find(TreeEntry key) throws IOException {
        int low = 0;
        int high = size() - 1;
        long pageId = -1;
        while (low <= high) {
            int mid = (low + high) >> 1;
            TreeEntry te = getTreeEntry(mid);
            int cmp = te.compareTo(key);
            if (cmp == 0) {
                return te;
            } else if (cmp < 0) {
                low = mid + 1;
                pageId = te.getNextPageId();
            } else {
                high = mid - 1;
                pageId = te.getPrevPageId();
            }
        }
        TreePage page = tree.lookupPage(pageId);
        if (page != null) {
            return page.find(key);
        }
        return null;
    }

    TreeEntry put(TreeEntry newEntry) throws IOException {
        TreeEntry result = null;
        if (isRoot()) {
            if (isEmpty()) {
                insertTreeEntry(0, newEntry);
            } else {
                result = doInsert(null, newEntry);
            }
        } else {
            throw new IllegalStateException("insert() should not be called on non root page - " + this);
        }
        return result;
    }

    TreeEntry remove(TreeEntry entry) throws IOException {
        TreeEntry result = null;
        if (isRoot()) {
            if (!isEmpty()) {
                result = doRemove(entry);
            }
        } else {
            throw new IllegalStateException("remove() should not be called on non root page");
        }
        return result;
    }

    private TreeEntry doInsert(Flavour flavour, TreeEntry newEntry) throws IOException {
        TreeEntry result = null;
        TreePageEntry closest = findClosestEntry(newEntry);
        if (closest != null) {
            TreeEntry closestEntry = closest.getTreeEntry();
            TreePage closestPage = closest.getTreePage();
            int cmp = closestEntry.compareTo(newEntry);
            if (cmp == 0) {
                // we actually just need to pass back the value
                long oldValue = closestEntry.getIndexOffset();
                closestEntry.setIndexOffset(newEntry.getIndexOffset());
                newEntry.setIndexOffset(oldValue);
                result = newEntry;
                save();
            } else if (closestPage != null) {
                result = closestPage.doInsert(closest.getFlavour(), newEntry);
            } else {
                if (!isFull()) {
                    insertTreeEntry(closest.getIndex(), newEntry);
                    save();
                } else {
                    doOverflow(flavour, newEntry);
                }
            }
        } else {
            if (!isFull()) {
                doInsertEntry(newEntry);
                save();
            } else {
                // need to insert the new entry and propogate up the hightest
                // value
                doOverflow(flavour, newEntry);
            }
        }
        return result;
    }

    private TreePage doOverflow(Flavour flavour, TreeEntry newEntry) throws IOException {
        TreePage result = this;
        TreeEntry theEntry = newEntry;
        if (!isFull()) {
            doInsertEntry(newEntry);
            save();
        } else {
            if (!isRoot() && flavour != null) {
                // we aren't the root, but to ensure the correct distribution we
                // need to
                // insert the new entry and take a node of the end of the page
                // and pass that up the tree to find a home
                doInsertEntry(newEntry);
                if (flavour == Flavour.LESS) {
                    theEntry = removeTreeEntry(0);
                    theEntry.reset();
                    theEntry.setNextPageId(getId());
                } else {
                    theEntry = removeTreeEntry(size() - 1);
                    theEntry.reset();
                    theEntry.setPrevPageId(getId());
                }
                save();

                result = getParent().doOverflow(flavour, theEntry);
                if (!theEntry.equals(newEntry)) {
                    // the newEntry stayed here
                    result = this;
                }
            } else {
                // so we are the root and need to split
                doInsertEntry(newEntry);
                int midIndex = size() / 2;
                TreeEntry midEntry = removeTreeEntry(midIndex);
                List<TreeEntry> subList = getSubList(midIndex, size());
                removeAllTreeEntries(subList);
                TreePage newRoot = tree.createRoot();
                newRoot.setLeaf(false);
                this.setParentId(newRoot.getId());
                save(); // we are no longer root - need to save - we maybe
                // looked up v. soon!
                TreePage rightPage = tree.createPage(newRoot.getId());
                rightPage.setEntries(subList);
                rightPage.checkLeaf();
                resetParentId(rightPage.getId(), rightPage.getEntries());
                midEntry.setNextPageId(rightPage.getId());
                midEntry.setPrevPageId(this.getId());
                newRoot.insertTreeEntry(0, midEntry);
                resetParentId(newRoot.getId(), newRoot.getEntries());
                save();
                rightPage.save();
                newRoot.save();
            }
        }
        return result;
    }

    private TreeEntry doRemove(TreeEntry entry) throws IOException {
        TreeEntry result = null;
        TreePageEntry closest = findClosestEntry(entry);
        if (closest != null) {
            TreeEntry closestEntry = closest.getTreeEntry();
            if (closestEntry != null) {
                TreePage closestPage = closest.getTreePage();
                int cmp = closestEntry.compareTo(entry);
                if (cmp == 0) {
                    result = closest.getTreeEntry();
                    int index = closest.getIndex();
                    removeTreeEntry(index);
                    save();
                    // ensure we don't loose children
                    doUnderflow(result, index);
                } else if (closestPage != null) {
                    closestPage.doRemove(entry);
                }
            }
        }
        return result;
    }

    /**
     * @return true if the page is removed
     * @throws IOException
     */
    private boolean doUnderflow() throws IOException {
        boolean result = false;
        boolean working = true;
        while (working && isUnderflowed() && !isEmpty() && !isLeaf()) {
            int lastIndex = size() - 1;
            TreeEntry entry = getTreeEntry(lastIndex);
            working = doUnderflow(entry, lastIndex);
        }
        if (isUnderflowed() && isLeaf()) {
            result = doUnderflowLeaf();
        }
        return result;
    }

    private boolean doUnderflow(TreeEntry entry, int index) throws IOException {
        boolean result = false;
        // pull an entry up from a leaf to fill the empty space
        if (entry.getNextPageId() != TreeEntry.NOT_SET) {
            TreePage page = tree.lookupPage(entry.getNextPageId());
            if (page != null && !page.isEmpty()) {
                TreeEntry replacement = page.removeTreeEntry(0);
                TreeEntry copy = replacement.copy();
                checkParentIdForRemovedPageEntry(copy, page.getId(), getId());
                if (!page.isEmpty()) {
                    copy.setNextPageId(page.getId());
                    page.setParentId(this.id);
                } else {
                    page.setLeaf(true);
                }
                int replacementIndex = doInsertEntry(copy);
                if (page.doUnderflow()) {
                    // page removed so update our replacement
                    resetPageReference(replacementIndex, copy.getNextPageId());
                    copy.setNextPageId(TreeEntry.NOT_SET);
                } else {
                    page.save();
                }
                save();
                result = true;
            }
        }
        // ensure we don't loose previous bit of the tree
        if (entry.getPrevPageId() != TreeEntry.NOT_SET) {
            TreeEntry prevEntry = (index > 0) ? getTreeEntry(index - 1) : null;
            if (prevEntry == null || prevEntry.getNextPageId() != entry.getPrevPageId()) {
                TreePage page = tree.lookupPage(entry.getPrevPageId());
                if (page != null && !page.isEmpty()) {
                    TreeEntry replacement = page.removeTreeEntry(page.getEntries().size() - 1);
                    TreeEntry copy = replacement.copy();
                    // check children pages of the replacement point to the
                    // correct place
                    checkParentIdForRemovedPageEntry(copy, page.getId(), getId());
                    if (!page.isEmpty()) {
                        copy.setPrevPageId(page.getId());
                    } else {
                        page.setLeaf(true);
                    }
                    insertTreeEntry(index, copy);
                    // if we overflow - the page the replacement ends up on
                    TreePage landed = null;
                    TreeEntry removed = null;
                    if (isOverflowed()) {
                        TreePage parent = getParent();
                        if (parent != null) {
                            removed = getTreeEntry(0);
                            Flavour flavour = getFlavour(parent, removed);
                            if (flavour == Flavour.LESS) {
                                removed = removeTreeEntry(0);
                                landed = parent.doOverflow(flavour, removed);
                            } else {
                                removed = removeTreeEntry(size() - 1);
                                landed = parent.doOverflow(Flavour.MORE, removed);
                            }
                        }
                    }
                    if (page.doUnderflow()) {
                        if (landed == null || landed.equals(this)) {
                            landed = this;
                        }

                        resetPageReference(copy.getNextPageId());
                        landed.resetPageReference(copy.getNextPageId());
                        copy.setPrevPageId(TreeEntry.NOT_SET);
                        landed.save();
                    } else {
                        page.save();
                    }
                    save();
                    result = true;
                }
                // now we need to check we haven't overflowed this page
            }
        }
        if (!result) {
            save();
        }
        // now see if we need to save this page
        result |= doUnderflowLeaf();
        save();
        return result;
    }

    private boolean doUnderflowLeaf() throws IOException {
        boolean result = false;
        // if we have unerflowed - and we are a leaf - push entries further up
        // the tree
        // and delete ourselves
        if (isUnderflowed() && isLeaf()) {
            List<TreeEntry> list = new ArrayList<TreeEntry>(treeEntries);
            treeEntries.clear();
            for (TreeEntry entry : list) {
                // need to check for each iteration - we might get promoted to
                // root
                TreePage parent = getParent();
                if (parent != null) {
                    Flavour flavour = getFlavour(parent, entry);
                    TreePage landedOn = parent.doOverflow(flavour, entry);
                    checkParentIdForRemovedPageEntry(entry, getId(), landedOn.getId());
                }
            }
            TreePage parent = getParent();
            if (parent != null) {
                parent.checkLeaf();
                parent.removePageId(getId());
                parent.doUnderflow();
                parent.save();
                tree.releasePage(this);
                result = true;
            }
        }
        return result;
    }

    private Flavour getFlavour(TreePage page, TreeEntry entry) {
        Flavour result = null;
        if (page != null && !page.getEntries().isEmpty()) {
            TreeEntry last = page.getEntries().get(page.getEntries().size() - 1);
            if (last.compareTo(entry) > 0) {
                result = Flavour.MORE;
            } else {
                result = Flavour.LESS;
            }
        }
        return result;
    }

    private void checkLeaf() {
        boolean result = false;
        for (TreeEntry entry : treeEntries) {
            if (entry.hasChildPagesReferences()) {
                result = true;
                break;
            }
        }
        setLeaf(!result);
    }

    private void checkParentIdForRemovedPageEntry(TreeEntry entry, long oldPageId, long newPageId)
        throws IOException {
        TreePage page = tree.lookupPage(entry.getPrevPageId());
        if (page != null && page.getParentId() == oldPageId) {
            page.setParentId(newPageId);
            page.save();
        }
        page = tree.lookupPage(entry.getNextPageId());
        if (page != null && page.getParentId() == oldPageId) {
            page.setParentId(newPageId);
            page.save();
        }
    }

    private void removePageId(long pageId) {
        for (TreeEntry entry : treeEntries) {
            if (entry.getNextPageId() == pageId) {
                entry.setNextPageId(TreeEntry.NOT_SET);
            }
            if (entry.getPrevPageId() == pageId) {
                entry.setPrevPageId(TreeEntry.NOT_SET);
            }
        }
    }

    private TreePageEntry findClosestEntry(TreeEntry key) throws IOException {
        TreePageEntry result = null;
        TreeEntry treeEntry = null;
        Flavour flavour = null;
        long pageId = -1;
        int low = 0;
        int high = size() - 1;
        int mid = low;
        while (low <= high) {
            mid = (low + high) >> 1;
            treeEntry = getTreeEntry(mid);
            int cmp = treeEntry.compareTo(key);
            if (cmp < 0) {
                low = mid + 1;
                pageId = treeEntry.getNextPageId();
                flavour = Flavour.LESS;
            } else if (cmp > 0) {
                high = mid - 1;
                pageId = treeEntry.getPrevPageId();
                flavour = Flavour.MORE;
            } else {
                // got exact match
                low = mid;
                break;
            }
        }
        if (treeEntry != null) {
            TreePage treePage = tree.lookupPage(pageId);
            result = new TreePageEntry(treeEntry, treePage, flavour, low);
        }
        return result;
    }

    private int doInsertEntry(TreeEntry newEntry) throws IOException {
        int low = 0;
        int high = size() - 1;
        while (low <= high) {
            int mid = (low + high) >> 1;
            TreeEntry midVal = getTreeEntry(mid);
            int cmp = midVal.compareTo(newEntry);
            if (cmp < 0) {
                low = mid + 1;
            } else if (cmp > 0) {
                high = mid - 1;
            }
        }
        insertTreeEntry(low, newEntry);
        return low;
    }

    private void insertTreeEntry(int index, TreeEntry entry) throws IOException {
        int p = index - 1;
        int n = index;
        TreeEntry prevEntry = (p >= 0 && p < treeEntries.size()) ? treeEntries.get(p) : null;
        TreeEntry nextEntry = (n >= 0 && n < treeEntries.size()) ? treeEntries.get(n) : null;
        if (prevEntry != null) {
            if (prevEntry.getNextPageId() == entry.getNextPageId()) {
                prevEntry.setNextPageId(TreeEntry.NOT_SET);
            }
            if (entry.getPrevPageId() == TreeEntry.NOT_SET) {
                entry.setPrevPageId(prevEntry.getNextPageId());
            }
        }
        if (nextEntry != null) {
            if (nextEntry.getPrevPageId() == entry.getPrevPageId()) {
                nextEntry.setPrevPageId(TreeEntry.NOT_SET);
            }
            if (entry.getNextPageId() == TreeEntry.NOT_SET) {
                entry.setNextPageId(nextEntry.getPrevPageId());
            }
        }
        addTreeEntry(index, entry);
    }

    private void resetPageReference(int index, long pageId) {
        int p = index - 1;
        int n = index;
        TreeEntry prevEntry = (p >= 0 && p < treeEntries.size()) ? treeEntries.get(p) : null;
        TreeEntry nextEntry = (n >= 0 && n < treeEntries.size()) ? treeEntries.get(n) : null;
        if (prevEntry != null) {
            if (prevEntry.getNextPageId() == pageId) {
                prevEntry.setNextPageId(TreeEntry.NOT_SET);
            }
        }
        if (nextEntry != null) {
            if (nextEntry.getPrevPageId() == pageId) {
                nextEntry.setPrevPageId(TreeEntry.NOT_SET);
            }
        }
    }

    private boolean resetPageReference(long pageId) {
        boolean updated = false;
        for (TreeEntry entry : treeEntries) {
            if (entry.getPrevPageId() == pageId) {
                entry.setPrevPageId(TreeEntry.NOT_SET);
                updated = true;
            }
            if (entry.getNextPageId() == pageId) {
                entry.setNextPageId(TreeEntry.NOT_SET);
                updated = true;
            }
        }
        return updated;
    }

    private void resetParentId(long newParentId, List<TreeEntry> entries) throws IOException {
        Set<Long> set = new HashSet<Long>();
        for (TreeEntry entry : entries) {
            if (entry != null) {
                set.add(entry.getPrevPageId());
                set.add(entry.getNextPageId());
            }
        }
        for (Long pageId : set) {
            TreePage page = tree.lookupPage(pageId);
            if (page != null) {
                page.setParentId(newParentId);
            }
        }
    }

    private void addTreeEntry(int index, TreeEntry entry) throws IOException {
        treeEntries.add(index, entry);
    }

    private TreeEntry removeTreeEntry(int index) throws IOException {
        TreeEntry result = treeEntries.remove(index);
        return result;
    }

    private void removeAllTreeEntries(List<TreeEntry> c) {
        treeEntries.removeAll(c);
    }

    private List<TreeEntry> getSubList(int from, int to) {
        return new ArrayList<TreeEntry>(treeEntries.subList(from, to));
    }

    private TreeEntry getTreeEntry(int index) {
        TreeEntry result = treeEntries.get(index);
        return result;
    }

    void saveHeader() throws IOException {
        tree.writePage(this);
    }

    void save() throws IOException {
        tree.writeFullPage(this);
    }

    protected void dump() throws IOException {
        LOG.info(this.toString());
        Set<Long> set = new HashSet<Long>();
        for (TreeEntry entry : treeEntries) {
            if (entry != null) {
                LOG.info(entry.toString());
                set.add(entry.getPrevPageId());
                set.add(entry.getNextPageId());
            }
        }
        for (Long pageId : set) {
            TreePage page = tree.lookupPage(pageId);
            if (page != null) {
                page.dump();
            }
        }
    }
}
