/**
 * @file llprimlinkinfo.h
 * @author andrew@lindenlab.com
 * @brief A template for determining which prims in a set are linkable
 *
 * $LicenseInfo:firstyear=2007&license=viewerlgpl$
 * Second Life Viewer Source Code
 * Copyright (C) 2010, Linden Research, Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation;
 * version 2.1 of the License only.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * Linden Research, Inc., 945 Battery Street, San Francisco, CA  94111  USA
 * $/LicenseInfo$
 */


#ifndef LL_PRIM_LINK_INFO_H
#define LL_PRIM_LINK_INFO_H

// system includes
#include <iostream>
#include <map>
#include <list>
#include <vector>

// common includes
#include "stdtypes.h"
#include "v3math.h"
#include "llquaternion.h"
#include "llsphere.h"


const F32 MAX_OBJECT_SPAN = 54.f;       // max distance from outside edge of an object to the farthest edge
const F32 OBJECT_SPAN_BONUS = 2.f;      // infinitesimally small prims can always link up to this distance
const S32 MAX_PRIMS_PER_OBJECT = 256;


template < typename DATA_TYPE >
class LLPrimLinkInfo
{
public:
    LLPrimLinkInfo();
    LLPrimLinkInfo( DATA_TYPE data, const LLSphere& sphere );
    ~LLPrimLinkInfo();

    void set( DATA_TYPE data, const LLSphere& sphere );
    void append( DATA_TYPE data, const LLSphere& sphere );
    void getData( std::list< DATA_TYPE >& data_list ) const;
    F32 getDiameter() const;
    LLVector3 getCenter() const;

    // returns 'true' if this info can link with other_info
    bool canLink( const LLPrimLinkInfo< DATA_TYPE >& other_info );

    S32 getPrimCount() const { return mDataMap.size(); }

    void mergeLinkableSet( typename std::list< LLPrimLinkInfo < DATA_TYPE > >& unlinked );

    void transform(const LLVector3& position, const LLQuaternion& rotation);

private:
    // returns number of merges made
    S32 merge(LLPrimLinkInfo< DATA_TYPE >& other_info);

    // returns number of collapses made
    static S32 collapse(typename std::list< LLPrimLinkInfo < DATA_TYPE > >& unlinked );

    void computeBoundingSphere();

    // Internal utility to encapsulate the link rules
    F32 get_max_linkable_span(const LLSphere& first, const LLSphere& second);
    F32 get_span(const LLSphere& first, const LLSphere& second);

private:
    std::map< DATA_TYPE, LLSphere > mDataMap;
    LLSphere mBoundingSphere;
};



template < typename DATA_TYPE >
LLPrimLinkInfo< DATA_TYPE >::LLPrimLinkInfo()
:   mBoundingSphere( LLVector3(0.f, 0.f, 0.f), 0.f )
{
}

template < typename DATA_TYPE >
LLPrimLinkInfo< DATA_TYPE >::LLPrimLinkInfo( DATA_TYPE data, const LLSphere& sphere)
:   mBoundingSphere(sphere)
{
    mDataMap[data] = sphere;
}

template < typename DATA_TYPE >
LLPrimLinkInfo< DATA_TYPE >::~LLPrimLinkInfo()
{
    mDataMap.clear();
}

template < typename DATA_TYPE >
void LLPrimLinkInfo< DATA_TYPE>::set( DATA_TYPE data, const LLSphere& sphere )
{
    if (!mDataMap.empty())
    {
        mDataMap.clear();
    }
    mDataMap[data] = sphere;
    mBoundingSphere = sphere;
}

template < typename DATA_TYPE >
void LLPrimLinkInfo< DATA_TYPE>::append( DATA_TYPE data, const LLSphere& sphere )
{
    mDataMap[data] = sphere;
    if (!mBoundingSphere.contains(sphere))
    {
        computeBoundingSphere();
    }
}

template < typename DATA_TYPE >
void LLPrimLinkInfo< DATA_TYPE >::getData( std::list< DATA_TYPE >& data_list) const
{
    typename std::map< DATA_TYPE, LLSphere >::const_iterator map_itr;
    for (map_itr = mDataMap.begin(); map_itr != mDataMap.end(); ++map_itr)
    {
        data_list.push_back(map_itr->first);
    }
}

