/***************************************************************************
 
Copyright (C) 1997/98  Jos den Bekker  <josdb@xs4all.nl>

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.

This library 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
Library General Public License for more details.

You should have received a copy of the GNU Library General Public
License along with this library; if not, write to the
Free Software Foundation, Inc., 59 Temple Place - Suite 330,
Boston, MA  02111-1307, USA.

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

/***************************************************************************
 GetTZ(char*,struct tz*)
   Parses timezone environment string (see TZ.DOC) and fills in 
   structure (struct tz) with timezone name, DST tag, GMT offset, DST 
   offset, plus begin and end date & time of DST period; times and 
   dates are in seconds; dates are seconds since 01.01.1970 00:00:00 h 
   and can be used by the standard lib time functions.
            
   The value `inrange' in struct tz indicates if current time falls 
   within the DST time-range. It is superfluous, but included for easy 
   use of GetTZ(). There is in fact an inherent bug with this value: 
   at the end of the DST period, a `grey zone' equal to the length of 
   the DST offset value occurs, when the clock might have been set 
   back to `normal time', causing the `inrange' flag te be valid, when 
   in fact it is not. I have chosen to set the flag only to valid if 
   current time is at least smaller than end of DST period minus DST 
   offset. This paradoxical situation can only be remedied by having 
   access to an external value, indicating if the internal clock has 
   been set to `winter time' or `summer time'. When using these 
   functions in a program that can take recourse to such a value, the 
   `inrange' value can be determined independently with the help of 
   the begin and end values in struct tz.
            
 MakeTZ(char *s,const struct tz *tz,int tag))
   Write GMT offset plus descriptive timezone tag into string s.
 
 10.96
  3.97
 
 $Id: gettz.c 1.5 1998/06/04 17:45:44 josdb Exp josdb $
****************************************************************************/
#include <stdio.h>
#include <time.h>
#include <string.h>
#include <ctype.h>
#include <stdlib.h>
#include <stdarg.h>
#ifndef UNIX
#include <mylib\proto.h>
#include <mylib\tz.h>
#else
#include <mylib/proto.h>
#include <mylib/tz.h>
#endif

#define MAIN 0
#define TEST 0

#ifdef __PUREC__
 #ifdef strdup
 #undef strdup
 #endif
#define strdup my_strdup
#endif

#define HOUR (60L*60L)
#define DAY (24L*HOUR)

#define ISLEAPYEAR(x) (((x)%4==0 && (x)%100!=0) || (x)%400==0)

extern char *strtoknst(char *s1,char *s2,char **ptr);

static enum tizo {none,GMT,UTC,UT,WAT,AT,AST,ATL,EST,CST,MST,PST,YST,ALA,CAT,HAW,HST,AHST,
           CET,MET,EET,MSK,WSU,BT,WAST,CCT,JST,EAST,GST,NZT,NZST} timezo = none;

extern void tzerror(char *s);

static void _tzerror_internal(char *fmt,...)
{
  va_list argp;
  char args[500+1];
  
  va_start(argp,fmt);
  vsprintf(args,fmt,argp);
  va_end(argp);
  tzerror(args);
}

static time_t seek_day(time_t mark,int wday)
{
  struct tm *datum;
  
  for(;;mark-=DAY)
  {
    datum=localtime(&mark);
    if (datum->tm_wday == wday)
      break;
  }
  return mark;
}

long calch(char *s)
{
  char *p,*tmp;
  int i;
  long c=0L;
  
  if(!*s)
    return 2L*HOUR;
  for(i=1,p=strtoknst(s,":",&tmp);p && i<=3;p=strtoknst(NULL,":",&tmp),i++)
    c += i==1 ? labs(atol(p))*HOUR :
       i==2 ? atol(p)*60L :
       atol(p);
  if (c<0L || c>DAY)
  {
    c=2L*HOUR;
    _tzerror_internal("Wrong hour value: %s\nUsing default: 2:0:0",s);
  }
  return *s=='-'?-c:c;
}

