/*
 * Copyright (c) 2012 The Native Client Authors. All rights reserved.
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

/*
 * NaCl Server Runtime time abstraction layer.
 * This is the host-OS-dependent implementation.
 */

#include <windows.h>
#include <mmsystem.h>
#include <sys/timeb.h>
#include <time.h>

#include "native_client/src/include/nacl_macros.h"
#include "native_client/src/shared/platform/nacl_time.h"
#include "native_client/src/shared/platform/win/nacl_time_types.h"

#include "native_client/src/shared/platform/nacl_log.h"
#include "native_client/src/shared/platform/nacl_sync.h"
#include "native_client/src/shared/platform/nacl_sync_checked.h"
#include "native_client/src/shared/platform/win/xlate_system_error.h"

#define kMillisecondsPerSecond 1000
#define kMicrosecondsPerMillisecond 1000
#define kMicrosecondsPerSecond 1000000
#define kCalibrateTimeoutMs 1000
#define kCoarseMks 10

/*
 * Here's some usefull links related to timing on Windows:
 * http://svn.wildfiregames.com/public/ps/trunk/docs/timing_pitfalls.pdf
 * Rob Arnold (mozilla) blog:
 * http://robarnold.org/measuring-performance/
 * https://bugzilla.mozilla.org/show_bug.cgi?id=563082
 */

/*
 * To get a 1ms resolution time-of-day clock, we have to jump thrugh
 * some hoops.  This approach has the following defect: ntp
 * adjustments will be nullified, until the difference is larger than
 * a recalibration threshold (below), at which point time may jump
 * (until we introduce a mechanism to slowly phase in time changes).
 * A system suspend/resume will generate a clock recalibration with
 * high probability.
 *
 * The algorithm is as follows:
 *
 * At start up, we sample the system time (coarse timer) via
 * GetSystemTimeAsFileTime until it changes.  At the state transition,
 * we record the value of the time-after-boot counter via timeGetTime.
 *
 * At subsequent gettimeofday syscall, we query the system time via
 * GetSystemTimeAsFileTime.  This is converted to an
 * expected/corresponding return value from timeGetTime.  The
 * difference from the actual return value is the time that elapsed
 * since the state transition of the system time counter, and we add
 * that difference (assuming it is smaller than the max-drift
 * threshold) to the system time and return that (minus the Unix
 * epoch, etc), as the result of the gettimeofday syscall.  If the
 * difference is larger than the max-drift threshold, we invoke the
 * calibration function.
 */

struct NaClTimeState g_NaCl_time_state;

static uint32_t const kMaxMillsecondDriftBeforeRecalibration = 60;
static uint32_t const kMaxCalibrationDiff = 4;

/*
 * Translate Windows system time to milliseconds.  Windows System time
 * is in units of 100nS.  Windows time is in 100e-9s == 1e-7s, and we
 * want units of 1e-3s, so ms/wt = 1e-4.
 */
static uint64_t NaClFileTimeToMs(FILETIME *ftp) {
  ULARGE_INTEGER t;
  t.u.HighPart = ftp->dwHighDateTime;
  t.u.LowPart = ftp->dwLowDateTime;
  return t.QuadPart / NACL_100_NANOS_PER_MILLI;
}

#define kCpuInfoPageSize 4  /* Not in bytes, but in sizeof(uint32_t). */

static char* CPU_GetBrandString(void) {
  const int kBaseInfoPage = 0x80000000;
  const int kCpuBrandStringPage1 = 0x80000002;
  const int kCpuBrandStringPage2 = 0x80000003;
  const int kCpuBrandStringPage3 = 0x80000004;

  static int cpu_info[kCpuInfoPageSize * 4] = {0};
  __cpuid(cpu_info, kBaseInfoPage);
  if (cpu_info[0] < kCpuBrandStringPage3)
    return "";

  __cpuid(&cpu_info[0], kCpuBrandStringPage1);
  __cpuid(&cpu_info[kCpuInfoPageSize], kCpuBrandStringPage2);
  __cpuid(&cpu_info[kCpuInfoPageSize * 2], kCpuBrandStringPage3);
  return (char*)cpu_info;
}

static int CPU_GetFamily(void) {
  const int kFeaturesPage = 1;
  int cpu_info[kCpuInfoPageSize];
  __cpuid(cpu_info, kFeaturesPage);
  return (cpu_info[0] >> 8) & 0xf;
}

