//	******  PRELIMINARY - needs work  ******

#define DEBUG 1


//
//    This file is part of Dire Wolf, an amateur radio packet TNC.
//



#include "direwolf.h"

#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <assert.h>
#include <ctype.h>
#include <string.h>
#include <time.h>
#include <errno.h>
#include <getopt.h>

#include "ax25_pad.h"	
#include "textcolor.h"
#include "agwlib.h"			// Network TNC interface.


/*------------------------------------------------------------------
 *
 * Module:      appserver.c
 *
 * Purpose:   	Simple application server for connected mode AX.25.
 *
 *		This demonstrates how you can write a application that will wait for
 *		a connection from another station and respond to commands.
 *		It can be used as a starting point for developing your own applications.
 *		
 * Description:	This attaches to an instance of Dire Wolf via the AGW network interface.
 *		It processes commands from other radio stations and responds.
 *
 *---------------------------------------------------------------*/


static void usage()
{
	      text_color_set(DW_COLOR_ERROR);
	      dw_printf ("Usage: \n");
	      dw_printf (" \n");
	      dw_printf ("appserver  [ -h hostname ] [ -p port ] mycall  \n");
	      dw_printf (" \n");
	      dw_printf ("        -h    hostname for TNC.  Default is localhost. \n");
	      dw_printf (" \n");
 	      dw_printf ("        -p    tcp port for TNC.  Default is 8000. \n");
	      dw_printf (" \n");
	      dw_printf ("        mycall    is required because that is the callsign for  \n");
	      dw_printf ("                  which the TNC will accept connections. \n");
	      dw_printf (" \n");
	      exit (EXIT_FAILURE);
}





static char mycall[AX25_MAX_ADDR_LEN];	/* Callsign, with SSID, for the application. */
					/* Future?  Could have multiple applications, on the same */
					/* radio channel, each with its own SSID. */

static char tnc_hostname[80];		/* DNS host name or IPv4 address. */
					/* Some of the code is there for IPv6 but */
					/* needs more work. */
					/* Defaults to "localhost" if not specified. */

static char tnc_port[8];		/* a TCP port number.  Default 8000.  */



/*
 * Maintain information about connections from users which we will call "sessions."
 * It should be possible to have multiple users connected at the same time.
 *
 * This allows a "who" command to see who is currently connected and a place to keep
 * possible state information for each user.
 *
 * Each combination of channel & callsign is a separate session.
 * The same user (callsign), on a different channel, is a different session.
 */


struct session_s {

	char client_addr[AX25_MAX_ADDR_LEN];	// Callsign of other station.
						// Clear to mean this table entry is not in use.

	int channel;				// Radio channel.

	time_t login_time;			// Time when connection established.

// For the timing test.
// Send specified number of frames, optional length.
// When finished summarize with statistics.

	time_t tt_start_time;
	volatile int tt_count;			// Number to send.
	int tt_length;				// Bytes in info part.
	int tt_next;				// Next sequence to send.

	volatile int tx_queue_len;		// Number in transmit queue.  For flow control.
};

#define MAX_SESSIONS  12

static struct session_s session[MAX_SESSIONS];

static int find_session (int chan, char *addr, int create);
static void poll_timing_test (void);



/*------------------------------------------------------------------
 *
 * Name: 	main
 *
 * Purpose:   	Attach to Dire Wolf TNC, wait for requests from users.
 *
 * Usage:	Described above.
 *
 *---------------------------------------------------------------*/