template < typename DATA_TYPE >
F32 LLPrimLinkInfo< DATA_TYPE >::getDiameter() const
{
    return 2.f * mBoundingSphere.getRadius();
}

template < typename DATA_TYPE >
LLVector3 LLPrimLinkInfo< DATA_TYPE >::getCenter() const
{
    return mBoundingSphere.getCenter();
}

template < typename DATA_TYPE >
F32 LLPrimLinkInfo< DATA_TYPE >::get_max_linkable_span(const LLSphere& first, const LLSphere& second)
{
    F32 max_span = 3.f * (first.getRadius() + second.getRadius()) + OBJECT_SPAN_BONUS;
    if (max_span > MAX_OBJECT_SPAN)
    {
        max_span = MAX_OBJECT_SPAN;
    }

    return max_span;
}

template < typename DATA_TYPE >
F32 LLPrimLinkInfo< DATA_TYPE >::get_span(const LLSphere& first, const LLSphere& second)
{
    F32 span = (first.getCenter() - second.getCenter()).length()
                + first.getRadius() + second.getRadius();
    return span;
}

// static
// returns 'true' if this info can link with any part of other_info
template < typename DATA_TYPE >
bool LLPrimLinkInfo< DATA_TYPE >::canLink(const LLPrimLinkInfo& other_info)
{
    F32 max_span = get_max_linkable_span(mBoundingSphere, other_info.mBoundingSphere);

    F32 span = get_span(mBoundingSphere, other_info.mBoundingSphere);

    if (span <= max_span)
    {
        // The entire other_info fits inside the max span.
        return TRUE;
    }
    else if (span > max_span + 2.f * other_info.mBoundingSphere.getRadius())
    {
        // there is no way any piece of other_info could link with this one
        return FALSE;
    }

    // there may be a piece of other_info that is linkable
    typename std::map< DATA_TYPE, LLSphere >::const_iterator map_itr;
    for (map_itr = other_info.mDataMap.begin(); map_itr != other_info.mDataMap.end(); ++map_itr)
    {
        const LLSphere& other_sphere = (*map_itr).second;
        max_span = get_max_linkable_span(mBoundingSphere, other_sphere);

        span = get_span(mBoundingSphere, other_sphere);

        if (span <= max_span)
        {
            // found one piece that is linkable
            return TRUE;
        }
    }
    return FALSE;
}

// merges elements of 'unlinked'
// returns number of links made (NOT final prim count, NOR linked prim count)
// and removes any linkable infos from 'unlinked'
template < typename DATA_TYPE >
void LLPrimLinkInfo< DATA_TYPE >::mergeLinkableSet(std::list< LLPrimLinkInfo< DATA_TYPE > > & unlinked)
{
    bool linked_something = true;
    while (linked_something)
    {
        linked_something = false;

        typename std::list< LLPrimLinkInfo< DATA_TYPE > >::iterator other_itr = unlinked.begin();
        while ( other_itr != unlinked.end()
               && getPrimCount() < MAX_PRIMS_PER_OBJECT )
        {
            S32 merge_count = merge(*other_itr);
            if (merge_count > 0)
            {
                linked_something = true;
            }
            if (0 == (*other_itr).getPrimCount())
            {
                unlinked.erase(other_itr++);
            }
            else
            {
                ++other_itr;
            }
        }
        if (!linked_something
            && unlinked.size() > 1)
        {
            S32 collapse_count = collapse(unlinked);
            if (collapse_count > 0)
            {
                linked_something = true;
            }
        }
    }
}

// transforms all of the spheres into a new reference frame
template < typename DATA_TYPE >
void LLPrimLinkInfo< DATA_TYPE >::transform(const LLVector3& position, const LLQuaternion& rotation)
{
    typename std::map< DATA_TYPE, LLSphere >::iterator map_itr;
    for (map_itr = mDataMap.begin(); map_itr != mDataMap.end(); ++map_itr)
    {
        (*map_itr).second.setCenter((*map_itr).second.getCenter() * rotation + position);
    }
    mBoundingSphere.setCenter(mBoundingSphere.getCenter() * rotation + position);
}

