/*--------------------------------*-C-*---------------------------------*
 * Copyright 2021 Marc Lehmann <schmorp@schmorp.de>
 * Copyright Wolfgang Pietsch
 * Copyright 1997 1998 Oezguer Kesim <kesim@math.fu-berlin.de>
 * Copyright 1992, 1993 Robert Nation <nation@rocket.sanders.lockheed.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *----------------------------------------------------------------------*/
#include <ctype.h>

#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <unistd.h>

#include <sys/types.h>
#include <sys/select.h>
#include <sys/stat.h>

#include <sys/time.h>
#include <time.h>

#include <dirent.h>

#include <X11/Xlib.h>
#include <X11/Xutil.h>

#include "ecb.h"

#include "version.h"

#define APL_CLASS	"Clock"
#define APL_NAME	"rclock"
#define MSG_CLASS	"Appointment"
#define MSG_NAME	"Appointment"
#define CONFIG_FILE	".rclock"

#ifndef EXIT_SUCCESS            /* missed from <stdlib.h> ? */
# define EXIT_SUCCESS	0
# define EXIT_FAILURE	1
#endif

/*--------------------------------*-C-*---------------------------------*
 * Compile-time configuration.
 *----------------------------------------------------------------------*
 * Copyright (C) 1997 1998 mj olesen <olesen@me.QueensU.CA>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *----------------------------------------------------------------------*/

/*----------------------------------------------------------------------*
 * #define ICONWIN
 *	to enable fancy (active) icon
 *
 * #define REMINDERS
 *	to enable the appointment reminder functions
 *
 * #define NO_REMINDER_EXEC
 *      to disable the execution of a program on an appointment
 *
 * #define DATE_ON_CLOCK_FACE
 *	to display today's date on the face of the clock
 *	Note:  this requires REMINDERS since it uses the same font
 *
 * #define MAIL
 *	to enable xbiff-type mail reminders
 *
 * #define MAIL_BELL
 *	to enable xbiff-type mail reminders with a beep
 *
 * #define MAIL_SPAWN	"xmh\ -font\ 7x14\&"
 *	to define a mail program to run
 *
 * #define MAIL_SPOOL	"/var/spool/mail/"
 *	to define the mail spool when the $MAIL variable isn't set
 *
 * program size approximately doubles from no options to all options
 *----------------------------------------------------------------------*/
#define ICONWIN
#define REMINDERS

/* #define NO_REMINDER_EXEC */
#define DATE_ON_CLOCK_FACE
#define MAIL

/* #define MAIL_BELL */ /* TODO: command line switch */

/* #define MAIL_SPAWN	"xmh\ -font\ 7x14\&" */ /* TODO: command line switch */
#define MAIL_SPOOL	"/var/spool/mail/"

/*----------------------------------------------------------------------*
 * #define CLOCKUPDATE	30
 * 	to define the frequency (seconds) to update the clock
 *
 * #define MAILUPDATE	60
 * 	to define the frequency (seconds) to check for new mail
 *
 * #define REMINDERS_TIME	10
 *	to define the frequency (minutes) to check ~/.rclock
 *
 * #define DEFER_TIME	3
 *	to define the amount (minutes) to defer a message
 *
 * #define ADJUST_TIME
 *	to add -adjust command-line option
 *
 * #define CENTURY	2000
 *	to set the base century for 2 digit year short-hand
 *----------------------------------------------------------------------*/
#define	CLOCKUPDATE	30
#define MAILUPDATE	60
#define REMINDERS_TIME	10
#define DEFER_TIME	3
#define ADJUST_TIME

#define CENTURY		2000 /* TODO: verify */

/*----------------------------------------------------------------------*
 * #define FONT_NAME	"7x14"
 * 	to define the font to be used for appointment reminders
 *
 * #define FG_COLOR_NAME	"black"
 * #define BG_COLOR_NAME	"white"
 * 	to define the foreground/background colors to use
 *----------------------------------------------------------------------*/
#define FONT_NAME	"7x14"
#define FG_COLOR_NAME	"black"
#define BG_COLOR_NAME	"white"

/*----------------------------------------------------------------------*
 * #define DAY_NAMES	"umtwrfs*"
 *	define this string appropriate for any language.
 *
 *	It starts with a symbol for Sunday, ends with Saturday, then '*'
 *	NOTE: 8 characters total - 7 days of the week plus '*'
 *----------------------------------------------------------------------*/
#define DAY_NAMES	"umtwrfs*"

/*----------------------------------------------------------------------*
 * #define SUBTICKS
 *	to show additional minute/second markings
 *----------------------------------------------------------------------*/
#define SUBTICKS

/*----------------------------------------------------------------------*
 * sort out conflicts
 *----------------------------------------------------------------------*/
#if defined (MAIL_BELL) || defined (MAIL_SPAWN) || defined (MAIL_SPOOL)
# ifndef MAIL
#  define MAIL
# endif
#endif

/*----------------------------------------------------------------------*/
// fourth-order fixed point sine approximation,
// adapted from https://www.coranac.com/2009/07/sines/

static int32_t
Sin (int32_t arg)
{
  int32_t x = (arg - 360) * 16384 / 360;

  int c, y;
  static const int qN = 13, qA = 12, B = 19900, C = 3516;

  c = x << (30 - qN);              // Semi-circle info into carry.
  x -= 1 << qN;                    // sine -> cosine calc

  x = x << (31 - qN);              // Mask with PI
  x = x >> (31 - qN);              // Note: SIGNED shift! (to qN)
  x = x * x >> (2 * qN - 14);      // x=x^2 To Q14

  y = B - (x * C >> 14);           // B - x^2*C
  y = (1 << qA) - (x * y >> 16);   // A - x^2*(B-x^2*C)

  x = c < 0 ? y : -y;

  return x;
}

/*----------------------------------------------------------------------*/

static Display *Xdisplay;       /* X display */
static int Xfd;                 /* file descriptor of server connection */
static GC Xgc, Xrvgc;           /* normal, reverse video GC */

