{ Copyright (C) 2005 Bas Steendijk and Peter Green
  For conditions of distribution and use, see copyright notice in zlib_license.txt
  which is included in the package
  ----------------------------------------------------------------------------- }
{
this unit returns unix timestamp with seconds and microseconds (as float)
works on windows/delphi, and on freepascal on unix.
}


unit btime;

interface

type
  float=extended;

const
  colorburst=39375000/11;  {3579545.4545....}

var
  timezone:integer;
  timezonestr:string;
  irctime,unixtime:integer;
  tickcount:integer;
  settimebias:integer;
  performancecountfreq:extended;

function irctimefloat:float;
function irctimeint:integer;

function unixtimefloat:float;
function unixtimeint:integer;

function wintimefloat:float;

procedure settime(newtime:integer);
procedure gettimezone;
procedure timehandler;
procedure init;

function timestring(i:integer):string;
function timestrshort(i:integer):string;

{$ifdef win32}
function unixtimefloat_systemtime:float;
{$endif}

function oletounixfloat(t:float):float;
function oletounix(t:tdatetime):integer;
function unixtoole(i:integer):tdatetime;

{$ifdef win32}
function mmtimefloat:float;
function qpctimefloat:float;
{$endif}

const
  mmtime_driftavgsize=32;
  mmtime_warmupnum=4;
  mmtime_warmupcyclelength=15;
var
  //this flag is to be set when btime has been running long enough to stabilise
  warmup_finished:boolean;

  timefloatbias:float;
  ticks_freq:float=0;
  ticks_freq2:float=0;
  ticks_freq_known:boolean=false;
  lastunixtimefloat:float=0;
  lastsynctime:float=0;
  lastsyncbias:float=0;

  mmtime_last:integer=0;
  mmtime_wrapadd:float;
  mmtime_lastsyncmm:float=0;
  mmtime_lastsyncqpc:float=0;
  mmtime_drift:float=1;
  mmtime_lastresult:float;
  mmtime_nextdriftcorrection:float;
  mmtime_driftavg:array[0..mmtime_driftavgsize] of float;
  mmtime_synchedqpc:boolean;

  mmtime_prev_drift:float;
  mmtime_prev_lastsyncmm:float;
  mmtime_prev_lastsyncqpc:float;

implementation

{$ifdef fpc}
  {$mode delphi}
{$endif}

uses
  {$ifdef UNIX}
    {$ifdef VER1_0}
      linux,
    {$else}
      baseunix,unix,unixutil, {needed for 2.0.2}
    {$endif}
  {$else}
    windows,unitsettc,mmsystem,
  {$endif}
  sysutils;

  {$include unixstuff.inc}


const
  daysdifference=25569;

function oletounixfloat(t:float):float;
begin
  t := (t - daysdifference) * 86400;
  result := t;
end;

function oletounix(t:tdatetime):integer;
begin
  result := trunc(oletounixfloat(t));
end;

function unixtoole(i:integer):tdatetime;
begin
  result := ((i)/86400)+daysdifference;
end;

const
  highdwordconst=65536.0 * 65536.0;

function utrunc(f:float):integer;
{converts float to integer, in 32 bits unsigned range}
begin
  if f >= (highdwordconst/2) then f := f - highdwordconst;
  result := trunc(f);
end;

function uinttofloat(i:integer):float;
{converts 32 bits unsigned integer to float}
begin
  result := i;
  if result < 0 then result := result + highdwordconst;
end;

{$ifdef unix}
{-----------------------------------------*nix/freepascal code to read time }

function unixtimefloat:float;
var
  tv:ttimeval;
begin
  gettimeofday(tv);
  result := tv.tv_sec+(tv.tv_usec/1000000);
end;

function wintimefloat:extended;
begin
  result := unixtimefloat;
end;

function unixtimeint:integer;
var
  tv:ttimeval;
begin
  gettimeofday(tv);
  result := tv.tv_sec;
end;

{$else} {delphi 3}
{------------------------------ windows/delphi code to read time}

{
time float: gettickcount
resolution: 9x: ~55 ms NT: 1/64th of a second
guarantees: continuous without any jumps
frequency base: same as system clock.
epoch: system boot
note: if called more than once per 49.7 days, 32 bits wrapping is compensated for and it keeps going on.
note: i handle the timestamp as signed integer, but with the wrap compensation that works as well, and is faster
}

function mmtimefloat:float;
const
  wrapduration=highdwordconst * 0.001;
var
  i:integer;