// private
// returns number of links made
template < typename DATA_TYPE >
S32 LLPrimLinkInfo< DATA_TYPE >::merge(LLPrimLinkInfo& other_info)
{
    S32 link_count = 0;

//  F32 other_radius = other_info.mBoundingSphere.getRadius();
//  other_info.computeBoundingSphere();
//  if ( other_radius != other_info.mBoundingSphere.getRadius() )
//  {
//      LL_INFOS() << "Other bounding sphere changed!!" << LL_ENDL;
//  }

//  F32 this_radius = mBoundingSphere.getRadius();
//  computeBoundingSphere();
//  if ( this_radius != mBoundingSphere.getRadius() )
//  {
//      LL_INFOS() << "This bounding sphere changed!!" << LL_ENDL;
//  }


    F32 max_span = get_max_linkable_span(mBoundingSphere, other_info.mBoundingSphere);

    //  F32 center_dist = (mBoundingSphere.getCenter() - other_info.mBoundingSphere.getCenter()).length();
    //  LL_INFOS() << "objects are " << center_dist << "m apart" << LL_ENDL;
    F32 span = get_span(mBoundingSphere, other_info.mBoundingSphere);

    F32 span_limit = max_span + (2.f * other_info.mBoundingSphere.getRadius());
    if (span > span_limit)
    {
        // there is no way any piece of other_info could link with this one
        // LL_INFOS() << "span too large:  " << span << " vs. " << span_limit << LL_ENDL;
        return 0;
    }

    bool completely_linkable = (span <= max_span) ? true : false;

    typename std::map< DATA_TYPE, LLSphere >::iterator map_itr = other_info.mDataMap.begin();
    while (map_itr != other_info.mDataMap.end()
            && getPrimCount() < MAX_PRIMS_PER_OBJECT )
    {
        DATA_TYPE other_data = (*map_itr).first;
        LLSphere& other_sphere = (*map_itr).second;

        if (!completely_linkable)
        {
            max_span = get_max_linkable_span(mBoundingSphere, other_sphere);

            F32 span = get_span(mBoundingSphere, other_sphere);

            if (span > max_span)
            {
                ++map_itr;
                continue;
            }
        }

        mDataMap[other_data] = other_sphere;
        ++link_count;

        if (!mBoundingSphere.contains(other_sphere) )
        {
            computeBoundingSphere();
        }

        // remove from the other info
        other_info.mDataMap.erase(map_itr++);
    }

    if (link_count > 0 && other_info.getPrimCount() > 0)
    {
        other_info.computeBoundingSphere();
    }
    return link_count;
}

// links any linkable elements of unlinked
template < typename DATA_TYPE >
S32 LLPrimLinkInfo< DATA_TYPE >::collapse(std::list< LLPrimLinkInfo< DATA_TYPE > > & unlinked)
{
    S32 link_count = 0;
    bool linked_something = true;
    while (linked_something)
    {
        linked_something = false;

        typename std::list< LLPrimLinkInfo< DATA_TYPE > >::iterator this_itr = unlinked.begin();
        typename std::list< LLPrimLinkInfo< DATA_TYPE > >::iterator other_itr = this_itr;
        ++other_itr;
        while ( other_itr != unlinked.end() )

        {
            S32 merge_count = (*this_itr).merge(*other_itr);
            if (merge_count > 0)
            {
                linked_something = true;
                link_count += merge_count;
            }
            if (0 == (*other_itr).getPrimCount())
            {
                unlinked.erase(other_itr++);
            }
            else
            {
                ++other_itr;
            }
        }
    }
    return link_count;
}


template < typename DATA_TYPE >
void LLPrimLinkInfo< DATA_TYPE >::computeBoundingSphere()
{
    std::vector< LLSphere > sphere_list;
    typename std::map< DATA_TYPE, LLSphere >::const_iterator map_itr;
    for (map_itr = mDataMap.begin(); map_itr != mDataMap.end(); ++map_itr)
    {
        sphere_list.push_back(map_itr->second);
    }
    mBoundingSphere = LLSphere::getBoundingSphere(sphere_list);
}


#endif