/*
 * Returns 1 if success, 0 in case of QueryPerformanceCounter is not supported.
 *
 * Calibration is done as described above.
 *
 * To ensure that the calibration is accurate, we interleave sampling
 * the microsecond resolution counter via QueryPerformanceCounter with the 1
 * millisecond resolution system clock via GetSystemTimeAsFileTime.
 * When we see the edge transition where the system clock ticks, we
 * compare the before and after microsecond counter values.  If it is
 * within a calibration threshold (kMaxCalibrationDiff), then we
 * record the instantaneous system time (1 msresolution) and the
 * 64-bit counter value (~0.5 mks reslution).  Since we have a before and
 * after counter value, we interpolate it to get the "average" counter
 * value to associate with the system time.
 */
static int NaClCalibrateWindowsClockQpc(struct NaClTimeState *ntsp) {
  FILETIME  ft_start;
  FILETIME  ft_prev;
  FILETIME  ft_now;
  LARGE_INTEGER  counter_before;
  LARGE_INTEGER  counter_after;
  int64_t  counter_diff;
  int64_t  counter_diff_ms;
  uint64_t end_of_calibrate;
  int sys_time_changed;
  int calibration_success = 0;

  NaClLog(5, "Entered NaClCalibrateWindowsClockQpc\n");

  GetSystemTimeAsFileTime(&ft_start);
  ft_prev = ft_start;
  end_of_calibrate = NaClFileTimeToMs(&ft_start) + kCalibrateTimeoutMs;

  do {
    if (!QueryPerformanceCounter(&counter_before))
      return 0;
    GetSystemTimeAsFileTime(&ft_now);
    if (!QueryPerformanceCounter(&counter_after))
      return 0;

    counter_diff = counter_after.QuadPart - counter_before.QuadPart;
    counter_diff_ms =
        (counter_diff * kMillisecondsPerSecond) / ntsp->qpc_frequency;
    sys_time_changed = (ft_now.dwHighDateTime != ft_prev.dwHighDateTime) ||
        (ft_now.dwLowDateTime != ft_prev.dwLowDateTime);

    if ((counter_diff >= 0) &&
        (counter_diff_ms <= kMaxCalibrationDiff) &&
        sys_time_changed) {
      calibration_success = 1;
      break;
    }
    if (sys_time_changed)
      ft_prev = ft_now;
  } while (NaClFileTimeToMs(&ft_now) < end_of_calibrate);

  ntsp->system_time_start_ms = NaClFileTimeToMs(&ft_now);
  ntsp->qpc_start = counter_before.QuadPart + (counter_diff / 2);
  ntsp->last_qpc = counter_after.QuadPart;

  NaClLog(5,
          "Leaving NaClCalibrateWindowsClockQpc : %d\n",
          calibration_success);
  return calibration_success;
}

/*
 * Calibration is done as described above.
 *
 * To ensure that the calibration is accurate, we interleave sampling
 * the millisecond resolution counter via timeGetTime with the 10-55
 * millisecond resolution system clock via GetSystemTimeAsFileTime.
 * When we see the edge transition where the system clock ticks, we
 * compare the before and after millisecond counter values.  If it is
 * within a calibration threshold (kMaxCalibrationDiff), then we
 * record the instantaneous system time (10-55 msresolution) and the
 * 32-bit counter value (1ms reslution).  Since we have a before and
 * after counter value, we interpolate it to get the "average" counter
 * value to associate with the system time.
 */