#define	Xscreen		DefaultScreen (Xdisplay)
#define Xcmap		DefaultColormap (Xdisplay, Xscreen)
#define Xroot		DefaultRootWindow (Xdisplay)

/* windows and their sizes */
typedef struct
{
  Window win;
  int width, height;
} mywindow_t;

static mywindow_t Clock = { None, 80, 80 };     /* parent window */

#define fgColor	0
#define bgColor 1
static const char *rs_color[2] = { FG_COLOR_NAME, BG_COLOR_NAME };

static unsigned long PixColors[2];
static const char *rs_geometry = NULL;

#ifdef ICONWIN
static const char *rs_iconGeometry = NULL;
static mywindow_t Icon = { None, 65, 65 };      /* icon window */

static int iconic_state = NormalState;  /* iconic startup? */
#endif

#ifdef REMINDERS
static mywindow_t Msg = { 0, 0, 0 };    /* message window */

static struct
{
  Window
# ifndef NO_REMINDER_EXEC
  Start,
# endif
  Dismiss, Defer;
  int width, height;
} msgButton;

static XFontStruct *Xfont;

# define FontHeight()	(Xfont->ascent + Xfont->descent)
static int Msg_Mapped = 0;      /* message window mapped? */
static int reminderTime = -1;
static char message[256] = "";

# ifndef NO_REMINDER_EXEC
static char execPrgm[256] = "";
# endif
static const char *reminders_file = NULL;       /* name of ~/.rclock file */

# ifdef DATE_ON_CLOCK_FACE
static int show_date = 1;       /* show date on face of clock */
# endif
#endif

#ifdef ADJUST_TIME
static int adjustTime = 0;
#else
# define	adjustTime	0
#endif

#ifdef	CENTURY
# if (CENTURY < 1900)
Error, Century incorrectly set.
# endif
#else
# define CENTURY 1900
#endif

static int clockUpdate = CLOCKUPDATE;

#ifdef MAIL
static int mailUpdate = MAILUPDATE;
static char *mail_file = NULL;

# ifndef MAIL_SPAWN
static char *mail_spawn = NULL;
# endif
static int is_maildir = 0;
#endif

static XSizeHints szHint = {
  PMinSize | PResizeInc | PBaseSize | PWinGravity,
  0, 0, 80, 80,                 /* x, y, width and height */
  1, 1,                         /* Min width and height */
  0, 0,                         /* Max width and height */
  1, 1,                         /* Width and height increments */
  {1, 1},                       /* x, y increments */
  {1, 1},                       /* Aspect ratio - not used */
  0, 0,                         /* base size */
  NorthWestGravity              /* gravity */
};

/* subroutine declarations */
static void geometry2sizehint (mywindow_t *win, const char *geom);
static void Create_Windows (int argc, char *argv[]);
static void getXevent ();
static void print_error (const char *fmt, ...);

static void Draw_Window (mywindow_t *this_win, int full_redraw);
static void Reminder ();
static void Next_Reminder (int update_only);

/* Arguments for Next_Reminder() */
#define REPLACE 0
#define UPDATE 1

/*----------------------------------------------------------------------*/

static void
usage ()
{
  int i;
  struct
  {
    const char *const opt;
    const char *const desc;
  } optList[] = {
#define optList_size() (sizeof (optList) / sizeof (optList[0]))
    {"-display displayname", "X server to contact"},
    {"-geometry geom", "size (in pixels) and position"},
    {"-bg color", "background color"},
    {"-fg color", "foreground color"},
#ifdef REMINDERS
    {"-fn fontname", "normal font for messages"},
# ifdef DATE_ON_CLOCK_FACE
    {"-nodate", "do not display date on the clock face"},
# endif
#endif
#ifdef ICONWIN
    {"-iconic", "start iconic"},
#endif
#ifdef ADJUST_TIME
    {"-adjust +/-ddhhmm", "adjust clock time"},
#endif
    {"-update seconds", "clock update interval"},
#ifdef MAIL
    {"-mail seconds", "check $MAIL interval"},
    {"-mailfile file", "file to use for mail checking"},
# ifndef MAIL_SPAWN
    {"-mailspawn cmd", "execute `cmd` when clock is clicked"},
# endif
#endif
    {"#geom", "icon window geometry"}
  };

  fprintf (stderr, "\nUsage for urclock version " VERSION "\n\n  urclock [options]\n\n" "where options include:\n");

  for (i = 0; i < (int)optList_size (); i++)
    fprintf (stderr, "    %-29s%s\n", optList[i].opt, optList[i].desc);
}


/****************
 * Check out if we are using a maildir drop (qmail)
 * Note: this changes  mail_dir to hold the "new" diretory
 */
#ifdef MAIL
static void
CheckMaildir ()
{
  struct stat st;
  char *buf, *p;

  if (!*mail_file || stat (mail_file, &st) || !S_ISDIR (st.st_mode))
    return;                     /* no */

  if (!(buf = (char *) malloc (strlen (mail_file) + 5)))
    {
      print_error ("malloc error");
      exit (EXIT_FAILURE);
    }
  strcpy (buf, mail_file);
  p = buf + strlen (buf);
  if (p[-1] != '/')
    *p++ = '/';

  strcpy (p, "tmp");
  if (stat (buf, &st) || !S_ISDIR (st.st_mode))
    goto leave;
  strcpy (p, "cur");
  if (stat (buf, &st) || !S_ISDIR (st.st_mode))
    goto leave;
  strcpy (p, "new");
  if (stat (buf, &st) || !S_ISDIR (st.st_mode))
    goto leave;

  mail_file = buf;
  is_maildir = 1;
  return;
leave:
  free (buf);
}
#endif

/*----------------------------------------------------------------------*
 * rclock - Rob's clock
 * simple X windows clock with appointment reminder
 *----------------------------------------------------------------------*/
