// Generic Geometry Library
//
// Boost.SpatialIndex - rtree implementation
//
// Copyright 2008 Federico J. Fernandez.
// Use, modification and distribution is subject to the Boost Software License,
// Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at
// http://www.boost.org/LICENSE_1_0.txt)

#ifndef GGL_EXTENSIONS_INDEX_RTREE_RTREE_HPP
#define GGL_EXTENSIONS_INDEX_RTREE_RTREE_HPP

#include <cstddef>
#include <iostream> // TODO: Remove if print() is removed
#include <stdexcept>
#include <utility>
#include <vector>

#include <boost/concept_check.hpp>
#include <boost/shared_ptr.hpp>

#include <ggl/algorithms/area.hpp>

#include <ggl/extensions/index/rtree/rtree_node.hpp>
#include <ggl/extensions/index/rtree/rtree_leaf.hpp>

namespace ggl { namespace index {

template <typename Box, typename Value >
class rtree
{
public:

    typedef boost::shared_ptr<rtree_node<Box, Value> > node_pointer;
    typedef boost::shared_ptr<rtree_leaf<Box, Value> > leaf_pointer;

    /**
     * \brief Creates a rtree with 'maximum' elements per node and 'minimum'.
     */
    rtree(unsigned int const& maximum, unsigned int const& minimum)
        : m_count(0)
        , m_min_elems_per_node(minimum)
        , m_max_elems_per_node(maximum)
        , m_root(new rtree_node<Box, Value>(node_pointer(), 1))
    {
    }

    /**
     * \brief Creates a rtree with maximum elements per node
     *        and minimum (box is ignored).
     */
    rtree(Box const& box, unsigned int const& maximum, unsigned int const& minimum)
        : m_count(0)
        , m_min_elems_per_node(minimum)
        , m_max_elems_per_node(maximum)
        , m_root(new rtree_node<Box, Value>(node_pointer(), 1))
    {
        boost::ignore_unused_variable_warning(box);
    }

    /**
     * \brief destructor (virtual because we have virtual functions)
     */
    virtual ~rtree() {}


    /**
     * \brief Remove elements inside the 'box'
     */
    inline void remove(Box const& box)
    {
        try
        {
            node_pointer leaf(choose_exact_leaf(box));
            typename rtree_leaf<Box, Value>::leaf_map q_leaves;

            leaf->remove(box);

            if (leaf->elements() < m_min_elems_per_node && elements() > m_min_elems_per_node)
            {
                q_leaves = leaf->get_leaves();

                // we remove the leaf_node in the parent node because now it's empty
                leaf->get_parent()->remove(leaf->get_parent()->get_box(leaf));
            }

            typename rtree_node<Box, Value>::node_map q_nodes;
            condense_tree(leaf, q_nodes);

            std::vector<std::pair<Box, Value> > s;
            for (typename rtree_node<Box, Value>::node_map::const_iterator it = q_nodes.begin();
                 it != q_nodes.end(); ++it)
            {
                typename rtree_leaf<Box, Value>::leaf_map leaves = it->second->get_leaves();

                // reinserting leaves from nodes
                for (typename rtree_leaf<Box, Value>::leaf_map::const_iterator itl = leaves.begin();
                     itl != leaves.end(); ++itl)
                {
                    s.push_back(*itl);
                }
            }

            for (typename std::vector<std::pair<Box, Value> >::const_iterator it = s.begin(); it != s.end(); ++it)
            {
                m_count--;
                insert(it->first, it->second);
            }

            // if the root has only one child and the child is not a leaf,
            // make it the root
            if (m_root->elements() == 1)
            {
                if (!m_root->first_element()->is_leaf())
                {
                    m_root = m_root->first_element();
                }
            }
            // reinserting leaves
            for (typename rtree_leaf<Box, Value>::leaf_map::const_iterator it = q_leaves.begin();
                 it != q_leaves.end(); ++it)
            {
                m_count--;
                insert(it->first, it->second);
            }

            m_count--;
        }
        catch(std::logic_error & e)
        {
            // TODO: mloskot - replace with ggl exception

            // not found
            std::cerr << e.what() << std::endl;
            return;
        }
    }

