/* The GIMP -- an image manipulation program
 * Copyright (C) 1995 Spencer Kimball and Peter Mattis
 *
 * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

#include <stdarg.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <unistd.h>
#include <time.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>

#include "config.h"

#ifdef HAVE_SYS_SELECT_H
#include <sys/select.h>
#endif /* HAVE_SYS_SELECT_H */

#include "libgimp/gimp.h"
#include "gtk/gtk.h"
#include "siod.h"
#include "script-fu-server.h"

#define COMMAND_HEADER  3
#define RESPONSE_HEADER 4
#define MAGIC   'G'

#ifdef NO_DIFFTIME
#define difftime(a,b) (((double)(a)) - ((double)(b)))
#endif

#ifndef NO_FD_SET
#  define SELECT_MASK fd_set
#else
#  ifndef _AIX
     typedef long fd_mask;
#  endif
#  if defined(_IBMR2)
#    define SELECT_MASK void
#  else
#    define SELECT_MASK int
#  endif
#endif


/*  image information  */

/*  Header format for incoming commands...
 *    bytes: 1          2          3
 *           MAGIC      CMD_LEN_H  CMD_LEN_L
 */

/*  Header format for outgoing responses...
 *    bytes: 1          2          3          4
 *           MAGIC      ERROR?     RSP_LEN_H  RSP_LEN_L
 */

#define MAGIC_BYTE      0

#define CMD_LEN_H_BYTE  1
#define CMD_LEN_L_BYTE  2

#define ERROR           1
#define RSP_LEN_H_BYTE  2
#define RSP_LEN_L_BYTE  3

/*
 *  Local Structures
 */

typedef struct
{
  gchar *command;
  gint   filedes;
  gint   request_no;
} SFCommand;

typedef struct
{
  GtkWidget *port_entry;
  GtkWidget *log_entry;

  gint       port;
  gchar     *logfile;

  gint       run;
} ServerInterface;

/*
 *  Local Functions
 */

static void   server_start       (gint       port,
				  gchar     *logfile);
static gint   execute_command    (SFCommand *cmd);
static gint   read_from_client   (gint       filedes);
static gint   make_socket        (guint      port);
static void   server_log         (gchar     *format,
				     ...);
static void   server_quit        (void);

static gint   server_interface   (void);
static void   ok_callback        (GtkWidget *widget,
				  gpointer   data);
static void   cancel_callback    (GtkWidget *widget,
				  gpointer   data);

/*
 *  Global variables
 */
gint server_mode = FALSE;

/*
 *  Local variables
 */
static gint   server_sock;
static GList *command_queue = NULL;
static gint   queue_length = 0;
static gint   request_no = 0;
static FILE  *server_log_file = NULL;
static GHashTable *clientname_ht = NULL;
static SELECT_MASK server_active, server_read;

static ServerInterface sint =
{
  NULL,  /*  port entry widget  */
  NULL,  /*  log entry widget  */

  10008, /*  default port number  */
  NULL,  /*  use stdout  */

  FALSE  /*  run  */
};

extern gint   script_fu_done;
extern char   siod_err_msg[];
extern LISP   repl_return_val;

/*
 *  Server interface functions
 */

void
script_fu_server_run (char     *name,
		      int       nparams,
		      GParam   *params,
		      int      *nreturn_vals,
		      GParam  **return_vals)
{
  static GParam values[1];
  GStatusType status = STATUS_SUCCESS;
  GRunModeType run_mode;

  run_mode = params[0].data.d_int32;

  switch (run_mode)
    {
    case RUN_INTERACTIVE:
      if (server_interface ())
	{
	  server_mode = TRUE;

	  /*  Start the server  */
	  server_start (sint.port, sint.logfile);
	}
      break;

    case RUN_NONINTERACTIVE:
      /*  Set server_mode to TRUE  */
      server_mode = TRUE;

      /*  Start the server  */
      server_start (params[1].data.d_int32, params[2].data.d_string);
      break;

    case RUN_WITH_LAST_VALS:
      status = STATUS_CALLING_ERROR;
      g_warning ("Script-Fu server does handle \"RUN_WITH_LAST_VALS\"");

    default:
      break;
    }

  *nreturn_vals = 1;
  *return_vals = values;

  values[0].type = PARAM_STATUS;
  values[0].data.d_status = status;
}

