/* Copyright (c) 2000  Kevin Sullivan <nite@gis.net>
 *
 * Please refer to the COPYRIGHT file for more information.
 */

#include <stdio.h>
#include <ncurses.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/socket.h>
#include <errno.h>
#include <sys/time.h>

#include "defines.h"
#include "codes.h"
#include "colors.h"
#include "scheck.h"
#include "timer.h"
#include "nap.h"
#include "winio.h"
#include "lists.h"

#ifdef MEMWATCH
  #include "memwatch.h"
#endif

extern info_t info;
extern int srch;
extern int reconnect;

/* the global connection list */
sock_t *socklist = NULL;

/* bandwidth observers for global uploads and global downloads */
bandwidth_t bwup, bwdown;

/* set by /quit to cause an immediate unconditional shutdown of nap,
   or by qevent() after a /tquit */
int quit_now = 0;

/* add a connection to end of global connection list. s is file
   descriptor, nm is name, t is type (S_R or S_W), func is handler
   function. d is left blank. nm is copied (strdup). Returns the new
   socklist element.  Also, if this is an upload or download
   connection, notify server (ugly!) */
/* Note that upload/dowload notification is done in event.c:dosend() and
   event.c:doget() for connections initiated by a remote client. */
sock_t *addsock(int s, char *nm, unsigned char t, int (*func)(WINDOW *, sock_t *))
{
  sock_t *cur, *sv;
  
  /* create a new socket */
  cur = (sock_t *)malloc(sizeof(sock_t));
  cur->fd = s;
  cur->t = t;
  cur->socknm = strdup(nm);
  cur->func = func;
  cur->next = NULL;
  cur->dxx = NULL;
  cur->shared_to_send = NULL;
  cur->utask = NULL;
  cur->dtask = NULL;
  cur->btask = NULL;

  cur->bwlimit = 0;

  /* an add it to the list */
  list_append(sock_t, socklist, cur);
  
  /* if it's an upload or a download, notify the server */
  sv = findsock("server");

  if (sv) {
    if (cur->socknm[0] == 'u' && cur->socknm[1] == ' ') {
      sendpack(sv->fd, NAP_UP, NULL);
    } else if (cur->socknm[0] == 'd' && cur->socknm[1] == ' ') {
      sendpack(sv->fd, NAP_DOWN, NULL);
    }
  }
  return cur;
}

/* delete a connection. It is uniquely identified by its file
   descriptor s.  This routine also notifies the server if an upload
   or download is being shut down (this is ugly, since this should
   rather be done in connection with the task structure, not the
   connection structure). It also sets srch to 0 in case we are
   deleting the server connection. It also shuts down the socket of
   the connection, and closes it, if it is indeed a socket connection
   (file descriptor >2). */
void delsock(int s)
{
  sock_t *cur, *sv;
  library_t *l;

  /* unlink the first connection whose fd is s, if any */
  list_unlink_cond(sock_t, socklist, cur, cur->fd == s);
  
  if (!cur) {
    return;
  }

  sv = findsock("server");
  
  if (sv) {
    if (cur->socknm[0] == 'd' && cur->socknm[1] == ' ') {
      sendpack(sv->fd, NAP_DOWNDONE, NULL);
    } else if (cur->socknm[0] == 'u' && cur->socknm[1] == ' ') {
      sendpack(sv->fd, NAP_UPDONE, NULL);
    }
  }

  if (!strcmp(cur->socknm, "server"))
    srch = 0;

  if (cur->shared_to_send) {
    list_forall_unlink(l, cur->shared_to_send) {
      free(l->lfn);
      free(l->hash);
      free(l);
    }
  }
  free(cur->socknm);
  free(cur);

  /* Note: a connection handler may not close the socket before or
   * after the call to delsock(), since it is always closed here, and
   * it can be harmful (segfault) to close a file twice. */
  if (s > 2) {
    shutdown(s, 2);
    close(s);
  }
  return;
}

/* finds a connection by name in the connection list */
sock_t *findsock(const char *nm)
{
  sock_t *elt;
  
  list_find(elt, socklist, !strcasecmp(elt->socknm, nm));  

  return(elt);
}

/* finds a connection by file descriptor in the connection list */
sock_t *findsockfd(int fd)
{
  sock_t *elt;
  
  list_find(elt, socklist, elt->fd == fd);  

  return(elt);
}

/* prints the list of connections to the specified window. */
void psocks(WINDOW *win)
{
  sock_t *cur;
  
  wp(win, "fd | nm\n");
  for (cur=socklist;cur!=NULL;cur=cur->next)
    wp(win, "%2d   %s\n", cur->fd, cur->socknm);
  drw(win);
}