    /**
     * \brief Remove element inside the box with value
     */
    void remove(Box const& box, Value const& value)
    {
        try
        {
            node_pointer leaf;

            // find possible leaves
            typedef typename std::vector<node_pointer > node_type;
            node_type nodes;
            m_root->find_leaves(box, nodes);

            // refine the result
            for (typename node_type::const_iterator it = nodes.begin(); it != nodes.end(); ++it)
            {
                leaf = *it;
                try
                {
                    leaf->remove(value);
                    break;
                } catch (...)
                {
                    leaf = node_pointer();
                }
            }

            if (!leaf)
                return;

            typename rtree_leaf < Box, Value >::leaf_map q_leaves;

            if (leaf->elements() < m_min_elems_per_node && elements() > m_min_elems_per_node)
            {
                q_leaves = leaf->get_leaves();

                // we remove the leaf_node in the parent node because now it's empty
                leaf->get_parent()->remove(leaf->get_parent()->get_box(leaf));
            }

            typename rtree_node<Box, Value>::node_map q_nodes;
            condense_tree(leaf, q_nodes);

            std::vector<std::pair<Box, Value> > s;
            for (typename rtree_node<Box, Value>::node_map::const_iterator it = q_nodes.begin();
                 it != q_nodes.end(); ++it)
            {
                typename rtree_leaf<Box, Value>::leaf_map leaves = it->second->get_leaves();

                // reinserting leaves from nodes
                for (typename rtree_leaf<Box, Value>::leaf_map::const_iterator itl = leaves.begin();
                     itl != leaves.end(); ++itl)
                {
                    s.push_back(*itl);
                }
            }

            for (typename std::vector<std::pair<Box, Value> >::const_iterator it = s.begin(); it != s.end(); ++it)
            {
                m_count--;
                insert(it->first, it->second);
            }

            // if the root has only one child and the child is not a leaf,
            // make it the root
            if (m_root->elements() == 1)
            {
                if (!m_root->first_element()->is_leaf())
                {
                    m_root = m_root->first_element();
                }
            }

            // reinserting leaves
            for (typename rtree_leaf<Box, Value>::leaf_map::const_iterator it = q_leaves.begin();
                 it != q_leaves.end(); ++it)
            {
                m_count--;
                insert(it->first, it->second);
            }

            m_count--;

        }
        catch(std::logic_error & e)
        {
            // TODO: mloskot - ggl exception

            // not found
            std::cerr << e.what() << std::endl;
            return;
        }
    }

    /**
     * \brief Returns the number of elements.
     */
    inline unsigned int elements() const
    {
        return m_count;
    }


    /**
     * \brief Inserts an element with 'box' as key with value.
     */
    inline void insert(Box const& box, Value const& value)
    {
        m_count++;

        node_pointer leaf(choose_corresponding_leaf(box));

        // check if the selected leaf is full to do the split if necessary
        if (leaf->elements() >= m_max_elems_per_node)
        {
            leaf->insert(box, value);

            // split!
            node_pointer n1(new rtree_leaf<Box, Value>(leaf->get_parent()));
            node_pointer n2(new rtree_leaf<Box, Value>(leaf->get_parent()));

            split_node(leaf, n1, n2);
            adjust_tree(leaf, n1, n2);
        }
        else
        {
            leaf->insert(box, value);
            adjust_tree(leaf);
        }
    }


    /**
     * \brief Returns all the values inside 'box'
     */
    inline std::deque<Value> find(Box const& box) const
    {
        std::deque<Value> result;
        m_root->find(box, result, false);
        return result;
    }

    /**
     * \brief Print Rtree (mainly for debug)
     */
    inline void print()
    {
        std::cerr << "===================================" << std::endl;
        std::cerr << " Min/Max: " << m_min_elems_per_node << " / " << m_max_elems_per_node << std::endl;
        std::cerr << "Leaves: " << m_root->get_leaves().size() << std::endl;
        m_root->print();
        std::cerr << "===================================" << std::endl;
    }

private:

    /// number of elements
    unsigned int m_count;

    /// minimum number of elements per node
    unsigned int m_min_elems_per_node;

    /// maximum number of elements per node
    unsigned int m_max_elems_per_node;

    /// tree root
    node_pointer m_root;