static void NaClCalibrateWindowsClockMu(struct NaClTimeState *ntsp) {
  FILETIME  ft_start;
  FILETIME  ft_now;
  DWORD     ms_counter_before;
  DWORD     ms_counter_after;
  uint32_t  ms_counter_diff;

  NaClLog(5, "Entered NaClCalibrateWindowsClockMu\n");
  GetSystemTimeAsFileTime(&ft_start);
  ms_counter_before = timeGetTime();
  for (;;) {
    GetSystemTimeAsFileTime(&ft_now);
    ms_counter_after = timeGetTime();
    ms_counter_diff = ms_counter_after - (uint32_t) ms_counter_before;
    NaClLog(5, "ms_counter_diff %u\n", ms_counter_diff);
    if (ms_counter_diff <= kMaxCalibrationDiff &&
        (ft_now.dwHighDateTime != ft_start.dwHighDateTime ||
         ft_now.dwLowDateTime != ft_start.dwLowDateTime)) {
      break;
    }
    ms_counter_before = ms_counter_after;
  }
  ntsp->system_time_start_ms = NaClFileTimeToMs(&ft_now);
  /*
   * Average the counter values.  Note unsigned computation of
   * ms_counter_diff, so that was mod 2**32 arithmetic, and the
   * addition of half the difference is numerically correct, whereas
   * (ms_counter_before + ms_counter_after)/2 is wrong due to
   * overflow.
   */
  ntsp->ms_counter_start = (DWORD) (ms_counter_before + (ms_counter_diff / 2));

  NaClLog(5, "Leaving NaClCalibrateWindowsClockMu\n");
}

void NaClAllowLowResolutionTimeOfDay(void) {
  g_NaCl_time_state.allow_low_resolution = 1;
}

void NaClTimeInternalInit(struct NaClTimeState *ntsp) {
  TIMECAPS    tc;
  SYSTEMTIME  st;
  FILETIME    ft;
  LARGE_INTEGER qpc_freq;

  /*
   * Maximize timer/Sleep resolution.
   */
  timeGetDevCaps(&tc, sizeof tc);

  if (ntsp->allow_low_resolution) {
    /* Set resolution to max so we don't over-promise. */
    ntsp->wPeriodMin = tc.wPeriodMax;
  } else {
    ntsp->wPeriodMin = tc.wPeriodMin;
    timeBeginPeriod(ntsp->wPeriodMin);
    NaClLog(4, "NaClTimeInternalInit: timeBeginPeriod(%u)\n", ntsp->wPeriodMin);
  }
  ntsp->time_resolution_ns = ntsp->wPeriodMin * NACL_NANOS_PER_MILLI;

  /*
   * Compute Unix epoch start; calibrate high resolution clock.
   */
  st.wYear = 1970;
  st.wMonth = 1;
  st.wDay = 1;
  st.wHour = 0;
  st.wMinute = 0;
  st.wSecond = 0;
  st.wMilliseconds = 0;
  SystemTimeToFileTime(&st, &ft);
  ntsp->epoch_start_ms = NaClFileTimeToMs(&ft);
  NaClLog(4, "Unix epoch start is  %"NACL_PRIu64"ms in Windows epoch time\n",
          ntsp->epoch_start_ms);

  NaClMutexCtor(&ntsp->mu);

  /*
   * We don't actually grab the lock, since the module initializer
   * should be called before going threaded.
   */
  ntsp->can_use_qpc = 0;
  if (!ntsp->allow_low_resolution) {
    ntsp->can_use_qpc = QueryPerformanceFrequency(&qpc_freq);
    /*
     * On Athlon X2 CPUs (e.g. model 15) QueryPerformanceCounter is
     * unreliable.  Fallback to low-res clock.
     */
    if (strstr(CPU_GetBrandString(), "AuthenticAMD") && (CPU_GetFamily() == 15))
        ntsp->can_use_qpc = 0;

    NaClLog(4,
            "CPU_GetBrandString->[%s] ntsp->can_use_qpc=%d\n",
            CPU_GetBrandString(),
            ntsp->can_use_qpc);

    if (ntsp->can_use_qpc) {
      ntsp->qpc_frequency = qpc_freq.QuadPart;
      NaClLog(4, "qpc_frequency = %"NACL_PRId64" (counts/s)\n",
              ntsp->qpc_frequency);
      if (!NaClCalibrateWindowsClockQpc(ntsp))
        ntsp->can_use_qpc = 0;
    }
    if (!ntsp->can_use_qpc)
      NaClCalibrateWindowsClockMu(ntsp);
  }
}

uint64_t NaClTimerResolutionNsInternal(struct NaClTimeState *ntsp) {
  return ntsp->time_resolution_ns;
}

void NaClTimeInternalFini(struct NaClTimeState *ntsp) {
  NaClMutexDtor(&ntsp->mu);
  if (!ntsp->allow_low_resolution)
    timeEndPeriod(ntsp->wPeriodMin);
}

void NaClTimeInit(void) {
  NaClTimeInternalInit(&g_NaCl_time_state);
}

