#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/un.h>
#include <termios.h>
#include <time.h>
#include <unistd.h>
#include <values.h>

static char *timezt;
static struct tm BrokenTime;
static time_t FirstTime, TimeNow, Diff;
static unsigned char time_bits[60], last_bits[60];
static unsigned long TotalMinutes, GoodMinutes, LostSync;
static int RunRoot, Debug, All, Done;

static char *Version = "DFC77 receiver version 1.1 October 1995 (C) Nemosoft Unv.";

/* Defaults */
static char *SerialLine = "/dev/dfc77";
static char *SocketName = "/tmp/.dfc";
static int IO_Speed = B50;
static int DFC_Type = 1;


static struct {
   int speed;
   int B;
   } B_Table[] = { {    50, B50    },
                   {    75, B75    },
                   {   110, B110   },
                   {   134, B134   },
                   {   150, B150   },
                   {   200, B200   },
                   {   300, B300   },	
                   {   600, B600   },
                   {  1200, B1200  },	
                   {  1800, B1800  },	
                   {  2400, B2400  },	
                   {  4800, B4800  },	
                   {  9600, B9600  },	
                   { 19200, B19200 },	
                   { 38400, B38400 },
                   {     0, 0      },
                 };                

static int check_par(char *bytes, int len)
{
   int i, j;
   
   i = 0;
   for (j = 0; j < len; j++)
      if (bytes[j]) 
        i ^= 1;
   return(i);
}

static int get_digit(char *bytes, int len)
{
   int i, j;
   
   i = 0;
   for (j = len - 1; j >= 0; j--) {
      i <<= 1;
      if (bytes[j])
        i |= 1;
   }
   return i;
}   


static int interp_bits(char *bytes, struct tm *T)
{
   struct {
      int startpos;
      int length;
      int max;
   } Bounds[] = { { 21, 4, 9 }, /* minutes 1  */
                  { 25, 3, 5 }, /* minutes 10 */
                  { 29, 4, 9 }, /* hours 1    */
                  { 33, 2, 2 }, /* hours 10   */
                  { 36, 4, 9 }, /* day 1      */
                  { 40, 2, 3 }, /* day 10     */
                  { 42, 3, 7 }, /* weekday    */
                  { 45, 4, 9 }, /* month 1    */
                  { 49, 1, 1 }, /* month 10   */
                  { 50, 4, 9 }, /* year 1     */
                  { 54, 4, 9 }, /* year 10    */
                };

   int Digits[11];
   int parity, i;

/*
   if (bytes[15])
     printf("Secondary transmitter is in use\n");
   if (bytes[16]) 
     printf("Announcing change Winter/Summer-time\n");
   if (bytes[17])
     printf("Summertime\n");
   if (bytes[18])
     printf("Wintertime\n");
   if (bytes[19])
     printf("Announcing leap-second\n");
   if (bytes[20])
     printf("Mysterious bit set\n");   
*/
   parity = check_par(&bytes[21], 8) | check_par(&bytes[29], 7) | check_par(&bytes[36], 23);
   if (parity)
     return 1;
     
   for (i = 0; i < 11; i++) {
      Digits[i] = get_digit(&bytes[Bounds[i].startpos], Bounds[i].length);
      if (Digits[i] > Bounds[i].max)
        return 1;
   }
   
   T->tm_sec = 0;
   T->tm_min   = Digits[0] + 10 * Digits[1];
   T->tm_hour  = Digits[2] + 10 * Digits[3];
   T->tm_mday  = Digits[4] + 10 * Digits[5];
   T->tm_wday  = Digits[6];
   T->tm_mon   = Digits[7] + 10 * Digits[8] - 1;
   i           = Digits[9] + 10 * Digits[10];
   T->tm_year  = (i < 95) ? i + 100 : i;
   
   if (T->tm_min > 59 || T->tm_hour > 23 || T->tm_mday > 31 || T->tm_mon == -1 || T->tm_mon > 11)
     return 1;
     
   return 0;
}

static void Usage()
{
   printf("%s\n", Version);
   printf("\nOptions (defaults are between () ):\n" 
          "-h          print this help\n"
          "-l device   name the serial device (/dev/dfc77)\n"
          "-s speed    specify serial speed (50)\n"
          "-n path     name the Unix socket-patch (/tmp/.dfc)\n"
          "-t type     set type of device in kernel-driver (1)\n"
          "-a          print time and statistics\n"
          "-d          increase debug level by 1\n"
          "-v          print version and creator\n"
          "\n"
          "dfc with no arguments tries to read the current time, or attach itself to the\n"
          "device and start synchronising the system. See the manual-page for details.\n"
          );
}