int
main (int argc, char *argv[])
{
  int i;
  char *opt, *val;
  const char *display_name = NULL;
  XGCValues gcv;

#ifdef REMINDERS
  const char *rs_font = FONT_NAME;

  /* find the ~/.rclock file */
  if ((val = getenv ("HOME")) != NULL)
    {
      char *p = (char *) malloc (strlen (CONFIG_FILE) + strlen (val) + 2);

      if (p == NULL)
        goto Malloc_Error;

      strcpy (p, val);
      strcat (p, "/" CONFIG_FILE);

      reminders_file = p;
    }
#endif
#ifdef MAIL
  val = getenv ("MAIL");        /* get the mail spool file name */
# ifdef MAIL_SPOOL
  if (val == NULL)              /* csh doesn't set $MAIL */
    {
      const char *spool = MAIL_SPOOL;
      char *user = getenv ("USER");     /* assume this works */

      if (user != NULL)
        {
          val = (char *) malloc (strlen (spool) + strlen (user) + 1);
          if (val == NULL)
            goto Malloc_Error;
          strcpy (val, spool);
          strcat (val, user);
        }
    }
# endif
  mail_file = val;
  if (mail_file)
    CheckMaildir ();
#endif

  /* parse the command line */
  for (i = 1; i < argc; i += 2)
    {
      opt = argv[i];
      val = argv[i + 1];

      switch (*opt++)
        {
          case '-':
            break;

          case '#':
#ifdef ICONWIN
            rs_iconGeometry = opt;      /* drop */
#endif
          default:
            continue;
            break;
        }

      if (*opt == 'd' && val)
        display_name = val;     /* "d", "display" */
      else if (*opt == 'g' && val)
        rs_geometry = val;      /* "g", "geometry" */
#ifdef ICONWIN
      else if (*opt == 'i')     /* "ic", "iconic" */
        {
          iconic_state = IconicState;
          i--;                  /* no argument */
        }
#endif
      else if (!strcmp (opt, "fg") && val)
        rs_color[fgColor] = val;
      else if (!strcmp (opt, "bg") && val)
        rs_color[bgColor] = val;
#ifdef REMINDERS
      else if (!strcmp (opt, "fn") && val)
        rs_font = val;
# ifdef DATE_ON_CLOCK_FACE
      else if (!strcmp (opt, "nodate"))
        {
          show_date = 0;
          i--;                  /* no argument */
        }
# endif
#endif
      else if (!strcmp (opt, "update") && val)
        {
          int x = atoi (val);

          if (x < 1 || x > 60)
            print_error ("update: %d sec", clockUpdate);
          else
            clockUpdate = x;
        }
#ifdef MAIL
      else if (!strcmp (opt, "mail") && val)
        {
          int x = atoi (val);

          if (x < 1)
            print_error ("mail update: %d sec", mailUpdate);
          else
            mailUpdate = x;
        }
      else if (!strcmp (opt, "mailfile") && val)
        {
          /* If the mail environment is not set, then mail_file was created
           * with a malloc.  We need to free it.
           */
          if (getenv ("MAIL") == NULL)
            free (mail_file);
          /* assume user knows what he's doing, don't check that file is valid... */
          mail_file = val;
        }
# ifndef MAIL_SPAWN
      else if (!strcmp (opt, "mailspawn") && val)
        {
          mail_spawn = val;
        }
# endif
#endif /* MAIL */
#ifdef ADJUST_TIME
      else if (!strcmp (opt, "adjust") && val)
        {
          /* convert ddhhmm to seconds, minimal error checking */
          int x = atoi (val);

          adjustTime = ((((abs (x) / 10000) % 100) * 24 /* days */
                         + ((abs (x) / 100) % 100)) * 60        /* hours */
                        + (abs (x) % 100)) * 60;        /* minutes */
          if (x < 0)
            adjustTime = -adjustTime;
        }
#endif /* ADJUST_TIME */
      else
        {
          usage ();
          goto Abort;
        }
    }

  /* open display */
  Xdisplay = XOpenDisplay (display_name);
  if (!Xdisplay)
    {
      print_error ("can't open display %s", display_name ? display_name :
                   getenv ("DISPLAY") ? getenv ("DISPLAY") : "as no -d given and DISPLAY not set");
      goto Abort;
    }

  /* get display info */
  Xfd = XConnectionNumber (Xdisplay);
  {
    const char *const color_msg = "can't load color \"%s\"";
    XColor xcol;

    /* allocate foreground/background colors */
    if (!XParseColor (Xdisplay, Xcmap, rs_color[fgColor], &xcol) || !XAllocColor (Xdisplay, Xcmap, &xcol))
      {
        print_error (color_msg, rs_color[fgColor]);
        goto Abort;
      }
    PixColors[fgColor] = xcol.pixel;

    if (!XParseColor (Xdisplay, Xcmap, rs_color[bgColor], &xcol) || !XAllocColor (Xdisplay, Xcmap, &xcol))
      {
        print_error (color_msg, rs_color[bgColor]);
        goto Abort;
      }
    PixColors[bgColor] = xcol.pixel;
  }

#ifdef REMINDERS
  /* load the font for messages */
  if ((Xfont = XLoadQueryFont (Xdisplay, rs_font)) == NULL)
    {
      print_error ("can't load font \"%s\"", rs_font);
      goto Abort;
    }
  gcv.font = Xfont->fid;
#endif

  Create_Windows (argc, argv);
  /*  Create the graphics contexts */
  gcv.foreground = PixColors[fgColor];
  gcv.background = PixColors[bgColor];

  Xgc = XCreateGC (Xdisplay, Clock.win,
#ifdef REMINDERS
                   GCFont |
#endif
                   GCForeground | GCBackground, &gcv);

  gcv.foreground = PixColors[bgColor];
  gcv.background = PixColors[fgColor];
  Xrvgc = XCreateGC (Xdisplay, Clock.win,
#ifdef REMINDERS
                     GCFont |
#endif
                     GCForeground | GCBackground, &gcv);

  getXevent ();
  return EXIT_SUCCESS;

Malloc_Error:
  print_error ("malloc error");
Abort:
  print_error ("aborting");
  return EXIT_FAILURE;
}