void NaClTimeFini(void) {
  NaClTimeInternalFini(&g_NaCl_time_state);
}

uint64_t NaClTimerResolutionNanoseconds(void) {
  return NaClTimerResolutionNsInternal(&g_NaCl_time_state);
}

int NaClGetTimeOfDayInternQpc(struct nacl_abi_timeval *tv,
                              struct NaClTimeState    *ntsp,
                              int allow_calibration) {
  FILETIME  ft_now;
  int64_t sys_now_mks;
  LARGE_INTEGER qpc;
  int64_t qpc_diff;
  int64_t qpc_diff_mks;
  int64_t qpc_now_mks;
  int64_t drift_mks;
  int64_t drift_ms;

  NaClLog(5, "Entered NaClGetTimeOfDayInternQpc\n");

  NaClXMutexLock(&ntsp->mu);

  GetSystemTimeAsFileTime(&ft_now);
  QueryPerformanceCounter(&qpc);
  sys_now_mks = NaClFileTimeToMs(&ft_now) * kMicrosecondsPerMillisecond;
  NaClLog(5, " sys_now_mks = %"NACL_PRId64" (us)\n", sys_now_mks);
  qpc_diff = qpc.QuadPart - ntsp->qpc_start;
  NaClLog(5, " qpc_diff = %"NACL_PRId64" (counts)\n", qpc_diff);
  /*
   * Coarse qpc_now_mks to 10 microseconds resolution,
   * to match the other platforms and not make a side-channel
   * attack any easier than it needs to be.
   */
  qpc_diff_mks = ((qpc_diff * (kMicrosecondsPerSecond / kCoarseMks)) /
      ntsp->qpc_frequency) * kCoarseMks;
  NaClLog(5, " qpc_diff_mks = %"NACL_PRId64" (us)\n", qpc_diff_mks);

  qpc_now_mks = (ntsp->system_time_start_ms * kMicrosecondsPerMillisecond) +
      qpc_diff_mks;
  NaClLog(5, " system_time_start_ms %"NACL_PRIu64"\n",
          ntsp->system_time_start_ms);
  NaClLog(5, " qpc_now_mks = %"NACL_PRId64" (us)\n", qpc_now_mks);

  if ((qpc_diff < 0) || (qpc.QuadPart < ntsp->last_qpc)) {
    NaClLog(5, " need recalibration\n");
    if (allow_calibration) {
      NaClCalibrateWindowsClockQpc(ntsp);
      NaClXMutexUnlock(&ntsp->mu);
      return NaClGetTimeOfDayInternQpc(tv, ntsp, 0);
    } else {
      NaClLog(5, " ... but using coarse, system time instead.\n");
      /* use GetSystemTimeAsFileTime(), not QPC */
      qpc_now_mks = sys_now_mks;
    }
  }
  ntsp->last_qpc = qpc.QuadPart;
  drift_mks = sys_now_mks - qpc_now_mks;
  if (qpc_now_mks > sys_now_mks)
    drift_mks = qpc_now_mks - sys_now_mks;

  drift_ms = drift_mks / kMicrosecondsPerMillisecond;
  NaClLog(5, " drift_ms = %"NACL_PRId64"\n", drift_ms);

  if (allow_calibration &&
      (drift_ms > kMaxMillsecondDriftBeforeRecalibration)) {
    NaClLog(5, "drift_ms recalibration\n");
    NaClCalibrateWindowsClockQpc(ntsp);
    NaClXMutexUnlock(&ntsp->mu);
    return NaClGetTimeOfDayInternQpc(tv, ntsp, 0);
  }

  NaClXMutexUnlock(&ntsp->mu);

  /* translate to unix time base */
  qpc_now_mks = qpc_now_mks
      - ntsp->epoch_start_ms * kMicrosecondsPerMillisecond;

  tv->nacl_abi_tv_sec =
      (nacl_abi_time_t)(qpc_now_mks / kMicrosecondsPerSecond);
  tv->nacl_abi_tv_usec =
      (nacl_abi_suseconds_t)(qpc_now_mks % kMicrosecondsPerSecond);
  return 0;
}

