/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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 <stddef.h>                     // for size_t
#include "Units.h"                      // for ScreenIntRect
#include "gfxRect.h"                    // for gfxRect
#include "gfxPrefs.h"                   // for gfxPrefs
#include "mozilla/gfx/Point.h"          // for IntSize, Point
#include "mozilla/gfx/Rect.h"           // for Rect
#include "mozilla/gfx/Types.h"          // for Color, SurfaceFormat
#include "mozilla/layers/Compositor.h"  // for Compositor
#include "mozilla/layers/CompositorTypes.h"
#include "mozilla/layers/Effects.h"     // for Effect, EffectChain, etc
#include "mozilla/TimeStamp.h"          // for TimeStamp, TimeDuration
#include "nsPoint.h"                    // for nsIntPoint
#include "nsRect.h"                     // for mozilla::gfx::IntRect
#include "nsIFile.h"                    // for nsIFile
#include "nsDirectoryServiceDefs.h"     // for NS_OS_TMP_DIR
#include "mozilla/Sprintf.h"
#include "FPSCounter.h"

namespace mozilla {
namespace layers {

using namespace mozilla::gfx;

FPSCounter::FPSCounter(const char* aName)
  : mWriteIndex(0)
  , mIteratorIndex(-1)
  , mFPSName(aName)
{
  Init();
}

FPSCounter::~FPSCounter() { }

void
FPSCounter::Init()
{
  for (int i = 0; i < kMaxFrames; i++) {
    mFrameTimestamps.AppendElement(TimeStamp());
  }
  mLastInterval = TimeStamp::Now();
}

// Returns true if we captured a full interval of data
bool
FPSCounter::CapturedFullInterval(TimeStamp aTimestamp) {
  TimeDuration duration = aTimestamp - mLastInterval;
  return duration.ToSeconds() >= kFpsDumpInterval;
}

void
FPSCounter::AddFrame(TimeStamp aTimestamp) {
  NS_ASSERTION(mWriteIndex < kMaxFrames, "We probably have a bug with the circular buffer");
  NS_ASSERTION(mWriteIndex >= 0, "Circular Buffer index should never be negative");

  int index = mWriteIndex++;
  if (mWriteIndex == kMaxFrames) {
    mWriteIndex = 0;
  }

  mFrameTimestamps[index] = aTimestamp;

  if (CapturedFullInterval(aTimestamp)) {
    PrintFPS();
    WriteFrameTimeStamps();
    mLastInterval = aTimestamp;
  }
}

double
FPSCounter::AddFrameAndGetFps(TimeStamp aTimestamp) {
  AddFrame(aTimestamp);
  return GetFPS(aTimestamp);
}

int
FPSCounter::GetLatestReadIndex()
{
  if (mWriteIndex == 0) {
    return kMaxFrames - 1;
  }

  return mWriteIndex - 1;
}

TimeStamp
FPSCounter::GetLatestTimeStamp()
{
  TimeStamp timestamp = mFrameTimestamps[GetLatestReadIndex()];
  MOZ_ASSERT(!timestamp.IsNull(), "Cannot use null timestamps");
  return timestamp;
}

// Returns true if we iterated over a full interval of data
bool
FPSCounter::IteratedFullInterval(TimeStamp aTimestamp, double aDuration) {
  MOZ_ASSERT(mIteratorIndex >= 0, "Cannot be negative");
  MOZ_ASSERT(mIteratorIndex < kMaxFrames, "Iterator index cannot be greater than kMaxFrames");

  TimeStamp currentStamp = mFrameTimestamps[mIteratorIndex];
  TimeDuration duration = aTimestamp - currentStamp;
  return duration.ToSeconds() >= aDuration;
}

void
FPSCounter::ResetReverseIterator()
{
  mIteratorIndex = GetLatestReadIndex();
}

/***
 * Returns true if we have another timestamp that is valid and
 * is within the given duration that we're interested in.
 * Duration is in seconds
 */
bool FPSCounter::HasNext(TimeStamp aTimestamp, double aDuration)
{
  // Order of evaluation here has to stay the same
  // otherwise IteratedFullInterval reads from mFrameTimestamps which cannot
  // be null
  return (mIteratorIndex != mWriteIndex) // Didn't loop around the buffer
          && !mFrameTimestamps[mIteratorIndex].IsNull() // valid data
          && !IteratedFullInterval(aTimestamp, aDuration);
}

TimeStamp
FPSCounter::GetNextTimeStamp()
{
  TimeStamp timestamp = mFrameTimestamps[mIteratorIndex--];
  MOZ_ASSERT(!timestamp.IsNull(), "Reading Invalid Timestamp Data");

  if (mIteratorIndex == -1) {
    mIteratorIndex = kMaxFrames - 1;
  }
  return timestamp;
}

/**
 * GetFPS calculates how many frames we've already composited from the current
 * frame timestamp and we iterate from the latest timestamp we recorded,
 * going back in time. When we hit a frame that is longer than the 1 second
 * from the current composited frame, we return how many frames we've counted.
 * Just a visualization:
 *
 *                                 aTimestamp
 * Frames: 1 2 3 4 5 6 7 8 9 10 11 12
 * Time   -------------------------->
 *
 * GetFPS iterates from aTimestamp, which is the current frame.
 * Then starting at frame 12, going back to frame 11, 10, etc, we calculate
 * the duration of the recorded frame timestamp from aTimestamp.
 * Once duration is greater than 1 second, we return how many frames
 * we composited.
 */
double
FPSCounter::GetFPS(TimeStamp aTimestamp)
{
  int frameCount = 0;
  int duration = 1.0; // Only care about the last 1s of data

  ResetReverseIterator();
  while (HasNext(aTimestamp, duration)) {
    GetNextTimeStamp();
    frameCount++;
  }

  return frameCount;
}

// Iterate the same way we do in GetFPS()
int
FPSCounter::BuildHistogram(std::map<int, int>& aFpsData)
{
  TimeStamp currentIntervalStart = GetLatestTimeStamp();
  TimeStamp currentTimeStamp = GetLatestTimeStamp();
  TimeStamp startTimeStamp = GetLatestTimeStamp();

  int frameCount = 0;
  int totalFrameCount = 0;

  ResetReverseIterator();
  while (HasNext(startTimeStamp)) {
    currentTimeStamp = GetNextTimeStamp();
    TimeDuration interval = currentIntervalStart - currentTimeStamp;

    if (interval.ToSeconds() >= 1.0 ) {
      currentIntervalStart = currentTimeStamp;
      aFpsData[frameCount]++;
      frameCount = 0;
    }

    frameCount++;
    totalFrameCount++;
  }

  TimeDuration totalTime = currentIntervalStart - currentTimeStamp;
  printf_stderr("Discarded %d frames over %f ms in histogram for %s\n",
    frameCount, totalTime.ToMilliseconds(), mFPSName);
  return totalFrameCount;
}

// Iterate the same way we do in GetFPS()
void
FPSCounter::WriteFrameTimeStamps(PRFileDesc* fd)
{
  const int bufferSize = 256;
  char buffer[bufferSize];
  int writtenCount = SprintfLiteral(buffer, "FPS Data for: %s\n", mFPSName);
  MOZ_ASSERT(writtenCount < bufferSize);
  if (writtenCount >= bufferSize) {
    return;
  }
  PR_Write(fd, buffer, writtenCount);

  ResetReverseIterator();
  TimeStamp startTimeStamp = GetLatestTimeStamp();

  MOZ_ASSERT(HasNext(startTimeStamp));
  TimeStamp previousSample = GetNextTimeStamp();

  MOZ_ASSERT(HasNext(startTimeStamp));
  TimeStamp nextTimeStamp = GetNextTimeStamp();

  while (HasNext(startTimeStamp)) {
    TimeDuration duration = previousSample - nextTimeStamp;
    writtenCount = SprintfLiteral(buffer, "%f,\n", duration.ToMilliseconds());
    MOZ_ASSERT(writtenCount < bufferSize);
    if (writtenCount >= bufferSize) {
      continue;
    }
    PR_Write(fd, buffer, writtenCount);

    previousSample = nextTimeStamp;
    nextTimeStamp = GetNextTimeStamp();
  }
}

double
FPSCounter::GetMean(std::map<int, int> aHistogram)
{
  double average = 0.0;
  double samples = 0.0;

  for (std::map<int, int>::iterator iter = aHistogram.begin();
    iter != aHistogram.end(); ++iter)
  {
    int fps = iter->first;
    int count = iter->second;

    average += fps * count;
    samples += count;
  }

  return average / samples;
}

double
FPSCounter::GetStdDev(std::map<int, int> aHistogram)
{
  double sumOfDifferences = 0;
  double average = GetMean(aHistogram);
  double samples = 0.0;

  for (std::map<int, int>::iterator iter = aHistogram.begin();
    iter != aHistogram.end(); ++iter)
  {
    int fps = iter->first;
    int count = iter->second;

    double diff = ((double) fps) - average;
    diff *= diff;

    for (int i = 0; i < count; i++) {
      sumOfDifferences += diff;
    }
    samples += count;
  }

  double stdDev = sumOfDifferences / samples;
  return sqrt(stdDev);
}

void
FPSCounter::PrintFPS()
{
  if (!gfxPrefs::FPSPrintHistogram()) {
    return;
  }

  std::map<int, int> histogram;
  int totalFrames = BuildHistogram(histogram);

  TimeDuration measurementInterval = mFrameTimestamps[GetLatestReadIndex()] - mLastInterval;
  printf_stderr("FPS for %s. Total Frames: %d Time Interval: %f seconds\n",
                mFPSName, totalFrames, measurementInterval.ToSecondsSigDigits());

  PrintHistogram(histogram);
}

void
FPSCounter::PrintHistogram(std::map<int, int>& aHistogram)
{
  if (aHistogram.size() == 0) {
    return;
  }

  int length = 0;
  const int kBufferLength = 512;
  int availableSpace = kBufferLength;
  char buffer[kBufferLength];

  for (std::map<int, int>::iterator iter = aHistogram.begin();
    iter != aHistogram.end(); iter++)
  {
    int fps = iter->first;
    int count = iter->second;

    int lengthRequired = snprintf(buffer + length, availableSpace,
                                  "FPS: %d = %d. ", fps, count);
    // Ran out of buffer space. Oh well - just print what we have.
    if (lengthRequired > availableSpace) {
      break;
    }
    length += lengthRequired;
    availableSpace -= lengthRequired;
  }

  printf_stderr("%s\n", buffer);
  printf_stderr("Mean: %f , std dev %f\n", GetMean(aHistogram), GetStdDev(aHistogram));
}

// Write FPS timestamp data to a file only if
// draw-fps.write-to-file is true
nsresult
FPSCounter::WriteFrameTimeStamps()
{
  if (!gfxPrefs::WriteFPSToFile()) {
    return NS_OK;
  }

  MOZ_ASSERT(mWriteIndex == 0);

  nsCOMPtr<nsIFile> resultFile;
  nsresult rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(resultFile));
  NS_ENSURE_SUCCESS(rv, rv);

  if (!strncmp(mFPSName, "Compositor", strlen(mFPSName))) {
    resultFile->Append(NS_LITERAL_STRING("fps.txt"));
  } else {
    resultFile->Append(NS_LITERAL_STRING("txn.txt"));
  }

  PRFileDesc* fd = nullptr;
  int mode = 644;
  int openFlags = PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE;
  rv = resultFile->OpenNSPRFileDesc(openFlags, mode, &fd);
  NS_ENSURE_SUCCESS(rv, rv);

  WriteFrameTimeStamps(fd);
  PR_Close(fd);

  printf_stderr("Wrote FPS data to file: %s\n",
                resultFile->HumanReadablePath().get());
  return NS_OK;
}

} // end namespace layers
} // end namespace mozilla
