--------------------------------------------------------------------------------
-- A library for the manipulation of dates and periods according to the
-- Gregorian calendar.
--
-- Credit: the Gregorian calendar routines contained in this library are 
-- ported from Claus Tøndering calendar algorithms:
-- http://www.tondering.dk/main/index.php/calendar-information .
--
-- Copyright (C) 2011-2016 Stefano Peluchetti. All rights reserved.
--------------------------------------------------------------------------------

local ffi  = require "ffi"

local C = ffi.C
local format = string.format
local floor, min = math.floor, math.min
local type, new, istype, tonumber = type, ffi.new, ffi.istype, tonumber

local int64_ct = ffi.typeof("int64_t")

local function T_int(x)
  if not (type(x) == "number" and x == floor(x)) then
    error("integer number expected")
  end
end

local function T_same(x, y)
  if not istype(x, y) then
    error("same type expected")
  end
end

-- Period ----------------------------------------------------------------------
-- Return string representation for a positive period.
local function posptostr(h, m, s, ms)
  return format("%02i:%02i:%02i.%06i", h, m, s, ms)
end

local p_ct

local function T_period(x)
  if not istype(p_ct, x) then
    error("period expected")
  end
end

local function p_64(ticks)
  return new(p_ct, ticks)
end

local function pfirst(x, y)
  if istype(p_ct, y) then
    return y, x
  else
    return x, y
  end
end

local p_mt = {
  __new = function(ct, h, m, s, ms)
    h = h or 0; m = m or 0; s = s or 0; ms = ms or 0;
    T_int(h); T_int(m); T_int(s); T_int(ms);
    return new(ct, h*(1e6*60*60LL)+m*(1e6*60LL)+s*(1e6*1LL)+ms)
  end,
  copy = function(self)
    return new(p_ct, self)
  end,
  __eq = function(self, rhs) T_same(self, rhs)
    return self._ticks == rhs._ticks
  end,
  __lt = function(self, rhs) T_same(self, rhs)
    return self._ticks < rhs._ticks
  end,
  __le = function(self, rhs) T_same(self, rhs)
    return self._ticks <= rhs._ticks
  end,
  __add = function(self, rhs) T_same(self, rhs) -- Commutative.
    return p_64(self._ticks + rhs._ticks) 
  end,
  __sub = function(self, rhs) T_same(self, rhs)
    return p_64(self._ticks - rhs._ticks)
  end,
  __unm = function(self)
    return p_64(-self._ticks)
  end,
  __mul = function(self, rhs) -- Commutative.
    local p, n = pfirst(self, rhs)
    T_int(n)
    return p_64(p._ticks*n)    
  end,
  -- Approximate ratio, non-reversible in both cases.
  __div = function(self, rhs)
    T_period(self) -- Disallow (not-a-period)/period.
    if type(rhs) == "number" then
      return p_64(self._ticks/rhs)
    elseif istype(p_ct, rhs) then
      return tonumber(self._ticks)/tonumber(rhs._ticks)
    else
      error("unexpected type")
    end
  end,
  __tostring = function(self)
    local h, m, s, ms = self:parts()
    if self._ticks >= 0 then
      return posptostr(h, m, s, ms)
    else
      return "-"..posptostr(-h, -m, -s, -ms)
    end
  end,
  ticks = function(self) -- Expose int64_t.
    return self._ticks 
  end,
  microseconds = function(self)
    return tonumber(self._ticks%1e6)
  end,
  seconds = function(self)
    return tonumber((self._ticks/1e6)%60)
  end,
  minutes = function(self)
    return tonumber((self._ticks/(1e6*60))%60)
  end,
  hours = function(self)
    return tonumber(self._ticks/(1e6*60*60))
  end,
  parts = function(self)
    return self:hours(), self:minutes(), self:seconds(), self:microseconds()
  end,
  tomicroseconds = function(self)
    return tonumber(self._ticks)
  end,
  tomilliseconds = function(self)
    return tonumber(self._ticks)/1e3
  end,
  toseconds = function(self)
    return tonumber(self._ticks)/1e6
  end,
  tominutes = function(self)
    return tonumber(self._ticks)/(1e6*60)
  end,
  tohours = function(self)
    return tonumber(self._ticks)/(1e6*60*60)
  end,
}
p_mt.__index = p_mt

p_ct = ffi.metatype("struct { int64_t _ticks; }", p_mt)