/* the next few functions are for bandwidth limiting. The idea is that
   a bandwidth "observer" is created for each bandwidth limit. Every
   time bytes travel through the connection, the observer is
   informed. The observer calculates, on demand, the number of
   microseconds one has to wait until the next packet may be
   sent/received. */

/* register n bytes that have been sent and are subject to bw */
void bandwidth_register(bandwidth_t *bw, int n) {
  bw->bytes += n;
}

/* initialize bw */
void bandwidth_init(bandwidth_t *bw) {
  bw->bandwidth = 0;
}

/* return the time in microseconds that this connection has to sleep
   in order to satisfy its bandwidth constraint. curbw is the
   bandwidth currently requested by the user; note that this may be
   different from bw->bandwidth if the requested bandwidth changed, or
   the very first time this is called for a connection. tp is a
   pointer to a timeval structure that holds the current time. */

long bandwidth_waittime(bandwidth_t *bw, int curbw, struct timeval *tp) {
  long ms;
  
  if (curbw < 0)
    curbw = 0;

  /* First check if the requested bandwidth differs from the stored
     one. If yes, re-calculate state. */
  
  if (bw->bandwidth != curbw) {
    if (bw->bandwidth == 0) {
      bw->bandwidth = curbw;
      bw->time0 = tp->tv_sec;
      bw->bytes = tp->tv_usec / 1000 * curbw;
    } else {
      bw->bytes = (bw->bytes / bw->bandwidth) * curbw;
      bw->bandwidth = curbw;
    }
  }

  if (curbw==0)
    return 0;       /* no bandwidth limit requested */

  /* next, adjust the time0 parameter to tp->tv_sec, i.e., to the
     integer component of the current time. */
  if (tp->tv_sec != bw->time0) {
    bw->bytes -= curbw * 1000 * (tp->tv_sec - bw->time0);
    bw->time0 = tp->tv_sec;
  }

  /* calculate the number of microseconds this connection needs to
     sleep */
  ms = bw->bytes / curbw * 1000 - tp->tv_usec;

  /* if we are more than a second behind, forfeit anything beyond one
     second of credit. I.e., normally we get credit for being "too
     slow" (and can make up for it later), but at most one second's
     worth of such credit is allowed */
  
  if (ms < -1000000) {
    bw->bytes = (tp->tv_usec / 1000 - 1000) * curbw;
    ms = bw->bytes / curbw * 1000 - tp->tv_usec;
  }
  
  return ms;
}


/* sockfunc: this is the main event loop of the nap program. It is
 * here that we wait for network activity and then tend to the various
 * different connections (including user input). In addition, we call
 * tevent() (in timer.c) once a second or so, to take care of
 * scheduled events.
 *
 * This function is called precisely once, from nap.c:main(). Once
 * we leave here, nap shuts down and terminates.
 *
 * up is the input window (equal to the global variable winput), and
 * win is the output window of the terminal interface (equal to the
 * global variable wchan). Why not just refer to the global variables,
 * as do some of the procedures that are called from here?
 *
 * The connection handlers (cur->func) are always supposed to return 1
 * (the return value is not currently checked). If a connection
 * handler wants to shut down, it needs to call delsock().
 * 
 **/

