/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "SimpleVelocityTracker.h"

#include "mozilla/ServoStyleConsts.h"  // for StyleComputedTimingFunction
#include "mozilla/StaticPrefs_apz.h"
#include "mozilla/StaticPtr.h"  // for StaticAutoPtr

static mozilla::LazyLogModule sApzSvtLog("apz.simplevelocitytracker");
#define SVT_LOG(...) MOZ_LOG(sApzSvtLog, LogLevel::Debug, (__VA_ARGS__))

namespace mozilla {
namespace layers {

// When we compute the velocity we do so by taking two input events and
// dividing the distance delta over the time delta. In some cases the time
// delta can be really small, which can make the velocity computation very
// volatile. To avoid this we impose a minimum time delta below which we do
// not recompute the velocity.
MOZ_RUNINIT const TimeDuration MIN_VELOCITY_SAMPLE_TIME =
    TimeDuration::FromMilliseconds(5);

extern StaticAutoPtr<StyleComputedTimingFunction> gVelocityCurveFunction;

SimpleVelocityTracker::SimpleVelocityTracker(Axis* aAxis)
    : mAxis(aAxis), mVelocitySamplePos(0) {}

void SimpleVelocityTracker::StartTracking(ParentLayerCoord aPos,
                                          TimeStamp aTimestamp) {
  Clear();
  mVelocitySampleTime = aTimestamp;
  mVelocitySamplePos = aPos;
}

Maybe<float> SimpleVelocityTracker::AddPosition(ParentLayerCoord aPos,
                                                TimeStamp aTimestamp) {
  if (aTimestamp <= mVelocitySampleTime + MIN_VELOCITY_SAMPLE_TIME) {
    // See also the comment on MIN_VELOCITY_SAMPLE_TIME.
    // We don't update either mVelocitySampleTime or mVelocitySamplePos so that
    // eventually when we do get an event with the required time delta we use
    // the corresponding distance delta as well.
    SVT_LOG("%p|%s skipping velocity computation for small time delta %f ms\n",
            mAxis->OpaqueApzcPointer(), mAxis->Name(),
            (aTimestamp - mVelocitySampleTime).ToMilliseconds());
    return Nothing();
  }

  float newVelocity =
      (float)(mVelocitySamplePos - aPos) /
      (float)(aTimestamp - mVelocitySampleTime).ToMilliseconds();

  newVelocity = ApplyFlingCurveToVelocity(newVelocity);

  SVT_LOG("%p|%s updating velocity to %f with touch\n",
          mAxis->OpaqueApzcPointer(), mAxis->Name(), newVelocity);
  mVelocitySampleTime = aTimestamp;
  mVelocitySamplePos = aPos;

  AddVelocityToQueue(aTimestamp, newVelocity);

  return Some(newVelocity);
}

Maybe<float> SimpleVelocityTracker::ComputeVelocity(TimeStamp aTimestamp) {
  float velocity = 0;
  int count = 0;
  for (const auto& e : mVelocityQueue) {
    TimeDuration timeDelta = (aTimestamp - e.first);
    if (timeDelta < TimeDuration::FromMilliseconds(
                        StaticPrefs::apz_velocity_relevance_time_ms())) {
      count++;
      velocity += e.second;
    }
  }
  mVelocityQueue.Clear();
  if (count > 1) {
    velocity /= count;
  }
  return Some(velocity);
}

void SimpleVelocityTracker::Clear() { mVelocityQueue.Clear(); }

void SimpleVelocityTracker::AddVelocityToQueue(TimeStamp aTimestamp,
                                               float aVelocity) {
  mVelocityQueue.AppendElement(std::make_pair(aTimestamp, aVelocity));
  if (mVelocityQueue.Length() >
      StaticPrefs::apz_max_velocity_queue_size_AtStartup()) {
    mVelocityQueue.RemoveElementAt(0);
  }
}

float SimpleVelocityTracker::ApplyFlingCurveToVelocity(float aVelocity) const {
  float newVelocity = aVelocity;
  if (StaticPrefs::apz_max_velocity_inches_per_ms() > 0.0f) {
    bool velocityIsNegative = (newVelocity < 0);
    newVelocity = fabs(newVelocity);

    float maxVelocity =
        mAxis->ToLocalVelocity(StaticPrefs::apz_max_velocity_inches_per_ms());
    newVelocity = std::min(newVelocity, maxVelocity);

    if (StaticPrefs::apz_fling_curve_threshold_inches_per_ms() > 0.0f &&
        StaticPrefs::apz_fling_curve_threshold_inches_per_ms() <
            StaticPrefs::apz_max_velocity_inches_per_ms()) {
      float curveThreshold = mAxis->ToLocalVelocity(
          StaticPrefs::apz_fling_curve_threshold_inches_per_ms());
      if (newVelocity > curveThreshold) {
        // here, 0 < curveThreshold < newVelocity <= maxVelocity, so we apply
        // the curve
        float scale = maxVelocity - curveThreshold;
        float funcInput = (newVelocity - curveThreshold) / scale;
        float funcOutput =
            gVelocityCurveFunction->At(funcInput, /* aBeforeFlag = */ false);
        float curvedVelocity = (funcOutput * scale) + curveThreshold;
        SVT_LOG("%p|%s curving up velocity from %f to %f\n",
                mAxis->OpaqueApzcPointer(), mAxis->Name(), newVelocity,
                curvedVelocity);
        newVelocity = curvedVelocity;
      }
    }

    if (velocityIsNegative) {
      newVelocity = -newVelocity;
    }
  }

  return newVelocity;
}

}  // namespace layers
}  // namespace mozilla