int main (int argc, char *argv[])
{
	int c;
	char *p;

#if __WIN32__
	setvbuf(stdout, NULL, _IONBF, 0);
#else
 	setlinebuf (stdout);
#endif

	memset (session, 0, sizeof(session));

	strlcpy (tnc_hostname, "localhost", sizeof(tnc_hostname));
	strlcpy (tnc_port, "8000", sizeof(tnc_port));

/*
 * Extract command line args.   
 */

	while ((c = getopt (argc, argv, "h:p:")) != -1) {
	  switch (c) {

	    case 'h':
	      strlcpy (tnc_hostname, optarg, sizeof(tnc_hostname));
	      break;

	    case 'p':
	      strlcpy (tnc_port, optarg, sizeof(tnc_port));
	      break;

      	    default:
	      usage ();
	  }
 	}

	if (argv[optind] == NULL) {
	  usage ();
	}

	strlcpy (mycall, argv[optind], sizeof(mycall));

	// Force to upper case.
	for (p = mycall; *p != '\0'; p++) {
	  if (islower(*p)) {
	    *p = toupper(*p);
	  }
	}

/*
 * Establish a TCP socket to the network TNC.
 * It starts up a thread, which listens for messages from the TNC,
 * and calls the corresponding agw_cb_... callback functions.
 *
 * After attaching to the TNC, the specified init function is called.
 * We pass it to the library, rather than doing it here, so it can
 * repeated automatically if the TNC goes away and comes back again.
 * We need to reestablish what it knows about the application.
 */

	if (agwlib_init (tnc_hostname, tnc_port, agwlib_G_ask_port_information) != 0) {
	  text_color_set(DW_COLOR_ERROR);
	  dw_printf ("Could not attach to network TNC %s:%s.\n", tnc_hostname, tnc_port);
	  exit (EXIT_FAILURE);
	}


/*
 * Send command to ask what channels are available.
 * The response will be handled by agw_cb_G_port_information.
 */
// FIXME:  Need to do this again if we lose TNC and reattach to it.

	///   should happen automatically now.   agwlib_G_ask_port_information ();


	while (1) {
	  SLEEP_SEC(1);		// other places based on 1 second assumption.
	  poll_timing_test ();
	}


} /* end main */



static void poll_timing_test (void) 
{
	int s;
	for (s = 0; s < MAX_SESSIONS; s++) {

	  if (session[s].tt_count == 0) {
	     continue;	// nothing to do
	  }
	  else if (session[s].tt_next <= session[s].tt_count) {
	    int rem = session[s].tt_count - session[s].tt_next + 1;  // remaining to send.
	    agwlib_Y_outstanding_frames_for_station (session[s].channel, mycall, session[s].client_addr);
	    SLEEP_MS(10);
	    if (session[s].tx_queue_len > 128) continue;  // enough queued up for now.
	    if (rem > 64) rem = 64;	// add no more than 64 at a time.
	    int i;
	    for (i = 0; i < rem; i++) {
	      char c = 'a';
	      char stuff[AX25_MAX_INFO_LEN+2];
	      snprintf (stuff, sizeof(stuff), "%06d ", session[s].tt_next);
	      int k;
	      for (k = strlen(stuff); k < session[s].tt_length - 1; k++) {
	        stuff[k] = c;
		c++;
		if (c == 'z' + 1) c = 'A';
	        if (c == 'Z' + 1) c = '0';
	        if (c == '9' + 1) c = 'a';
	      }
	      stuff[k++] = '\r';
	      stuff[k++] = '\0';
	      agwlib_D_send_connected_data (session[s].channel, 0xF0, mycall, session[s].client_addr, strlen(stuff), stuff);
	      session[s].tt_next++;
	    }
	  }
	  else {
	    // All done queuing up the packets.
	    // Wait until they have all been sent and ack'ed by other end.

	    agwlib_Y_outstanding_frames_for_station (session[s].channel, mycall, session[s].client_addr);
	    SLEEP_MS(10);

	    if (session[s].tx_queue_len > 0) continue;  // not done yet.

	    int elapsed = time(NULL) - session[s].tt_start_time;
	    if (elapsed <= 0) elapsed = 1;	// avoid divide by 0

	    int byte_count = session[s].tt_count * session[s].tt_length;
	    char summary[100];
	    snprintf (summary, sizeof(summary), "%d bytes in %d seconds, %d bytes/sec, efficiency %d%% at 1200, %d%% at 9600.\r",
					byte_count, elapsed, byte_count/elapsed, 
					byte_count * 8 * 100 / elapsed / 1200,
					byte_count * 8 * 100 / elapsed / 9600);

	    agwlib_D_send_connected_data (session[s].channel, 0xF0, mycall, session[s].client_addr, strlen(summary), summary);
	    session[s].tt_count = 0;	// all done.
	  }
	}

}  // end poll_timing_test



/*-------------------------------------------------------------------
 *
 * Name:        agw_cb_C_connection_received
 *
 * Purpose:     Callback for the "connection received" command from the TNC.
 *		
 * Inputs:	chan		- Radio channel, first is 0.
 *
 *		call_from	- Address of other station.
 *
 *		call_to		- Callsign I responded to.  (could be an alias.)
 *
 *		data_len	- Length of data field.
 *
 *		data		- Should look something like this for incoming:
 *					*** CONNECTED to Station xxx\r	
 *
 * Description:	Add to the sessions table.
 *
 *--------------------------------------------------------------------*/