begin
  i := gettickcount; {timegettime}
  if i < mmtime_last then begin
    mmtime_wrapadd := mmtime_wrapadd + wrapduration;
  end;
  mmtime_last := i;
  result := mmtime_wrapadd + i * 0.001;

  if (ticks_freq <> 0) and ticks_freq_known then result := int((result / ticks_freq)+0.5) * ticks_freq; //turn the float into an exact multiple of 1/64th sec to improve accuracy of things using this
end;

procedure measure_ticks_freq;
var
  f,g:float;
  o:tosversioninfo;
  isnt:boolean;
  is9x:boolean;
begin
  if (performancecountfreq = 0) then qpctimefloat;
  ticks_freq_known := false;
  settc;
  f := mmtimefloat;
  repeat g := mmtimefloat until g > f;
  unsettc;
  f := g - f;
  fillchar(o,sizeof(o),0);
  o.dwOSVersionInfoSize := sizeof(o);
  getversionex(o);
  isnt := o.dwPlatformId = VER_PLATFORM_WIN32_NT;
  is9x := o.dwPlatformId = VER_PLATFORM_WIN32_WINDOWS;

  ticks_freq2 := f;
  mmtime_synchedqpc := false;
  {
  NT 64 Hz
  identify mode as: nt64
  QPC rate: either 3579545 or TSC freq
  QPC synched to gettickcount: no
  duration between 2 ticks is constant: yes
  gettickcount tick duration: 64 Hz
  }
  if (f >= 0.014) and (f <= 0.018) and isnt then begin
    ticks_freq_known := true;
    ticks_freq := 1/64;
    mmtime_synchedqpc := false;
  end;

  {
  NT 100 Hz
  identify mode as: nt100
  QPC rate: 1193182
  QPC synched to gettickcount: yes
  duration between 2 ticks is constant: no?
  gettickcount tick duration: ~99.85 Hz
  }
  if (performancecountfreq = 1193182) and (f >= 0.008) and (f <= 0.012) and isnt then begin
    ticks_freq_known := true;
    ticks_freq2 := 11949 / (colorburst / 3);
   //  ticks_freq2 := 11949 / 1193182;
    ticks_freq := 0;
    {the ticks freq should be very close to the real one but if it's not exact, it will cause drift and correction jumps}
    mmtime_synchedqpc := true;
  end;

  {9x}
  if (performancecountfreq = 1193182) and (g >= 0.050) and (g <= 0.060) then begin
    ticks_freq_known := true;
    ticks_freq := 65536 / (colorburst / 3);
    mmtime_synchedqpc := true;
  end;
  ticks_freq_known := true;
  if ticks_freq <> 0 then ticks_freq2 := ticks_freq;
//  writeln(formatfloat('0.000000',ticks_freq));
end;

{
time float: QueryPerformanceCounter
resolution: <1us
guarantees: can have forward jumps depending on hardware. can have forward and backwards jitter on dual core.
frequency base: on NT, not the system clock, drifts compared to it.
epoch: system boot
}
function qpctimefloat:extended;
var
  p:packed record
    lowpart:longint;
    highpart:longint
  end;
  p2:tlargeinteger absolute p;
  e:extended;
begin
  if performancecountfreq = 0 then begin
    QueryPerformancefrequency(p2);
    e := p.lowpart;
    if e < 0 then e := e + highdwordconst;
    performancecountfreq := ((p.highpart*highdwordconst)+e);
  end;
  queryperformancecounter(p2);
  e := p.lowpart;
  if e < 0 then e := e + highdwordconst;

  result := ((p.highpart*highdwordconst)+e)/performancecountfreq;
end;

{
time float: QPC locked to gettickcount
resolution: <1us
guarantees: continuous without any jumps
frequency base: same as system clock.
epoch: system boot
}

function mmqpctimefloat:float;
const
  maxretries=5;
  margin=0.002;
var
  jump:float;
  mm,f,qpc,newdrift,f1,f2:float;
  qpcjumped:boolean;
  a,b,c:integer;
  retrycount:integer;
begin
  if not ticks_freq_known then measure_ticks_freq;
  retrycount := maxretries;

  qpc := qpctimefloat;
  mm := mmtimefloat;
  f := (qpc - mmtime_lastsyncqpc) * mmtime_drift + mmtime_lastsyncmm;
  //writeln('XXXX ',formatfloat('0.000000',qpc-mm));
  qpcjumped := ((f-mm) > ticks_freq2+margin) or ((f-mm) < -margin);
//  if qpcjumped then writeln('qpc jumped ',(f-mm));
  if ((qpc > mmtime_nextdriftcorrection) and not mmtime_synchedqpc) or qpcjumped then begin

    mmtime_nextdriftcorrection := qpc + 1;
    repeat
      mmtime_prev_drift := mmtime_drift;
      mmtime_prev_lastsyncmm := mmtime_lastsyncmm;
      mmtime_prev_lastsyncqpc := mmtime_lastsyncqpc;

      mm := mmtimefloat;
      dec(retrycount);
      settc;
      result := qpctimefloat;
      f := mmtimefloat;
      repeat
        if f = mm then result := qpctimefloat;
        f := mmtimefloat
      until f > mm;
      qpc := qpctimefloat;

      unsettc;
      if (qpc > result + 0.0001) then begin
        continue;
      end;
      mm := f;

      if (mmtime_lastsyncqpc <> 0) and not qpcjumped then begin
        newdrift := (mm - mmtime_lastsyncmm) / (qpc - mmtime_lastsyncqpc);
        mmtime_drift := newdrift;
     {   writeln('raw drift: ',formatfloat('0.00000000',mmtime_drift));}
        move(mmtime_driftavg[0],mmtime_driftavg[1],sizeof(mmtime_driftavg[0])*high(mmtime_driftavg));
        mmtime_driftavg[0] := mmtime_drift;

{        write('averaging drift ',formatfloat('0.00000000',mmtime_drift),' -> ');}
{        mmtime_drift := 0;}
        b := 0;
        for a := 0 to high(mmtime_driftavg) do begin
          if mmtime_driftavg[a] <> 0 then inc(b);
{          mmtime_drift := mmtime_drift + mmtime_driftavg[a];}
        end;
{        mmtime_drift := mmtime_drift / b;}
        if (b = 1) then a := 5 else if (b = 2) then a := 15 else if (b = 3) then a := 30 else if (b = 4) then a := 60 else if (b = 5) then a := 120 else if (b >= 5) then a := 120;
        mmtime_nextdriftcorrection := qpc + a;
        if (b >= 2) then warmup_finished := true;
{        writeln(formatfloat('0.00000000',mmtime_drift));}
       if mmtime_synchedqpc then mmtime_drift := 1;
      end;

      mmtime_lastsyncqpc := qpc;
      mmtime_lastsyncmm := mm;
  {   writeln(formatfloat('0.00000000',mmtime_drift));}
      break;
    until false;


    qpc := qpctimefloat;

    result := (qpc - mmtime_lastsyncqpc) * mmtime_drift + mmtime_lastsyncmm;
    f := (qpc - mmtime_prev_lastsyncqpc) * mmtime_prev_drift + mmtime_prev_lastsyncmm;

    jump := result-f;
    {writeln('jump ',formatfloat('0.000000',jump),'   drift ',formatfloat('0.00000000',mmtime_drift),' duration ',formatfloat('0.000',(mmtime_lastsyncqpc-mmtime_prev_lastsyncqpc)),' ',formatfloat('0.00000000',jump/(mmtime_lastsyncqpc-mmtime_prev_lastsyncqpc)));}

    f := result;
  end;

  result := f;

  if (result < mmtime_lastresult) then result := mmtime_lastresult + 0.000001;
  mmtime_lastresult := result;
end;

{ free pascals tsystemtime is incomaptible with windows api calls
 so we declare it ourselves - plugwash
}
{$ifdef fpc}
type
  TSystemTime = record
     wYear: Word;
     wMonth: Word;
     wDayOfWeek: Word;
     wDay: Word;
     wHour: Word;
     wMinute: Word;
     wSecond: Word;
     wMilliseconds: Word;
  end;
 {$endif}
function Date_utc: extended;
var
  SystemTime: TSystemTime;
begin
  {$ifdef fpc}
    GetsystemTime(@SystemTime);
  {$else}
    GetsystemTime(SystemTime);
  {$endif}
  with SystemTime do Result := EncodeDate(wYear, wMonth, wDay);
end;

function Time_utc: extended;
var
  SystemTime: TSystemTime;
begin
  {$ifdef fpc}
    GetsystemTime(@SystemTime);
  {$else}
    GetsystemTime(SystemTime);
  {$endif}
  with SystemTime do
    Result := EncodeTime(wHour, wMinute, wSecond, wMilliSeconds);
end;

function Now_utc: extended;
begin
  Result := round(Date_utc) + Time_utc;
end;

function unixtimefloat_systemtime:float;
begin
  {result := oletounixfloat(now_utc);}

  {this method gives exactly the same result with extended precision, but is less sensitive to float rounding in theory}
  result := oletounixfloat(int(date_utc+0.5))+time_utc*86400;
end;

function wintimefloat:extended;
begin
  result := mmqpctimefloat;
end;

function unixtimefloat:float;
const
  margin = 0.0012;
var
  f,g,h:float;
begin
  result := wintimefloat+timefloatbias;
  f := result-unixtimefloat_systemtime;
  if ((f > ticks_freq2+margin) or (f < -margin)) or (timefloatbias = 0) then begin
//    writeln('unixtimefloat init');
    f := unixtimefloat_systemtime;
    settc;
    repeat g := unixtimefloat_systemtime; h := wintimefloat until g > f;
    unsettc;
    timefloatbias := g-h;
    result := unixtimefloat;
  end;

  {for small changes backwards, guarantee no steps backwards}
  if (result <= lastunixtimefloat) and (result > lastunixtimefloat-1.5) then result := lastunixtimefloat + 0.0000001;
  lastunixtimefloat := result;
end;

function unixtimeint:integer;
begin
  result := trunc(unixtimefloat);
end;

{$endif}
{-----------------------------------------------end of platform specific}

function irctimefloat:float;
begin
  result := unixtimefloat+settimebias;
end;

function irctimeint:integer;
begin
  result := unixtimeint+settimebias;
end;


procedure settime(newtime:integer);
var
  a:integer;
begin
  a := irctimeint-settimebias;
  if newtime = 0 then settimebias := 0 else settimebias := newtime-a;

  irctime := irctimeint;
end;

procedure timehandler;
begin
  if unixtime = 0 then init;
  unixtime := unixtimeint;
  irctime := irctimeint;
  if unixtime and 63 = 0 then begin
    {update everything, apply timezone changes, clock changes, etc}
    gettimezone;
    timefloatbias := 0;
    unixtime := unixtimeint;
    irctime := irctimeint;
  end;
end;


procedure gettimezone;
var
  {$ifdef UNIX}
    {$ifndef ver1_9_4}
      {$ifndef ver1_0}
        {$define above194}
      {$endif}
    {$endif}
    {$ifndef above194}
      hh,mm,ss:word;
    {$endif}
  {$endif}
  l:integer;
begin
  {$ifdef UNIX}
    {$ifdef above194}
      timezone := tzseconds;
    {$else}
      gettime(hh,mm,ss);
      timezone := (longint(hh) * 3600 + mm * 60 + ss) - (unixtimeint mod 86400);
    {$endif}
  {$else}
  timezone := round((now-now_utc)*86400);
  {$endif}

  while timezone > 43200 do dec(timezone,86400);
  while timezone < -43200 do inc(timezone,86400);

  if timezone >= 0 then timezonestr := '+' else timezonestr := '-';
  l := abs(timezone) div 60;
  timezonestr := timezonestr + char(l div 600 mod 10+48)+char(l div 60 mod 10+48)+':'+char(l div 10 mod 6+48)+char(l mod 10+48);
end;

function timestrshort(i:integer):string;
const
  weekday:array[0..6] of string[4]=('Thu','Fri','Sat','Sun','Mon','Tue','Wed');
  month:array[0..11] of string[4]=('Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec');
var
  y,m,d,h,min,sec,ms:word;
  t:tdatetime;
begin
  t := unixtoole(i+timezone);
  decodedate(t,y,m,d);
  decodetime(t,h,min,sec,ms);
  result := weekday[(i+timezone) div 86400 mod 7]+' '+month[m-1]+' '+inttostr(d)+' '+
  inttostr(h div 10)+inttostr(h mod 10)+':'+inttostr(min div 10)+inttostr(min mod 10)+':'+inttostr(sec div 10)+inttostr(sec mod 10)+' '+
  inttostr(y);
end;

function timestring(i:integer):string;
const
  weekday:array[0..6] of string[10]=('Thursday','Friday','Saturday','Sunday','Monday','Tuesday','Wednesday');
  month:array[0..11] of string[10]=('January','February','March','April','May','June','July','August','September','October','November','December');
var
  y,m,d,h,min,sec,ms:word;
  t:tdatetime;
begin
  t := unixtoole(i+timezone);
  decodedate(t,y,m,d);
  decodetime(t,h,min,sec,ms);
  result := weekday[(i+timezone) div 86400 mod 7]+' '+month[m-1]+' '+inttostr(d)+' '+inttostr(y)+' -- '+
  inttostr(h div 10)+inttostr(h mod 10)+':'+inttostr(min div 10)+inttostr(min mod 10)+':'+inttostr(sec div 10)+inttostr(sec mod 10)+' '+
  timezonestr;
end;

procedure init;
begin
  {$ifdef win32}timebeginperiod(1);{$endif} //ensure stable unchanging clock
  fillchar(mmtime_driftavg,sizeof(mmtime_driftavg),0);
  settimebias := 0;
  gettimezone;
  unixtime := unixtimeint;
  irctime := irctimeint;
end;

initialization init;

end.