static void ParseArgs(int argc, char *argv[])
{
   char ch;
   int s, t;
   
   while ((ch = (char) getopt(argc, argv, "l:t:s:dhvn:a")) != EOF) {
      switch(ch) {
        case 'h':
          Usage();
          exit(0);
          break;
          
        case 'l':
          SerialLine = optarg;
          break;
        
        case 'n':
          SocketName = optarg;
          break;
          
        case 'a':
          All = 1;
          break;
          
        case 't':
          DFC_Type = atoi(optarg);
          if (DFC_Type < 1) {
            printf("Invalid type of receiver.\n");
            Usage();
            exit(1);
          }
          break;
          
        case 'd':
          Debug++;
          break;
          
        case 'v':
          printf("%s\n", Version);
          exit(0);
          break;
          
        case 's':
          IO_Speed = 0;
          s = atoi(optarg);
          t = 0;
          while (B_Table[t].speed != 0) {
             if (B_Table[t].speed == s) {
               IO_Speed = B_Table[t].B;
               break;
             }
             t++;        
          }
          if (IO_Speed == 0) {
            printf("Invalid speed.\n");
            Usage();
            exit(1);
          }
          break;
        
        default:
          Usage();
          exit(1);
          break;
      }
   }
}


/* if test = 0: try only to see if there already is a daemon running; 
      if so, do a connect.
   if test = 1; actually create socket 
 */
static int MakeSocket(int test)
{
   int blap, err;
   struct sockaddr_un Sock;
   
   Sock.sun_family = AF_UNIX;
   strncpy(Sock.sun_path, SocketName, sizeof(Sock.sun_path));
   blap = socket(AF_UNIX, SOCK_STREAM, 0);
   if (blap < 0)
     return blap;
     
   if (!test) { /* Try a connect */
     err = connect(blap, (struct sockaddr *) &Sock, sizeof(struct sockaddr_un));
     if (err < 0)
       return err;
   }
   else {
     /* Create the socket */
     err = bind(blap, (struct sockaddr *) &Sock, sizeof(struct sockaddr_un));
     if (err < 0)
       return err;
     chmod(SocketName, 0666); /* We'll assume this works */
     err = listen(blap, 5);
     if (err < 0)
       return err;
   }
   return blap;
}

static int OpenAndSetupDevice()
{
   struct termios Cterm;
   int fd, err;
   
   fd = open(SerialLine, O_NDELAY | O_NOCTTY);
   if (fd < 0) 
     return fd;

   tcgetattr(fd, &Cterm);
   Cterm.c_cflag = CS8 | CLOCAL | CREAD | HUPCL;
   Cterm.c_iflag = IGNCR;
   Cterm.c_lflag = 0;
   Cterm.c_oflag = 0;
   cfsetispeed(&Cterm, IO_Speed);
   cfsetospeed(&Cterm, IO_Speed);
   err = tcsetattr(fd, TCSANOW, &Cterm);
   
   if (err) {
     close(fd);
     return err;
   }

   /* Important: the patches in the kernel are called with this ioctl() */
   err = ioctl(fd, TIOCSERDFC77, &DFC_Type);
   if (err < 0) {
     close(fd);
     return err;
   }
   tcflush(fd, TCIOFLUSH);
   return fd;
}   


static int OpenUpSocket(int fd, fd_set *Set)
{
   int new_fd, len;
   struct sockaddr_un Incoming;

   len = sizeof(struct sockaddr_un);
   new_fd = accept(fd, (struct sockaddr *) &Incoming, &len);
   if (new_fd <= 0)
     return new_fd;

   if (fcntl(new_fd, F_SETFL, O_NONBLOCK) < 0) {
     close(new_fd); /* We won't accept a blocking fd */
     new_fd = -1;
   }
   else
     FD_SET(new_fd, Set);
   return new_fd;
}

static void HandleSocket(int fd, fd_set *Set)
{
   char Buf[10];
   int i;
   
   i = read(fd, &Buf[0], 10);
   if (i == 0) { /* socket closed */
     close(fd);
     FD_CLR(fd, Set);
   }
   /* ignore data... */
}

static int PumpTime(int fd)
{
   time_t KernelTime;
   char TimeBuf[80], ws;
   
   if (!GoodMinutes) { /* Not synced yet */
     return -1;
   }
   
   time(&KernelTime);
   KernelTime -= Diff; /* Since we may not run as root, and thus can't set the kernel time,
                          we may need to adjust (calculated at every hour-sync) */
   strcpy(TimeBuf, ctime(&KernelTime));
   if (write(fd, &TimeBuf[0], strlen(TimeBuf)) == MAXINT && errno == EAGAIN)
     return -1;
     
   ws = '-';
   if (last_bits[17])
     ws = 'S';
   if (last_bits[18])
     ws = 'W';
   sprintf(&TimeBuf[0], "%ld %ld %ld %ld %ld %ld %c%c %c\n",
           TimeNow, FirstTime, TotalMinutes, GoodMinutes, LostSync, Diff,
           ws, last_bits[16] ? '!' : '-', last_bits[20] ? 'S' : 'P');
   write(fd, &TimeBuf[0], strlen(TimeBuf));
   return 0;
}

