;; Copyright (c) 2012 Alan Watson. All rights reserved. BSD-style
;; license: http://synthcode.com/license.txt

;; This library implements procedures that give the TAI to UTC offset a
;; specified instant in the UTC or TAI timescales.

(define-library (scheme time tai-to-utc-offset)
  
  (export tai-to-utc-offset-at-utc-day
          tai-to-utc-offset-at-tai-second
          set-open-leap-seconds-list-port!
          set-update-exception-handler!)
  
  (import (scheme base))
  (import (scheme file))
  (import (scheme read))
  (import (scheme process-context))
  
  (cond-expand
   (threads
    (import (srfi 18)))
   (else
    (begin
      (define (make-thread thunk name) #f)
      (define (thread-start! th) #f)
      (define (thread-sleep! secs) #f))))
  
  (cond-expand
   (chibi
    (begin
      (define *file-name-environment-variable* "SEXP_LEAP_SECONDS_LIST_FILE")))
   (else
    (begin
      (error "Need to define *file-name-environment-variable*."))))
  
  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  
  (cond-expand
   
   ;; We ensure thread-safe atomic access to mutated bindings using
   ;; atomic boxes.
   
   (chibi
    (begin
      
      ;; This implementation relies on record accessors and mutators
      ;; being atomic in Chibi Scheme.
      
      (define-record-type atomic-box-record-type
        (make-atomic-box value)
        atomic-box?
        (value atomic-box-value atomic-box-value-set!))))
   
   (else
    (begin
      
      ;; This implementation uses SRFI-18 mutexes.
      
      (define (make-atomic-box value)
        (let ((mutex (make-mutex)))
          (mutex-specific-set! mutex value)
          mutex))
      
      (define atomic-box? mutex?)
      
      (define (atomic-box-value mutex)
        (mutex-lock! mutex)
        (let ((value (mutex-specific mutex)))
          (mutex-unlock! mutex)
          value))
      
      (define (atomic-box-value-set! mutex value)
        (mutex-lock! mutex)
        (mutex-specific-set! mutex value)
        (mutex-unlock! mutex)))))
  
  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  
  (begin
    
    (define seconds-per-day 86400)
    
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    
    ;; First, a remark on timescales.
    ;;
    ;; The TAI timescale used in this library has an epoch of 1970-01-01
    ;; 00:00:00 TAI. The epoch used by the current-second procedure in
    ;; the draft R7RS (scheme time) library is the "TAI-10" timescale
    ;; with an epoch of 1970-01-01 00:00:10 TAI.
    
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    
    ;; The library maintains a cache of values derived from the leap
    ;; seconds list.
    ;;
    ;; The first cached value is the utc-day-alist. This is an alist
    ;; whose cars are the UTC days since 1970-01-01 00:00:00 UTC and
    ;; whose cdrs are the corresponding TAI to UTC offsets.
    ;;
    ;; The second cached value is the tai-second-alist. This is an alist
    ;; whose cars are the TAI seconds since 1970-01-01 00:00:00 TAI and
    ;; whose cdrs are the corresponding TAI to UTC offsets.
    
    (define-record-type cache-record-type
      (make-cache utc-day-alist tai-second-alist)
      cache?
      (utc-day-alist cache-utc-day-alist)
      (tai-second-alist cache-tai-second-alist))
    
    (define (make-cache-from-port port)
      (let ((utc-day-alist (read-leap-seconds-list port)))
        (make-cache utc-day-alist
                    (utc-day-alist->tai-second-alist utc-day-alist))))
    
    ;; The utc-day-alist->tai-second-alist procedure converts an alist
    ;; indexed by the number of UTC day since 1970-01-01 00:00:00 UTC
    ;; into an equivalent alist indexed by the number of TAI seconds
    ;; since 1970-01-01 00:00:00 TAI.
    ;;
    ;; This procedure does not have to worry about the complications of
    ;; transforming UTC dates prior to 1972-01-01 00:00:00 UTC to TAI,
    ;; since there were no leap seconds prior to this date.
    
    (define (utc-day-alist->tai-second-alist utc-day-alist)
      (map
       (lambda (p)
         (let* ((utc-day      (car p))
                (leap-seconds (cdr p))
                (tai-second   (+ (* utc-day seconds-per-day) leap-seconds)))
           (cons tai-second leap-seconds)))
       utc-day-alist))
    
    ;; The library updates the cache: when the library is loaded; when
    ;; the set-open-leap-seconds-list-port! is called; and once per day.
    ;; These automatic periodic updates are useful in long-running
    ;; programs.
    ;;
    ;; The choice of daily updates is motivated by the following
    ;; considerations. ITU-R TF.460-6, which contains the current
    ;; definition of UTC, requires that the IERS should announce leap
    ;; seconds with at least eight weeks in advance. However, NIST,
    ;; which maintains the leap-seconds.list file, only guarantees one
    ;; month. Recently the IERS and NIST have managed six months of
    ;; notice, but we should not rely on this.
    ;;
    ;; During updates, we install the update-exception-handler.
    
    (define *cache-lifetime* (* 1 seconds-per-day))
    
    (define (update-cache! open-port)
      (with-exception-handler
          (update-exception-handler)
        (lambda ()
          (let ((port (open-port)))
            (when port
              (set-cache! (make-cache-from-port port))
              (close-input-port port))))))
    
    (when (get-environment-variable *file-name-environment-variable*)
      (thread-start!
       (make-thread
        (lambda ()
          (let loop ()
            (thread-sleep! *cache-lifetime*)
            (update-cache! (open-leap-seconds-list-port))
            (loop)))
        "leap-second-update-poll")))
    
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    
    ;; The read-leap-seconds-list procedure reads text from the port
    ;; argument. The text must follow the format of a NIST leap seconds
    ;; file, for example,
    ;;
    ;;   ftp://time.nist.gov/pub/leap-seconds.list
    ;;
    ;; The procedure returns an alist. The cars of the pairs are the
    ;; number of whole UTC days since 1970-01-01 00:00:00 UTC and the
    ;; cdrs of the pairs are the corresponding TAI to UTC offset. The
    ;; alist is ordered by decreasing car. The cars and cdrs are exact
    ;; integers.
    ;;
    ;; TODO: Do not rely on the input file being correctly ordered. Do
    ;; not rely on the input data being exact integers.
    ;;
    ;; TODO: Check known leap seconds.
    
    (define (read-leap-seconds-list port)
      
      (define (ntp-second->utc-day ntp-second)
        ;; The NTP initial epoch is 1900-01-01 00:00:00 UTC. The UTC
        ;; initial epoch is 1970-01-01 00:00:00 UTC. There are 70 years,
        ;; containing 17 leap days, between these epochs.
        (- (quotient ntp-second seconds-per-day) (* 70 365) 17))
      
      (define (leap-seconds-line->pair line)
        (let* ((line-port         (open-input-string line))
               (ntp-second        (read line-port))
               (tai-to-utc-offset (read line-port))
               (utc-day           (ntp-second->utc-day ntp-second))
               (leap-seconds      tai-to-utc-offset))
          (cons utc-day leap-seconds)))
      
      (define (leap-seconds-comment-line? line)
        (char=? #\# (string-ref line 0)))
      
      (define (read-leap-seconds-line port)
        (let ((line (read-line port)))
          (cond
           ((eof-object? line) line)
           ((leap-seconds-comment-line? line) (read-leap-seconds-line port))
           (else line))))
      
      (let loop ((alist '()))
        (let ((line (read-leap-seconds-line port)))
          (if (eof-object? line)
            alist
            (loop (cons (leap-seconds-line->pair line) alist))))))
    
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    
    ;; (set-open-leap-seconds-list-port! p)
    ;;
    ;; The set-open-leap-seconds-list-port! procedure sets the value of
    ;; procedure called to obtain the leap second list is read to its
    ;; argument p and then performs a cache update. The procedure should
    ;; return either #f, signifying that no leap second list is
    ;; available, or an input port, from which a leap-second list,
    ;; following the format of the NIST leap-second list, will be read.
    ;; The NIST leap-second file can be found here:
    ;;
    ;;   ftp://time.nist.gov/pub/leap-seconds.list
    ;;
    ;; The default procedure attempts to open the file named by the
    ;; environment variable SEXP_LEAP_SECOND_LIST_FILE and returns the
    ;; port. If the environment variable is not set or if the file does
    ;; not exist, it return #f.
    
    (define *open-leap-seconds-list-port*
      (make-atomic-box
       (lambda ()
         (when *file-name-environment-variable*
           (let ((file-name
                  (get-environment-variable *file-name-environment-variable*)))
             (if file-name
                 (open-input-file file-name)
                 #f))))))
    
    (define (open-leap-seconds-list-port)
      (atomic-box-value *open-leap-seconds-list-port*))
    
    (define (set-open-leap-seconds-list-port! p)
      (atomic-box-value-set! *open-leap-seconds-list-port* p)
      (update-cache! (open-leap-seconds-list-port)))
    
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    
    ;; (set-update-exception-handler! p)
    ;;
    ;; The set-update-exception-handler! procedure sets the value of the
    ;; error handler installed during cache updates. The default error
    ;; handler simply raises the exception again.
    
    (define *update-exception-handler*
      (make-atomic-box raise))
    
    (define (update-exception-handler)
      (atomic-box-value *update-exception-handler*))
    
    (define (set-update-exception-handler! p)
      (atomic-box-value-set! *update-exception-handler* p))
    
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    
    (define *cache*
      (make-atomic-box #f))
    
    (define (cache)
      (atomic-box-value *cache*))
    
    (define (set-cache! value)
      (atomic-box-value-set! *cache* value))
    
    (update-cache!
     (lambda ()
       (open-input-string
        (string-append
         
         ;; These strings are lines extracted from:
         ;;
         ;;   ftp://time.nist.gov/pub/leap-seconds.3676924800
         ;;
         ;; The original file contains extensive comments on the format
         ;; and provenance of the data, which have been removed from
         ;; this version.
         
         "2272060800 10  # 1 Jan 1972\n"
         "2287785600 11  # 1 Jul 1972\n"
         "2303683200 12  # 1 Jan 1973\n"
         "2335219200 13  # 1 Jan 1974\n"
         "2366755200 14  # 1 Jan 1975\n"
         "2398291200 15  # 1 Jan 1976\n"
         "2429913600 16  # 1 Jan 1977\n"
         "2461449600 17  # 1 Jan 1978\n"
         "2492985600 18  # 1 Jan 1979\n"
         "2524521600 19  # 1 Jan 1980\n"
         "2571782400 20  # 1 Jul 1981\n"
         "2603318400 21  # 1 Jul 1982\n"
         "2634854400 22  # 1 Jul 1983\n"
         "2698012800 23  # 1 Jul 1985\n"
         "2776982400 24  # 1 Jan 1988\n"
         "2840140800 25  # 1 Jan 1990\n"
         "2871676800 26  # 1 Jan 1991\n"
         "2918937600 27  # 1 Jul 1992\n"
         "2950473600 28  # 1 Jul 1993\n"
         "2982009600 29  # 1 Jul 1994\n"
         "3029443200 30  # 1 Jan 1996\n"
         "3076704000 31  # 1 Jul 1997\n"
         "3124137600 32  # 1 Jan 1999\n"
         "3345062400 33  # 1 Jan 2006\n"
         "3439756800 34  # 1 Jan 2009\n"
         "3550089600 35  # 1 Jul 2012\n"
         "3644697600 36  # 1 Jul 2015\n"
         "3692217600 37  # 1 Jan 2017\n"))))
    
    (update-cache! (open-leap-seconds-list-port))
    
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    
    ;; (tai-to-utc-offset-at-utc-day utc-day)
    ;;
    ;; The tai-to-utc-offset-at-utc-day procedure returns the TAI to UTC
    ;; offset at the instant specified by its argument utc-day. The
    ;; instant is specified by the number of UTC days since 1970-01-01
    ;; 00:00:00 UTC.
    ;;
    ;; To convert a UTC time since 1972-01-01 00:00:00 UTC to number of
    ;; TAI seconds since 1972-01-01 00:00:00 TAI, first find the number
    ;; of whole UTC days D since 1970-01-01 00:00:00 UTC and the number
    ;; of UTC seconds S since the start of the current day. The number
    ;; of TAI seconds since 1972-01-01 00:00:00 TAI is then
    ;;
    ;;   (+ (* D 86400) S (tai-to-utc-offset-at-utc-day D))
    ;;
    ;; This implementation does not return the correct result for
    ;; instants prior to 1972-01-01 00:00:00 UTC.
    ;;
    ;; Converting a UTC time prior to 1972-01-01 00:00:00 UTC to TAI is
    ;; more involved, since prior to this date UTC and TAI seconds were
    ;; not equal. See,
    ;;
    ;;   http://hpiers.obspm.fr/eop-pc/index.php?index=TAI-UTC_tab
    
    (define (tai-to-utc-offset-at-utc-day-loop utc-day alist)
      (cond ((null? alist) 10)
            ((>= utc-day (caar alist)) (cdar alist))
            (else (tai-to-utc-offset-at-utc-day-loop utc-day (cdr alist)))))
    
    (define (tai-to-utc-offset-at-utc-day utc-day)
      (tai-to-utc-offset-at-utc-day-loop
       utc-day
       (cache-utc-day-alist (cache))))
    
    ;; (tai-to-utc-offset-at-tai-second tai-second)
    ;;
    ;; The tai-to-utc-offset-at-tai-second procedure returns TAI to UTC
    ;; offset at the instant specified by its argument tai-second. The
    ;; instant is specified by the number of TAI seconds since
    ;; 1970-01-01 00:00:00 TAI.
    ;;
    ;; This implementation does not return the correct result for
    ;; instants prior to 1972-01-01 00:00:00 UTC.
    
    (define (tai-to-utc-offset-at-tai-second-loop tai-second alist)
      (cond
       ((null? alist) 10)
       ((>= tai-second (caar alist)) (cdar alist))
       (else (tai-to-utc-offset-at-tai-second-loop tai-second (cdr alist)))))
    
    (define (tai-to-utc-offset-at-tai-second tai-second)
      (tai-to-utc-offset-at-tai-second-loop
       tai-second
       (cache-tai-second-alist (cache))))))