/*-------------------------------------------------------------------
 *
 * Name:        on_C_connection_received
 *
 * Purpose:     Callback for the "connection received" command from the TNC.
 *		
 * Inputs:	chan		- Radio channel, first is 0.
 *
 *		call_from	- Address of other station.
 *
 *		call_to		- My call.
 *				  In the case of an incoming connect request (i.e. to
 *				  a server) this is the callsign I responded to.
 *				  It is possible to define additional aliases and respond
 *				  to any one of them.  It would be possible to have a server
 *				  that responds to multiple names and behaves differently
 *				  depending on the name.
 *
 *		incoming	- true(1) if other station made connect request.
 *				  false(0) if I made request and other statio accepted.
 *
 *		data		- Should look something like this for incoming:
 *					*** CONNECTED to Station xxx\r
 *				  and ths for my request being accepted:
 *					*** CONNECTED With Station xxx\r
 *
 *		session_id	- Session id to be used in data transfer and
 *				  other control functions related to this connection.
 *				  Think of it like a file handle.  Once it is open
 *				  we usually don't care about the name anymore and
 *				  and just refer to the handle.  This is used to
 *				  keep track of multiple connections at the same
 *				  time.  e.g. a server could be handling multiple
 *				  clients at once on the same or different channels.
 *
 * Description:	Add to the table of clients.
 *
 *--------------------------------------------------------------------*/


// old void agw_cb_C_connection_received (int chan, char *call_from, char *call_to, int data_len, char *data)
void on_C_connection_received (int chan, char *call_from, char *call_to, int incoming, char *data)
{
	int s;
	char *p;
	char greeting[256];


	for (p = data; *p != '\0'; p++) {
	  if (! isprint(*p)) *p = '\0';		// Remove any \r character at end.
	}

	s = find_session (chan, call_from, 1);

	if (s >= 0) {

	  text_color_set(DW_COLOR_INFO);
 	  dw_printf ("Begin session %d: %s\n", s, data);

// Send greeting.

	  snprintf (greeting, sizeof(greeting), "Welcome!  Type ? for list of commands or HELP <command> for details.\r");
	  agwlib_D_send_connected_data (chan, 0xF0, mycall, call_from, strlen(greeting), greeting);
	}
	else {

	  text_color_set(DW_COLOR_INFO);
 	  dw_printf ("Too many users already: %s\n", data);

// Sorry, too many users already.

	  snprintf (greeting, sizeof(greeting), "Sorry, maximum number of users has been exceeded.  Try again later.\r");
	  agwlib_D_send_connected_data (chan, 0xF0, mycall, call_from, strlen(greeting), greeting);

	  // FIXME: Ideally we'd want to wait until nothing in the outgoing queue
	  // to that station so we know the rejection message was received.
	  SLEEP_SEC (10);
	  agwlib_d_disconnect (chan, mycall, call_from);
	}

} /* end agw_cb_C_connection_received */



/*-------------------------------------------------------------------
 *
 * Name:        agw_cb_d_disconnected
 *
 * Purpose:     Process the "disconnected" command from the TNC.
 *		
 * Inputs:	chan		- Radio channel.
 *
 *		call_from	- Address of other station.
 *
 *		call_to		- Callsign I responded to.  (could be aliases.)
 *
 *		data_len	- Length of data field.
 *
 *		data		- Should look something like one of these:
 *					*** DISCONNECTED RETRYOUT With xxx\r
 *					*** DISCONNECTED From Station xxx\r
 *
 * Description:	Remove from the sessions table.
 *
 *--------------------------------------------------------------------*/



void agw_cb_d_disconnected (int chan, char *call_from, char *call_to, int data_len, char *data)
{
	int s;
	char *p;

	s = find_session (chan, call_from, 0);

	for (p = data; *p != '\0'; p++) {
	  if (! isprint(*p)) *p = '\0';		// Remove any \r character at end.
	}

	text_color_set(DW_COLOR_INFO);
 	dw_printf ("End session %d: %s\n", s, data);

// Remove from session table.

	if (s >= 0) {
	  memset (&(session[s]), 0, sizeof(struct session_s));
	}

} /* end agw_cb_d_disconnected */



/*-------------------------------------------------------------------
 *
 * Name:        agw_cb_D_connected_data
 *
 * Purpose:     Process "connected ax.25 data" from the TNC.
 *		
 * Inputs:	chan		- Radio channel.
 *
 *		addr		- Address of other station.
 *
 *		msg		- What the user sent us.  Probably a command.
 *
 * Global In:	tnc_sock	- Socket for TNC.
 *
 * Description:	Remove from the session table.
 *
 *--------------------------------------------------------------------*/


