/*****************************************************************************/
/*
                                    gmt.c

Greenwich Mean Time related functions.

A little convoluted because SYS$TIMEZONE_DIFFERENTIAL was often unreliable
(at least administered) under VMS earlier than V7.0, and WASD's stated
objective of support back to VMS V6.0, hence the HTTPD$GMT hack used by WASD.


COPYRIGHT
---------
Copyright (C) 2005-2025 Mark G.Daniel
This program, comes with ABSOLUTELY NO WARRANTY.
This is free software, and you are welcome to redistribute it under the
conditions of the GNU GENERAL PUBLIC LICENSE, version 3, or any later version.


VERSION HISTORY
---------------
01-FEB-2005  MGD  initial
*/
/*****************************************************************************/

#ifdef SOYMAIL_VMS_V7
#undef _VMS_V6_SOURCE
#define _VMS_V6_SOURCE
#undef __VMS_VER
#define __VMS_VER 70000000
#undef __CRTL_VER
#define __CRTL_VER 70000000
#endif

#pragma nomember_alignment

/* standard C header files */
#include <ctype.h>
#include <errno.h>
#include <ints.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* VMS related header files */
#include <descrip.h>
#include <lnmdef.h>
#include <ssdef.h>
#include <stsdef.h>

/* application header file */
#include <gmt.h>

#define VMSok(x) ((x) & STS$M_SUCCESS)
#define VMSnok(x) !(((x) & STS$M_SUCCESS))

#define BOOL int
#define TRUE 1
#define FALSE 0

#define FI_LI "GMT", __LINE__

/******************/
/* global storage */
/******************/

BOOL  GmtTimeAhead;
long  GmtSecondsOffset;
int64  GmtTimeDelta64;
char  GmtTimeString [32],
      GmtVmsTimeString [32];

/**************/
/* prototypes */
/**************/

int lib$add_times (__unknown_params);
int lib$cvt_vectim (__unknown_params);
int lib$day_of_week (__unknown_params);
int lib$mult_delta_time (__unknown_params);
int lib$sub_times (__unknown_params);
int sys$bintim (__unknown_params);
int sys$fao (__unknown_params);
int sys$gettim (__unknown_params);
int sys$numtim (__unknown_params);
int sys$trnlnm (__unknown_params);

/********************/
/* external storage */
/********************/

extern BOOL  Debug,
             WatchEnabled;

/*****************************************************************************/
/*
Create an HTTP format Greenwich Mean Time (UTC) time string in the storage 
pointed at by 'TimeString', e.g. "Fri, 25 Aug 1995 17:32:40 GMT" (RFC 1123). 
This must be at least 31 characters capacity.  If 'BinTimePtr' is NULL the 
time string represents the current time.  If it points to a quadword, VMS time 
value the string represents that time.
*/

