/*
  $Header: /cvs/src/chrony/local.c,v 1.21 2003/09/22 21:22:30 richard Exp $

  =======================================================================

  chronyd/chronyc - Programs for keeping computer clocks accurate.

 **********************************************************************
 * Copyright (C) Richard P. Curnow  1997-2003
 * 
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of version 2 of the GNU General Public License as
 * published by the Free Software Foundation.
 * 
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 * 
 **********************************************************************

  =======================================================================

  The routines in this file present a common local (system) clock
  interface to the rest of the software.

  They interface with the system specific driver files in sys_*.c
  */

#include <assert.h>
#include <stddef.h>

#include "local.h"
#include "localp.h"
#include "memory.h"
#include "util.h"
#include "logging.h"

/* ================================================== */

/* Variable to store the current frequency, in ppm */
static double current_freq_ppm;

/* ================================================== */
/* Store the system dependent drivers */

static lcl_ReadFrequencyDriver drv_read_freq;
static lcl_SetFrequencyDriver drv_set_freq;
static lcl_AccrueOffsetDriver drv_accrue_offset;
static lcl_ApplyStepOffsetDriver drv_apply_step_offset;
static lcl_OffsetCorrectionDriver drv_offset_convert;
static lcl_SetLeapDriver drv_set_leap;

/* ================================================== */

/* Types and variables associated with handling the parameter change
   list */

typedef struct _ChangeListEntry {
  struct _ChangeListEntry *next;
  struct _ChangeListEntry *prev;
  LCL_ParameterChangeHandler handler;
  void *anything;
} ChangeListEntry;

static ChangeListEntry change_list;

/* ================================================== */

/* Types and variables associated with handling the parameter change
   list */

typedef struct _DispersionNotifyListEntry {
  struct _DispersionNotifyListEntry *next;
  struct _DispersionNotifyListEntry *prev;
  LCL_DispersionNotifyHandler handler;
  void *anything;
} DispersionNotifyListEntry;

static DispersionNotifyListEntry dispersion_notify_list;

/* ================================================== */

static int precision_log;
static double precision_quantum;

/* ================================================== */

/* Define the number of increments of the system clock that we want
   to see to be fairly sure that we've got something approaching
   the minimum increment.  Even on a crummy implementation that can't
   interpolate between 10ms ticks, we should get this done in
   under 1s of busy waiting. */
#define NITERS 100

static void
calculate_sys_precision(void)
{
  struct timeval tv, old_tv, first_tv;
  struct timezone tz;
  int dusec, best_dusec;
  int iters;

  gettimeofday(&old_tv, &tz);
  first_tv = old_tv;
  best_dusec = 1000000; /* Assume we must be better than a second */
  iters = 0;
  do {
    gettimeofday(&tv, &tz);
    dusec = 1000000*(tv.tv_sec - old_tv.tv_sec) + (tv.tv_usec - old_tv.tv_usec);
    old_tv = tv;
    if (dusec > 0)  {
      if (dusec < best_dusec) {
        best_dusec = dusec;
      }
      iters++;
    }
  } while (iters < NITERS);
  if (!(best_dusec > 0)) {
    CROAK("best_dusec should be positive");
  }
  precision_log = 0;
  while (best_dusec < 500000) {
    precision_log--;
    best_dusec *= 2;
  }

  precision_quantum = 1.0 / (double)(1<<(-precision_log));

  return;
}

/* ================================================== */

void
LCL_Initialise(void)
{
  change_list.next = change_list.prev = &change_list;

  dispersion_notify_list.next = dispersion_notify_list.prev = &dispersion_notify_list;

  /* Null out the system drivers, so that we die
     if they never get defined before use */
  
  drv_read_freq = NULL;
  drv_set_freq = NULL;
  drv_accrue_offset = NULL;
  drv_offset_convert = NULL;

  /* This ought to be set from the system driver layer */
  current_freq_ppm = 0.0;

  calculate_sys_precision();
}