/*
 * translate geometry string to appropriate sizehint
 */
static void
geometry2sizehint (mywindow_t *win, const char *geom)
{
  int x, y, flags;
  unsigned int width, height;

  /* copy in values */
  szHint.width = win->width;
  szHint.height = win->height;

  if (geom == NULL)
    return;

  flags = XParseGeometry (geom, &x, &y, &width, &height);

  if (flags & WidthValue)
    {
      szHint.width = width + szHint.base_width;
      szHint.flags |= USSize;
    }
  if (flags & HeightValue)
    {
      szHint.height = height + szHint.base_height;
      szHint.flags |= USSize;
    }

  if (flags & XValue)
    {
      if (flags & XNegative)
        {
          x += (DisplayWidth (Xdisplay, Xscreen) - szHint.width);
          szHint.win_gravity = NorthEastGravity;
        }
      szHint.x = x;
      szHint.flags |= USPosition;
    }
  if (flags & YValue)
    {
      if (flags & YNegative)
        {
          y += (DisplayHeight (Xdisplay, Xscreen) - szHint.height);
          szHint.win_gravity = (szHint.win_gravity == NorthEastGravity ? SouthEastGravity : SouthWestGravity);
        }
      szHint.y = y;
      szHint.flags |= USPosition;
    }

  /* copy out values */
  win->width = szHint.width;
  win->height = szHint.height;
}

/*
 * Open and map the windows
 */
static void
Create_Windows (int argc, char *argv[])
{
  XClassHint classHint;
  XWMHints wmHint;

  geometry2sizehint (&Clock, rs_geometry);
  Clock.win = XCreateSimpleWindow (Xdisplay, Xroot,
                                   szHint.x, szHint.y, Clock.width, Clock.height, 0, PixColors[fgColor], PixColors[bgColor]);

#ifdef ICONWIN
  geometry2sizehint (&Icon, rs_iconGeometry);
  Icon.win = XCreateSimpleWindow (Xdisplay, Xroot, szHint.x, szHint.y, Icon.width, Icon.height, 0, PixColors[fgColor], PixColors[bgColor]);
  wmHint.initial_state = iconic_state;
  wmHint.icon_window = Icon.win;
  wmHint.flags = InputHint | StateHint | IconWindowHint;
#else
  wmHint.flags = InputHint;
#endif
  wmHint.input = True;

  classHint.res_name = (char *)APL_NAME;
  classHint.res_class = (char *)APL_CLASS;
  XSetWMProperties (Xdisplay, Clock.win, NULL, NULL, argv, argc, &szHint, &wmHint, &classHint);

  XSelectInput (Xdisplay, Clock.win, (ExposureMask | StructureNotifyMask | ButtonPressMask));

#ifdef ICONWIN
  XSelectInput (Xdisplay, Icon.win, (ExposureMask | ButtonPressMask));
#endif
  XMapWindow (Xdisplay, Clock.win);

  /* create, but don't map a window for appointment reminders */
#ifdef REMINDERS
  Msg.win = XCreateSimpleWindow (Xdisplay, Xroot,
                                 szHint.x, szHint.y, szHint.width, szHint.height, 0, PixColors[fgColor], PixColors[bgColor]);

  szHint.flags |= USPosition;
  /* ignore warning about discarded `const' */
  classHint.res_name = (char *)MSG_NAME;
  classHint.res_class = (char *)MSG_CLASS;
  wmHint.input = True;
  wmHint.flags = InputHint;

  XSetWMProperties (Xdisplay, Msg.win, NULL, NULL, argv, argc, &szHint, &wmHint, &classHint);

  {
    const char *str = MSG_NAME;

    XStoreName (Xdisplay, Msg.win, str);
    XSetIconName (Xdisplay, Msg.win, str);
  }

  XSelectInput (Xdisplay, Msg.win, (ExposureMask | ButtonPressMask | KeyPressMask));

  /* font already loaded */

  msgButton.width = 4 + 5 * XTextWidth (Xfont, "M", 1);
  msgButton.height = 4 + FontHeight ();

  msgButton.Dismiss = XCreateSimpleWindow (Xdisplay, Msg.win,
                                           0, 0, msgButton.width, msgButton.height, 0, PixColors[bgColor], PixColors[fgColor]);

  XMapWindow (Xdisplay, msgButton.Dismiss);

  msgButton.Defer = XCreateSimpleWindow (Xdisplay, Msg.win,
                                         0, 0, msgButton.width, msgButton.height, 0, PixColors[bgColor], PixColors[fgColor]);
  XMapWindow (Xdisplay, msgButton.Defer);

# ifndef NO_REMINDER_EXEC
  msgButton.Start = XCreateSimpleWindow (Xdisplay, Msg.win,
                                         0, 0, msgButton.width, msgButton.height, 0, PixColors[bgColor], PixColors[fgColor]);
  XMapWindow (Xdisplay, msgButton.Start);
# endif/* NO_REMINDER_EXEC */
#endif
}

static time_t
mk_time (struct tm *tmval)
{
  return (tmval->tm_min
          + 60 * (tmval->tm_hour
                  + 24 * (tmval->tm_mday
                          + 31 * ((tmval->tm_mon + 1)
                                  + 12 * tmval->tm_year))));
}

#ifdef MAIL
static int
MailAvailable ()
{
  struct stat st;

  if (is_maildir)
    {
      DIR *dirp;
      struct dirent *d;

      if ((dirp = opendir (mail_file)))
        {
          while ((d = readdir (dirp)))
            {
              if (*d->d_name == '.')
                continue;
              if (isdigit (*d->d_name))
                {
                  closedir (dirp);
                  return 1;
                }
            }
          closedir (dirp);
        }
      return 0;
    }
  else
    return !stat (mail_file, &st) && (st.st_size > 0) && (st.st_mtime >= st.st_atime);
}
#endif