void sockfunc(WINDOW *win, WINDOW *up)
{
  fd_set fs, fw; /* sets of file descriptors to watch, see "man select" */
  int r;
  sock_t *cur;
  struct timeval sec; /* note originally used <time.h> timespec here
			 instead of <select.h> timeval - this was a
			 type error */
  long timeout, msup, msdown;
  struct timeval tv;
  library_t *lib;

  bandwidth_init(&bwup);
  bandwidth_init(&bwdown);

  while (1)
  {
    dochecks();  /* notify terminal that it has been resized, if
		    necessary. */

    drw(up);      /* redraw input window to position cursor */

    /* if the --autorestart option is set, and there is no server, try to 
       reconnect to a server. */

    if (info.autorestart == 1) { 
      for (cur=socklist;cur!=NULL;cur=cur->next) {
	if (!strcasecmp(cur->socknm, "server"))
	  break;
      }
      if (cur==NULL) {
	sleep(1);  /* sleep to avoid excessive load in case of infinite loop */
	wp(win, "Connection to server lost, reconnecting...\n");
	drw(win);
	dreconnect(-1, "reconnect", NULL, 0, win);
      }
    }
    
    /* we return if this was requested by the /quit command or by
       /tquit in conjunction with qevent() */
    if (quit_now) {
      goto quit;
    }

    /* we reconnect if this was requested by a SIGUSR1 signal */
    if (reconnect) {
      reconnect = 0;
      dreconnect(-1, "reconnect", NULL, 0, win);
    }

    /* determine current time */
    gettimeofday(&tv, NULL);

    msup = bandwidth_waittime(&bwup, info.bandwidthup, &tv);
    msdown = bandwidth_waittime(&bwdown, info.bandwidthdown, &tv);

    timeout = 1000000;  /* let select() time out after at most this many
                           microsecs */

    /* let fs be the set of "read" file descriptors, and fw the list
       of "write" file descriptors, from the connection list */

    FD_ZERO(&fs);
    FD_ZERO(&fw);
    for (cur=socklist;cur!=NULL;cur=cur->next)
    {
      if (cur->bwlimit) {
	/* bandwidth limiting code */
	int ms;

	/* calculate the sleep time for this connection */
	if (cur->t == S_R) {
	  ms = bandwidth_waittime(&cur->bw, info.bandwidthdownconn, &tv);
	  if (msdown > ms) {
	    ms = msdown;
	  }
	} else {
	  ms = bandwidth_waittime(&cur->bw, info.bandwidthupconn, &tv);
	  if (msup > ms) {
	    ms = msup;
	  }
	}
	
	/* if ms<=0, activity is immediately allowed and we proceed to
	   watch this connection in the "select" call below. Else if
	   ms>0, activity is not allowed, but we make sure "select"
	   will time out after ms microseconds so that this connection
	   can be added at that time. */

	if (ms>0) {
	  if (timeout > ms) {
	    timeout = ms;
	  }
	  continue;
	}
      }

      if (cur->t == S_R)
        FD_SET(cur->fd, &fs);
      else if (cur->t == S_W)
        FD_SET(cur->fd, &fw);

      /* special case: the server is normally polled for reading, but
	 if we have stuff scheduled to send to it, we poll for writing
	 as well */

      if (strcmp(cur->socknm, "server")==0 && cur->shared_to_send) {
	FD_SET(cur->fd, &fw);
      }

    }
    
    /* prepare timeout, then wait for activity on connections.  On fs
       connections, wait for characters to be available for reading.
       On fw connections, wait for ability to write. */
    sec.tv_sec = timeout / 1000000;
    sec.tv_usec = timeout % 1000000;
    r = select(FD_SETSIZE, &fs, &fw, NULL, &sec);

    /* timeout guarantees calling the following at least once per second */
    tevent();   /* check for timer events */
    qevent();   /* check queues */

    /* if there is an error, print it, but then continue. However,
       EINTR means select was interrupted. This happens e.g. when the
       user resizes the window.  */
    if (r == -1) {
      if (errno != EINTR) {  
	wp(win, ""RED"* Error while watching connections: %s"WHITE"\n", \
	   strerror(errno));
	drw(win);
      }
      continue;
    }

    /* if there was no network activity, continue main loop */
    if (r == 0)
      continue;
    
    /* else do something for every connection that had activity. 
       Essentially, just call the handler (cur->func) for that connection. */
    for (cur=socklist;cur!=NULL;cur=cur->next)
    {
      if (cur->t == S_R && FD_ISSET(cur->fd, &fs))
      {
	FD_CLR(cur->fd, &fs);
        cur->func(win, cur);
	if (quit_now) {
	  goto quit;
	}
	cur=socklist;  /* need to start from beginning since sockets might
		      have been deleted in the meantime */
      }
      else if (cur->t == S_W && FD_ISSET(cur->fd, &fw))
      {
	FD_CLR(cur->fd, &fw);
        cur->func(win, cur);
	if (quit_now) {
	  goto quit;
	}
	cur=socklist;
      }
      else if (strcmp(cur->socknm, "server")==0 && cur->shared_to_send && FD_ISSET(cur->fd, &fw))
      {
	FD_CLR(cur->fd, &fw);
	/* send scheduled stuff to server */
	/* unlink first element */
	lib = cur->shared_to_send;
	cur->shared_to_send = lib->next;
	r = sendpack(cur->fd, NAP_SFILE, "\"%s\" %s %i %i %i %i", lib->lfn, lib->hash, lib->sz, lib->bitrate, lib->freq, lib->len);
	free(lib->lfn);
	free(lib->hash);
	free(lib);
	cur=socklist; /* not strictly necessary, but just in case... */
      }
    }
  }

  /* end of loop not reachable */

 quit:
  /* we get here if the user issues the /quit command. */
  while (socklist!=NULL)
    delsock(socklist->fd);
  return;
}