static time_t get_startyear(int year)
{
  struct tm thisyear;
  
  memset(&thisyear,0,sizeof(struct tm));
  thisyear.tm_mday=1;
  thisyear.tm_year=year;
  return mktime(&thisyear);
}

time_t calcd(char *s,const struct tm *date,time_t startyear)
{
  char *p,*tmp;
  time_t c=0L;
  int m,w,d=0;
  
  if (!*s)
    return 0L;
  
  p=strtoknst(s,".",&tmp);
  m=atoi(p)-1;
  if (m<0 || m>11)
  {
    _tzerror_internal("Wrong month value: %d",m+1);
    return -1L;
  }
  c+=(time_t)m*31L;
  c-=m<2?0L:m<8?2L+(time_t)m/2L:1L+(time_t)m/2L;
  c*=DAY;
  
  if ((p=strtoknst(NULL,".",&tmp))==NULL)
  {
    _tzerror_internal("Week value missing.");
    return -1L;
  }
  else
  {
    w=atoi(p);
    if (w<1 || w>5)
    {
      _tzerror_internal("Wrong week value: %d",w);
      return -1L;
    }
  }
  if ((p=strtoknst(NULL,".",&tmp))==NULL)
  {
    _tzerror_internal("Day value missing.");
    return -1L;
  }
  else
  {
    d=atoi(p);
    if (d<0 || d>6)
    {
      _tzerror_internal("Wrong day value: %d",d);
      return -1L;
    }
  }
  c += startyear==-1L ? get_startyear(date->tm_year) : startyear;
  if (w==5)
  {
    struct tm *datum;
   
    c+=31L*DAY;
    datum=localtime(&c);
    while (datum->tm_mon!=m)
    {
      c-=DAY;
      datum=localtime(&c);
    }
  }
  else
    c+=((w*7L)-1L)*DAY;
  c+=3L*HOUR;
  c=seek_day(c,d);
  c-=3L*HOUR;
  return startyear==-1L ? c : c-startyear;
}

static int range(const struct tz *tz,time_t now,struct tm *date)
{
  if (tz->begin==0 || tz->end==0)
  {  
    char s[10];
    
    switch (timezo)
    {
      case none:
        return 0;
      case GMT:
      case UTC:
      case MET:
      case CET:
      case EET:
      case MSK:
      case WSU:
      case BT:
        strcpy(s,"M3.5.0/2");
        tz->begin=calc_be(s,date);
        strcpy(s,"M10.5.0/3");
        tz->end=calc_be(s,date);
        break;
      case ATL:
      case AST:
      case EST:
      case CST:
      case MST:
      case PST:
        strcpy(s,"M4.1.0/2");
        tz->begin=calc_be(s,date);
        strcpy(s,"M10.5.0/3");
        tz->end=calc_be(s,date);
        break;
      case WAST:
      case EAST:
      case GST:
      case NZT:
      case NZST:
        strcpy(s,"M10.5.0/2");
        tz->begin=calc_be(s,date);
        strcpy(s,"M3.5.0/3");
        tz->end=calc_be(s,date);
    }
  }
  if (now>=tz->begin && now<(tz->end-(tz->dst_offset-tz->gmt_offset)))
    return 1;
  return 0;
}

