/*
 * Copyright 2010-2014 Ning, Inc.
 *
 * Ning 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 com.ning.billing.invoice.tree;

import java.util.List;

import org.joda.time.LocalDate;

import com.ning.billing.invoice.api.InvoiceItem;

import com.google.common.collect.Lists;

public class NodeInterval {

    private LocalDate start;
    private LocalDate end;
    private ItemsInterval items;

    private NodeInterval parent;
    private NodeInterval leftChild;
    private NodeInterval rightSibling;

    public NodeInterval() {
        this.items = new ItemsInterval();
    }

    public NodeInterval(final NodeInterval parent, final InvoiceItem item) {
        this.start = item.getStartDate();
        this.end = item.getEndDate();
        this.items = new ItemsInterval(item);
        this.parent = parent;
        this.leftChild = null;
        this.rightSibling = null;
    }

    public void build(final List<InvoiceItem> output) {

        /*
        // Compute start and end
        if (isRoot()) {
            this.start = leftChild.getStart();
            NodeInterval cur = leftChild;
            while (cur.getRightSibling() != null) {
                cur = cur.getRightSibling();
            }
            this.end = cur.getEnd();
        }
        */

        // There is no sub-interval, just add our own items.
        if (leftChild == null) {
            items.build(output);
            return;
        }

        LocalDate curDate = start;
        NodeInterval curChild = leftChild;
        while (curChild != null) {
            if (curChild.getStart().compareTo(curDate) > 0) {
                output.add(items.createRecuringItem(curDate, curChild.getStart()));
            }
            curChild.build(output);
            curDate = curChild.getEnd();
            curChild = curChild.getRightSibling();
        }
        if (curDate.compareTo(end) < 0) {
            output.add(items.createRecuringItem(curDate, end));
        }
    }

    public boolean isItemContained(final InvoiceItem item) {
        return (item.getStartDate().compareTo(start) >= 0 &&
                item.getStartDate().compareTo(end) <= 0 &&
                item.getEndDate().compareTo(start) >= 0 &&
                item.getEndDate().compareTo(end) <= 0);
    }

    public boolean isItemOverlap(final InvoiceItem item) {
        return ((item.getStartDate().compareTo(start) < 0 &&
                 item.getEndDate().compareTo(end) >= 0) ||
                (item.getStartDate().compareTo(start) <= 0 &&
                 item.getEndDate().compareTo(end) > 0));
    }

    public void addItem(final InvoiceItem item) {

        if (!isRoot() && item.getStartDate().compareTo(start) == 0 && item.getEndDate().compareTo(end) == 0) {
            items.insertSortedItem(item);
            return;
        }

        final NodeInterval newNode = new NodeInterval(this, item);
        computeRootInterval(newNode);
        addNode(newNode);
    }

    private void addNode(final NodeInterval newNode) {
        final InvoiceItem item = newNode.getItems().get(0);
        if (leftChild == null) {
            leftChild = newNode;
            return;
        }

        NodeInterval prevChild = null;
        NodeInterval curChild = leftChild;
        do {
            if (curChild.isItemContained(item)) {
                curChild.addItem(item);
                return;
            }

            if (curChild.isItemOverlap(item)) {
                rebalance(newNode);
                return;
            }

            if (item.getStartDate().compareTo(curChild.getStart()) < 0) {
                newNode.rightSibling = curChild;
                if (prevChild == null) {
                    leftChild = newNode;
                } else {
                    prevChild.rightSibling = newNode;
                }
                return;
            }
            prevChild = curChild;
            curChild = curChild.rightSibling;
        } while (curChild != null);

        prevChild.rightSibling = newNode;
    }

    private void rebalance(final NodeInterval newNode) {

        final InvoiceItem invoiceItem = newNode.getItems().get(0);

        NodeInterval prevRebalanced = null;
        NodeInterval curChild = leftChild;
        List<NodeInterval> toBeRebalanced = Lists.newLinkedList();
        do {
            if (curChild.isItemOverlap(invoiceItem)) {
                toBeRebalanced.add(curChild);
            } else {
                if (toBeRebalanced.size() > 0) {
                    break;
                }
                prevRebalanced = curChild;
            }
            curChild = curChild.rightSibling;
        } while (curChild != null);

        newNode.rightSibling = toBeRebalanced.get(toBeRebalanced.size() - 1).rightSibling;
        if (prevRebalanced == null) {
            leftChild = newNode;
        } else {
            prevRebalanced.rightSibling = newNode;
        }

        for (NodeInterval cur : toBeRebalanced) {
            newNode.addNode(cur);
        }
    }

    private void computeRootInterval(final NodeInterval newNode) {
        if (!isRoot()) {
            return;
        }
        this.start = (start == null || start.compareTo(newNode.getStart()) > 0) ? newNode.getStart() : start;
        this.end = (end == null || end.compareTo(newNode.getEnd()) < 0) ? newNode.getEnd() : end;
    }

    public boolean isRoot() {
        return parent == null;
    }

    public LocalDate getStart() {
        return start;
    }

    public LocalDate getEnd() {
        return end;
    }

    public NodeInterval getParent() {
        return parent;
    }

    public NodeInterval getLeftChild() {
        return leftChild;
    }

    public NodeInterval getRightSibling() {
        return rightSibling;
    }

    public List<InvoiceItem> getItems() {
        return items.getItems();
    }

    @Override
    public boolean equals(final Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof NodeInterval)) {
            return false;
        }

        final NodeInterval that = (NodeInterval) o;

        if (end != null ? !end.equals(that.end) : that.end != null) {
            return false;
        }
        if (items != null ? !items.equals(that.items) : that.items != null) {
            return false;
        }
        if (leftChild != null ? !leftChild.equals(that.leftChild) : that.leftChild != null) {
            return false;
        }
        if (parent != null ? !parent.equals(that.parent) : that.parent != null) {
            return false;
        }
        if (rightSibling != null ? !rightSibling.equals(that.rightSibling) : that.rightSibling != null) {
            return false;
        }
        if (start != null ? !start.equals(that.start) : that.start != null) {
            return false;
        }

        return true;
    }

    @Override
    public int hashCode() {
        int result = start != null ? start.hashCode() : 0;
        result = 31 * result + (end != null ? end.hashCode() : 0);
        result = 31 * result + (items != null ? items.hashCode() : 0);
        result = 31 * result + (parent != null ? parent.hashCode() : 0);
        result = 31 * result + (leftChild != null ? leftChild.hashCode() : 0);
        result = 31 * result + (rightSibling != null ? rightSibling.hashCode() : 0);
        return result;
    }
}