/* ================================================== */

void
LCL_Finalise(void)
{
  return;
}

/* ================================================== */

/* Routine to read the system precision as a log to base 2 value. */
int
LCL_GetSysPrecisionAsLog(void)
{
  return precision_log;
}

/* ================================================== */
/* Routine to read the system precision in terms of the actual time step */

double
LCL_GetSysPrecisionAsQuantum(void)
{
  return precision_quantum;
}

/* ================================================== */

void
LCL_AddParameterChangeHandler(LCL_ParameterChangeHandler handler, void *anything)
{
  ChangeListEntry *ptr, *new_entry;

  /* Check that the handler is not already registered */
  for (ptr = change_list.next; ptr != &change_list; ptr = ptr->next) {
    if (!(ptr->handler != handler || ptr->anything != anything)) {
      CROAK("a handler is already registered");
    }
  }

  new_entry = MallocNew(ChangeListEntry);

  new_entry->handler = handler;
  new_entry->anything = anything;

  /* Chain it into the list */
  new_entry->next = &change_list;
  new_entry->prev = change_list.prev;
  change_list.prev->next = new_entry;
  change_list.prev = new_entry;

  return;
}

/* ================================================== */

/* Remove a handler */
extern 
void LCL_RemoveParameterChangeHandler(LCL_ParameterChangeHandler handler, void *anything)
{

  ChangeListEntry *ptr;
  int ok;

  ptr = NULL;
  ok = 0;

  for (ptr = change_list.next; ptr != &change_list; ptr = ptr->next) {
    if (ptr->handler == handler && ptr->anything == anything) {
      ok = 1;
      break;
    }
  }

  if (!ok) {
    CROAK("did not find a matching handler");
  }

  /* Unlink entry from the list */
  ptr->next->prev = ptr->prev;
  ptr->prev->next = ptr->next;

  free(ptr);

  return;
}

/* ================================================== */

void
LCL_AddDispersionNotifyHandler(LCL_DispersionNotifyHandler handler, void *anything)
{
  DispersionNotifyListEntry *ptr, *new_entry;

  /* Check that the handler is not already registered */
  for (ptr = dispersion_notify_list.next; ptr != &dispersion_notify_list; ptr = ptr->next) {
    if (!(ptr->handler != handler || ptr->anything != anything)) {
      CROAK("a handler is already registered");
    }
  }

  new_entry = MallocNew(DispersionNotifyListEntry);

  new_entry->handler = handler;
  new_entry->anything = anything;

  /* Chain it into the list */
  new_entry->next = &dispersion_notify_list;
  new_entry->prev = dispersion_notify_list.prev;
  dispersion_notify_list.prev->next = new_entry;
  dispersion_notify_list.prev = new_entry;

  return;
}

/* ================================================== */

/* Remove a handler */
extern 
void LCL_RemoveDispersionNotifyHandler(LCL_DispersionNotifyHandler handler, void *anything)
{

  DispersionNotifyListEntry *ptr;
  int ok;

  ptr = NULL;
  ok = 0;

  for (ptr = dispersion_notify_list.next; ptr != &dispersion_notify_list; ptr = ptr->next) {
    if (ptr->handler == handler && ptr->anything == anything) {
      ok = 1;
      break;
    }
  }

  if (!ok) {
    CROAK("no matching handler found");
  }

  /* Unlink entry from the list */
  ptr->next->prev = ptr->prev;
  ptr->prev->next = ptr->next;

  free(ptr);

  return;
}

/* ================================================== */
/* At the moment, this is just gettimeofday(), because
   I can't think of a Unix system where it would not be */

void
LCL_ReadRawTime(struct timeval *result)
{
  struct timezone tz;

  if (!(gettimeofday(result, &tz) >= 0)) {
    CROAK("Could not get time of day");
  }
  return;

}

/* ================================================== */