int GmtBinTimeToString
(
char *TimeString,
int64 *Time64Ptr,
unsigned long UnixTime
)
{
   static char  *DayNames [] =
      { "", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun" };
   static char  *MonthName [] =
      { "", "Jan", "Feb", "Mar", "Apr", "May", "Jun",
            "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };

   static $DESCRIPTOR (HttpTimeFaoDsc, "!AZ, !2ZW !AZ !4ZW !2ZW:!2ZW:!2ZW GMT");
   static $DESCRIPTOR (TimeStringDsc, "");

   int  status;
   int64  Time64,
          GmTime64;
   unsigned short  Length;
   unsigned short  NumTime [7];
   unsigned long  DayOfWeek;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "GmtBinTimeToString()\n");

   if (UnixTime)
      GmtUnixToVmsBinTime (UnixTime, &GmTime64);
   else
   if (!Time64Ptr)
      sys$gettim (&GmTime64);
   else
      GmTime64 = *Time64Ptr;
   status = GmtTimeAdjust (TRUE, &GmTime64);
   if (VMSnok (status)) return (status);

   status = sys$numtim (&NumTime, &GmTime64);
   if (Debug)
      fprintf (stdout, "sys$numtim() %%X%08.08X %d %d %d %d %d %d %d\n",
               status, NumTime[0], NumTime[1], NumTime[2],
               NumTime[3], NumTime[4], NumTime[5], NumTime[6]);

   status = lib$day_of_week (&GmTime64, &DayOfWeek);
   if (VMSnok (status)) return (status);
   if (Debug)
      fprintf (stdout, "lib$day_of_week() %%X%08.08X is %d\n",
               status, DayOfWeek);

   /* set the descriptor address and size of the resultant time string */
   TimeStringDsc.dsc$w_length = 30;
   TimeStringDsc.dsc$a_pointer = TimeString;

   status = sys$fao (&HttpTimeFaoDsc, &Length, &TimeStringDsc,
                     DayNames[DayOfWeek], NumTime[2], MonthName[NumTime[1]],
                     NumTime[0], NumTime[3], NumTime[4], NumTime[5]);
   if (VMSnok (status))
   {
      if (Debug) fprintf (stdout, "sys$fao() %%X%08.08X\n", status);
      TimeString[0] = '\0';
   }
   else
      TimeString[Length] = '\0';
   if (Debug) fprintf (stdout, "|%s|\n", TimeString);

   return (status);
}

/*****************************************************************************/
/*
*/ 

/* we've discovered it! */
#define DISCOVER_VMS_AT_1970 0

int GmtUnixToVmsBinTime
(
unsigned long UnixTime,
int64 *Time64Ptr
)
{
#if DISCOVER_VMS_AT_1970
   static $DESCRIPTOR (UnixEpoch, "1-JAN-1970 00:00:00.00");
#endif /* DISCOVER_VMS_AT_1970 */

   static int64  OneSecondDelta64 = -10000000;
   static int64  BinTime1970 = 1273708544 + ((uint64)8164711 << 32);

   int  status;
   int64  UnixTime64;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "GmtUnixToVmsBinTime()\n");

#if DISCOVER_VMS_AT_1970
   sys$bintim (&UnixEpoch, &BinTime1970);
   /* shows the binary time at 1970 */
   if (Debug) fprintf (stdout, "%d %d\n", BinTime1970[0], BinTime1970[1]);
#endif /* DISCOVER_VMS_AT_1970 */

   UnixTime64 = OneSecondDelta64;

   status = lib$mult_delta_time (&UnixTime64, &UnixTime64);
   if (Debug) fprintf (stdout, "lib$mult_delta_time() %%X%08.08X\n", status);

   status = lib$add_times (&UnixTime64, &BinTime1970, Time64Ptr);
   if (Debug) fprintf (stdout, "lib$add_times() %%X%08.08X\n", status);

   return (status);
}

/*****************************************************************************/
/*
Given a string such as "Fri, 25 Aug 1995 17:32:40 GMT" (RFC 1123), or "Friday,
25-Aug-1995 17:32:40 GMT" (RFC 1036), create an internal, local, binary time
with the current GMT offset.  See complementary function GmtBinToString().
Returns a VMS status.
*/ 

