/*  wmbday
 *  Copyright (C) 2003-2005 astratis <steffen@x-berg.de>
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public Licensse 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

#include "main.h"
#include "config.h"
#include "ring.h"
#include "date.h"
#include "datafile.h"

#include "wmbday_text.xpm"
#include "wmbday_on.xpm"
#include "wmbday_off.xpm"

#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>
#include <pwd.h>
#include <signal.h>

#include <getopt.h>

#include <X11/xpm.h>
#include <X11/extensions/shape.h>


/* global variables */
Window window;
Window iconwin;
GC theGC;
Pixmap pix_on;
Pixmap pix_off;
Pixmap pix_text;
Pixmap pix_bg;
Pixmap pix_font;
Colormap colormap;
struct ring* nextbdays[4];
int todays_bday = 0;
int day = 0; /* current day */
struct settings mySettings; /* holds global program settings */


void get_user_homedir(void) {
  struct passwd *user;

  if(!(user = getpwuid(geteuid()))) {
    fprintf(stderr, "Couldn't determine user's homedir\n");
    exit(1);
    return;
  }

  if(snprintf(mySettings.datafile, sizeof(mySettings.datafile), "%s/.wmbday/data", user->pw_dir) > sizeof(mySettings.datafile)) {
    fprintf(stderr, "Path to datafile exceeded buffer size of %d chars\n", sizeof(mySettings.datafile));
    exit(1);
  }
  return;
}


static void PrintHelp(void) {
  printf("Usage: " PACKAGE " [options]\n"
         "  -d, --display <string>   X display to use\n"
         "  -t, --text               use the classical text-style interface\n"
         "  -f, --file <file>        use <file> as data file instead of ~/.wmbday/data\n"
         "  -b, --backlight          turn on backlight\n"
         "  -B, --bgcolor <color>    sets color (as text) used for filling the background\n"
         "  -F, --fontcolor <color>  sets color (as text) used for text\n"
         "  -H, --hicolor <color>    sets color (as text) used for text when blinking\n"
         "  -h, --help               outputs this text..\n"
         "  -v, --version            prints version\n");
  exit(0);
  return;
}


static void PrintVersion(void) {
  printf(PACKAGE " " VERSION "\n");
  printf("Copyright (C) 2003-2005 astratis <steffen@x-berg.de>\n");
  exit(0);
  return;
}


void ParseCMDLine(int argc, char **argv) {
  int c;
  struct stat statbuf;
  struct option const long_options[] = {
    {"display", required_argument, 0, 'd'},
    {"text", no_argument, 0, 't'},
    {"backlight", no_argument, 0, 'b'},
    {"bgcolor", required_argument, 0, 'B'},
    {"fontcolor", required_argument, 0, 'F'},
    {"hicolor", required_argument, 0, 'H'},
    {"file", required_argument, 0, 'f'},
    {"help", no_argument, 0, 'h'},
    {"version", no_argument, 0, 'v'},
    {NULL, 0, NULL, 0} };

  /* set default values first */
  mySettings.gui_style = GUI_GUI;
  mySettings.color_bg = NULL;
  mySettings.color_font = NULL;
  mySettings.color_hilight = NULL;
  mySettings.display = NULL;

  while((c = getopt_long(argc, argv, "d:tbf:B:F:H:hv", long_options, NULL)) != -1) {
    switch(c) {
      case 'd': /* display */
        if(optarg) {
          mySettings.display = XOpenDisplay(optarg);
          if(mySettings.display == NULL) {
            fprintf(stderr, "Could not open display: %s\n", optarg);
            exit(1);
          }
        }
        else {
          fprintf(stderr, "no display given\n");
          exit(1);
        }
        break;

      case 't': /* gui version */
        mySettings.gui_style = GUI_TEXT;
        break;

      case 'b': /* backlight on/off */
        mySettings.backlight = LIGHT_ON;
        break;

      case 'B': /* bgcolor */
        if(optarg)
          mySettings.color_bg = strdup(optarg);
        else {
          fprintf(stderr, "no bg-color given\n");
          exit(1);
        }
        break;

      case 'F': /* fontcolor */
        if(optarg)
          mySettings.color_font = strdup(optarg);
        else {
          fprintf(stderr, "no font-color given\n");
          exit(1);
        }
        break;

      case 'H': /* hicolor */
        if(optarg)
          mySettings.color_hilight = strdup(optarg);
        else {
          fprintf(stderr, "no highlight-color given\n");
          exit(1);
        }
        break;

      case 'f': /* data-file */
        if(optarg) {
          if(snprintf(mySettings.datafile, sizeof(mySettings.datafile), "%s", optarg) > sizeof(mySettings.datafile)) {
            fprintf(stderr, "Path to datafile exceeded buffer size of %d chars\n", sizeof(mySettings.datafile));
            exit(1);
          }

          /* check if it is a file */
          if(stat(mySettings.datafile, &statbuf) != 0) {
            perror("stat");
            exit(1);
          }

          if(!S_ISREG(statbuf.st_mode)) {
            fprintf(stderr, "Data file specified is not a regular file.\n");
            exit(1);
          }
        }
        else {
          printf("no file given\n");
          exit(1);
        }
        break;

      case 'h': /* help */
        PrintHelp();
        break;

      case 'v': /* version */
        PrintVersion();
        break;

      default:
        PrintHelp();
        break;
    }
  }

  if(mySettings.display == NULL) {
    if((mySettings.display = XOpenDisplay(NULL)) == NULL) {
      fprintf(stderr, "Could not open display\n");
      exit(1);
    }
  }
  mySettings.screen = DefaultScreen(mySettings.display);

  if(mySettings.color_bg == NULL)
    mySettings.color_bg = strdup("#6bc739");

  if(mySettings.color_font == NULL)
    mySettings.color_font = strdup("black");

  if(mySettings.color_hilight == NULL)
    mySettings.color_hilight = strdup("red");

  return;
}