    /**
     * \brief Reorganize the tree after a removal. It tries to
     *        join nodes with less elements than m.
     */
    void condense_tree(node_pointer const& leaf,
        typename rtree_node<Box, Value>::node_map& q_nodes)
    {
        if (leaf.get() == m_root.get())
        {
            // if it's the root we are done
            return;
        }

        node_pointer parent = leaf->get_parent();
        parent->adjust_box(leaf);

        if (parent->elements() < m_min_elems_per_node)
        {
            if (parent.get() == m_root.get())
            {
                // if the parent is underfull and it's the root we just exit
                return;
            }

            // get the nodes that we should reinsert
            typename rtree_node<Box, Value>::node_map this_nodes = parent->get_nodes();
            for(typename rtree_node<Box, Value>::node_map::const_iterator it = this_nodes.begin();
                it != this_nodes.end(); ++it)
            {
                q_nodes.push_back(*it);
            }

            // we remove the node in the parent node because now it should be
            // re inserted
            parent->get_parent()->remove(parent->get_parent()->get_box(parent));
        }

        condense_tree(parent, q_nodes);
    }

    /**
     * \brief After an insertion splits nodes with more than 'maximum' elements.
     */
    inline void adjust_tree(node_pointer& node)
    {
        if (node.get() == m_root.get())
        {
            // we finished the adjust
            return;
        }

        // as there are no splits just adjust the box of the parent and go on
        node_pointer parent = node->get_parent();
        parent->adjust_box(node);
        adjust_tree(parent);
    }

    /**
     * \brief After an insertion splits nodes with more than maximum elements
     *        (recursive step with subtrees 'n1' and 'n2' to be joined).
     */
    void adjust_tree(node_pointer& leaf, node_pointer& n1, node_pointer& n2)
    {
        // check if we are in the root and do the split
        if (leaf.get() == m_root.get())
        {
            node_pointer new_root(new rtree_node<Box,Value>(node_pointer (), leaf->get_level() + 1));
            new_root->add_node(n1->compute_box(), n1);
            new_root->add_node(n2->compute_box(), n2);

            n1->set_parent(new_root);
            n2->set_parent(new_root);

            n1->update_parent(n1);
            n2->update_parent(n2);

            m_root = new_root;
            return;
        }

        node_pointer parent = leaf->get_parent();

        parent->replace_node(leaf, n1);
        parent->add_node(n2->compute_box(), n2);

        // if parent is full, split and readjust
        if (parent->elements() > m_max_elems_per_node)
        {
            node_pointer p1(new rtree_node<Box, Value>(parent->get_parent(), parent->get_level()));
            node_pointer p2(new rtree_node<Box, Value>(parent->get_parent(), parent->get_level()));

            split_node(parent, p1, p2);
            adjust_tree(parent, p1, p2);
        }
        else
        {
            adjust_tree(parent);
        }
    }

