#include "viewdata.hpp"

#include "quadtreenode.hpp"

#include <algorithm>

namespace Terrain
{

    ViewData::ViewData()
        : mNumEntries(0)
        , mLastUsageTimeStamp(0.0)
        , mChanged(false)
        , mHasViewPoint(false)
        , mWorldUpdateRevision(0)
    {
    }

    ViewData::~ViewData() {}

    void ViewData::copyFrom(const ViewData& other)
    {
        mNumEntries = other.mNumEntries;
        mEntries = other.mEntries;
        mChanged = other.mChanged;
        mHasViewPoint = other.mHasViewPoint;
        mViewPoint = other.mViewPoint;
        mActiveGrid = other.mActiveGrid;
        mWorldUpdateRevision = other.mWorldUpdateRevision;
        mNodes = other.mNodes;
    }

    void ViewData::add(QuadTreeNode* node)
    {
        unsigned int index = mNumEntries++;

        if (index + 1 > mEntries.size())
            mEntries.resize(index + 1);

        ViewDataEntry& entry = mEntries[index];
        if (entry.set(node))
        {
            mChanged = true;
            mNodes.clear();
        }
    }

    void ViewData::setViewPoint(const osg::Vec3f& viewPoint)
    {
        mViewPoint = viewPoint;
        mHasViewPoint = true;
    }

    // NOTE: As a performance optimisation, we cache mRenderingNodes from previous frames here.
    // If this cache becomes invalid (e.g. through mWorldUpdateRevision), we need to use clear() instead of reset().
    void ViewData::reset()
    {
        // clear any unused entries
        for (unsigned int i = mNumEntries; i < mEntries.size(); ++i)
            mEntries[i].set(nullptr);

        // reset index for next frame
        mNumEntries = 0;
        mChanged = false;
        mNodes.clear();
    }

    void ViewData::clear()
    {
        for (unsigned int i = 0; i < mEntries.size(); ++i)
            mEntries[i].set(nullptr);
        mNumEntries = 0;
        mLastUsageTimeStamp = 0;
        mChanged = false;
        mHasViewPoint = false;
        mNodes.clear();
    }

    bool ViewData::suitableToUse(const osg::Vec4i& activeGrid) const
    {
        return hasViewPoint() && activeGrid == mActiveGrid && getNumEntries();
    }

    bool ViewData::contains(const QuadTreeNode* node) const
    {
        return std::binary_search(mNodes.begin(), mNodes.end(), node);
    }

    void ViewData::buildNodeIndex()
    {
        if (!mNodes.empty())
            return;

        mNodes.reserve(mNumEntries);
        for (std::size_t i = 0; i < mNumEntries; ++i)
            if (const QuadTreeNode* node = mEntries[i].mNode)
                mNodes.push_back(node);

        std::sort(mNodes.begin(), mNodes.end());
    }

    void ViewData::removeNodeFromIndex(const QuadTreeNode* node)
    {
        const auto it = std::lower_bound(mNodes.begin(), mNodes.end(), node);
        if (it == mNodes.end() || *it != node)
            return;
        mNodes.erase(it);
    }

    ViewDataEntry::ViewDataEntry()
        : mNode(nullptr)
        , mLodFlags(0)
    {
    }

    bool ViewDataEntry::set(QuadTreeNode* node)
    {
        if (node == mNode)
            return false;
        else
        {
            mNode = node;
            // clear cached data
            mRenderingNode = nullptr;
            return true;
        }
    }

    ViewData* ViewDataMap::getViewData(
        osg::Object* viewer, const osg::Vec3f& viewPoint, const osg::Vec4i& activeGrid, bool& needsUpdate)
    {
        ViewerMap::const_iterator found = mViewers.find(viewer);
        ViewData* vd = nullptr;
        if (found == mViewers.end())
        {
            vd = createOrReuseView();
            mViewers[viewer] = vd;
        }
        else
            vd = found->second;
        needsUpdate = false;

        if (!(vd->suitableToUse(activeGrid)
                && (vd->getViewPoint() - viewPoint).length2() < mReuseDistance * mReuseDistance
                && vd->getWorldUpdateRevision() >= mWorldUpdateRevision))
        {
            float shortestDist = viewer ? mReuseDistance * mReuseDistance : std::numeric_limits<float>::max();
            const ViewData* mostSuitableView = nullptr;
            for (const ViewData* other : mUsedViews)
            {
                if (other->suitableToUse(activeGrid) && other->getWorldUpdateRevision() >= mWorldUpdateRevision)
                {
                    float dist = (viewPoint - other->getViewPoint()).length2();
                    if (dist < shortestDist)
                    {
                        shortestDist = dist;
                        mostSuitableView = other;
                    }
                }
            }
            if (mostSuitableView && mostSuitableView != vd)
            {
                vd->copyFrom(*mostSuitableView);
                return vd;
            }
            else if (!mostSuitableView)
            {
                if (vd->getWorldUpdateRevision() != mWorldUpdateRevision)
                {
                    vd->setWorldUpdateRevision(mWorldUpdateRevision);
                    vd->clear();
                }
                vd->setViewPoint(viewPoint);
                vd->setActiveGrid(activeGrid);
                needsUpdate = true;
            }
        }
        return vd;
    }

    ViewData* ViewDataMap::createOrReuseView()
    {
        ViewData* vd = nullptr;
        if (mUnusedViews.size())
        {
            vd = mUnusedViews.front();
            mUnusedViews.pop_front();
        }
        else
        {
            mViewVector.emplace_back();
            vd = &mViewVector.back();
        }
        mUsedViews.push_back(vd);
        vd->setWorldUpdateRevision(mWorldUpdateRevision);
        return vd;
    }

    ViewData* ViewDataMap::createIndependentView() const
    {
        ViewData* vd = new ViewData;
        vd->setWorldUpdateRevision(mWorldUpdateRevision);
        return vd;
    }

    void ViewDataMap::clearUnusedViews(double referenceTime)
    {
        for (ViewerMap::iterator it = mViewers.begin(); it != mViewers.end();)
        {
            if (it->second->getLastUsageTimeStamp() + mExpiryDelay < referenceTime)
                mViewers.erase(it++);
            else
                ++it;
        }
        for (std::deque<ViewData*>::iterator it = mUsedViews.begin(); it != mUsedViews.end();)
        {
            if ((*it)->getLastUsageTimeStamp() + mExpiryDelay < referenceTime)
            {
                (*it)->clear();
                mUnusedViews.push_back(*it);
                it = mUsedViews.erase(it);
            }
            else
                ++it;
        }
    }

    void ViewDataMap::rebuildViews()
    {
        ++mWorldUpdateRevision;
    }

}