Window CreateWindow(void) {
  XClassHint  classHint;
  XWMHints  thewmhints;
  Pixmap    pixmask;
  XpmAttributes attrib;
  XpmColorSymbol gui_color[1] = { {"Back0", NULL, 0} };
  XColor color0;

  window = XCreateSimpleWindow(mySettings.display, RootWindow(mySettings.display, mySettings.screen), 0, 0, WMBDAY_WIDTH, WMBDAY_HEIGHT, 1, BlackPixel(mySettings.display, mySettings.screen), WhitePixel(mySettings.display, mySettings.screen));
  iconwin = XCreateSimpleWindow(mySettings.display, window, 0, 0, WMBDAY_WIDTH, WMBDAY_HEIGHT, 1, BlackPixel(mySettings.display, mySettings.screen), WhitePixel(mySettings.display, mySettings.screen));



  if(mySettings.color_bg != NULL) {
    color0 = GetColor(mySettings.color_bg);
    gui_color[0].pixel = color0.pixel;
    XFreeColors(mySettings.display, colormap, &color0.pixel, 1, 0);
  }

  if(mySettings.gui_style == GUI_GUI) {
    attrib.valuemask = XpmCloseness;
    attrib.closeness = 40000;

    /* load window xpms */
    if(XpmCreatePixmapFromData(mySettings.display, iconwin, wmbday_off_xpm, &pix_off, &pixmask, &attrib) != 0) {
      fprintf(stderr, "Couldn't initialise background image.\n");
      exit(1);
    }

    attrib.colorsymbols = gui_color;
    attrib.numsymbols = 1;
    attrib.valuemask |= XpmColorSymbols;

    if(XpmCreatePixmapFromData(mySettings.display, iconwin, wmbday_on_xpm, &pix_on, NULL, &attrib) != 0) {
      fprintf(stderr, "Couldn't initialise background image.\n");
      exit(1);
    }

    XShapeCombineMask(mySettings.display, window, ShapeBounding, 3, 3, pixmask, ShapeSet);
    XShapeCombineMask(mySettings.display, iconwin, ShapeBounding, 3, 3, pixmask, ShapeSet);

    if(mySettings.backlight == LIGHT_OFF)
      SetBackground(pix_off);
    else
      SetBackground(pix_on);

  }
  else {
    pixmask = XCreateBitmapFromData(mySettings.display, window, wmbday_xpm_text, WMBDAY_WIDTH, WMBDAY_HEIGHT);

    XShapeCombineMask(mySettings.display, window, ShapeBounding, 0, 0, pixmask, ShapeSet);
    XShapeCombineMask(mySettings.display, iconwin, ShapeBounding, 0, 0, pixmask, ShapeSet);
  }

  classHint.res_name = PACKAGE;
  classHint.res_class = "DockApp";
  XSetClassHint(mySettings.display, window, &classHint);

  thewmhints.initial_state = WithdrawnState;
  thewmhints.window_group = window;
  thewmhints.icon_window = iconwin;
  thewmhints.icon_x = 0;
  thewmhints.icon_y = 0;
  thewmhints.flags = StateHint | WindowGroupHint | IconWindowHint | IconPositionHint;
  XSetWMHints(mySettings.display, window, &thewmhints);


  XSelectInput(mySettings.display, window, ExposureMask | PointerMotionMask | ButtonPressMask);
  XSelectInput(mySettings.display, iconwin, ExposureMask | PointerMotionMask | ButtonPressMask);

  return window;
}