void agw_cb_D_connected_data (int chan, char *call_from, char *call_to, int data_len, char *data)
{
	int s;
	char *p;
	char logit[AX25_MAX_INFO_LEN+100];
	char *pcmd;
	char *save;

	s = find_session (chan, call_from, 0);

	for (p = data; *p != '\0'; p++) {
	  if (! isprint(*p)) *p = '\0';		// Remove any \r character at end.
	}

	// TODO: Should timestamp to all output.

	snprintf (logit, sizeof(logit), "%d,%d,%s: %s\n", s, chan, call_from, data); 
	text_color_set(DW_COLOR_INFO);
 	dw_printf ("%s", logit);

	if (s < 0) {
	  // Uh oh. Data from some station when not connected.
	  text_color_set(DW_COLOR_ERROR);
 	  dw_printf ("Internal error.  Incoming data, no corresponding session.\n");
	  return;
	}

// Process the command from user.

	pcmd = strtok_r (data, " ", &save);
	if (pcmd == NULL || strlen(pcmd) == 0) {

	  char greeting[80];
	  strlcpy (greeting, "Type ? for list of commands or HELP <command> for details.\r", sizeof(greeting));
	  agwlib_D_send_connected_data (chan, 0xF0, mycall, call_from, strlen(greeting), greeting);
	  return;
	}

	if (strcasecmp(pcmd, "who") == 0) {

// who - list people currently logged in.

	  int n;
	  char greeting[80];

	  snprintf (greeting, sizeof(greeting), "Session Channel User   Since\r");
	  agwlib_D_send_connected_data (chan, 0xF0, mycall, call_from, strlen(greeting), greeting);

	  for (n = 0; n < MAX_SESSIONS; n++) {
	    if (session[n].client_addr[0]) {
	      snprintf (greeting, sizeof(greeting), "  %2d       %d    %-9s [time later]\r", 
			n, session[n].channel, session[n].client_addr);
	      agwlib_D_send_connected_data (chan, 0xF0, mycall, call_from, strlen(greeting), greeting);
	    }
	  }
	}
	else if (strcasecmp(pcmd, "test") == 0) {

// test - timing test
// Send specified number of frames with optional length.

	  char *pcount = strtok_r (NULL, " ", &save);
	  char *plength = strtok_r (NULL, " ", &save);

	  session[s].tt_start_time = time(NULL);
	  session[s].tt_next = 1;
	  session[s].tt_length = 256;
	  session[s].tt_count = 1;

	  if (plength != NULL) {
	    session[s].tt_length = atoi(plength);
	    if (session[s].tt_length < 16) session[s].tt_length = 16;
	    if (session[s].tt_length > AX25_MAX_INFO_LEN) session[s].tt_length = AX25_MAX_INFO_LEN;
	  }
	  if (pcount != NULL) {
	    session[s].tt_count = atoi(pcount);
	  }
	    
	  // The background polling will take it from here. 
	}
	else if (strcasecmp(pcmd, "bye") == 0) {

// bye - disconnect.

	  char greeting[80];
	  strlcpy (greeting, "Thank you folks for kindly droppin' in.  Y'all come on back now, ya hear?\r", sizeof(greeting));
	  agwlib_D_send_connected_data (chan, 0xF0, mycall, call_from, strlen(greeting), greeting);
	  // Ideally we'd want to wait until nothing in the outgoing queue
	  // to that station so we know the message was received.
	  SLEEP_SEC (10);
	  agwlib_d_disconnect (chan, mycall, call_from);
	}
	else if (strcasecmp(pcmd, "help") == 0 || strcasecmp(pcmd, "?") == 0) {

// help.

	  char greeting[80];
	  strlcpy (greeting, "Help not yet available.\r", sizeof(greeting));
	  agwlib_D_send_connected_data (chan, 0xF0, mycall, call_from, strlen(greeting), greeting);
	}
	else {

// command not recognized.

	  char greeting[80];
	  strlcpy (greeting, "Invalid command. Type ? for list of commands or HELP <command> for details.\r", sizeof(greeting));
	  agwlib_D_send_connected_data (chan, 0xF0, mycall, call_from, strlen(greeting), greeting);
	}

} /* end agw_cb_D_connected_data */




/*-------------------------------------------------------------------
 *
 * Name:        agw_cb_G_port_information
 *
 * Purpose:     Process the port information "radio channels available" response from the TNC.
 *		
 *
 * Inputs:	num_chan_avail		- Number of radio channels available.
 *
 *		chan_descriptions	- Array of string pointers to form "Port99 description".
 *					  Port1 is channel 0.
 *
 *--------------------------------------------------------------------*/