void
script_fu_server_listen (gint timeout)
{
  struct sockaddr_in clientname;
  struct timeval tv;
  struct timeval *tvp;
  gint i;
  size_t size;

  /*  Set time struct  */
  if (timeout)
    {
      tv.tv_sec = timeout / 1000;
      tv.tv_usec = timeout % 1000;
      tvp = &tv;
    }
  else
    tvp = NULL;

  /* Block until input arrives on one or more active sockets or timeout occurs. */
  server_read = server_active;
  if (select (FD_SETSIZE, &server_read, NULL, NULL, tvp) < 0)
    {
      perror ("select");
      return;
    }

  /* Service all the sockets with input pending. */
  for (i = 0; i < FD_SETSIZE; ++i)
    if (FD_ISSET (i, &server_read))
      {
	if (i == server_sock)
	  {
	    /* Connection request on original socket. */
	    gint new;

	    size = sizeof (clientname);
	    new = accept (server_sock,
			  (struct sockaddr *) &clientname,
			  &size);
	    if (new < 0)
	      {
		perror ("accept");
		return;
	      }

	    /*  Associate the client address with the socket  */
	    g_hash_table_insert (clientname_ht,
				 (gpointer) new,
				 g_strdup (inet_ntoa (clientname.sin_addr)));
	    /*
	    server_log ("Server: connect from host %s, port %d.\n",
			inet_ntoa (clientname.sin_addr),
			(unsigned int) ntohs (clientname.sin_port));
			*/

	    FD_SET (new, &server_active);
	  }
	else
	  {
	    if (read_from_client (i) < 0)
	      {
		/*  Disassociate the client address with the socket  */
		g_hash_table_remove (clientname_ht, (gpointer) i);

		/*
		server_log ("Server: disconnect from host %s, port %d.\n",
			    inet_ntoa (clientname.sin_addr),
			    (unsigned int) ntohs (clientname.sin_port));
			    */

		close (i);
		FD_CLR (i, &server_active);
	      }
	  }
      }
}

static void
server_start (gint   port,
	      gchar *logfile)
{
  SFCommand *cmd;

  /*  Set up the clientname hash table  */
  clientname_ht = g_hash_table_new (g_direct_hash, NULL);

  /*  Setup up the server log file  */
  if (logfile)
    server_log_file = fopen (logfile, "a");
  else
    server_log_file = NULL;
  if (server_log_file == NULL)
    server_log_file = stdout;

  /* Create the socket and set it up to accept connections. */
  server_sock = make_socket (port);
  if (listen (server_sock, 5) < 0)
    {
      perror ("listen");
      return;
    }

  server_log ("Script-fu initialized and listening...\n");

  /* Initialize the set of active sockets. */
  FD_ZERO (&server_active);
  FD_SET (server_sock, &server_active);

  /*  Loop until the server is finished  */
  while (! script_fu_done)
    {
      script_fu_server_listen (0);

      while (command_queue)
	{
	  /*  Get the current command  */
	  cmd = (SFCommand *) command_queue->data;

	  /*  Process the command  */
	  execute_command (cmd);

	  /*  Remove the command from the list  */
	  command_queue = g_list_remove (command_queue, cmd);
	  queue_length--;

	  /*  Free the request  */
	  g_free (cmd->command);
	  g_free (cmd);
	}
    }

  server_quit ();

  /*  Close the server log file  */
  if (server_log_file != stdout)
    fclose (server_log_file);
}