local function weeks(x)        T_int(x) return p_64(x*(1e6*60*60*24*7LL)) end
local function days(x)         T_int(x) return p_64(x*(1e6*60*60*24LL)) end
local function hours(x)        T_int(x) return p_64(x*(1e6*60*60LL)) end
local function minutes(x)      T_int(x) return p_64(x*(1e6*60LL)) end
local function seconds(x)      T_int(x) return p_64(x*(1e6*1LL)) end
local function milliseconds(x) T_int(x) return p_64(x*(1e3*1LL)) end
local function microseconds(x) T_int(x) return p_64(x*1LL) end

local function toperiod(x)
  if type(x) == "string" then
    local f1, l1, h, m, s, ms = x:find("(%d+):(%d+):(%d+).(%d+)")
    if h == nil or ms == nil or l1 ~= #x then
      error("'"..x.."' is not a string representation of a period")
    end
    local ton = tonumber
    return p_ct(ton(h), ton(m), ton(s), ton(ms))
  elseif istype(int64_ct, x) then
    return p_64(x)
  else
    error("unexpected type")
  end
end

-- Months ----------------------------------------------------------------------
local months_mt = {
  __new = function(ct, x) T_int(x)
    return new(ct, x)
  end,
}

local months_ct = ffi.metatype("struct { int32_t _m; }", months_mt)

-- Years -----------------------------------------------------------------------
local years_mt = {
  __new = function(ct, x) T_int(x)
    return new(ct, x)
  end,
}

local years_ct = ffi.metatype("struct { int32_t _y; }", years_mt)

-- Date ------------------------------------------------------------------------
-- It's date(1582, 1, 1):
local d_ticks_min = 198622713600000000LL 
-- It's date(9999,12,31) + period(23, 59, 59, 999999):
local d_ticks_max = 464269103999999999LL

local d_ct

local function T_date(x)
  if not istype(d_ct, x) then
    error("date expected")
  end
end

local function T_date_range(ticks)
  if not (d_ticks_min <= ticks and ticks <= d_ticks_max) then
    error("resulting date is outside the allowed range")
  end
end

local function d_64(ticks) T_date_range(ticks)
  return new(d_ct, ticks)
end

local function dfirst(x, y)
  if istype(d_ct, y) then
    return y, x
  else
    return x, y
  end
end

-- 1582 adoption, 9999 to keep 4 chars for years part:
local function T_year(year) T_int(year)
  if not (1582 <= year and year <= 9999) then
    error("year "..year.." outside the allowed range [1582, 9999]")
  end
end

local function T_month(month) T_int(month)
  if not (1 <= month and month <= 12) then
    error("month "..month.." outside the allowed range [1, 12]")
  end
end

local function isleapyear(year) T_year(year)
  return year%4 == 0 and (year%100 ~= 0 or year%400 == 0)
end

local eom = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }

local function endofmonth(year, month) T_year(year) T_month(month)
  return (month == 2 and isleapyear(year)) and 29 or eom[month]
end

local function T_day(year, month, day)
  if not (1 <= day and day <= endofmonth(year, month)) then
    error(year.."-"..month.."-"..day.." is not a valid date")
  end
end

local function weekday(year, month, day) T_year(year) T_month(month) 
  T_day(year, month, day)
  local a = floor((14 - month)/12)
  local y = year - a
  local m = month + 12*a - 2
  local d = (day + y + floor(y/4) - floor(y/100) + floor(y/400) +
    floor((31*m)/12)) % 7
  return d == 0 and 7 or d -- Days of week from 1 = Monday to 7 = Sunday.
end

local function shift_months(y, m, deltam)
  local newm = (m - 1 + deltam) % 12 + 1
  local newy = y + floor((m - 1 + deltam)/12)
  T_year(newy)
  T_month(newm)
  return newy, newm
end

local function ymd_to_julian(year, month, day)
  -- Range of numbers suffices for this:
  local a = floor((14 - month)/12)
  local y = year + 4800 - a
  local m = month + 12*a - 3
  return day + floor((153*m + 2)/5) + 365*y + floor(y/4) - floor(y/100) +
    floor(y/400) - 32045
end

local function julian_to_ymd(julian)
  -- Range of numbers suffices for this:
  local a = julian + 32044
  local b = floor((4*a + 3)/146097)
  local c = a - floor((146097*b)/4)
  local d = floor((4*c + 3)/1461)
  local e = c - floor((1461*d)/4)
  local m = floor((5*e + 2)/153)
  local day = e - floor((153*m + 2)/5) + 1
  local month = m + 3 - 12*floor(m/10)
  local year = 100*b + d - 4800 + floor(m/10)
  return year, month, day