static void GetTime(int fd)
{
   char Buf[256], *p;
   int i;
   
   i = read(fd, &Buf[0], 256);
   if (i < 0) {
     printf("Error reading data from daemon! (%s)\n", strerror(errno));
     return;
   }
   
   if (i == 0) {
     printf("Daemon not synced yet.\n");
     return;
   }
   
   Buf[i] = '\0';
   p = strchr(Buf, '\n');
   if (!All)
     *++p = '\0'; /* truncated after time */
   printf("%s", Buf);  
}   
   

static int ReceiveBit(int fd)
{
   int par;
   static int count = 0, sync = 0;
   char received_bit;
   
   par = read(fd, &received_bit, 1);
   if (par < 0) {
     syslog(LOG_WARNING, "Read of serial data failed. Bummer.");
     return 1;
   }
   
   /* Ok, the trick is this... The driver in the kernel times intervals
      between characters. If the time is more than 1.1 second, bit 0
      of the received character is set to 1. If the time is more than 2.2
      seconds, bit 1 is set also. The upper 4 bits indicate the received 
      databit.
      
      If the time-interval is between 1.1 and 2.2 seconds, we can
      assume that this was the missing 59th bit. So the byte we
      have just received is the start of the next minute! (assuming
      we received exactly 59 bytes before). If the time is larger
      than 2.2 seconds that probably means a timeout at startup or
      bad reception.
      
      I use a kernel driver because the timing with select() is probably 
      not accurate enough, esp. under heavy loads. The ultimate would be
      to couple all of this code to the serial interrupt and have the
      time directly fed into the kernel, but that would require extensive
      changes to the kernel. It would be more accurate, but only for the
      nit-picks.
    */
      
   if (received_bit & 0x02) { 
     /* Total timeout. Reset all */
     sync = 0;
     count = 0;
     if (Debug > 1)
       printf("**\n");
     return 0;
   }
   
   if (received_bit & 0x01) {
     if (Debug > 1)
       printf("\n");
     /* This could be interesting... did we receive exactly 59 bytes ? */
     TotalMinutes++;
     if (count == 59) {
       if (!interp_bits(time_bits, &BrokenTime)) {
         sync = 1; /* assume we're synced now */
         memcpy(last_bits, time_bits, 60);
         TimeNow = mktime(&BrokenTime);
         
         /* set or display Real Time clock every whole hour, and the very first time */
         if (!GoodMinutes || BrokenTime.tm_min == 0) {
           if (!GoodMinutes) {
             syslog(LOG_NOTICE, "DFC-clock first synchronized at %s", asctime(&BrokenTime));
             FirstTime = TimeNow;
           }
     
           if (RunRoot) 
             stime(&TimeNow);
           else {
             printf("Ding! Dong! %s", asctime(&BrokenTime));
             time(&Diff); /* We need this to calculate the offset later */
             Diff -= TimeNow;
           }
           
         }
         
         if (Debug > 0)
           printf("%s", ctime(&TimeNow));
         GoodMinutes++;
       }
     }
     else {
       if (sync)
         LostSync++;
       sync = 0;
     }
     count = 0;
   }

   received_bit &= 0xF0;
   if (Debug > 1)
     printf("%c", received_bit ? '0' : '1');
   if (count < 60)
     time_bits[count++] = received_bit ? 0 : 1;
   else { /* Overflow. Out of sync */
     if (sync)
       LostSync++;
     sync = 0;
   } 
   if (Debug > 0)
     fflush(stdout);
   
   return 0;
}

static void CatchSignal(int number)
{
   syslog(LOG_WARNING, "Aborting dfc on signal: %d", number);
   Done = 1;
}