/*----------------------------------------------------------------------*
 * Redraw the whole window after an exposure or size change.
 * After a timeout, only redraw the hands.
 * Provide reminder if needed.
 *----------------------------------------------------------------------*/
static void
Draw_Window (mywindow_t *W, int full_redraw)
{
  static int savedDay = -1;

  time_t currentTime;
  struct tm *tmval;
  int ctr_x, ctr_y;

  typedef struct
  {
    int h_x, h_y;               /* hour */
    int m_x, m_y;               /* minute */
    int s_x, s_y;               /* second */
  } hands_t;                    /* hand positions (x,y) */

  hands_t HandsNow, *pHandsOld;

  GC X_gc, X_rvgc;

  static hands_t HandsOld = { -1 };
#ifdef ICONWIN
  static hands_t HandsOld_icon = { -1 };
#endif
#ifdef REMINDERS
  static int lastUpdateTime = -10;
#endif
#ifdef DATE_ON_CLOCK_FACE
  static char clockdate[10];
#endif

#ifdef MAIL
  static time_t mailTime = 0;
  static int MailUp = 0, MailUp_rvideo = 0;

# ifdef ICONWIN
  static int MailUp_icon = 0;
# endif
#endif /* MAIL */

  currentTime = time (NULL) + adjustTime;       /* get the current time */
  tmval = localtime (&currentTime);

#ifdef MAIL
# ifdef REMINDERS
  if (W->win != Msg.win)
# endif
    {
      int *pMailUp = (
# ifdef ICONWIN
                       W->win == Icon.win ? &MailUp_icon :
# endif
                       &MailUp);

      if ((currentTime - mailTime) >= mailUpdate)
        {
          if (
# ifdef ICONWIN
               MailUp != MailUp_icon ? MailUp :
# endif
               ((mail_file != NULL) && MailAvailable ()))
            {
              if (!*pMailUp)
                {
                  *pMailUp = 1;
                  full_redraw = 1;
                  XSetWindowBackground (Xdisplay, W->win, PixColors[fgColor]);
# ifdef MAIL_BELL
                  XBell (Xdisplay, 0);
# endif
                }
            }
          else
            {
              if (*pMailUp)
                {
                  *pMailUp = 0;
                  full_redraw = 1;
                  XSetWindowBackground (Xdisplay, W->win, PixColors[bgColor]);
                }
            }
# ifdef ICONWIN
          if (MailUp == MailUp_icon)
# endif
            mailTime = currentTime;

          MailUp_rvideo = *pMailUp;
        }
    }
#endif /* MAIL */

  /* once every day, update the window and icon name */
  if (tmval->tm_yday != savedDay)
    {
      char str[20];

      savedDay = tmval->tm_yday;
      strftime (str, sizeof (str), "%a %h %d", tmval);
      XStoreName (Xdisplay, Clock.win, str);
      XSetIconName (Xdisplay, Clock.win, str);

#if defined(REMINDERS) && defined(DATE_ON_CLOCK_FACE)
      if (show_date)
        {
          strftime (clockdate, sizeof (clockdate), "%d", tmval);
          full_redraw = 1;
        }
    }

#endif
  if (full_redraw)
    XClearWindow (Xdisplay, W->win);

#ifdef REMINDERS
  /* for a message window, just re-draw the message */
  if (W->win == Msg.win)
    {
      char *beg, *next;
      int lines;

      for (beg = message, lines = 0; beg; beg = next, lines++)
        {
          char *end;

          if ((end = strstr (beg, "\\n")) == NULL)
            {
              end = beg + strlen (beg);
              next = NULL;
            }
          else
            {
              next = end + 2;
            }

          XDrawString (Xdisplay, Msg.win,
                       Xgc,
                       (Msg.width -
                        XTextWidth (Xfont, beg, (end - beg))) / 2, 10 + Xfont->ascent + FontHeight () * lines, beg, (end - beg));
        }

      XDrawString (Xdisplay, msgButton.Dismiss, Xrvgc, (msgButton.width - XTextWidth (Xfont, "Done" , 4)) / 2, Xfont->ascent + 2, "Done" , 4);

      XDrawString (Xdisplay, msgButton.Defer  , Xrvgc, (msgButton.width - XTextWidth (Xfont, "Defer", 5)) / 2, Xfont->ascent + 2, "Defer", 5);

# ifndef NO_REMINDER_EXEC
      XDrawString (Xdisplay, msgButton.Start  , Xrvgc, (msgButton.width - XTextWidth (Xfont, "Start", 5)) / 2, Xfont->ascent + 2, "Start", 5);

      if (strlen (execPrgm) > 1)
        XMapWindow (Xdisplay, msgButton.Start);
      else
        XUnmapWindow (Xdisplay, msgButton.Start);
# endif /* NO_REMINDER_EXEC */
      return;
    }

  /*
   * Convert multi-field time info to a single integer with a resolution
   * in minutes.
   */
  currentTime = mk_time (tmval);

  /* is there a reminder pending? */
  if (reminderTime >= 0 && currentTime >= reminderTime)
    Reminder ();

  /* every 10 minutes, or at start of day, check for revised entries */
  if (!Msg_Mapped &&
      (currentTime > lastUpdateTime + REMINDERS_TIME || (currentTime != lastUpdateTime && tmval->tm_hour == 0 && tmval->tm_min == 0)))
    {
      Next_Reminder (UPDATE);
      lastUpdateTime = currentTime;
    }
#endif

  /*
   * draw clock
   */

  ctr_x = W->width  / 2;
  ctr_y = W->height / 2;

#define XPOS(i,val) (ctr_x + (W->width  * Sin (i      ) + 4096) / (819200 / (val)))
#define YPOS(i,val) (ctr_y - (W->height * Sin (i + 180) + 4096) / (819200 / (val)))
  /*
   * how to draw the clock face
   */

  /* calculate the positions of the hands */
  {
    int angle = (tmval->tm_hour % 12) * 60 + tmval->tm_min;

    HandsNow.h_x = XPOS (angle, 60);
    HandsNow.h_y = YPOS (angle, 60);
  }
  {
    int angle = (tmval->tm_min * 12);

    HandsNow.m_x = XPOS (angle, 80);
    HandsNow.m_y = YPOS (angle, 80);
  }
  if (clockUpdate == 1)
    {
      int angle = (tmval->tm_sec * 12);

      HandsNow.s_x = XPOS (angle, 85);
      HandsNow.s_y = YPOS (angle, 85);
    }

  pHandsOld = (
#ifdef ICONWIN
                W->win == Icon.win ? &HandsOld_icon :
#endif
                &HandsOld);

#ifdef MAIL
  if (MailUp_rvideo)
    {
      X_gc = Xrvgc;
      X_rvgc = Xgc;
    }
  else
#endif
    {
      X_gc = Xgc;
      X_rvgc = Xrvgc;
    }

  /*
   * Draw the date in the lower half of the clock window.
   * The code is enclosed in REMINDERS because it uses the same
   * font as the reminders code.
   */
#if defined(REMINDERS) && defined(DATE_ON_CLOCK_FACE)
  if (show_date)
    XDrawString (Xdisplay, W->win, X_gc,
                 ctr_x - XTextWidth (Xfont, clockdate, strlen (clockdate)) / 2,
                 ctr_y + FontHeight () + (ctr_y - FontHeight ()) / 2, clockdate, strlen (clockdate));
#endif

  if (full_redraw)
    {
      int mintick;

      /*
       * draw clock face
       */
#ifdef SUBTICKS
      for (mintick = 0; mintick < 60; mintick++)
        XDrawPoint (Xdisplay, W->win, X_gc, XPOS ((mintick * 12), 95), YPOS ((mintick * 12), 95));
#endif
      for (mintick = 0; mintick < 60; mintick += 5)
        XDrawLine (Xdisplay, W->win, X_gc,
                   XPOS ((mintick * 12), 90), YPOS ((mintick * 12), 90), XPOS ((mintick * 12), 100), YPOS ((mintick * 12), 100));
    }
  else if (memcmp (pHandsOld, &HandsNow, sizeof (hands_t)))
    {
      int i, j;

      /*
       * erase old hands
       */
      for (i = -1; i < 2; i++)
        for (j = -1; j < 2; j++)
          {
            /* hour/minute hands */
            XDrawLine (Xdisplay, W->win, X_rvgc, ctr_x + i, ctr_y + j, pHandsOld->h_x, pHandsOld->h_y);
            XDrawLine (Xdisplay, W->win, X_rvgc, ctr_x + i, ctr_y + j, pHandsOld->m_x, pHandsOld->m_y);
          }

      if (clockUpdate == 1)     /* seconds hand */
        XDrawLine (Xdisplay, W->win, X_rvgc, ctr_x, ctr_y, pHandsOld->s_x, pHandsOld->s_y);
    }

  if (full_redraw || memcmp (pHandsOld, &HandsNow, sizeof (hands_t)))
    {
      int i, j;

      /*
       * draw new hands
       */
      for (i = -1; i < 2; i++)
        for (j = -1; j < 2; j++)
          {
            /* hour/minute hands */
            XDrawLine (Xdisplay, W->win, X_gc, ctr_x + i, ctr_y + j, HandsNow.h_x, HandsNow.h_y);

            XDrawLine (Xdisplay, W->win, X_gc, ctr_x + i, ctr_y + j, HandsNow.m_x, HandsNow.m_y);
          }
      if (clockUpdate == 1)     /* seconds hand */
        XDrawLine (Xdisplay, W->win, X_gc, ctr_x, ctr_y, HandsNow.s_x, HandsNow.s_y);

      *pHandsOld = HandsNow;
    }
}