static void parse_tz(char *tz_evar,struct tz *tz)
{
  char d[9+1],*p=tz_evar,*q=NULL;
  
  d[0]='\0';
  while (isspace(*p)) p++;
  if (*p!='\0' && isalpha(*p))     /* timezone string */
  {
    for(q=tz->tz_string;isalpha(*p);p++,q++)
      *q=*p;
    *q='\0';
  }
  while (isspace(*p)) p++;
  if (*p!='\0' && (isdigit(*p) || *p=='-' || *p=='+')) /* gmt_offset value */
  {
    for(;*p=='-'||*p=='+'||isdigit(*p)||*p==':';p++)
      strncat(d,p,1);
    tz->gmt_offset=calch(d);
    d[0]='\0';
  }
  else
    tz->gmt_offset=-99L*HOUR;
  while (isspace(*p)) p++;
  if (*p!='\0' && isalpha(*p))     /* DST string */
  {
    /*if (!strncmp(p+1,"DT",2)||!strncmp(p+1,"ST",2)||!strncmp(p+2,"DT",2)||
        !strncmp(p+2,"ST",2))
      *i=1;*/
    for(q=tz->dst_string;isalpha(*p);p++,q++)
      *q=*p;
    *q='\0';
  }
  while (isspace(*p)) p++;
  if (*p!='\0' && (isdigit(*p) || *p=='-' || *p=='+')) /* dst_offset value */
  {
    for(;*p=='-'||*p=='+'||isdigit(*p)||*p==':';p++)
      strncat(d,p,1);
    tz->dst_offset=calch(d);
  }
  else
    tz->dst_offset=-99L*HOUR;
}

/*
 0000 `BST' for British Summer.
+0400 `ADT' for Atlantic Daylight.
+0500 `EDT' for Eastern Daylight.
+0600 `CDT' for Central Daylight.
+0700 `MDT' for Mountain Daylight.
+0800 `PDT' for Pacific Daylight.
+0900 `YDT' for Yukon Daylight.
+1000 `HDT' for Hawaii Daylight.
-0100 `MEST' for Middle European Summer,
      `MESZ' for Middle European Summer,
      `CEST' for Central European Summer,
      `SST' for Swedish Summer and
      `FST' for French Summer.
-0700 `WADT' for West Australian Daylight.
-1000 `EADT' for Eastern Australian Daylight.
-1200 `NZDT' for New Zealand Daylight.
*/

static void make_tag(char *tag,const struct tz *tz)
{
  const char *dsts[]=
  {"BST","ADT","EDT","CDT","MDT","PDT","YDT","HDT","MEST","MESZ","CEST",
   "SST","FST","WADT","EADT","NZDT",NULL},**p;
  
  if (!*tz->tz_string && !*tz->dst_string)
    return;
  strcpy(tag," (");
  if (!*tz->dst_string || (*tz->tz_string && strcmp(tz->dst_string,"DST")==0))
  {
    strncat(tag,tz->tz_string,strlen(tz->tz_string));
    if (*tz->dst_string && tz->inrange)
    {
      strncat(tag," ",1);
      strncat(tag,tz->dst_string,strlen(tz->dst_string));
    }
  }
  else
  {
    int copied=0;
    if (tz->inrange)
    {
      for(p=dsts;*p;p++)
      {
        if (strcmp(tz->dst_string,*p)==0)
        {
          strncat(tag,tz->dst_string,strlen(tz->dst_string));
          copied=1;
          break;
        }
      }
    }
    if (!copied && *tz->tz_string)
    {
      strncat(tag,tz->tz_string,strlen(tz->tz_string));
      strncat(tag," DST",4);
    }
    if (!copied && !*tz->tz_string)
      {tag[0]='\0';return;}
  }
  strncat(tag,")",1);
}

time_t calc_be(char *s,const struct tm *date)
{
  char *p;
  long h=2L*HOUR;
  time_t ret,d;
  int val;
  
  if (!s || !*s || !date)
    return -1L;
  
  ret=get_startyear(date->tm_year);
  
  if ((p=strchr(s,'/'))!=NULL)
  {
    *p++='\0';
    h=calch(p);
  }
  
  if (*s=='M')
  {
    if ((d=calcd(s+1,date,ret))==-1L)
      return -1L;
    ret+=d;
  }
  else if (*s=='J' || isdigit(*s))
  {
    val=atoi(*s=='J' ? s+1 : s);
    if ((*s=='J' ? val<1 : val<0) || val>365)
    {
      _tzerror_internal("Wrong %sJulian day: %s",*s=='J' ? "" : "zero-based ",s);
      return -1L;
    }
    if ((*s=='J' && val<60) || (!ISLEAPYEAR(date->tm_year+1900) && val>59))
      val--;
    ret+=(time_t)val*DAY;
  }
  else
  {
    _tzerror_internal("Wrong syntax: %s",s);
    return -1L;
  }
  return ret+h;
}