int GmtStringToBinTime
(
char *TimeString,
int64 *Time64Ptr
)
{
   static char  *MonthName [] =
      { "", "Jan", "Feb", "Mar", "Apr", "May", "Jun",
            "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };

   int  status;
   char  *tptr;
   unsigned short  Length;
   unsigned short  NumTime [7] = { 0,0,0,0,0,0,0 };
   unsigned long  DayOfWeek;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "GmtStringToBinTime() |%s|\n", TimeString);

   tptr = TimeString;
   /* hunt straight for the comma after the weekday name! */
   while (*tptr && *tptr != ',') tptr++;
   if (*tptr) tptr++;
   /* span white space between weekday name and date */
   while (*tptr && isspace(*tptr)) tptr++;
   if (Debug) fprintf (stdout, "tptr |%s|\n", tptr);
   if (!*tptr) return (STS$K_ERROR);

   /* get the date and then skip to month name */
   if (isdigit(*tptr)) NumTime[2] = atoi (tptr);
   while (*tptr && isdigit(*tptr)) tptr++;
   while (*tptr && (*tptr == '-' || isspace(*tptr))) tptr++;
   if (Debug) fprintf (stdout, "tptr |%s|\n", tptr);
   if (!*tptr) return (STS$K_ERROR);

   /* get the month number from the name and skip to the year */
   for (NumTime[1] = 1; NumTime[1] <= 12; NumTime[1]++)
      if (!strncmp (tptr, MonthName[NumTime[1]], 3)) break;
   if (NumTime[1] > 12) return (STS$K_ERROR);
   while (*tptr && isalpha(*tptr)) tptr++;
   while (*tptr && (*tptr == '-' || isspace(*tptr))) tptr++;
   if (Debug) fprintf (stdout, "tptr |%s|\n", tptr);
   if (!*tptr) return (STS$K_ERROR);

   /* get the year and then skip to the hour */
   if (isdigit(*tptr))
   {
      NumTime[0] = atoi (tptr);
      if (NumTime[0] < 100) NumTime[0] += 1900;
   }
   while (*tptr && isdigit(*tptr)) tptr++;
   while (*tptr && isspace(*tptr)) tptr++;
   if (Debug) fprintf (stdout, "tptr |%s|\n", tptr);
   if (!*tptr) return (STS$K_ERROR);

   /* get the hour, minute and second */
   if (isdigit(*tptr)) NumTime[3] = atoi (tptr);
   while (*tptr && isdigit(*tptr)) tptr++;
   if (*tptr == ':') tptr++;
   if (isdigit(*tptr)) NumTime[4] = atoi (tptr);
   while (*tptr && isdigit(*tptr)) tptr++;
   if (*tptr == ':') tptr++;
   if (isdigit(*tptr)) NumTime[5] = atoi (tptr);
   while (*tptr && isdigit(*tptr)) tptr++;
   if (*tptr == ':') tptr++;
   if (!*tptr) return (STS$K_ERROR);

   /* the only thing remaining should be the "GMT" */
   while (*tptr && isspace(*tptr)) tptr++;
   if (Debug) fprintf (stdout, "tptr |%s|\n", tptr);
   if (strncmp (tptr, "GMT", 3)) return (STS$K_ERROR);

   /*******************************************/
   /* convert what looks like legitimate GMT! */
   /*******************************************/

   if (Debug)
      fprintf (stdout, "NumTime[] %d %d %d %d %d %d %d\n",
               NumTime[0], NumTime[1], NumTime[2], NumTime[3], 
               NumTime[4], NumTime[5], NumTime[6]); 
   status = lib$cvt_vectim (&NumTime, Time64Ptr);
   if (VMSnok (status)) return (status);
   if (Debug) fprintf (stdout, "lib$cvt_vectim() %%X%08.08X\n", status);

   return (GmtTimeAdjust (FALSE, Time64Ptr));
}

/*****************************************************************************/
/*
Determine the offset from GMT (UTC) using either the HTTPD$GMT or
SYS$TIMEZONE_DIFFERENTIAL logicals. If HTTPD$GMT is not defined time should be
set from the timezone differential logical.
*/

int GmtSetDelta ()

{
   static BOOL  UseTimezoneDifferential = FALSE;

   int  status;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "GmtSetDelta()\n");

   if (!UseTimezoneDifferential)
   {
      status = GmtSetDeltaFromHttpdGmt();
      /* return if error and that error was not that the name did not exist */
      if (VMSok (status) || status != SS$_NOLOGNAM) return (status);
   }

   UseTimezoneDifferential = TRUE;
   GmtSetDeltaFromTimezone();

   return (status);
}

/*****************************************************************************/
/*
The SYS$TIMEZONE_DIFFERENTIAL logical contains the number of seconds offset
from GMT (UTC) as a positive (ahead) or negative (behind) number.  Set the
'GmtTimeString' global storage to a "+hh:mm" or "-hh:mm" string equivalent, and
the 'GmtTimeAhead' global boolean and 'GmtVmsTimeString' delta-time global
string.
*/

int GmtSetDeltaFromTimezone ()