void agw_cb_G_port_information (int num_chan_avail, char *chan_descriptions[])
{
	char *p;
	int n;

	text_color_set(DW_COLOR_INFO);
 	dw_printf("TNC has %d radio channel%s available:\n", num_chan_avail, (num_chan_avail != 1) ? "s" : "");
	
	for (n = 0; n < num_chan_avail; n++) {

	  p = chan_descriptions[n];

	    // Expecting something like this:  "Port1 first soundcard mono"

	    if (strncasecmp(p, "Port", 4) == 0 && isdigit(p[4])) {
	    
	      int chan = atoi(p+4) - 1;	// "Port1" is our channel 0.
	      if (chan >= 0 && chan < MAX_CHANS) {

	        char *desc = p + 4;
	        while (*desc != '\0' && (*desc == ' ' || isdigit(*desc))) {
	          desc++;
	        }

	        text_color_set(DW_COLOR_INFO);
 	        dw_printf("  Channel %d: %s\n", chan, desc);

		// Later? Use 'g' to get speed and maybe other properties?
	        // Though I'm not sure why we would care here.

/*
 * Send command to register my callsign for incoming connect requests.
 */

	        agwlib_X_register_callsign (chan, mycall);

	      }
	      else {
	        text_color_set(DW_COLOR_ERROR);
 	        dw_printf("Radio channel number is out of bounds: %s\n", p);
	      }
	    }
	    else {
	      text_color_set(DW_COLOR_ERROR);
 	      dw_printf("Radio channel description not in expected format: %s\n", p);
	    }
	}

} /* end agw_cb_G_port_information */


/*-------------------------------------------------------------------
 *
 * Name:        agw_cb_Y_outstanding_frames_for_station
 *
 * Purpose:     Process the "disconnected" command from the TNC.
 *		
 * Inputs:	chan		- Radio channel.
 *
 *		call_from	- Should be my call.
 *
 *		call_to		- Callsign of other station.
 *
 *		frame_count
 *
 * Description:	Remove from the sessions table.
 *
 *--------------------------------------------------------------------*/



void agw_cb_Y_outstanding_frames_for_station (int chan, char *call_from, char *call_to, int frame_count)
{
	int s;

	s = find_session (chan, call_to, 0);

	text_color_set(DW_COLOR_DEBUG);		// FIXME temporary
 	dw_printf ("debug ----------------------> session %d, callback Y outstanding frame_count %d\n", s, frame_count);

// Update the transmit queue length

	if (s >= 0) {
	  session[s].tx_queue_len  = frame_count;
	}
	else {
	  text_color_set(DW_COLOR_ERROR);
 	  dw_printf ("Oops!  Did not expect to be here.\n");
	}

} /* end agw_cb_Y_outstanding_frames_for_station */



/*-------------------------------------------------------------------
 *
 * Name:        find_session
 *
 * Purpose:     Given a channel number and address (callsign), find existing
 *		table entry or create a new one.
 *
 * Inputs:	chan	- Radio channel number.
 *
 *		addr	- Address of station contacting us.
 *
 *		create	- If true, try create a new entry if not already there.
 *
 * Returns:	"session id" which is an index into "session" array or -1 for failure.
 *
 *--------------------------------------------------------------------*/

static int find_session (int chan, char *addr, int create)
{
 	int i;
	int s = -1;

// Is it there already?


//#if DEBUG
//
//	text_color_set(DW_COLOR_DEBUG);
// 	dw_printf("find_session (%d, %s, %d)\n", chan, addr, create);
//#endif

	for (i = 0; i < MAX_SESSIONS; i++) {
	  if (session[i].channel == chan && strcmp(session[i].client_addr, addr) == 0) {
	    s = i;
	    break;
	  }
	}

	if (s >= 0) return (s);

	if (! create) return (-1);

// No, and there is a request to add a new entry.
// See if we have any available space.

	s = -1;
	for (i = 0; i < MAX_SESSIONS; i++) {
	  if (strlen(session[i].client_addr) == 0) {
	    s = i;
	    break;
	  }
	}

	if (s < 0) return (-1);

	strlcpy (session[s].client_addr, addr, sizeof(session[s].client_addr));
	session[s].channel = chan;
	session[s].login_time = time(NULL);

	return (s);

} /* end find_session */


/* end appserver.c */