end

-- Assumes only violation of valid date may be in day outside of end of month
-- due to months and years shifts. Cap the day and returns a valid date.
local function valid_date_cap_day(year, month, day)
  day = min(day, endofmonth(year, month))
  return d_ct(year, month, day)
end

local d_mt = {
  __new = function(ct, year, month, day) T_year(year) T_month(month) 
    T_day(year, month, day)
    return new(ct, ymd_to_julian(year, month, day)*(86400LL*1e6))
  end,
  copy = function(self)
    return new(d_ct, self)
  end,
  __eq = p_mt.__eq,
  __lt = p_mt.__lt,
  __le = p_mt.__le,
  __add = function(self, rhs) -- Commutative.
    local d, s = dfirst(self, rhs)    
    if istype(p_ct, s) then 
      return d_64(d._ticks + s._ticks)
    elseif istype(months_ct, s) then
      local year, month, day = d:ymd()
      year, month = shift_months(year, month, s._m)
      return valid_date_cap_day(year, month, day) + d:period()
    elseif istype(years_ct, s) then
      local year, month, day = d:ymd()
      year = year + s._y
      return valid_date_cap_day(year, month, day) + d:period()
    else
      error("unexpected type")
    end
  end,
  __sub = function(self, rhs)
    T_date(self) -- Disallow (not-a-date)-date.
    if istype(p_ct, rhs) then 
      return d_64(self._ticks - rhs._ticks)
    elseif istype(months_ct, rhs) then
      local year, month, day = self:ymd()
      year, month = shift_months(year, month, -rhs._m)
      return valid_date_cap_day(year, month, day) + self:period()
    elseif istype(years_ct, rhs) then
      local year, month, day = self:ymd()
      year = year - rhs._y
      return valid_date_cap_day(year, month, day) + self:period()
    elseif istype(d_ct, rhs) then
      return p_64(self._ticks - rhs._ticks)
    else
      error("unexpected type")
    end
  end,
  __tostring = function(self)
    local year, month, day = self:ymd()
    local h, m, s, ms = self:period():parts()
    return format("%i-%02i-%02iT", year, month, day)..posptostr(h, m, s, ms)
  end,
  ticks = p_mt.ticks,
  ymd = function(self)
    local julian = tonumber(self._ticks/(86400LL*1e6))
    return julian_to_ymd(julian)
  end,
  year  = function(self) local y, m, d = self:ymd() return y end,
  month = function(self) local y, m, d = self:ymd() return m end,
  day   = function(self) local y, m, d = self:ymd() return d end,
  period = function(self)
    return p_64(self._ticks%(86400LL*1e6))
  end,
  isleapyear = function(self) 
    local y = self:ymd()
    return isleapyear(y) 
  end,
  endofmonth = function(self) 
    local y, m = self:ymd()
    return endofmonth(y, m)
  end,
  weekday = function(self)
    local y, m, d = self:ymd()
    return weekday(y, m, d)
  end,
}
d_mt.__index = d_mt

d_ct = ffi.metatype("struct { int64_t _ticks; }", d_mt)

local function todate(x)
  if type(x) == "string" then
    local f1, l1, year, month, day, h, m, s, ms = 
      x:find("(%d+)-(%d+)-(%d+)T(%d+):(%d+):(%d+).(%d+)")
    if year == nil or ms == nil or l1 ~= #x then
      error("'"..x.."' is not a string representation of a date")
    end
    local ton = tonumber
    return d_ct(ton(year), ton(month), ton(day)) + 
      p_ct(ton(h), ton(m), ton(s), ton(ms))
  elseif istype(int64_ct, x) then
    return d_64(x)
  else
    error("unexpected type")
  end
end

-- System-dependent functions --------------------------------------------------
local nowlocal, nowutc, sleep