void
LCL_ReadCookedTime(struct timeval *result, double *err)
{
  struct timeval raw;
  double correction;

  LCL_ReadRawTime(&raw);

  /* For now, cheat and set the error to zero in all cases.
   */

  *err = 0.0;

  /* Call system specific driver to get correction */
  (*drv_offset_convert)(&raw, &correction);
  UTI_AddDoubleToTimeval(&raw, correction, result);

  return;
}

/* ================================================== */

double
LCL_GetOffsetCorrection(struct timeval *raw)
{
  double correction;
  (*drv_offset_convert)(raw, &correction);
  return correction;
}

/* ================================================== */
/* This is just a simple passthrough of the system specific routine */

double
LCL_ReadAbsoluteFrequency(void)
{
  return (*drv_read_freq)();
}

/* ================================================== */
/* This involves both setting the absolute frequency with the
   system-specific driver, as well as calling all notify handlers */

void
LCL_SetAbsoluteFrequency(double afreq_ppm)
{
  ChangeListEntry *ptr;
  struct timeval raw, cooked;
  double correction;
  double dfreq;
  
  /* Call the system-specific driver for setting the frequency */
  
  (*drv_set_freq)(afreq_ppm);

  dfreq = 1.0e-6 * (afreq_ppm - current_freq_ppm) / (1.0 - 1.0e-6 * current_freq_ppm);

  LCL_ReadRawTime(&raw);
  (drv_offset_convert)(&raw, &correction);
  UTI_AddDoubleToTimeval(&raw, correction, &cooked);

  /* Dispatch to all handlers */
  for (ptr = change_list.next; ptr != &change_list; ptr = ptr->next) {
    (ptr->handler)(&raw, &cooked, dfreq, afreq_ppm, 0.0, 0, ptr->anything);
  }

  current_freq_ppm = afreq_ppm;

}

/* ================================================== */

void
LCL_AccumulateDeltaFrequency(double dfreq)
{
  ChangeListEntry *ptr;
  struct timeval raw, cooked;
  double correction;

  /* Work out new absolute frequency.  Note that absolute frequencies
   are handled in units of ppm, whereas the 'dfreq' argument is in
   terms of the gradient of the (offset) v (local time) function. */

  current_freq_ppm = (1.0 - dfreq) * current_freq_ppm +
    (1.0e6 * dfreq);

  /* Call the system-specific driver for setting the frequency */
  (*drv_set_freq)(current_freq_ppm);

  LCL_ReadRawTime(&raw);
  (drv_offset_convert)(&raw, &correction);
  UTI_AddDoubleToTimeval(&raw, correction, &cooked);

  /* Dispatch to all handlers */
  for (ptr = change_list.next; ptr != &change_list; ptr = ptr->next) {
    (ptr->handler)(&raw, &cooked, dfreq, current_freq_ppm, 0.0, 0, ptr->anything);
  }

}

/* ================================================== */

void
LCL_AccumulateOffset(double offset)
{
  ChangeListEntry *ptr;
  struct timeval raw, cooked;
  double correction;

  /* In this case, the cooked time to be passed to the notify clients
     has to be the cooked time BEFORE the change was made */

  LCL_ReadRawTime(&raw);
  (drv_offset_convert)(&raw, &correction);
  UTI_AddDoubleToTimeval(&raw, correction, &cooked);

  (*drv_accrue_offset)(offset);

  /* Dispatch to all handlers */
  for (ptr = change_list.next; ptr != &change_list; ptr = ptr->next) {
    (ptr->handler)(&raw, &cooked, 0.0, current_freq_ppm, offset, 0, ptr->anything);
  }

}

/* ================================================== */

void
LCL_ApplyStepOffset(double offset)
{
  ChangeListEntry *ptr;
  struct timeval raw, cooked;
  double correction;

  /* In this case, the cooked time to be passed to the notify clients
     has to be the cooked time BEFORE the change was made */

  LCL_ReadRawTime(&raw);
  (drv_offset_convert)(&raw, &correction);
  UTI_AddDoubleToTimeval(&raw, correction, &cooked);

  (*drv_apply_step_offset)(offset);

  /* Dispatch to all handlers */
  for (ptr = change_list.next; ptr != &change_list; ptr = ptr->next) {
    (ptr->handler)(&raw, &cooked, 0.0, current_freq_ppm, offset, 1, ptr->anything);
  }

}

