/** 
 * @file fsregioncross.cpp
 * @brief Improvements to region crossing display
 * @author nagle@animats.com
 *
 * $LicenseInfo:firstyear=2013&license=viewerlgpl$
 * Phoenix Firestorm Viewer Source Code
 * Copyright (C) 2020 Animats
 *
 * 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
 *
 * The Phoenix Firestorm Project, Inc., 1831 Oakwood Drive, Fairmont, Minnesota 56031-3225 USA
 * http://www.firestormviewer.org
 */

#include "llviewerprecompiledheaders.h"
#include "fsregioncross.h"
#include "llframetimer.h"
#include "llmath.h"
#include "llviewercontrol.h"
#include "llviewerobject.h"

//  Improved region crossing time limit prediction.
//
//  Applied when 
//  1) object has an avatar sitting on it
//  2) object has been seen to move since sat on
//  3) object is outside the 0..256m region boundaries.
//
//  In other words, a vehicle with an avatar doing a region crossing.
//
//  This computes a safe time limit for extrapolating vehicle position and 
//  rotation during a region crossing, when the simulator to simulator handoff
//  is in progress and no object updates are being received. Goal is to keep
//  the position and rotation errors within set bounds.
//
//  The smoother the movement of the vehicle approaching the sim crossing,
//  the longer the allowed extrapolation will be.
//
//
//  Constructor -- called when something sits on the object.
//
//  Potential vehicle, but might be just a chair.
//  We don't start the movement averaging until there's movement at least once.
//
RegionCrossExtrapolateImpl::RegionCrossExtrapolateImpl(const LLViewerObject& vo) :
    mOwner(vo),
    mPreviousUpdateTime(0),
    mMoved(false)
    {
        LL_INFOS() << "Object " << vo.getID().asString() << " has sitter." << LL_ENDL;
    }

//  update -- called for each object update message to "vehicle" objects.
//
//  This is called for any object with an avatar sitting on it.
//  If the object never moves, it's not a vehicle and we can skip this.
//  If it has moved, we treat it as a vehicle.
//
void RegionCrossExtrapolateImpl::update()
{
    if (!mMoved)
    {
        LLVector3 rawvel = mOwner.getVelocity();                // velocity in world coords
        if (rawvel.mV[VX] != 0.f || rawvel.mV[VY] != 0.f || rawvel.mV[VZ] != 0.f) // check for nonzero velocity
        {
            mMoved = true;                                      // moved, has seated avatar, thus is vehicle
        }
        else
        {
            return;                                             // sitting on stationary object, skip this
        }
    }

    //  Moving seat - do the extrapolation calculations
    F64 dt = 1.0 / 45.0;                                        // dt used on first value - one physics frame on server  
    F64 now = LLFrameTimer::getElapsedSeconds();
    if (mPreviousUpdateTime != 0.0)
    {
        dt = now - mPreviousUpdateTime;                         // change since last update
        //  Could adjust here for ping time and time dilation, but the filter isn't that
        //  sensitive to minor variations dt and it would just complicate things.
    }

    mPreviousUpdateTime = now;
    LLQuaternion rot = mOwner.getRotationRegion();                // transform in global coords
    const LLQuaternion& inverserot = rot.conjugate();             // transform global to local
    LLVector3 vel = mOwner.getVelocity() * inverserot;            // velocity in object coords
    LLVector3 angvel = mOwner.getAngularVelocity() * inverserot;  // angular velocity in object coords
    mFilteredVel.update(vel, dt);                                 // accum into filter in object coords
    mFilteredAngVel.update(angvel, dt);                           // accum into filter in object coords
}

//  dividesafe -- floating divide with divide by zero check
//
//  Returns infinity for a divide by near zero.
//
static inline F32 dividesafe(F32 num, F32 denom)
{
    return ((denom > FP_MAG_THRESHOLD || denom < -FP_MAG_THRESHOLD) // avoid divide by zero
        ? (num / denom) : std::numeric_limits<F32>::infinity());    // return infinity if zero divide
}

//  getextraptimelimit -- don't extrapolate further ahead than this during a region crossing.
//
//  Returns seconds of extrapolation that will probably stay within set limits of error.
//
F32 RegionCrossExtrapolateImpl::getextraptimelimit() const
{
    //  Debug tuning parameters. This code will try to limit the maximum position and angle error to the specified limits.
    //  The limits can be adjusted as debug symbols or in settings.xml, but that should not be necessary.
    static LLCachedControl<F32> fsRegionCrossingPositionErrorLimit(gSavedSettings, "FSRegionCrossingPositionErrorLimit");
    static LLCachedControl<F32> fsRegionCrossingAngleErrorLimit(gSavedSettings, "FSRegionCrossingAngleErrorLimit");   

    //  Time limit is max allowed error / error. Returns worst case (smallest) of vel and angular vel limits.
    LLQuaternion rot = mOwner.getRotationRegion();              // transform in global coords
    const LLQuaternion& inverserot = rot.conjugate();           // transform global to local

    //  Calculate safe extrapolation time limit.
    F32 extrapTimeLimit = llmin(dividesafe(fsRegionCrossingPositionErrorLimit, ((mOwner.getVelocity() * inverserot - mFilteredVel.get()).length())),
        dividesafe(fsRegionCrossingAngleErrorLimit, ((mOwner.getAngularVelocity() * inverserot - mFilteredAngVel.get()).length())));

    LL_INFOS() << "Region cross extrapolation safe limit " << extrapTimeLimit << " secs." << LL_ENDL;
    return extrapTimeLimit;                                                   // do not extrapolate more than this
}

//  ifsaton -- True if object is being sat upon.
//
//  Potential vehicle.
//
bool RegionCrossExtrapolate::ifsaton(const LLViewerObject& vo)  // true if root object and being sat on
{
    if (!vo.isRoot())
    {
        return false;                                           // not root, cannot be sat upon
    }

    LLViewerObject::const_child_list_t& children = vo.getChildren();
    for (const auto &child : children)
    {
        if (child->isAvatar())
        {
            return true;
        }
    }

    return false;                                               // no avatar children, not sat on
}

//  LowPassFilter  -- the low pass filter for smoothing velocities.
//
//  Works on vectors.
//
//  Filter function is scaled by 1- 1/((1+(1/filterval))^secs)
//  This is so samples which represent more time are weighted more.
//
//  The filterval function is a curve which goes through (0,0) and gradually
//  approaches 1 for larger values of secs.  Secs is the time since the last update.
//  We assume that the time covered by an update had reasonably uniform velocity
//  and angular velocity, since if those were changing rapidly, the server would send
//  more object updates.
//
//  Setting filter smoothing time to zero turns off the filter and results in the predictor
//  always returning a very large value and not affecting region crossing times.
//  So that's how to turn this off, if desired.
//
void LowPassFilter::update(const LLVector3& val, F32 secs)      // add new value into filter
{
    static LLCachedControl<F32> fsRegionCrossingSmoothingTime(gSavedSettings, "FSRegionCrossingSmoothingTime");
    if (!mInitialized)
    {
        mFiltered = val;                                        // just use new value
        mInitialized = true;
        return;
    }

    F32 filtermult = 1.0f;                                      // no filtering if zero filter time
    if (fsRegionCrossingSmoothingTime > F_ALMOST_ZERO)          // avoid divide by zero
    {
        filtermult = 1.0f - 1.0f / pow(1.0f + 1.0f / fsRegionCrossingSmoothingTime, secs); // filter scale factor
    }
    mFiltered = val * filtermult + mFiltered * (1.0f - filtermult);  // low pass filter
}