int CreateWindowGC(void) {
  XGCValues values;

  if((theGC = XCreateGC(mySettings.display, RootWindow(mySettings.display, mySettings.screen), 0, &values)) == 0)
    return 0; /* error */
  else
    return 1;
}


void SetBackground(Pixmap pix_bg) {
  Pixmap tmp;

  tmp = XCreatePixmap(mySettings.display, window, WMBDAY_WIDTH, WMBDAY_HEIGHT, DefaultDepth(mySettings.display, mySettings.screen));

  XCopyArea(mySettings.display, pix_bg, tmp, theGC, 0, 0, WMBDAY_WIDTH, WMBDAY_HEIGHT, 3, 3);

  XSetWindowBackgroundPixmap(mySettings.display, window, tmp);
  XSetWindowBackgroundPixmap(mySettings.display, iconwin, tmp);

  XClearWindow(mySettings.display, window);
  XClearWindow(mySettings.display, iconwin);

  XFreePixmap(mySettings.display, tmp);

  return;
}

void signal_handler(int sig) {
  time_t tnow;
  struct tm *now;

  static int run = 0;
  int i;
  XColor color_font;
  XColor color_hilight;

  color_font = GetColor(mySettings.color_font);
  color_hilight = GetColor(mySettings.color_hilight);

  switch(sig) {
    case SIGCHLD: /* avoid zombie */
      waitpid(-1, NULL, WNOHANG);
      break;

    case SIGALRM: /* animate the text */
      /* draw text */
      XSetForeground(mySettings.display, theGC, run & 1 ? color_font.pixel : color_hilight.pixel);

      for(i = 0; i < todays_bday; i++) {
        if(mySettings.gui_style == GUI_GUI) {
          XDrawString(mySettings.display, iconwin, theGC, 8, 17+i*13, nextbdays[i]->name, strlen(nextbdays[i]->name));
          XDrawString(mySettings.display, window, theGC, 8, 17+i*13, nextbdays[i]->name, strlen(nextbdays[i]->name));
        }
        else {
          XDrawString(mySettings.display, iconwin, theGC, 8, 15+i*14, nextbdays[i]->name, strlen(nextbdays[i]->name));
          XDrawString(mySettings.display, window, theGC, 8, 15+i*14, nextbdays[i]->name, strlen(nextbdays[i]->name));
        }
      }

      run = !run;
      break;

    case SIGHUP: /* reload data file */
      fprintf(stderr, "Caught SIGHUP. Reloading datafile...\n");

      /* load datafile, get next birthdays again and redraw window so that changes are being shown */
      LoadDatafile();

      tnow = time(NULL);
      now = localtime(&tnow);
      ring_goto_date(now->tm_mday, now->tm_mon+1);
      ring_get_next(nextbdays, 4);

      day = 0; /* let main-loop check for today's birthdays again */

      RedrawWindow(NULL);
      break;
  }

  XFreeColors(mySettings.display, colormap, &color_font.pixel, 1, 0);
  XFreeColors(mySettings.display, colormap, &color_hilight.pixel, 1, 0);
}