{
   static unsigned short  Length;
   static $DESCRIPTOR (TimezoneLogicalNameDsc, "SYS$TIMEZONE_DIFFERENTIAL");
   static $DESCRIPTOR (LnmSystemDsc, "LNM$SYSTEM");
   static $DESCRIPTOR (GmtTimeStringFaoDsc, "!AZ!2ZL:!2ZL");
   static $DESCRIPTOR (GmtVmsTimeStringFaoDsc, "0 !2ZL:!2ZL");
   static $DESCRIPTOR (GmtTimeStringDsc, GmtTimeString);
   static struct {
      short int  buf_len;
      short int  item;
      void  *buf_addr;
      unsigned short  *ret_len;
   } LnmItems [] =
   {
      { sizeof(GmtTimeString)-1, LNM$_STRING, GmtTimeString, &Length },
      { 0,0,0,0 }
   };

   int  status;
   long  Hours,
         Minutes,
         Seconds;
   char  *SignPtr;
   $DESCRIPTOR (GmtVmsTimeStringDsc, GmtVmsTimeString);

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "GmtSetDeltaFromTimezone()\n");

   status = sys$trnlnm (0, &LnmSystemDsc, &TimezoneLogicalNameDsc, 0,
                        &LnmItems);
   if (Debug) fprintf (stdout, "sys$trnlnm() %%X%08.08X\n", status);
   if (VMSnok (status)) return (status);

   GmtTimeString[Length] = '\0';
   if (Debug) fprintf (stdout, "GmtTimeString |%s|\n", GmtTimeString);
   GmtSecondsOffset = Seconds = atol(GmtTimeString);
   if (Debug) fprintf (stdout, "GmtSecondsOffset: %d\n", GmtSecondsOffset);
   if (Seconds < 0)
   {
      GmtTimeAhead = FALSE;
      Seconds = -Seconds;
      SignPtr = "-";
   }
   else
   {
      GmtTimeAhead = TRUE;
      SignPtr = "+";
   }
   Hours = Seconds / 3600;
   Minutes = (Seconds - Hours * 3600) / 60;
   if (Debug)
      fprintf (stdout, "%d %s%d:%d\n", Seconds, SignPtr, Hours, Minutes);
   sys$fao (&GmtTimeStringFaoDsc, &Length, &GmtTimeStringDsc,
            SignPtr, Hours, Minutes);
   GmtTimeString[Length] = '\0';
   if (Debug) fprintf (stdout, "GmtTimeString |%s|\n", GmtTimeString);

   sys$fao (&GmtVmsTimeStringFaoDsc, &Length, &GmtVmsTimeStringDsc,
            Hours, Minutes);
   GmtVmsTimeString[Length] = '\0';
   if (Debug) fprintf (stdout, "GmtVmsTimeString |%s|\n", GmtVmsTimeString);

   GmtVmsTimeStringDsc.dsc$w_length = Length;

   status = sys$bintim (&GmtVmsTimeStringDsc, &GmtTimeDelta64);
   if (VMSnok (status)) return (status);
   if (GmtTimeDelta64) return (status);
   /* time must have been zero, make it one one-hundreth of a second */
   GmtTimeDelta64 = -100000;
   return (SS$_NORMAL);
}

/*****************************************************************************/
/*
Translate the logical HTTPD$GMT (defined to be something like "+10:30" or "-
01:15") and convert it into a delta time structure and store in 
'GmtTimeDelta64'.  Store whether it is in advance or behind GMT in boolean 
'GmtTimeAhead'.  Store the logical string in 'GmtTimeString'.
*/

int GmtSetDeltaFromHttpdGmt ()

