/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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/. */

#ifndef SystemTimeConverter_h
#define SystemTimeConverter_h

#include <limits>
#include <type_traits>
#include "mozilla/TimeStamp.h"

namespace mozilla {

// Utility class that converts time values represented as an unsigned integral
// number of milliseconds from one time source (e.g. a native event time) to
// corresponding mozilla::TimeStamp objects.
//
// This class handles wrapping of integer values and skew between the time
// source and mozilla::TimeStamp values.
//
// It does this by using an historical reference time recorded in both time
// scales (i.e. both as a numerical time value and as a TimeStamp).
//
// For performance reasons, this class is careful to minimize calls to the
// native "current time" function (e.g. gdk_x11_server_get_time) since this can
// be slow.
template <typename Time, typename TimeStampNowProvider = TimeStamp>
class SystemTimeConverter {
 public:
  SystemTimeConverter()
      : mReferenceTime(Time(0)),
        mReferenceTimeStamp()  // Initializes to the null timestamp
        ,
        mLastBackwardsSkewCheck(Time(0)),
        kTimeRange(std::numeric_limits<Time>::max()),
        kTimeHalfRange(kTimeRange / 2),
        kBackwardsSkewCheckInterval(Time(2000)) {
    static_assert(!std::is_signed_v<Time>, "Expected Time to be unsigned");
  }

  template <typename CurrentTimeGetter>
  mozilla::TimeStamp GetTimeStampFromSystemTime(
      Time aTime, CurrentTimeGetter& aCurrentTimeGetter) {
    TimeStamp roughlyNow = TimeStampNowProvider::Now();

    // If the reference time is not set, use the current time value to fill
    // it in.
    if (mReferenceTimeStamp.IsNull()) {
      // This sometimes happens when ::GetMessageTime returns 0 for the first
      // message on Windows.
      if (!aTime) return roughlyNow;
      UpdateReferenceTime(aTime, aCurrentTimeGetter);
    }

    // Check for skew between the source of Time values and TimeStamp values.
    // We do this by comparing two durations (both in ms):
    //
    // i.  The duration from the reference time to the passed-in time.
    //     (timeDelta in the diagram below)
    // ii. The duration from the reference timestamp to the current time
    //     based on TimeStamp::Now.
    //     (timeStampDelta in the diagram below)
    //
    // Normally, we'd expect (ii) to be slightly larger than (i) to account
    // for the time taken between generating the event and processing it.
    //
    // If (ii) - (i) is negative then the source of Time values is getting
    // "ahead" of TimeStamp. We call this "forwards" skew below.
    //
    // For the reverse case, if (ii) - (i) is positive (and greater than some
    // tolerance factor), then we may have "backwards" skew. This is often
    // the case when we have a backlog of events and by the time we process
    // them, the time given by the system is comparatively "old".
    //
    // The IsNewerThanTimestamp function computes the equivalent of |aTime| in
    // the TimeStamp scale and returns that in |timeAsTimeStamp|.
    //
    // Graphically:
    //
    //                    mReferenceTime              aTime
    // Time scale:      ........+.......................*........
    //                          |--------timeDelta------|
    //
    //                  mReferenceTimeStamp             roughlyNow
    // TimeStamp scale: ........+...........................*....
    //                          |------timeStampDelta-------|
    //
    //                                                  |---|
    //                                         roughlyNow-timeAsTimeStamp
    //
    TimeStamp timeAsTimeStamp;
    bool newer = IsTimeNewerThanTimestamp(aTime, roughlyNow, &timeAsTimeStamp);

    // Tolerance when detecting clock skew.
    static const TimeDuration kTolerance = TimeDuration::FromMilliseconds(30.0);

    // Check for forwards skew
    if (newer) {
      // Make aTime correspond to roughlyNow
      UpdateReferenceTime(aTime, roughlyNow);

      // We didn't have backwards skew so don't bother checking for
      // backwards skew again for a little while.
      mLastBackwardsSkewCheck = aTime;

      return roughlyNow;
    }

    if (roughlyNow - timeAsTimeStamp <= kTolerance) {
      // If the time between event times and TimeStamp values is within
      // the tolerance then assume we don't have clock skew so we can
      // avoid checking for backwards skew for a while.
      mLastBackwardsSkewCheck = aTime;
    } else if (aTime - mLastBackwardsSkewCheck > kBackwardsSkewCheckInterval) {
      aCurrentTimeGetter.GetTimeAsyncForPossibleBackwardsSkew(roughlyNow);
      mLastBackwardsSkewCheck = aTime;
    }

    // Finally, calculate the timestamp
    return timeAsTimeStamp;
  }