#ifdef REMINDERS

/*
 * Read a single integer from *pstr, returns default value if it finds "*"
 * DELIM = trailing delimiter to skip
 */
static int
GetOneNum (char **pstr, int def)
{
  int num, hit = 0;

  for (num = 0; isdigit (**pstr); (*pstr)++)
    {
      num = num * 10 + (**pstr - '0');
      hit = 1;
    }
  if (!hit)
    {
      num = def;
      while (**pstr == '*')
        (*pstr)++;
    }
  return num;
}

/*
 * find if TODAY is found in PSTR
 */
static int
isToday (char **pstr, int wday)
{
  const char *dayNames = DAY_NAMES;
  int rval, today;

  today = dayNames[wday];
  /* no day specified is same as wildcard */
  if (!strchr (dayNames, tolower (**pstr)))
    return 1;

  for (rval = 0; strchr (dayNames, tolower (**pstr)); (*pstr)++)
    {
      if (today == tolower (**pstr) || **pstr == '*')
        rval = 1;               /* found it */
    }
  return rval;
}

static char *
trim_string (char *str)
{
  if (str && *str)
    {
      int n;

      while (*str && isspace (*str))
        str++;

      n = strlen (str) - 1;
      while (n > 0 && isspace (str[n]))
        n--;
      str[n + 1] = '\0';
    }
  return str;
}

# ifndef NO_REMINDER_EXEC
static char *
extract_program (char *text)
{
  char *prgm = text;

  while ((prgm = strchr (prgm, ';')) != NULL)
    {
      if (*(prgm - 1) == '\\')  /* backslash escaped */
        {
          /* remove backslash - avoid memmove() */
          int i, n = strlen (prgm);

          for (i = 0; i <= n; i++)
            prgm[i - 1] = prgm[i];
        }
      else
        {
          *prgm++ = '\0';
          /* remove leading/trailing space */
          prgm = trim_string (prgm);
          break;
        }
    }
  return prgm;
}
# endif /* NO_REMINDER_EXEC */