{
   static unsigned short  Length;
   static $DESCRIPTOR (GmtLogicalNameDsc, "HTTPD$GMT");
   static $DESCRIPTOR (LnmFileDevDsc, "LNM$FILE_DEV");
   static struct {
      short int  buf_len;
      short int  item;
      void  *buf_addr;
      unsigned short  *ret_len;
   } LnmItems [] =
   {
      { sizeof(GmtTimeString)-1, LNM$_STRING, GmtTimeString, &Length },
      { 0,0,0,0 }
   };

   int  status, hours, minutes;
   char  *cptr, *sptr;
   $DESCRIPTOR (GmtVmsTimeStringDsc, GmtVmsTimeString);

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "GmtSetDeltaFromHttpdGmt()\n");

   status = sys$trnlnm (0, &LnmFileDevDsc, &GmtLogicalNameDsc, 0, &LnmItems);
   if (Debug) fprintf (stdout, "sys$trnlnm() %%X%08.08X\n", status);
   if (VMSnok (status)) return (status);

   GmtTimeString[Length] = '\0';
   if (Debug) fprintf (stdout, "GmtTimeString |%s|\n", GmtTimeString);

   if (GmtTimeString[0] == '$') return (SS$_NORMAL);

   if (*(cptr = GmtTimeString) == '-')
      GmtTimeAhead = FALSE;
   else
      GmtTimeAhead = TRUE;
   if (*cptr == '+' || *cptr == '-') cptr++;
   sptr = GmtVmsTimeString;
   *sptr++ = '0';
   *sptr++ = ' ';
   while (*cptr) *sptr++ = *cptr++;
   *sptr = '\0';
   if (Debug) fprintf (stdout, "GmtVmsTimeString |%s|\n", GmtVmsTimeString);

   cptr = GmtVmsTimeString;
   if (*cptr == '+' || *cptr == '-') cptr++;
   hours = atol(cptr);
   while (*sptr && *cptr != ':') cptr++;
   if (*cptr == ':') cptr++;
   minutes = atol(cptr);
   GmtSecondsOffset = hours * 3600 + minutes * 60;
   if (!GmtTimeAhead) GmtSecondsOffset = -GmtSecondsOffset;
   if (Debug) fprintf (stdout, "GmtSecondsOffset: %d\n", GmtSecondsOffset);

   GmtVmsTimeStringDsc.dsc$w_length = sptr - GmtVmsTimeString;

   if (VMSnok (status)) return (status);
   status = sys$bintim (&GmtVmsTimeStringDsc, &GmtTimeDelta64);
   if (GmtTimeDelta64) return (status);
   /* time must have been zero, make it one one-hundreth of a second */
   GmtTimeDelta64 = -100000;
   return (SS$_NORMAL);
}

/*****************************************************************************/
/*
The GMT is generated by calculating an offset using  'GmtTimeDelta64' and
boolean 'GmtTimeAhead'.  Adjust either to or from  GMT.
*/ 

int GmtTimeAdjust
(
BOOL ToGmTime,
int64 *Time64Ptr
)
{
   int  status;
   int64  AdjustedTime64;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "GmtTimeAdjust() ToGmTime: %d\n", ToGmTime);

   if (GmtTimeDelta64) GmtSetDelta ();

   if ((ToGmTime && GmtTimeAhead) || (!ToGmTime && !GmtTimeAhead))
   {
      /* to GMT from local and ahead of GMT, or to local from GMT and behind */
      status = lib$sub_times (Time64Ptr, &GmtTimeDelta64, &AdjustedTime64);
      if (Debug) fprintf (stdout, "lib$sub_times() %%X%08.08X\n", status);
   }
   else
   {
      /* to GMT from local and behind GMT, or to local from GMT and ahead */
      status = lib$add_times (Time64Ptr, &GmtTimeDelta64, &AdjustedTime64);
      if (Debug) fprintf (stdout, "lib$add_times() %%X%08.08X\n", status);
   }

   if (Debug)
   {
      unsigned short  Length;
      char  String [64];
      $DESCRIPTOR (AdjustedTimeFaoDsc, "AdjustedTime64: |!%D|\n");
      $DESCRIPTOR (StringDsc, String);

      sys$fao (&AdjustedTimeFaoDsc, &Length, &StringDsc, &AdjustedTime64);
      String[Length] = '\0';
      fprintf (stdout, "%s", String);
   }

   *Time64Ptr = AdjustedTime64;

   return (status);
}

/*****************************************************************************/