  void CompensateForBackwardsSkew(Time aReferenceTime,
                                  const TimeStamp& aLowerBound) {
    // Check if we actually have backwards skew. Backwards skew looks like
    // the following:
    //
    //        mReferenceTime
    // Time:      ..+...a...b...c..........................
    //
    //     mReferenceTimeStamp
    // TimeStamp: ..+.....a.....b.....c....................
    //
    // Converted
    // time:      ......a'..b'..c'.........................
    //
    // What we need to do is bring mReferenceTime "forwards".
    //
    // Suppose when we get (c), we detect possible backwards skew and trigger
    // an async request for the current time (which is passed in here as
    // aReferenceTime).
    //
    // We end up with something like the following:
    //
    //        mReferenceTime     aReferenceTime
    // Time:      ..+...a...b...c...v......................
    //
    //     mReferenceTimeStamp
    // TimeStamp: ..+.....a.....b.....c..........x.........
    //                                ^          ^
    //                          aLowerBound  TimeStamp::Now()
    //
    // If the duration (aLowerBound - mReferenceTimeStamp) is greater than
    // (aReferenceTime - mReferenceTime) then we know we have backwards skew.
    //
    // If that's not the case, then we probably just got caught behind
    // temporarily.
    if (IsTimeNewerThanTimestamp(aReferenceTime, aLowerBound, nullptr)) {
      return;
    }

    // We have backwards skew; the equivalent TimeStamp for aReferenceTime lies
    // somewhere between aLowerBound (which was the TimeStamp when we triggered
    // the async request for the current time) and TimeStamp::Now().
    //
    // If aReferenceTime was waiting in the event queue for a long time, the
    // equivalent TimeStamp might be much closer to aLowerBound than
    // TimeStamp::Now() so for now we just set it to aLowerBound. That's
    // guaranteed to be at least somewhat of an improvement.
    UpdateReferenceTime(aReferenceTime, aLowerBound);
  }

 private:
  template <typename CurrentTimeGetter>
  void UpdateReferenceTime(Time aReferenceTime,
                           const CurrentTimeGetter& aCurrentTimeGetter) {
    Time currentTime = aCurrentTimeGetter.GetCurrentTime();
    TimeStamp currentTimeStamp = TimeStampNowProvider::Now();
    Time timeSinceReference = currentTime - aReferenceTime;
    TimeStamp referenceTimeStamp =
        currentTimeStamp - TimeDuration::FromMilliseconds(timeSinceReference);
    UpdateReferenceTime(aReferenceTime, referenceTimeStamp);
  }

  void UpdateReferenceTime(Time aReferenceTime,
                           const TimeStamp& aReferenceTimeStamp) {
    mReferenceTime = aReferenceTime;
    mReferenceTimeStamp = aReferenceTimeStamp;
  }

  bool IsTimeNewerThanTimestamp(Time aTime, TimeStamp aTimeStamp,
                                TimeStamp* aTimeAsTimeStamp) {
    Time timeDelta = aTime - mReferenceTime;

    // Cast the result to signed 64-bit integer first since that should be
    // enough to hold the range of values returned by ToMilliseconds() and
    // the result of converting from double to an integer-type when the value
    // is outside the integer range is undefined.
    // Then we do an implicit cast to Time (typically an unsigned 32-bit
    // integer) which wraps times outside that range.
    TimeDuration timeStampDelta = (aTimeStamp - mReferenceTimeStamp);
    int64_t wholeMillis = static_cast<int64_t>(timeStampDelta.ToMilliseconds());
    Time wrappedTimeStampDelta = wholeMillis;  // truncate to unsigned

    Time timeToTimeStamp = wrappedTimeStampDelta - timeDelta;
    bool isNewer = false;
    if (timeToTimeStamp == 0) {
      // wholeMillis needs no adjustment
    } else if (timeToTimeStamp < kTimeHalfRange) {
      wholeMillis -= timeToTimeStamp;
    } else {
      isNewer = true;
      wholeMillis += (-timeToTimeStamp);
    }
    if (aTimeAsTimeStamp) {
      *aTimeAsTimeStamp =
          mReferenceTimeStamp + TimeDuration::FromMilliseconds(wholeMillis);
    }

    return isNewer;
  }

  Time mReferenceTime;
  TimeStamp mReferenceTimeStamp;
  Time mLastBackwardsSkewCheck;

  const Time kTimeRange;
  const Time kTimeHalfRange;
  const Time kBackwardsSkewCheckInterval;
};

}  // namespace mozilla

#endif /* SystemTimeConverter_h */