/* ================================================== */

void
LCL_AccumulateFrequencyAndOffset(double dfreq, double doffset)
{
  ChangeListEntry *ptr;
  struct timeval raw, cooked;
  double correction;
  double old_freq_ppm;

  LCL_ReadRawTime(&raw);
  (drv_offset_convert)(&raw, &correction);
  /* Due to modifying the offset, this has to be the cooked time prior
     to the change we are about to make */
  UTI_AddDoubleToTimeval(&raw, correction, &cooked);

  old_freq_ppm = current_freq_ppm;

  /* Work out new absolute frequency.  Note that absolute frequencies
   are handled in units of ppm, whereas the 'dfreq' argument is in
   terms of the gradient of the (offset) v (local time) function. */
  current_freq_ppm = (1.0 - dfreq) * old_freq_ppm +
    (1.0e6 * dfreq);

#ifdef TRACEON
  LOG(LOGS_INFO, LOGF_Local, "old_freq=%.3fppm new_freq=%.3fppm offset=%.6fsec",
      old_freq_ppm, current_freq_ppm, doffset);
#endif

  /* Call the system-specific driver for setting the frequency */
  (*drv_set_freq)(current_freq_ppm);
  (*drv_accrue_offset)(doffset);

  /* Dispatch to all handlers */
  for (ptr = change_list.next; ptr != &change_list; ptr = ptr->next) {
    (ptr->handler)(&raw, &cooked, dfreq, current_freq_ppm, doffset, 0, ptr->anything);
  }


}

/* ================================================== */

void
lcl_InvokeDispersionNotifyHandlers(double dispersion)
{
  DispersionNotifyListEntry *ptr;

  for (ptr = dispersion_notify_list.next; ptr != &dispersion_notify_list; ptr = ptr->next) {
    (ptr->handler)(dispersion, ptr->anything);
  }

}

/* ================================================== */

void
lcl_RegisterSystemDrivers(lcl_ReadFrequencyDriver read_freq,
                          lcl_SetFrequencyDriver set_freq,
                          lcl_AccrueOffsetDriver accrue_offset,
                          lcl_ApplyStepOffsetDriver apply_step_offset,
                          lcl_OffsetCorrectionDriver offset_convert,
                          lcl_SetLeapDriver set_leap)
{
  drv_read_freq = read_freq;
  drv_set_freq = set_freq;
  drv_accrue_offset = accrue_offset;
  drv_apply_step_offset = apply_step_offset;
  drv_offset_convert = offset_convert;
  drv_set_leap = set_leap;

  current_freq_ppm = (*drv_read_freq)();

#ifdef TRACEON
  LOG(LOGS_INFO, LOGF_Local, "Local freq=%.3fppm", current_freq_ppm);
#endif

  return;
}

/* ================================================== */
/* Look at the current difference between the system time and the NTP
   time, and make a step to cancel it if it's larger than the threshold. */

int
LCL_MakeStep(double threshold)
{
  struct timeval raw;
  double correction;

  LCL_ReadRawTime(&raw);
  correction = LCL_GetOffsetCorrection(&raw);

  if (fabs(correction) <= threshold)
    return 0;

  /* Cancel remaining slew and make the step */
  LCL_AccumulateOffset(correction);
  LCL_ApplyStepOffset(-correction);

  LOG(LOGS_WARN, LOGF_Local, "System clock was stepped by %.3f seconds", correction);

  return 1;
}

/* ================================================== */

void
LCL_SetLeap(int leap)
{
  if (drv_set_leap) {
    (drv_set_leap)(leap);
  }

  return;
}

/* ================================================== */