/*
 * Read the ~/.rclock file and find the next reminder
 *
 * update_only = 1
 *	look for a reminder whose time is greater than the current time,
 *	but less than the currently set reminder time
 *
 * update_only = 0
 *	look for a reminder whose time is greater than the reminder that
 *	just went off
 */
static void
Next_Reminder (int update_only)
{
  struct tm *tmval;
  char buffer[256];

# ifndef INT_MAX
#  define INT_MAX	1e8
# endif
  time_t currentTime;
  int savedTime = INT_MAX;
  FILE *fd;

  if (reminders_file == NULL || (fd = fopen (reminders_file, "r")) == NULL)
    {
      reminderTime = -1;        /* no config file, no reminders */
      return;
    }

  currentTime = time (NULL) + adjustTime;       /* get the current time */
  tmval = localtime (&currentTime);
  currentTime = mk_time (tmval);

  /* initial startup */
  if (reminderTime < 0)
    {
      /* ignore reminders that have already occurred */
      reminderTime = currentTime;
# ifndef NO_REMINDER_EXEC
      /* scan for programs run on start-up */
      while (fgets (buffer, sizeof (buffer), fd))
        {
          char *prgm, *text;

          text = trim_string (buffer);
          if (*text != ';')
            continue;

          prgm = extract_program (text);
          if (prgm != NULL && strlen (prgm) > 1)
            system (prgm);
        }
      rewind (fd);
# endif /* NO_REMINDER_EXEC */
    }

  /* now scan for next reminder */
  while (fgets (buffer, sizeof (buffer), fd))
    {
      int testTime, hh, mm, mo, dd, yy;
      char *text;

      text = trim_string (buffer);
      if (*text == '#')
        continue;               /* comment */
      if (*text == ';')
        continue;               /* program run on startup */
      /*
       * parse the line, format is hh:mm mo/dd/yr message; program
       * any of hh, mm, mo, dd, yr could be a wildcard `*'
       */
      hh = GetOneNum (&text, tmval->tm_hour);
      if (*text == ':')
        text++;
      mm = GetOneNum (&text, 0);

      while (isspace (*text))
        text++;
      if (!isToday (&text, tmval->tm_wday))
        continue;
      while (isspace (*text))
        text++;

      mo = GetOneNum (&text, tmval->tm_mon + 1);
      if (*text == '/')
        text++;
      dd = GetOneNum (&text, tmval->tm_mday);
      if (*text == '/')
        text++;
      yy = GetOneNum (&text, tmval->tm_year);

      /* handle 20th/21st centuries */
      if (yy > CENTURY)
        yy -= 1900;
      else if (yy < CENTURY)
        yy += (CENTURY - 1900);

      while (isspace (*text))
        text++;
      if (!*text)
        continue;

      testTime = (mm + 60 * (hh + 24 * (dd + 31 * (mo + 12 * yy))));

      if (testTime > (update_only ? currentTime : reminderTime))
        {
# ifndef NO_REMINDER_EXEC
          char *prgm = extract_program (text);
# endif/* NO_REMINDER_EXEC */
          /* trim leading/trailing space */
          text = trim_string (text);

          /*
           * have a reminder whose time is greater than the last
           * reminder, now make sure it is the smallest available
           */
          if (testTime < savedTime)
            {
              savedTime = testTime;
              strncpy (message, text, sizeof (message));
# ifndef NO_REMINDER_EXEC
              strncpy (execPrgm, (prgm ? prgm : ""), sizeof (execPrgm));
# endif
            }
          else if (testTime == savedTime)
            {
              if (strlen (text))
                {
                  int n = (sizeof (message) - strlen (message) - 3);

                  if (n > 0)
                    {
                      /* for co-occurring events */
                      strcat (message, "\\n");
                      strncat (message, text, n);
                    }
                }
# ifndef NO_REMINDER_EXEC
              if (prgm != NULL)
                {
                  int n = (sizeof (execPrgm) - strlen (execPrgm) - 2);

                  if ((n > 0) && (n >= strlen (prgm)))
                    {
                      /* for co-occurring programs */
                      switch (execPrgm[strlen (execPrgm) - 1])
                        {
                          case '&':
                          case ';':
                            break;
                          default:
                            strcat (execPrgm, ";");
                            break;
                        }
                      strncat (execPrgm, prgm, n);
                    }
                }
# endif/* NO_REMINDER_EXEC */
            }
        }
    }

  reminderTime = (savedTime < INT_MAX) ? savedTime : -1;
  fclose (fd);
}

/*
 * Provide reminder by mapping the message window
 */
static void
Reminder ()
{
  char *beg, *next;
  int lines;

  if (Msg_Mapped)
    return;

# ifndef NO_REMINDER_EXEC
  if (strlen (message) == 0)
    {
      if (strlen (execPrgm) > 1)
        {
          system (execPrgm);
          Next_Reminder (REPLACE);
        }
      return;                   /* some sort of error */
    }
# endif

  /* compute the window size */
# ifdef NO_REMINDER_EXEC
  Msg.width = 10 * XTextWidth (Xfont, "M", 1);
# else
  Msg.width = 18 * XTextWidth (Xfont, "M", 1);
# endif

  for (beg = message, lines = 1; beg; beg = next, lines++)
    {
      int width;
      char *end;

      if ((end = strstr (beg, "\\n")) == NULL)
        {
          end = beg + strlen (beg);
          next = NULL;
        }
      else
        {
          next = end + 2;
        }

      width = XTextWidth (Xfont, beg, (end - beg));
      if (Msg.width < width)
        Msg.width = width;
    }

  Msg.width += 30;
  Msg.height = (lines + 1) * FontHeight () + 30;

  /* resize and centre the window */
  XMoveResizeWindow (Xdisplay, Msg.win,
                     (DisplayWidth (Xdisplay, Xscreen) - Msg.width) / 2,
                     (DisplayHeight (Xdisplay, Xscreen) - Msg.height) / 2, Msg.width, Msg.height);

# define BUTTON_MARGIN	8

  XMoveWindow (Xdisplay, msgButton.Dismiss, BUTTON_MARGIN, (Msg.height - msgButton.height - BUTTON_MARGIN));
  XMoveWindow (Xdisplay, msgButton.Defer, (Msg.width - msgButton.width - BUTTON_MARGIN), (Msg.height - msgButton.height - BUTTON_MARGIN));
# ifndef NO_REMINDER_EXEC
  XMoveWindow (Xdisplay, msgButton.Start, (Msg.width - msgButton.width) / 2, (Msg.height - msgButton.height - BUTTON_MARGIN));
# endif

  XMapRaised (Xdisplay, Msg.win);
  XBell (Xdisplay, 0);
  Msg_Mapped = 1;
}
#endif /* REMINDERS */

