/* SPDX-FileCopyrightText: 2023 Blender Authors
 *
 * SPDX-License-Identifier: GPL-2.0-or-later */

/** \file
 * \ingroup edscene
 *
 * Define `ED_scene_fps_*` functions.
 */

#include <algorithm>
#include <cmath>

#include "BLI_math_base.h"

#include "DNA_scene_types.h"

#include "ED_scene.hh"

#include "MEM_guardedalloc.h"

using FrameSampleT = uint32_t;
using FrameSumT = uint64_t;

/**
 * This values gives enough precision while not overflowing a 64-bit integer when accumulating.
 * Compared to the same calculation with `double` the error rate is less than 0.5 microsecond,
 * more than enough precision for FPS display.
 */
#define FIXED_UNIT FrameSampleT(65535)

/** Use report the difference (error) between fixed-point arithmetic and double-precision. */
// #define USE_DEBUG_REPORT_ERROR_MARGIN

struct FrameSample {
  FrameSampleT value;
#ifdef USE_DEBUG_REPORT_ERROR_MARGIN
  double value_db;
#endif
};

/**
 * For playback frame-rate info stored during runtime as `scene->fps_info`.
 */
struct ScreenFrameRateInfo {
  double time_curr;
  double time_prev;

#ifdef USE_DEBUG_REPORT_ERROR_MARGIN
  double error_sum;
  int error_samples;
#endif

  /** When the target FPS is not a whole number. */
  bool fps_target_is_fractional;

  /** The target FPS, use to reset on change. */
  float fps_target;
  /** Final result, ignore when -1.0. */
  float fps_average;

  int times_fps_index;
  int times_fps_num;
  int times_fps_num_set;

  FrameSumT times_fps_sum;

  /** Over allocate (containing `times_fps_num` elements). */
  FrameSample times_fps[0];
};

void ED_scene_fps_average_clear(Scene *scene)
{
  if (scene->fps_info == nullptr) {
    return;
  }

#ifndef NDEBUG
  /* Assert this value has not somehow become out of sync as this would mean
   * the reported frame-rate would be wrong. */
  ScreenFrameRateInfo *fpsi = static_cast<ScreenFrameRateInfo *>(scene->fps_info);
  FrameSumT times_fps_sum_cmp = 0;
  for (int i = 0; i < fpsi->times_fps_num_set; i++) {
    times_fps_sum_cmp += fpsi->times_fps[i].value;
  }
  BLI_assert(fpsi->times_fps_sum == times_fps_sum_cmp);
#endif /* !NDEBUG */

  MEM_freeN(static_cast<ScreenFrameRateInfo *>(scene->fps_info));
  scene->fps_info = nullptr;
}

void ED_scene_fps_average_accumulate(Scene *scene, const short fps_samples, const double ltime)
{
  const float fps_target = float(FPS);
  const int times_fps_num = (fps_samples > 0) ? fps_samples : max_ii(1, int(ceilf(fps_target)));

  ScreenFrameRateInfo *fpsi = static_cast<ScreenFrameRateInfo *>(scene->fps_info);
  if (fpsi) {
    /* Reset when the target FPS changes.
     * Needed redraw times from when a different FPS was set do not contribute
     * to an average that is over/under the new target. */
    if ((fpsi->fps_target != fps_target) || (fpsi->times_fps_num != times_fps_num)) {
      MEM_freeN(fpsi);
      fpsi = nullptr;
      scene->fps_info = nullptr;
    }
  }

  /* If there isn't any info, initialize it first. */
  if (fpsi == nullptr) {
    scene->fps_info = MEM_callocN(
        sizeof(ScreenFrameRateInfo) + (sizeof(FrameSample) * times_fps_num), __func__);
    fpsi = static_cast<ScreenFrameRateInfo *>(scene->fps_info);
    fpsi->fps_target = fps_target;
    fpsi->times_fps_num = times_fps_num;
    fpsi->times_fps_num_set = 0;

    /* Use 100 for 2 decimal places (currently used for FPS display), could be configurable. */
    const double decimal_places = 100.0;
    fpsi->fps_target_is_fractional = std::round(fps_target) !=
                                     (std::round(fps_target * decimal_places) / decimal_places);
  }

  /* Update the values. */
  fpsi->time_curr = fpsi->time_prev;
  fpsi->time_prev = ltime;

  /* Mark as outdated. */
  fpsi->fps_average = -1.0f;
}

bool ED_scene_fps_average_calc(const Scene *scene, SceneFPS_State *r_state)
{
  ScreenFrameRateInfo *fpsi = static_cast<ScreenFrameRateInfo *>(scene->fps_info);
  if (fpsi == nullptr) {
    return false;
  }

  if (fpsi->time_prev == 0.0 || fpsi->time_curr == 0.0) {
    /* The user should never see this. */
    fpsi->fps_average = -1.0f;
    return false;
  }

  if (fpsi->fps_average == -1.0) {
    /* Doing an average for a more robust calculation. */
    if (fpsi->times_fps_index >= fpsi->times_fps_num) {
      fpsi->times_fps_index = 0;
    }

    const double fps_sample = 1.0 / (fpsi->time_prev - fpsi->time_curr);

    {
      const FrameSampleT fps_prev = (fpsi->times_fps_num_set == fpsi->times_fps_num) ?
                                        fpsi->times_fps[fpsi->times_fps_index].value :
                                        FrameSampleT(0);
      const double fps_curr_db = std::round(fps_sample * double(FIXED_UNIT));
      BLI_assert(fps_curr_db >= 0.0f);
      const FrameSampleT fps_curr = FrameSampleT(fps_curr_db);
      fpsi->times_fps[fpsi->times_fps_index].value = fps_curr;
      fpsi->times_fps_sum -= fps_prev;
      fpsi->times_fps_sum += fps_curr;
    }
#ifdef USE_DEBUG_REPORT_ERROR_MARGIN
    fpsi->times_fps[fpsi->times_fps_index].value_db = fps_sample;
#endif

    fpsi->times_fps_index++;
    fpsi->times_fps_num_set = std::max(fpsi->times_fps_index, fpsi->times_fps_num_set);
    BLI_assert(fpsi->times_fps_num_set > 0);

    fpsi->fps_average = float((double(fpsi->times_fps_sum) / double(fpsi->times_fps_num_set)) /
                              FIXED_UNIT);

#ifdef USE_DEBUG_REPORT_ERROR_MARGIN
    {
      double fps_average_ref = 0.0f;
      for (int i = 0; i < fpsi->times_fps_num_set; i++) {
        fps_average_ref += fpsi->times_fps[i].value_db;
      }
      fps_average_ref = float(fps_average_ref / double(fpsi->times_fps_num_set));

      const float error = float(fps_average_ref) - fpsi->fps_average;
      fpsi->error_sum += error;
      fpsi->error_samples += 1;
      if ((fpsi->error_samples % 100) == 0) {
        printf("%s error: %.16f over %d samples (average %.16f)\n",
               __func__,
               fpsi->error_sum,
               fpsi->error_samples,
               fpsi->error_sum / double(fpsi->error_samples));
      }
    }
#endif /* USE_DEBUG_REPORT_ERROR_MARGIN */
  }

  r_state->fps_average = fpsi->fps_average;
  r_state->fps_target = fpsi->fps_target;
  r_state->fps_target_is_fractional = fpsi->fps_target_is_fractional;
  return true;
}