static gint
execute_command (SFCommand *cmd)
{
  guchar buffer[RESPONSE_HEADER];
  gchar *response;
  time_t clock1, clock2;
  gint response_len;
  gint error;
  gint i;

  /*  Get the client address from the address/socket table  */
  server_log ("Processing request #%d\n", cmd->request_no);
  time (&clock1);

  /*  run the command  */
  if (repl_c_string (cmd->command, 0, 0, 1) != 0)
    {
      error = TRUE;
      response_len = strlen (siod_err_msg);
      response = siod_err_msg;

      server_log ("%s\n", siod_err_msg);
    }
  else
    {
      error = FALSE;

      if (TYPEP (repl_return_val, tc_string))
	response = get_c_string (repl_return_val);
      else
	response = "Success";

      response_len = strlen (response);

      time (&clock2);
      server_log ("Request #%d processed in %f seconds, finishing on %s",
		  cmd->request_no, difftime (clock2, clock1), ctime (&clock2));
    }

  buffer[MAGIC_BYTE] = MAGIC;
  buffer[ERROR] = (error) ? 1 : 0;
  buffer[RSP_LEN_H_BYTE] = (guchar) (response_len >> 8);
  buffer[RSP_LEN_L_BYTE] = (guchar) (response_len & 0xFF);

  /*  Write the response to the client  */
  for (i = 0; i < RESPONSE_HEADER; i++)
    if (write (cmd->filedes, buffer + i, 1) < 0)
      {
	/*  Write error  */
	perror ("write");
	return 0;
      }

  for (i = 0; i < response_len; i++)
    if (write (cmd->filedes, response + i, 1) < 0)
      {
	/*  Write error  */
	perror ("write");
	return 0;
      }

  return 0;
}

static gint
read_from_client (gint filedes)
{
  SFCommand *cmd;
  guchar buffer[COMMAND_HEADER];
  gchar *command;
  gchar *clientaddr;
  time_t clock;
  gint command_len;
  gint nbytes;
  gint i;

  for (i = 0; i < COMMAND_HEADER; i++)
    {
      if ((nbytes = read (filedes, buffer + i, 1)) < 0)
	{
	  /* Read error. */
	  perror ("read");
	  return 0;
	}
      else if (nbytes == 0)
	/* End-of-file. */
	return -1;
    }

  if (buffer[MAGIC_BYTE] != MAGIC)
    {
      server_log ("Error in script-fu command transmission.\n");
      return -1;
    }
  command_len = (buffer [CMD_LEN_H_BYTE] << 8) | buffer [CMD_LEN_L_BYTE];
  command = g_new (gchar, command_len + 1);

  for (i = 0; i < command_len; i++)
    if (read (filedes, command + i, 1) == 0)
      {
	server_log ("Error reading command.  Read %d out of %d bytes.\n", i, command_len);
	return -1;
      }

  command[command_len] = '\0';
  cmd = g_new (SFCommand, 1);
  cmd->filedes = filedes;
  cmd->command = command;
  cmd->request_no = request_no ++;

  /*  Add the command to the queue  */
  command_queue = g_list_append (command_queue, cmd);
  queue_length ++;

  /*  Get the client address from the address/socket table  */
  clientaddr = g_hash_table_lookup (clientname_ht, (gpointer) cmd->filedes);
  time (&clock);
  server_log ("Received request #%d from IP address %s: %s on %s, [Request queue length: %d]",
	      cmd->request_no, clientaddr, cmd->command, ctime (&clock), queue_length);

  return 0;
}

static gint
make_socket (guint port)
{
  gint sock;
  struct sockaddr_in name;
  gint v = 1;

  /* Create the socket. */
  sock = socket (PF_INET, SOCK_STREAM, 0);
  if (sock < 0)
    {
      perror ("socket");
      gimp_quit ();
    }
  setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &v, sizeof(v));

  /* Give the socket a name. */
  name.sin_family = AF_INET;
  name.sin_port = htons (port);
  name.sin_addr.s_addr = htonl (INADDR_ANY);
  if (bind (sock, (struct sockaddr *) &name, sizeof (name)) < 0)
    {
      perror ("bind");
      gimp_quit ();
    }

  return sock;
}

static void
server_log (gchar *format, ...)
{
  va_list args;
  char *buf;

  va_start (args, format);
  buf = g_strdup_vprintf (format, args);
  va_end (args);

  fputs (buf, server_log_file);
  if (server_log_file != stdout)
    fflush (server_log_file);
}

static void
server_quit (void)
{
  int i;

  for (i = 0; i < FD_SETSIZE; ++i)
    if (FD_ISSET (i, &server_active))
      shutdown (i, 2);
}

