/*
 *  Written by Hans Verkuil (hans@wyst.hobby.nl)
 *
 *  battclock.resource patch idea was shamelessly stolen from the unixclock
 *  utility written by Geert Uytterhoeven. unixclock is available on Aminet.
 */

#include <stdlib.h>
#include <stdio.h>
#include <time.h>

#include <ixemul.h>
#include <exec/memory.h>
#include <clib/exec_protos.h>
#include <dos/var.h>

#include <sys/time.h>

#define LVOReadBattClock	(-12)
#define LVOWriteBattClock	(-18)

/* Flags. Currently only bit 0 is used to tell whether Daylight Saving Time
   is in effect or not. However, ixtimezone only sets this flag, but it doesn't
   test it. And neither does ixemul.library. In fact, the library completely
   ignores the flag field. */

#define DST_ON	0x01

typedef struct
{
  long          offset;
  unsigned char flags;
} ixtime;

struct Library *BattClockBase;

char VERSION[] = "\000$VER: ixtimezone 1.0 (10.11.95)";

/* battclock.resource patch code */
asm("
	.text
	.globl _NewReadBattClock
	.globl _NewWriteBattClock
	.globl _OldReadBattClock
	.globl _OldWriteBattClock
	.globl _GMTOffset
	.globl _EndOfPatch

_NewReadBattClock:
	.int	0x207a0014, 0x4e9090ba, 0x00164e75

/*	Since the GNU assembler is currently unable to properly compile
	PC-relative code, I'm using the hex-code directly. As soon as the
	assembler can handle PC-relative code, the line above should be
	replaced by:

	move.l		_OldReadBattClock(pc),a0
	jsr		(a0)
	sub.l		_GMTOffset(pc),d0
	rts
*/

_NewWriteBattClock:
	.int	0xd0ba0010, 0x207a0008
	.short	0x4ed0

/*	Since the GNU assembler is currently unable to properly compile
	PC-relative code, I'm using the hex-code directly. As soon as the
	assembler can handle PC-relative code, the line above should be
	replaced by:

	add.l		_GMTOffset(pc),d0
	move.l		_OldWriteBattClock(pc),a0
	jmp		(a0)
*/

_OldReadBattClock:
	.int		0
_OldWriteBattClock:
	.int		0
_GMTOffset:
	.int		0
_EndOfPatch:
");

extern char NewReadBattClock;
extern char NewWriteBattClock;
extern char OldReadBattClock;
extern char OldWriteBattClock;
extern long GMTOffset; 
extern long EndOfPatch;
extern struct ixemul_base *ixemulbase;

static ixtime *read_ixtime(void)
{
  static ixtime t;
  char buf[10];
  
  /*
   * Read the GMT offset. This environment variable is 5 bytes long. The
   * first 4 form a long that contains the offset in seconds and the fifth
   * byte contains flags.
   */
  if (GetVar("IXGMTOFFSET", buf, 6, GVF_BINARY_VAR) == 5 && IoErr() == 5)
  {
    memcpy(&t.offset, buf, 4);
    t.flags = buf[4];
    return &t;
  }
  return NULL;
}

static void create_ixtime(ixtime *t, char *pathname)
{
  FILE *f = fopen(pathname, "w");
  
  if (f)
  {
    fwrite(t, 5, 1, f);
    fclose(f);
  }
}

static void write_ixtime(ixtime *t, int write_also_to_envarc)
{
  ix_set_gmt_offset(t->offset);
  create_ixtime(t, "/ENV/IXGMTOFFSET");
  if (write_also_to_envarc)
    create_ixtime(t, "/ENVARC/IXGMTOFFSET");
}

static void set_clock(long offset)
{
  struct timeval tv;
  
  gettimeofday(&tv, NULL);
  tv.tv_sec += offset;
  settimeofday(&tv, NULL);
}

static void reset_clock(void)
{
  struct timeval tv;
  
  tv.tv_usec = 0;
  tv.tv_sec = ReadBattClock();
  tv.tv_sec += (8*365+2) * 24 * 3600 + ix_get_gmt_offset();
  settimeofday(&tv, NULL);
}

static long *get_function_addr(void)
{
  return *((long **)((char *)BattClockBase + LVOReadBattClock + 2));
}

static void patch_batt_resource(long offset)
{
  char *mem;
  long size = (long)&EndOfPatch - (long)&NewReadBattClock;
  long mem_offset;
  long *oldread, *oldwrite, *gmt;

  BattClockBase = (struct Library *)OpenResource("battclock.resource");
  if (*(gmt = get_function_addr()) == 0x207a0014)
  {
    printf("battclock.resource was already patched.\n");
    gmt = (long *)(((char *)&GMTOffset) + (long)((char *)gmt - &NewReadBattClock));
    if (*gmt != offset)
    {
      *gmt = offset;
      reset_clock();
    }
    return;  /* already patched */
  }
  GMTOffset = offset;
  mem = AllocMem(size, MEMF_PUBLIC);
  mem_offset = mem - &NewReadBattClock;
  memcpy(mem, &NewReadBattClock, size);
  CacheClearE(mem, size, CACRF_ClearI);  /* clear instruction cache */
  oldread = (long *)(&OldReadBattClock + mem_offset);
  oldwrite = (long *)(&OldWriteBattClock + mem_offset);
  Disable();
  *oldread = (long)SetFunction(BattClockBase, LVOReadBattClock, (void *)(&NewReadBattClock + mem_offset));
  *oldwrite = (long)SetFunction(BattClockBase, LVOWriteBattClock, (void *)(&NewWriteBattClock + mem_offset));
  Enable();
  reset_clock();
  printf("patched battclock.resource.\n");
}

static void remove_patch(void)
{
  long *p, mem_offset, *oldread, *oldwrite;
  long size = (long)&EndOfPatch - (long)&NewReadBattClock;

  BattClockBase = (struct Library *)OpenResource("battclock.resource");
  if (*(p = get_function_addr()) != 0x207a0014)
  {
    printf("battclock.resource wasn't patched.\n");
    exit(0);  /* not patched */
  }
  mem_offset = (char *)p - &NewReadBattClock;
  oldread = (long *)(&OldReadBattClock + mem_offset);
  oldwrite = (long *)(&OldWriteBattClock + mem_offset);
  Disable();
  SetFunction(BattClockBase, LVOReadBattClock, (void *)*oldread);
  SetFunction(BattClockBase, LVOWriteBattClock, (void *)*oldwrite);
  Enable();
  FreeMem(p, size);
  reset_clock();
  printf("removed battclock.resource patch.\n");
  exit(0);
}

static void test(void)
{
  time_t t;
  
  time(&t);
  printf("GMT:   %s", asctime(gmtime(&t)));
  printf("Local: %s", asctime(localtime(&t)));
  exit(0);
}

static void usage(void)
{
  fprintf(stderr, "Usage: ixtimezone <option>
Where <option> is one of:

-test		Show GMT and localtime
-get-offset	Get GMT offset and patch ixemul.library
-check-dst	As -get-offset, but also automatically adjust the Amiga
		time if Daylight Saving Time has gone in effect (or vice
		versa)
-patch-resource	As -get-offset, but also patch the battclock.resource
-remove-patch	Remove the battclock.resource patch\n");
  exit(1);
}

int main(int argc, char **argv)
{
  struct tm *local_tm, *gmt_tm;
  int local_hms, gmt_hms;
  time_t t, local_t, gmt_t;
  ixtime *old, new;
  int set_the_clock = 0, patch_resource = 0;
  int write_to_envarc = 0;

  if (ixemulbase->ix_lib.lib_Version < 42)
  {
    fprintf(stderr, "ixtimezone requires ixemul.library version 42 or higher.\n");
    exit(1);
  }
  if (argc != 2)
    usage();

  if (!strcmp(argv[1], "-test"))
    test();
  else if (!strcmp(argv[1], "-check-dst"))
    set_the_clock = 1;
  else if (!strcmp(argv[1], "-patch-resource"))
    patch_resource = 1;
  else if (!strcmp(argv[1], "-remove-patch"))
    remove_patch();
  else if (strcmp(argv[1], "-get-offset"))
    usage();

  /*
   * Get current time, both GMT and local.  
   * We don't care if these values are correct or not, we are only interested
   * in the difference between the two.
   */

  time(&t);
  gmt_tm = gmtime(&t);
  gmt_hms = gmt_tm->tm_hour * 3600 + gmt_tm->tm_min * 60 + gmt_tm->tm_sec;
  local_tm = localtime(&t);
  local_hms = local_tm->tm_hour * 3600 + local_tm->tm_min * 60 + local_tm->tm_sec;
  new.flags = (local_tm->tm_isdst ? DST_ON : 0);
  new.offset = 0;
  if (gmt_hms != local_hms)
  {
    /* They are not the same. So compute the difference between them */

    local_tm->tm_isdst = 0;     /* don't let these values influence the result! */
    local_tm->tm_zone = NULL;
    local_tm->tm_gmtoff = 0;
    local_t = mktime(local_tm);
    gmt_tm = gmtime(&t);
    gmt_tm->tm_isdst = 0;
    gmt_tm->tm_zone = NULL;
    gmt_tm->tm_gmtoff = 0;
    gmt_t = mktime(gmt_tm);
    new.offset = gmt_t - local_t;     /* obtain the difference */
  }

  old = read_ixtime();
  if (old == NULL || old->offset != new.offset)
  {
    write_to_envarc = 1;
    if (set_the_clock && old)
      set_clock(old->offset - new.offset);
  }
  write_ixtime(&new, write_to_envarc);
  if (patch_resource)
    patch_batt_resource(new.offset);
}