int MakeTZ(char *s,const struct tz *tz,int tag)
{
  char tagstr[12+1];
  long offset,h;
  
  if (!s || !tz)
    return 1;
  
  if (tz->gmt_offset==-99L*HOUR)
    return 1;
    
  if (tag) make_tag(tagstr,tz);
  
  if (tz->gmt_offset==0L && (!tz->dst_string[0] && (tz->dst_offset==0L || tz->dst_offset==-99L*60L*60L)))
  {
    strcpy(s,"GMT");
    return 0;
  }

  if ((tz->dst_string[0] || tz->dst_offset!=-99L*HOUR) && tz->inrange)
    offset=tz->dst_offset!=-99L*HOUR ? tz->dst_offset : tz->gmt_offset-HOUR;
  else
    offset=tz->gmt_offset;
  if (offset==0L)
    sprintf(s,"%s%s","GMT",tag ? tagstr : "");
  else
  {
    h=((offset/(HOUR))*100L)+((offset%(HOUR))/60L);
    sprintf(s,"%+05ld%s",-h,tag ? tagstr : "");
  }
  return 0;
}

/*
example: EST5EDT4,M4.1.0/02:00:00,M10.5.0/03:00:00
*/

int GetTZ(char *tz_evar,struct tz *tz)
{
  char *p=NULL,*tzdup,*tmp;
  struct tm *date;
  time_t now;
  
  if (!tz_evar || !*tz_evar)
    return 1;

  memset(tz,0,sizeof(*tz));
  
  now=time(NULL);
  date=localtime(&now);
  
  tzdup=strdup(tz_evar);
  
  if ((p=strtoknst(tzdup,",",&tmp))!=NULL)
    parse_tz(p,tz);
  if ((p=strtoknst(NULL,",",&tmp))!=NULL)
    tz->begin=calc_be(p,date);
  if ((p=strtoknst(NULL,",",&tmp))!=NULL)
    tz->end=calc_be(p,date);
  
  free(tzdup);
  
  if (tz->begin==-1L || tz->end==-1L)
    return 1;
  
  if (*tz->tz_string)
  {
    if (0);
#define TZ(x,y) else if (strcmp(tz->tz_string,x)==0) timezo=y;
    /* Greenwich Mean Time */
    TZ("GMT",GMT)
    /* Universal Time Coordinated = GMT */
    TZ("UTC",UTC)
    TZ("UT",UT)
    /* West Africa (+0100) */
    TZ("WAT",WAT)
    /* Azores (+0200) */
    TZ("AT",AT)
    /* Atlantic Standard Time (+0400) */
    TZ("AST",AST)
    TZ("ATL",ATL)
    /* Eastern Standard Time (New York - +0500) */
    TZ("EST",EST)
    /* Central Standard Time (+0600) */
    TZ("CST",CST)
    /* Mountain Standard Time (+0700) */
    TZ("MST",MST)
    /* Pacific Standard Time (+0800) */
    TZ("PST",PST)
    /* Yukon Standard (+0900) */
    TZ("YST",YST)
    /* Alaska (+1000) */
    TZ("ALA",ALA)
    TZ("CAT",CAT)
    /* Hawaii (+1000) */
    TZ("HAW",HAW)
    TZ("HST",HST)
    /* Alaska and Hawaii */
    TZ("AHST",AHST)
    /* Middle/Central European Time (-0100) */
    TZ("MET",MET)
    TZ("CET",CET)
    /* Eastern European Time (-0200) */
    TZ("EET",EET)
    /* Moscow/West Soviet Union/Baghdad (-0300) */
    TZ("MSK",MSK)
    TZ("WSU",WSU)
    TZ("BT",BT)
    /* West Australian Standard (-0700) */
    TZ("WAST",WAST)
    /* China Coast/USSR Zone 7 (-0800) */
    TZ("CCT",CCT)
    /* Japan Standard/USSR Zone 8 (-0900) */
    TZ("JST",JST)
    /* East Australian Standard/Guam Standard/USSR Zone 9 (-1000) */
    TZ("EAST",EAST)
    TZ("GST",GST)
    /* New Zealand (-1200) */
    TZ("NZST",NZST)
    TZ("NZT",NZT)
  }

  if (tz->gmt_offset!=-99L*HOUR && tz->dst_offset==-99L*HOUR)
    tz->dst_offset=tz->gmt_offset-(HOUR);
    
  tz->inrange=range(tz,now,date);

  if (tz->gmt_offset!=-99L*HOUR)
    return 0;

/* fall through in case of insufficient information in timezone variable;
 * an "educated guess" is made of the timezone plus daylight saving
 * time;
 */

  if (timezo==none)
    return 1;
  
  {
    struct
    {
      enum tizo timezo;
      long dst_offset,gmt_offset;
    }tab[]={{GMT,-HOUR,0L},
            {UTC,-HOUR,0L},
            {UT,-HOUR,0L},
            {WAT,0L,HOUR},
            {AT,HOUR,2L*HOUR},
            {ATL,3L*HOUR,4L*HOUR},
            {AST,3L*HOUR,4L*HOUR},
            {EST,4L*HOUR,5L*HOUR},
            {CST,5L*HOUR,6L*HOUR},
            {MST,6L*HOUR,7L*HOUR},
            {PST,7L*HOUR,8L*HOUR},
            {YST,9L*HOUR,10L*HOUR},
            {ALA,10L*HOUR,11L*HOUR},
            {CAT,10L*HOUR,11L*HOUR},
            {HAW,10L*HOUR,11L*HOUR},
            {HST,10L*HOUR,11L*HOUR},
            {AHST,10L*HOUR,11L*HOUR},
            {MET,2L*-HOUR,-HOUR},
            {CET,2L*-HOUR,-HOUR},
            {EET,3L*-HOUR,2L*-HOUR},
            {MSK,4L*-HOUR,3L*-HOUR},
            {WSU,4L*-HOUR,3L*-HOUR},
            {BT,4L*-HOUR,3L*-HOUR},
            {WAST,8L*-HOUR,7L*-HOUR},
            {CCT,9L*-HOUR,8L*-HOUR},
            {JST,10L*-HOUR,9L*-HOUR},
            {EAST,11L*-HOUR,10L*-HOUR},
            {GST,11L*-HOUR,10L*-HOUR},
            {NZT,13L*-HOUR,12L*-HOUR},
            {NZST,13L*-HOUR,12L*-HOUR},
            {none,0L,0L}},*r;

    for (r=tab;r->timezo!=none;r++)
    {
      if (r->timezo==timezo)
      {
        tz->gmt_offset=r->gmt_offset;
        if (tz->dst_string[0] && tz->dst_offset==-99L*HOUR)
          tz->dst_offset=r->dst_offset;
        return 0;
      }
    }
  }
  return 1;
}
#undef TZ
#undef HOUR
#undef DAY
#undef ISLEAPYEAR

#if MAIN
void tzerror(char *s)
{
  puts(s);
}

int main(int argc,char **argv)
{
  char s[80+1];
  FILE *fp;
  struct tz tz;
  
  if (argc!=2) return 1;
  if ((fp=fopen(argv[1],"r"))==NULL)
    return 1;
  fgets(s,(int)sizeof(s),fp);
  s[strlen(s)-1]='\0';
  fclose(fp);
  if (GetTZ(s,&tz))
    return 1;
  printf("tz_string = %s\ndst_string = %s\n"
         "gmt_offset = %ld\ndst_offset = %ld\nbegin = %ld\n"
         "end = %ld\ninrange = %d\n",
         tz.tz_string,tz.dst_string,tz.gmt_offset,tz.dst_offset,
         tz.begin,tz.end,tz.inrange);
  printf(ctime(&tz.begin));
  printf(ctime(&tz.end));
  MakeTZ(s,&tz,1);
  puts(s);
  return 0;
}
#endif