void RedrawWindow(XEvent* theevent) {
  int i;
  XColor color_font;
  XColor color_bg;

  color_font = GetColor(mySettings.color_font);
  color_bg = GetColor(mySettings.color_bg);

  if(mySettings.gui_style == GUI_GUI) {
    if(mySettings.backlight == LIGHT_ON)
      SetBackground(pix_on);
    else
      SetBackground(pix_off);

    XSetForeground(mySettings.display, theGC, color_font.pixel);

    for(i = 0; i < 4; i++) {
      if(nextbdays[i] != NULL) {
        XDrawString(mySettings.display, iconwin, theGC, 8, 17+i*13, nextbdays[i]->name, strlen(nextbdays[i]->name));
        XDrawString(mySettings.display, window, theGC, 8, 17+i*13, nextbdays[i]->name, strlen(nextbdays[i]->name));
      }
    }
  }
  else {
    XSetForeground(mySettings.display, theGC, color_bg.pixel);

    if(theevent != NULL)
      XFillRectangle(mySettings.display, iconwin, theGC, theevent->xexpose.x, theevent->xexpose.y, theevent->xexpose.width, theevent->xexpose.height);
    else
      XFillRectangle(mySettings.display, iconwin, theGC, 0, 0, WMBDAY_WIDTH, WMBDAY_HEIGHT);

    XSetForeground(mySettings.display, theGC, color_font.pixel);

    for(i = 0; i < 4; i++) {
      if(nextbdays[i] != NULL) {
        XDrawString(mySettings.display, iconwin, theGC, 8, 15+i*14, nextbdays[i]->name, strlen(nextbdays[i]->name));
        XDrawString(mySettings.display, window, theGC, 8, 15+i*14, nextbdays[i]->name, strlen(nextbdays[i]->name));
      }
    }

    XFreeColors(mySettings.display, colormap, &color_font.pixel, 1, 0);
    XFreeColors(mySettings.display, colormap, &color_bg.pixel, 1, 0);
  }

  return;
}


XColor GetColor(char* name) {
  XColor color;

  if(!XParseColor(mySettings.display, colormap, name, &color)) {
    fprintf(stderr, "Couldn't parse \"%s\"\n", name);
    exit(1);
  }

  if(!XAllocColor(mySettings.display, colormap, &color)) {
    fprintf(stderr, "Couldn't alloc \"%s\"\n", name);
    exit(1);
  }

  return color;
}


void StartTimer(void) {
  struct itimerval timer;

  timer.it_value.tv_sec = 0;
  timer.it_value.tv_usec = 250000;

  timer.it_interval.tv_sec = 0;
  timer.it_interval.tv_usec = 250000;

  setitimer(ITIMER_REAL, &timer, NULL);

  return;
}


void StopTimer(void) {
  struct itimerval timer;

  timer.it_value.tv_sec = 0;
  timer.it_value.tv_usec = 0;

  timer.it_interval.tv_sec = 0;
  timer.it_interval.tv_usec = 0;

  setitimer(ITIMER_REAL, &timer, NULL);
  RedrawWindow(NULL);

  return;
}