/*
 * Loops forever, looking for stuff to do. Sleeps 1 minute if nothing to do
 */
static void
getXevent ()
{
  XEvent ev;
  int num_fds;                  /* number of file descriptors being used */
  struct timeval tm;
  struct tm *tmval;
  Atom wmDeleteWindow;
  fd_set in_fdset;

  /* Enable delete window protocol */
  wmDeleteWindow = XInternAtom (Xdisplay, "WM_DELETE_WINDOW", False);
  XSetWMProtocols (Xdisplay, Clock.win, &wmDeleteWindow, 1);
#ifdef ICONWIN
  XSetWMProtocols (Xdisplay, Icon.win, &wmDeleteWindow, 1);
#endif
#ifdef REMINDERS
  XSetWMProtocols (Xdisplay, Msg.win, &wmDeleteWindow, 1);
#endif

  num_fds = sysconf (_SC_OPEN_MAX);
#ifdef FD_SETSIZE
  if (num_fds > FD_SETSIZE)
    num_fds = FD_SETSIZE;
#endif

  while (1)
    {
      /* take care of all pending X events */
      while (XPending (Xdisplay))
        {
          XNextEvent (Xdisplay, &ev);
          switch (ev.type)
            {
              case ClientMessage:
                /* check for delete window requests */
                if ((ev.xclient.format == 32) && (ev.xclient.data.l[0] == (long)wmDeleteWindow))
                  {
#ifdef REMINDERS
                    if (ev.xany.window == Msg.win)
                      {
                        XUnmapWindow (Xdisplay, Msg.win);
                        Msg_Mapped = 0;
                        Next_Reminder (REPLACE);
                      }
                    else
#endif
                      return;   /* delete window is how this terminates */
                  }
                break;

              case Expose:
              case GraphicsExpose:
                /* need to re-draw a window */
                if (ev.xany.window == Clock.win)
                  Draw_Window (&Clock, 1);
#ifdef ICONWIN
                else if (ev.xany.window == Icon.win)
                  Draw_Window (&Icon, 1);
#endif
#ifdef REMINDERS
                else
                  Draw_Window (&Msg, 1);
#endif
                break;

              case ConfigureNotify:
                /* window has been re-sized */
                if (ev.xany.window == Clock.win)
                  {
                    Clock.width = ev.xconfigure.width;
                    Clock.height = ev.xconfigure.height;
                  }
                break;

#ifdef REMINDERS
              case KeyPress:
                /* any key press to dismiss message window */
                if (ev.xany.window == Msg.win)
                  {
                    Next_Reminder (REPLACE);
                    Msg_Mapped = 0;
                    XUnmapWindow (Xdisplay, Msg.win);
                  }
                break;
#endif

              case ButtonPress:
#ifdef REMINDERS
                /* button press to dismiss message window */
                if (ev.xany.window == Msg.win)
                  {
                    if (ev.xbutton.subwindow == msgButton.Dismiss)
                      {
                        Next_Reminder (REPLACE);
                        Msg_Mapped = 0;
                        XUnmapWindow (Xdisplay, Msg.win);
                      }
                    else if (ev.xbutton.subwindow == msgButton.Defer)
                      {
                        time_t t = time (NULL) + adjustTime;

                        tmval = localtime (&t);
                        reminderTime = mk_time (tmval) + DEFER_TIME;
                        Msg_Mapped = 0;
                        XUnmapWindow (Xdisplay, Msg.win);
                      }
# ifndef NO_REMINDER_EXEC
                    else if (ev.xbutton.subwindow == msgButton.Start)
                      {
                        system (execPrgm);
                        Next_Reminder (REPLACE);
                        Msg_Mapped = 0;
                        XUnmapWindow (Xdisplay, Msg.win);
                      }
# endif/* NO_REMINDER_EXEC */
                  }
#endif
#ifdef MAIL
                if (ev.xany.window == Clock.win)
                  {
# ifdef MAIL_SPAWN
                    /* left button action - spawn a mail reader */
                    if (ev.xbutton.button == Button1)
                      system (MAIL_SPAWN);
# else
                    if ((ev.xbutton.button == Button1) && (mail_spawn != NULL))
                      system (mail_spawn);
# endif
                    /* redraw the window */
                    Draw_Window (&Clock, 1);
                  }
#endif
                break;
            }
        }

      /* Now wait for time out or new X event */
      FD_ZERO (&in_fdset);
      FD_SET (Xfd, &in_fdset);
      tm.tv_sec = clockUpdate;
      tm.tv_usec = 0;
      select (num_fds, &in_fdset, NULL, NULL, &tm);

      Draw_Window (&Clock, 0);
#ifdef ICONWIN
      Draw_Window (&Icon, 0);
#endif
    }
}

/*
 * Print an error message.
 */
static void
print_error (const char *fmt, ...)
{
  va_list arg_ptr;

  va_start (arg_ptr, fmt);
  fprintf (stderr, APL_NAME ": ");
  vfprintf (stderr, fmt, arg_ptr);
  fprintf (stderr, "\n");
  va_end (arg_ptr);
}

/*----------------------- end-of-file (C source) -----------------------*/