    /**
     * \brief Splits 'n' in 'n1' and 'n2'
     */
    void split_node(node_pointer const& n, node_pointer& n1, node_pointer& n2) const
    {
        unsigned int seed1 = 0;
        unsigned int seed2 = 0;
        std::vector<Box> boxes = n->get_boxes();

        n1->set_parent(n->get_parent());
        n2->set_parent(n->get_parent());

        linear_pick_seeds(n, seed1, seed2);

        if (n->is_leaf())
        {
            n1->add_value(boxes[seed1], n->get_value(seed1));
            n2->add_value(boxes[seed2], n->get_value(seed2));
        }
        else
        {
            n1->add_node(boxes[seed1], n->get_node(seed1));
            n2->add_node(boxes[seed2], n->get_node(seed2));
        }

        unsigned int index = 0;

        if (n->is_leaf())
        {
            // TODO: mloskot - add assert(node.size() >= 2); or similar

            typename rtree_leaf<Box, Value>::leaf_map nodes = n->get_leaves();
            unsigned int remaining = nodes.size() - 2;

            for (typename rtree_leaf<Box, Value>::leaf_map::const_iterator it = nodes.begin();
                 it != nodes.end(); ++it, index++)
            {
                if (index != seed1 && index != seed2)
                {
                    if (n1->elements() + remaining == m_min_elems_per_node)
                    {
                        n1->add_value(it->first, it->second);
                        continue;
                    }
                    if (n2->elements() + remaining == m_min_elems_per_node)
                    {
                        n2->add_value(it->first, it->second);
                        continue;
                    }

                    remaining--;

                    /// current boxes of each group
                    Box b1, b2;

                    /// enlarged boxes of each group
                    Box eb1, eb2;
                    b1 = n1->compute_box();
                    b2 = n2->compute_box();

                    /// areas
                    double b1_area, b2_area;
                    double eb1_area, eb2_area;
                    b1_area = ggl::area(b1);
                    b2_area = ggl::area(b2);
                    eb1_area = compute_union_area(b1, it->first);
                    eb2_area = compute_union_area(b2, it->first);

                    if (eb1_area - b1_area > eb2_area - b2_area)
                    {
                        n2->add_value(it->first, it->second);
                    }
                    if (eb1_area - b1_area < eb2_area - b2_area)
                    {
                        n1->add_value(it->first, it->second);
                    }
                    if (eb1_area - b1_area == eb2_area - b2_area)
                    {
                        if (b1_area < b2_area)
                        {
                            n1->add_value(it->first, it->second);
                        }
                        if (b1_area > b2_area)
                        {
                            n2->add_value(it->first, it->second);
                        }
                        if (b1_area == b2_area)
                        {
                            if (n1->elements() > n2->elements())
                            {
                                n2->add_value(it->first, it->second);
                            }
                            else
                            {
                                n1->add_value(it->first, it->second);
                            }
                        }
                    }
                }
            }
        }
        else
        {
            // TODO: mloskot - add assert(node.size() >= 2); or similar

            typename rtree_node<Box, Value>::node_map nodes = n->get_nodes();
            unsigned int remaining = nodes.size() - 2;
            for(typename rtree_node<Box, Value>::node_map::const_iterator it = nodes.begin();
                it != nodes.end(); ++it, index++)
            {

                if (index != seed1 && index != seed2)
                {

                    if (n1->elements() + remaining == m_min_elems_per_node)
                    {
                        n1->add_node(it->first, it->second);
                        continue;
                    }
                    if (n2->elements() + remaining == m_min_elems_per_node)
                    {
                        n2->add_node(it->first, it->second);
                        continue;
                    }

                    remaining--;

                    /// current boxes of each group
                    Box b1, b2;

                    /// enlarged boxes of each group
                    Box eb1, eb2;
                    b1 = n1->compute_box();
                    b2 = n2->compute_box();

                    /// areas
                    double b1_area, b2_area;
                    double eb1_area, eb2_area;
                    b1_area = ggl::area(b1);
                    b2_area = ggl::area(b2);

                    eb1_area = compute_union_area(b1, it->first);
                    eb2_area = compute_union_area(b2, it->first);

                    if (eb1_area - b1_area > eb2_area - b2_area)
                    {
                        n2->add_node(it->first, it->second);
                    }
                    if (eb1_area - b1_area < eb2_area - b2_area)
                    {
                        n1->add_node(it->first, it->second);
                    }
                    if (eb1_area - b1_area == eb2_area - b2_area)
                    {
                        if (b1_area < b2_area)
                        {
                            n1->add_node(it->first, it->second);
                        }
                        if (b1_area > b2_area)
                        {
                            n2->add_node(it->first, it->second);
                        }
                        if (b1_area == b2_area)
                        {
                            if (n1->elements() > n2->elements())
                            {
                                n2->add_node(it->first, it->second);
                            }
                            else
                            {
                                n1->add_node(it->first, it->second);
                            }
                        }
                    }

                }
            }
        }
    }

    /**
     * \brief Choose initial values for the split algorithm (linear version)
     */
    void linear_pick_seeds(node_pointer const& n, unsigned int &seed1, unsigned int &seed2) const
    {
        // get boxes from the node
        std::vector<Box>boxes = n->get_boxes();
        if (boxes.size() == 0)
        {
            // TODO: mloskot - throw ggl exception
            throw std::logic_error("Empty Node trying to Pick Seeds");
        }

        // only two dim for now
        // unsigned int dimensions =
        //   ggl::point_traits<Point>::coordinate_count;

        // find the first two elements
        double separation_x, separation_y;
        unsigned int first_x, second_x;
        unsigned int first_y, second_y;
        find_normalized_separations<0u>(boxes, separation_x, first_x, second_x);
        find_normalized_separations<1u>(boxes, separation_y, first_y, second_y);

        if (separation_x > separation_y)
        {
            seed1 = first_x;
            seed2 = second_x;
        }
        else
        {
            seed1 = first_y;
            seed2 = second_y;
        }
    }