static gint
server_interface ()
{
  GtkWidget *dlg;
  GtkWidget *button;
  GtkWidget *label;
  GtkWidget *table;
  gchar **argv;
  gint argc;

  argc = 1;
  argv = g_new (gchar *, 1);
  argv[0] = g_strdup ("script-fu");

  gtk_init (&argc, &argv);
  gtk_rc_parse (gimp_gtkrc ());

  dlg = gtk_dialog_new ();
  gtk_window_set_title (GTK_WINDOW (dlg), "Script-Fu Server Options");
  gtk_window_position (GTK_WINDOW (dlg), GTK_WIN_POS_MOUSE);
  gtk_signal_connect (GTK_OBJECT (dlg), "destroy",
		      (GtkSignalFunc) cancel_callback,
		      NULL);
  gtk_container_border_width (GTK_CONTAINER (GTK_DIALOG (dlg)->action_area), 2);

  /*  Action area  */
  button = gtk_button_new_with_label ("OK");
  GTK_WIDGET_SET_FLAGS (button, GTK_CAN_DEFAULT);
  gtk_signal_connect (GTK_OBJECT (button), "clicked",
                      (GtkSignalFunc) ok_callback,
                      dlg);
  gtk_box_pack_start (GTK_BOX (GTK_DIALOG (dlg)->action_area), button, TRUE, TRUE, 0);
  gtk_widget_grab_default (button);
  gtk_widget_show (button);

  button = gtk_button_new_with_label ("Cancel");
  GTK_WIDGET_SET_FLAGS (button, GTK_CAN_DEFAULT);
  gtk_signal_connect_object (GTK_OBJECT (button), "clicked",
			     (GtkSignalFunc) gtk_widget_destroy,
			     GTK_OBJECT (dlg));
  gtk_box_pack_start (GTK_BOX (GTK_DIALOG (dlg)->action_area), button, TRUE, TRUE, 0);
  gtk_widget_show (button);

  /*  The table to hold port & logfile entries  */
  table = gtk_table_new (2, 2, FALSE);
  gtk_container_border_width (GTK_CONTAINER (table), 4);
  gtk_box_pack_start (GTK_BOX (GTK_DIALOG (dlg)->vbox), table, TRUE, TRUE, 0);

  /*  The server port  */
  label = gtk_label_new ("Server Port: ");
  gtk_misc_set_alignment (GTK_MISC (label), 0.0, 0.5);
  gtk_table_attach (GTK_TABLE (table), label, 0, 1, 0, 1,
		    GTK_SHRINK | GTK_FILL, GTK_SHRINK, 0, 1);
  gtk_widget_show (label);

  sint.port_entry = gtk_entry_new ();
  gtk_table_attach (GTK_TABLE (table), sint.port_entry, 1, 2, 0, 1,
		    GTK_EXPAND | GTK_SHRINK | GTK_FILL, GTK_SHRINK, 1, 1);
  gtk_entry_set_text (GTK_ENTRY (sint.port_entry), "10008");
  gtk_widget_show (sint.port_entry);

  /*  The server logfile  */
  label = gtk_label_new ("Server Logfile: ");
  gtk_misc_set_alignment (GTK_MISC (label), 0.0, 0.5);
  gtk_table_attach (GTK_TABLE (table), label, 0, 1, 1, 2,
		    GTK_SHRINK | GTK_FILL, GTK_SHRINK, 0, 1);
  gtk_widget_show (label);

  sint.log_entry = gtk_entry_new ();
  gtk_table_attach (GTK_TABLE (table), sint.log_entry, 1, 2, 1, 2,
		    GTK_EXPAND | GTK_SHRINK | GTK_FILL, GTK_SHRINK, 1, 1);
  gtk_widget_show (sint.log_entry);

  gtk_widget_show (table);
  gtk_widget_show (dlg);

  gtk_main ();

  gdk_flush ();

  return sint.run;
}

static void
ok_callback (GtkWidget *widget,
	     gpointer   data)
{
  sint.port = atoi (gtk_entry_get_text (GTK_ENTRY (sint.port_entry)));
  sint.logfile = g_strdup (gtk_entry_get_text (GTK_ENTRY (sint.log_entry)));
  sint.run = TRUE;

  gtk_widget_destroy (GTK_WIDGET (data));
}

static void
cancel_callback (GtkWidget *widget,
		 gpointer   data)
{
  gtk_main_quit ();
}