if jit.os == "Windows" then -- On Windows sizeof(long) == 4 on both x86 and x64.
  ffi.cdef[[
  typedef unsigned long    DWORD;
  typedef unsigned short   WORD;
  typedef unsigned __int64 ULONGLONG;

  typedef struct _SYSTEMTIME {
    WORD wYear;
    WORD wMonth;
    WORD wDayOfWeek;
    WORD wDay;
    WORD wHour;
    WORD wMinute;
    WORD wSecond;
    WORD wMilliseconds;
  } SYSTEMTIME, *PSYSTEMTIME;

  typedef union _ULARGE_INTEGER {
    struct {
      DWORD LowPart;
      DWORD HighPart;
    };
    struct {
      DWORD LowPart;
      DWORD HighPart;
    } u;
    ULONGLONG QuadPart;
  } ULARGE_INTEGER, *PULARGE_INTEGER;

  typedef struct _FILETIME {
    DWORD dwLowDateTime;
    DWORD dwHighDateTime;
  } FILETIME, *PFILETIME;

  void GetLocalTime(
    PSYSTEMTIME lpSystemTime
  );
  //void GetSystemTime(
  //  PSYSTEMTIME lpSystemTime
  //);
  void GetSystemTimeAsFileTime(
    PFILETIME lpSystemTimeAsFileTime
  );
  void GetSystemTimeAsPreciseFileTime(
    PFILETIME lpSystemTimeAsFileTime
  );
  void Sleep(
    DWORD dwMilliseconds
  );
  ]]
  
  local st = ffi.new("SYSTEMTIME")
  local ft = ffi.new("FILETIME")
  local ul = ffi.new("ULARGE_INTEGER")

  nowlocal = function()
    C.GetLocalTime(st) 
    return d_ct(st.wYear, st.wMonth, st.wDay) + p_ct(st.wHour, st.wMinute,
      st.wSecond, st.wMilliseconds*1000)
  end

  local epoch_offset = d_ct(1601, 1, 1):ticks()
  local function if_available(lib, fname)
    local ok, f = pcall(function() return lib[fname] end)
    return ok and f
  end
  local get_system_time = if_available(C, 'GetSystemTimeAsPreciseFileTime')
    or C.GetSystemTimeAsFileTime

  nowutc = function()
    -- Resolution: 1 microsecond.
    -- Accuracy  : 1 millisecond up to Windows 7, higher otherwise.
    get_system_time(ft)
    ul.LowPart  = ft.dwLowDateTime
    ul.HighPart = ft.dwHighDateTime
    return new(d_ct, epoch_offset + ul.QuadPart/10)
  end

  sleep = function(p)
    if p < p_ct() then
      error("cannot sleep a negative amount of time")
    end
    C.Sleep(p:ticks()/1000)
  end

else -- Linux and OSX.
  ffi.cdef[[
  typedef long time_t;
  typedef int useconds_t;

  typedef struct timeval {
    long tv_sec;
    int tv_usec;
  } timeval;
  typedef struct tm {
    int tm_sec;
    int tm_min;
    int tm_hour;
    int tm_mday;
    int tm_mon;
    int tm_year;
    int tm_wday;
    int tm_yday;
    int tm_isdst;
    long tm_gmtoff;
    char *tm_zone;
  } tm;

  int gettimeofday(
    struct timeval * restrict, 
    void * restrict
  );
  struct tm *localtime(
    const time_t *
  );
  int usleep(
    useconds_t useconds
  );
  ]]

  -- C.host_get_clock_service it's slower and we don't need higher resolution.
  -- C.mach_absolute_time does not report real clock.
  local epoch_offset  = d_ct(1970, 1, 1):ticks()
  local tv = ffi.new("timeval[1]")
  local tt = ffi.new("time_t[1]")

  nowlocal = function()
    C.gettimeofday(tv, nil)
    tt[0] = tv[0].tv_sec
    local tm = C.localtime(tt)
    return d_ct(1900 + tm.tm_year, 1 + tm.tm_mon, tm.tm_mday) +
           p_ct(tm.tm_hour, tm.tm_min, tm.tm_sec, tv[0].tv_usec)
  end

  nowutc = function()
    -- Resolution: 1 microsecond.
    -- Accuracy  : 1 microsecond.
    C.gettimeofday(tv, nil)
    return new(d_ct, epoch_offset + tv[0].tv_sec*1000000LL + tv[0].tv_usec)
  end

 sleep = function(p)
    if p < p_ct() then
      error("cannot sleep a negative amount of time")
    end
    C.usleep(p:ticks())
  end
end

return {
  period       = p_ct,   
  toperiod     = toperiod,
  
  weeks        = weeks,
  days         = days,
  hours        = hours,
  minutes      = minutes,
  seconds      = seconds,
  milliseconds = milliseconds,
  microseconds = microseconds,
  
  date         = d_ct,
  todate       = todate,
  
  isleapyear   = isleapyear,
  endofmonth   = endofmonth,
  weekday      = weekday,
  
  months       = months_ct,
  years        = years_ct,
  
  sleep        = sleep,
  nowlocal     = nowlocal,
  nowutc       = nowutc,
}