    /**
     * \brief Find distances between possible initial values for the
     *        pick_seeds algorithm.
     */
    template <std::size_t D>
    void find_normalized_separations(const std::vector<Box>& boxes, double &separation,
        unsigned int &first, unsigned int &second) const
    {
        if (boxes.size() < 2)
        {
            throw std::logic_error("At least two boxes needed to split");
        }

        // find the lowest high
        typename std::vector<Box>::const_iterator it = boxes.begin();
        double lowest_high = ggl::get<max_corner, D>(*it);
        unsigned int lowest_high_index = 0;
        unsigned int index = 1;
        ++it;
        for(; it != boxes.end(); ++it)
        {
            if (ggl::get<max_corner, D>(*it) < lowest_high)
            {
                lowest_high = ggl::get<max_corner, D>(*it);
                lowest_high_index = index;
            }
            index++;
        }

        // find the highest low
        double highest_low = 0;
        unsigned int highest_low_index = 0;
        if (lowest_high_index == 0)
        {
            highest_low = ggl::get<min_corner, D>(boxes[1]);
            highest_low_index = 1;
        }
        else
        {
            highest_low = ggl::get<min_corner, D>(boxes[0]);
            highest_low_index = 0;
        }

        index = 0;
        for (typename std::vector<Box>::const_iterator it = boxes.begin();
             it != boxes.end(); ++it, index++)
        {
            if (ggl::get<min_corner, D>(*it) >= highest_low && index != lowest_high_index)
            {
                highest_low = ggl::get<min_corner, D>(*it);
                highest_low_index = index;
            }
        }

        // find the lowest low
        it = boxes.begin();
        double lowest_low = ggl::get<min_corner, D>(*it);
        ++it;
        for(; it != boxes.end(); ++it)
        {
            if (ggl::get<min_corner, D>(*it) < lowest_low)
            {
                lowest_low = ggl::get<min_corner, D>(*it);
            }
        }

        // find the highest high
        it = boxes.begin();
        double highest_high = ggl::get<max_corner, D>(*it);
        ++it;
        for(; it != boxes.end(); ++it)
        {
            if (ggl::get<max_corner, D>(*it) > highest_high)
            {
                highest_high = ggl::get<max_corner, D>(*it);
            }
        }

        double const width = highest_high - lowest_low;

        separation = (highest_low - lowest_high) / width;
        first = highest_low_index;
        second = lowest_high_index;
    }

    /**
     * \brief Choose one of the possible leaves to make an insertion
     */
    inline node_pointer choose_corresponding_leaf(Box const& e)
    {
        node_pointer node = m_root;

        // if the tree is empty add an initial leaf
        if (m_root->elements() == 0)
        {
            leaf_pointer new_leaf(new rtree_leaf<Box, Value>(m_root));
            m_root->add_leaf_node(Box (), new_leaf);

            return new_leaf;
        }

        while (!node->is_leaf())
        {
            /// traverse node's map to see which node we should select
            node = node->choose_node(e);
        }
        return node;
    }

    /**
     * \brief Choose the exact leaf where an insertion should be done
     */
    node_pointer choose_exact_leaf(Box const&e) const
    {
        // find possible leaves
        typedef typename std::vector<node_pointer> node_type;
        node_type nodes;
        m_root->find_leaves(e, nodes);

        // refine the result
        for (typename node_type::const_iterator it = nodes.begin(); it != nodes.end(); ++it)
        {
            typedef std::vector<std::pair<Box, Value> > leaves_type;
            leaves_type leaves = (*it)->get_leaves();

            for (typename leaves_type::const_iterator itl = leaves.begin();
                 itl != leaves.end(); ++itl)
            {

                if (itl->first.max_corner() == e.max_corner()
                    && itl->first.min_corner() == e.min_corner())
                {
                    return *it;
                }
            }
        }

        // TODO: mloskot - ggl exception
        throw std::logic_error("Leaf not found");
    }
};

}} // namespace ggl::index

#endif // GGL_EXTENSIONS_INDEX_RTREE_RTREE_HPP