int NaClGetTimeOfDayIntern(struct nacl_abi_timeval *tv,
                           struct NaClTimeState    *ntsp) {
  FILETIME  ft_now;
  DWORD     ms_counter_now;
  uint64_t  t_ms;
  DWORD     ms_counter_at_ft_now;
  uint32_t  ms_counter_diff;
  uint64_t  unix_time_ms;

  if (ntsp->can_use_qpc)
    return NaClGetTimeOfDayInternQpc(tv, ntsp, 1);

  GetSystemTimeAsFileTime(&ft_now);
  ms_counter_now = timeGetTime();
  t_ms = NaClFileTimeToMs(&ft_now);

  NaClXMutexLock(&ntsp->mu);

  if (!ntsp->allow_low_resolution) {
    NaClLog(5, "ms_counter_now       %"NACL_PRIu32"\n",
            (uint32_t) ms_counter_now);
    NaClLog(5, "t_ms                 %"NACL_PRId64"\n", t_ms);
    NaClLog(5, "system_time_start_ms %"NACL_PRIu64"\n",
            ntsp->system_time_start_ms);

    ms_counter_at_ft_now = (DWORD)
        (ntsp->ms_counter_start +
         (uint32_t) (t_ms - ntsp->system_time_start_ms));

    NaClLog(5, "ms_counter_at_ft_now %"NACL_PRIu32"\n",
            (uint32_t) ms_counter_at_ft_now);

    ms_counter_diff = ms_counter_now - (uint32_t) ms_counter_at_ft_now;

    NaClLog(5, "ms_counter_diff      %"NACL_PRIu32"\n", ms_counter_diff);

    if (ms_counter_diff <= kMaxMillsecondDriftBeforeRecalibration) {
      t_ms = t_ms + ms_counter_diff;
    } else {
      NaClCalibrateWindowsClockMu(ntsp);
      t_ms = ntsp->system_time_start_ms;
    }

    NaClLog(5, "adjusted t_ms =      %"NACL_PRIu64"\n", t_ms);
  }

  unix_time_ms = t_ms - ntsp->epoch_start_ms;

  NaClXMutexUnlock(&ntsp->mu);

  NaClLog(5, "unix_time_ms  =      %"NACL_PRId64"\n", unix_time_ms);
  /*
   * Unix time is measured relative to a different epoch, Jan 1, 1970.
   * See the module initialization for epoch_start_ms.
   */

  tv->nacl_abi_tv_sec = (nacl_abi_time_t) (unix_time_ms / 1000);
  tv->nacl_abi_tv_usec = (nacl_abi_suseconds_t) ((unix_time_ms % 1000) * 1000);

  NaClLog(5, "nacl_abi_tv_sec =    %"NACL_PRIdNACL_TIME"\n",
          tv->nacl_abi_tv_sec);
  NaClLog(5, "nacl_abi_tv_usec =   %"NACL_PRId32"\n", tv->nacl_abi_tv_usec);

  return 0;
}

int NaClGetTimeOfDay(struct nacl_abi_timeval *tv) {
  return NaClGetTimeOfDayIntern(tv, &g_NaCl_time_state);
}

int NaClNanosleep(struct nacl_abi_timespec const *req,
                  struct nacl_abi_timespec       *rem) {
  DWORD                     sleep_ms;
  uint64_t                  resolution;
  DWORD                     resolution_gap = 0;

  UNREFERENCED_PARAMETER(rem);

  /* round up from ns resolution to ms resolution */
  /* TODO(bsy): report an error or loop if req->tv_sec does not fit in DWORD */
  sleep_ms = ((DWORD) req->tv_sec * NACL_MILLIS_PER_UNIT +
              NACL_UNIT_CONVERT_ROUND(req->tv_nsec, NACL_NANOS_PER_MILLI));

  /* round up to minimum timer resolution */
  resolution = NaClTimerResolutionNanoseconds();
  NaClLog(4, "Resolution %"NACL_PRId64"\n", resolution);
  if (0 != resolution) {
    resolution = NACL_UNIT_CONVERT_ROUND(resolution, NACL_NANOS_PER_MILLI);
    resolution_gap = (DWORD) (sleep_ms % resolution);
    if (0 != resolution_gap) {
      resolution_gap = (DWORD) (resolution - resolution_gap);
    }
  }
  NaClLog(4, "Resolution gap %d\n", resolution_gap);
  sleep_ms += resolution_gap;

  NaClLog(4, "Sleep(%d)\n", sleep_ms);
  Sleep(sleep_ms);

  return 0;
}