int main(int argc, char **argv) {
  XEvent event;
  time_t tnow;
  struct tm *now;

  struct itimerval timerinfo;
  int row;
  int days_left;
  int rollover = 0;
  int fork_child;
  char buffer[1024];
  mySettings.datafile[0] = 0x0;
  const char* enum_months[] = { "January", "February", "March", "April", "May", "June", "July",
                          "August", "September", "October", "November", "December" };

  /* parse parameters from command line */
  ParseCMDLine(argc, argv);

  colormap = DefaultColormap(mySettings.display, mySettings.screen);

  /* create a graphics context for window for drawing */
  if(!CreateWindowGC())
    fprintf(stderr, "Couldn't create window GC\n");
  window = CreateWindow();

  /* if no datafile was specified at command line, we set to default */
  if(mySettings.datafile[0] == 0x0)
    get_user_homedir();

  /* now show the window */
  XMapWindow(mySettings.display, window);
  XFlush(mySettings.display);

  /* set handler on SIGHUP which makes wmbday reload the data from data-file */
  if(signal(SIGHUP, signal_handler) == SIG_ERR) {
    fprintf(stderr, "Couldn't set signal handler for SIGHUP\n");
    return 1;
  }

  /* set handler on SIGALRM for animation-timer */
  if(signal(SIGALRM, signal_handler) == SIG_ERR) {
    fprintf(stderr, "Couldn't set signal handler for SIGALRM\n");
    return 1;
  }

  /* set handler on SIGCHLD to avoid zombies */
  if(signal(SIGCHLD, signal_handler) == SIG_ERR) {
    fprintf(stderr, "Couldn't set signal handler for SIGCHLD\n");
    return 1;
  }

  /* load data from data file into ring */
  LoadDatafile();

  /* main loop */
  while(1) {
    tnow = time(NULL);
    now = localtime(&tnow);

    /* day changed */
    if(day != now->tm_mday){
      ring_goto_date(now->tm_mday, now->tm_mon+1);
      ring_get_next(nextbdays, 4);
      RedrawWindow(NULL);

      if((todays_bday = ring_check_bday(now->tm_mday, now->tm_mon+1)) > 0)
        StartTimer();

      day = now->tm_mday;
    }

    while(XPending(mySettings.display)) {
      XNextEvent(mySettings.display, &event);
      switch(event.type) {
        case Expose: /* redraw window */
          RedrawWindow(&event);
          break;

        case ButtonPress: /* mouse click */
          /* left mouse button */
          if(event.xbutton.button == 1) {
            getitimer(ITIMER_REAL, &timerinfo);

            /* show info about clicked person */
            if(timerinfo.it_value.tv_usec == 0) {
              row = event.xbutton.y / 16;
              if(nextbdays[row] != NULL) {
                days_left = days_between_dates(day, now->tm_mon + 1, nextbdays[row]->day, nextbdays[row]->month, now->tm_year + 1900);

                /* for people whose bday is next year we have to add 1 when calculating their age */
                if(compare_dates(day, now->tm_mon + 1, nextbdays[row]->day, nextbdays[row]->month) == 1)
                    rollover = 1;
                else
                    rollover = 0;

                switch(days_left) {
                  case 0:
                    snprintf(buffer, sizeof(buffer), "%s\nhas a birthday today! The lucky person turned %d.", nextbdays[row]->name, now->tm_year+1900 - nextbdays[row]->year);
                    break;
                  case 1:
                    snprintf(buffer, sizeof(buffer), "%s\nhas a birthday in %d day on %s %d\nand will turn %d then.", nextbdays[row]->name, days_left, enum_months[nextbdays[row]->month-1], nextbdays[row]->day, now->tm_year+1900 - nextbdays[row]->year + rollover);
                    break;
                  default:
                    snprintf(buffer, sizeof(buffer), "%s\nhas a birthday in %d days on %s %d\nand will turn %d then.", nextbdays[row]->name, days_left, enum_months[nextbdays[row]->month-1], nextbdays[row]->day, now->tm_year+1900 - nextbdays[row]->year + rollover);
                    break;
                }

                /* fork() to create a messagebox with xmessage */
                if((fork_child = fork()) == -1) {
                  perror("Couldn't fork()");
                  exit(1);
                }

                if(fork_child == 0) {
                  if(execlp(XM_PATH, "xmessage", "-center", buffer, (char*)NULL) < 0) {
                    perror("Couldn't execute \"xmessage\"");
                    exit(1);
                  }
                  exit(0);
                }
              }
            }
            else
              StopTimer(); /* stop animation */
          }
          else if(event.xbutton.button == 3) { /* right button, switch background */
            /* only take action if it's GUI-style */
            if(mySettings.gui_style != GUI_GUI)
              break;

            if(mySettings.backlight == LIGHT_OFF) {
              mySettings.backlight = LIGHT_ON;
              SetBackground(pix_on);
            }
            else {
              mySettings.backlight = LIGHT_OFF;
              SetBackground(pix_off);
            }

            RedrawWindow(NULL);
          }
          break;
      }
    }
    usleep(10000);
  }

  /* free memory after strdup()'s */
  free(mySettings.color_bg);
  free(mySettings.color_font);
  free(mySettings.color_hilight);

  /* close connection to xserver */
  XCloseDisplay(mySettings.display);
  return 0;
}