void main(int argc, char *argv[])
{
   int cfd, sfd;
   
   int i, par;
   fd_set WaitAll, WaitSock;
   struct timeval WaitTime;

   Debug = 0;
   All = 0;
   TotalMinutes = 0;
   GoodMinutes = 0;
   LostSync = 0;
   Diff = (time_t) 0;
      
   /* Get us known to syslogd */
   openlog("dfc-77", 0, LOG_CRON);
   
   /* Check if we run as root; only then we can set the time in the kernel. */
   RunRoot = (geteuid() == 0);

   /* Get options etc. */
   ParseArgs(argc, argv);

   /* Get Timezone var */
   timezt = getenv("TZ");
   if (timezt == NULL)
     timezt = "";
   else {
     timezt = strchr(timezt, '=');
     if (timezt == NULL)
       timezt = "";
   }
   if (Debug > 1)
     printf("Time-zone: TZ=%s %c%c\n", timezt, *tzname[0], *tzname[1]);
   
   /* See if there is already a daemon running */
   sfd = MakeSocket(0);
   
   /* At this point we have a idea on how to run... There are several possibilities:
   
                    |        root                |      non-root (user)
      --------------+----------------------------+---------------------------
      first time    | Open device, setup socket, | Open device, setup socket,
      (no socket)   | receive bytes and keep     | receive bytes, but can't
                    | kernel-time                | set kernel time
      --------------+----------------------------+---------------------------
      later         | Open socket, get time +    | idem
                    | statistics, close socket   |
    */
    
   if (sfd > 0) {
     /* Ah! There's already a dfc running. Very good... */
     GetTime(sfd);
     close(sfd);
     exit(0);
   }
   
   cfd = OpenAndSetupDevice();
   
   if (cfd < 0) {
     printf("Error opening serial device for DFC (%s).\n", strerror(errno));
     syslog(LOG_NOTICE, "Failed to open serial device %s", SerialLine);
     exit(1);
   }
   
   sfd = MakeSocket(1); /* Now we make the socket */
   if (sfd < 0 ) {
     if (errno == EINVAL) { /* this could indicate a dead socket. unlink and try again */
       unlink(SocketName);
       sfd = MakeSocket(1);
       if (sfd < 0) {
         syslog(LOG_NOTICE, "Creation of socket %s failed. Giving up.", SocketName);
         exit(1);
       }
     }
   }

   signal(SIGHUP,  CatchSignal);
   signal(SIGINT,  CatchSignal);
   signal(SIGQUIT, CatchSignal);
   signal(SIGTERM, CatchSignal);

   if (!RunRoot) {
     printf("Warning! The dfc program will run and receive the time, but because it does not\n"
            "run as root it cannot adjust the kernel time. Subsequent instances of the dfc\n"
            "program wil get the correct time by polling %s.\n", SocketName);
   }
   else { /* retreat */
     i = fork();
     if (i < 0) /* fork failed... */
       syslog(LOG_ERR, "fork() failed");
     if (i) /* parent exits */
       exit(0);
     ioctl(0, TIOCNOTTY);
     close(0);
     close(1);
     close(2);
     Debug = 0;
     syslog(LOG_INFO, "Starting up dfc as daemon");
   }
      
   Done = 0;
   FD_ZERO(&WaitSock);
   while (!Done) {
      FD_ZERO(&WaitAll);
      /* Copy bits from sockets into main waiting array */
      for (i = 0; i < FD_SETSIZE; i++)
         if (FD_ISSET(i, &WaitSock)) 
           FD_SET(i, &WaitAll);
      FD_SET(cfd, &WaitAll);
      FD_SET(sfd, &WaitAll);
      WaitTime.tv_sec = 60; /* long time-out */
      WaitTime.tv_usec = 0;
      par = select(FD_SETSIZE, &WaitAll, (fd_set *) NULL, (fd_set *) NULL, &WaitTime);
      
      if (par < 0) {
        if (errno != EINTR)
          syslog(LOG_ERR, "select() failed! (%s)", strerror(errno));
        break;
      }
   
      if (par == 0) { /* Timeout. Better break? */
        syslog(LOG_WARNING, "Timeout in receiving char.\n");
        break;
      }
      
      if (FD_ISSET(sfd, &WaitAll)) { /* A new connection has arrived */
        i = OpenUpSocket(sfd, &WaitSock);
        if (i >= 0)
          if (PumpTime(i) < 0) { /* error on socket, close */
            close(i);
            FD_CLR(i, &WaitSock);
          }
      }
      
      if (FD_ISSET(cfd, &WaitAll)) /* data is waiting... */
        Done = ReceiveBit(cfd);
        
      for (i = 0; i < FD_SETSIZE; i++)
         if (FD_ISSET(i, &WaitAll) && FD_ISSET(i, &WaitSock)) /* some data or action on the socket */
           HandleSocket(i, &WaitSock);
   } /* ..while */

   /* End of daemon */
   close(cfd);
   close(sfd);
   for (i = 0; i < FD_SETSIZE; i++)
      if (FD_ISSET(i, &WaitSock))
        close(i);
   unlink(SocketName);
   syslog(LOG_NOTICE, "DFC terminated.");
}
