/* streamfuncs.c: -*- C -*-  Functions for stream manuipulation. */

/* Author: Brian J. Fox (bfox@ai.mit.edu) Tue Sep 26 21:55:45 1995.

   This file is part of <Meta-HTML>(tm), a system for the rapid deployment
   of Internet and Intranet applications via the use of the Meta-HTML
   language.

   Copyright (c) 1995, 1996, Brian J. Fox (bfox@ai.mit.edu).
   Copyright (c) 1996, Universal Access Inc. (http://www.ua.com).

   Meta-HTML is free software; you can redistribute it and/or modify
   it under the terms of the UAI Free Software License as published
   by Universal Access Inc.; either version 1, 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
   UAI Free Software License for more details.

   You should have received a copy of the UAI Free Software License
   along with this program; if you have not, you may obtain one by
   writing to:

   Universal Access Inc.
   129 El Paseo Court
   Santa Barbara, CA
   93101  */

#include "language.h"

static void pf_with_open_stream (PFunArgs);
static void pf_stream_put (PFunArgs);
static void pf_stream_put_contents (PFunArgs);
static void pf_stream_get (PFunArgs);
static void pf_stream_get_contents (PFunArgs);
static void pf_stream_readable (PFunArgs);
static void pf_stream_writable (PFunArgs);
static void pf_stream_shutdown (PFunArgs);

PFunDesc streamfunc_table[] = {
  { "WITH-OPEN-STREAM",		1, 0, pf_with_open_stream },
  { "STREAM-PUT",		0, 0, pf_stream_put },
  { "STREAM-GET",		0, 0, pf_stream_get },
  { "STREAM-PUT-CONTENTS",	0, 0, pf_stream_put_contents },
  { "STREAM-GET-CONTENTS",	0, 0, pf_stream_get_contents },
  { "STREAM-READABLE",		0, 0, pf_stream_readable },
  { "STREAM-WRITABLE",		0, 0, pf_stream_writable },
  { "STREAM-SHUTDOWN",		0, 0, pf_stream_shutdown },
  { (char *)NULL,		0, 0, (PFunHandler *)NULL }
};

typedef enum { stream_FILE, stream_TCP, stream_CLOSED } StreamType;

#define stream_MUST_SEEK 0x01
#define stream_NOTIMEOUT 0x02
#define op_NONE  0
#define op_READ  1
#define op_WRITE 2

typedef struct {
  char *name;		/* The name used to open this stream. */
  StreamType type;	/* The type of stream: stream_FILE, stream_TCP, etc. */
  int fd;		/* The file descriptor that describes this stream. */
  int mode;		/* Access mode. */
  int flags;		/* Special flags indicate changes in behaviour. */
  int last_op;		/* The last operation done on this stream. */
} Stream;

typedef void mhtml_sigfun (int);
#undef SIG_DFL
#define SIG_DFL (mhtml_sigfun *)NULL

static int stream_environment_level = 0;
static Stream **open_streams = (Stream **)NULL;
int open_streams_slots = 0;
int open_streams_index = 0;

extern int tcp_to_host (char *host, char *service);
extern jmp_buf page_jmp_buffer;

PACKAGE_INITIALIZER (initialize_stream_functions)
void
initialize_stream_functions (Package *package)
{
  register int i;
  Symbol *sym;

  for (i = 0; streamfunc_table[i].tag != (char *)NULL; i++)
    {
      sym = symbol_intern_in_package (package, streamfunc_table[i].tag);
      sym->type = symtype_FUNCTION;
      sym->values = (char **)(&streamfunc_table[i]);
    }
}

/* Install this stream, and return the index which is used to identify it. */
static int
add_open_stream (char *name, StreamType type, int fd, int mode, int flags)
{
  Stream *stream = (Stream *)xmalloc (sizeof (Stream));
  int result = open_streams_index;

  stream->name = strdup (name);
  stream->type = type;
  stream->fd = fd;
  stream->mode = mode;
  stream->flags = flags;
  stream->last_op = op_NONE;

  if (open_streams_index + 2 > open_streams_slots)
    open_streams = (Stream **)xrealloc
      (open_streams, ((open_streams_slots += 5) * sizeof (Stream *)));

  open_streams[open_streams_index++] = stream;
  open_streams[open_streams_index] = (Stream *)NULL;

  return (result);
}

/* De-install this stream. */
static void
discard_stream (int stream_index)
{
  /* This really can only be called as the result of ending the
     function with-open-stream.  That means that stream_index should
     point to the stream on the TOS. */
  if (open_streams_index)
    {
      Stream *stream = open_streams[--open_streams_index];

      open_streams[open_streams_index] = (Stream *)NULL;
      if (stream->name) free (stream->name);

      if ((stream->fd != fileno (stdin)) &&
	  (stream->fd != fileno (stdout)) &&
	  (stream->fd != fileno (stderr)))
	close (stream->fd);

      free (stream);
    }
}

static int
stream_mode (char *modename)
{
  int mode = O_RDONLY;

  if (modename != (char *)NULL)
    {
      if (strcasecmp (modename, "write") == 0)
	mode = O_WRONLY;
      else if (strcasecmp (modename, "read-write") == 0)
	mode = O_RDWR;
      else if (strcasecmp (modename, "append") == 0)
	mode = O_APPEND | O_WRONLY | O_CREAT;
      else if (strcasecmp (modename, "write-create") == 0)
	mode = O_TRUNC | O_RDWR | O_CREAT;
    }

  return (mode);
}

static int
stream_type (char *typename)
{
  StreamType type = stream_FILE;

  if ((typename != (char *)NULL) && (strcasecmp (typename, "tcp") == 0))
    type = stream_TCP;

  return (type);
}

/* <with-open-stream variable filename [type=[file | tcp]
	   mode=[writer/reader/write-create]>
   [code using the open stream]
   </with-open-stream>
   Opens the file specified by FILENAME, and stores a referent to it
   in VARIABLE.  The file is opened in the mode specified by MODE.  If
   MODE is not specified, the file is opened read-only.  If the
   operation fails, the value of VARIABLE is the empty string.
   TYPE can be specified as "TCP", in which case, a TCP connection is
   opened.  In that case, FILENAME looks like HOST:PORT. */
int mhtml_stdin_fileno = 0;
int mhtml_stdout_fileno = 1;
int mhtml_stderr_fileno = 2;

static void
pf_with_open_stream (PFunArgs)
{
  char *varname = mhtml_evaluate_string (get_positional_arg (vars, 0));
  char *filename = mhtml_evaluate_string (get_positional_arg (vars, 1));
  int jump_again = 0;

  jump_again = 0;

  if ((!empty_string_p (varname)) && (!empty_string_p (filename)))
    {
      char *modename = mhtml_evaluate_string (get_value (vars, "MODE"));
      char *typename = mhtml_evaluate_string (get_value (vars, "TYPE"));
      char *notimeout = mhtml_evaluate_string (get_value (vars, "NOTIMEOUT"));
      int mode = stream_mode (modename);
      int type = stream_type (typename);
      int flags = notimeout ? stream_NOTIMEOUT : 0;
      int stream_fd = -1;

      if (modename) free (modename);
      if (typename) free (typename);
      if (notimeout) free (notimeout);

      switch (type)
	{
	case stream_FILE:
	  if (strcasecmp (filename, "*standard-input*") == 0)
	    stream_fd = mhtml_stdin_fileno;
	  else if (strcasecmp (filename, "*standard-output*") == 0)
	    stream_fd = mhtml_stdout_fileno;
	  else if (strcasecmp (filename, "*standard-error*") == 0)
	    stream_fd = mhtml_stderr_fileno;
	  else
	    stream_fd = os_open (filename, mode, 0666);
	  break;

	case stream_TCP:
	  {
	    char *host_part = strdup (filename);
	    char *port_part = strchr (host_part, ':');

	    if (port_part)
	      *port_part++ = '\0';
	    else
	      port_part = "80"; /* Default the port for HTTP connections. */

	    stream_fd = tcp_to_host (host_part, port_part);
	    free (host_part);
	    flags |= stream_MUST_SEEK;
	  }
	  break;
	}

      if (stream_fd > -1)
	{
	  char stream_value[40];
	  PAGE *body_code = page_copy_page (body);
	  int page_value;

	  page_value =
	    add_open_stream (filename, type, stream_fd, mode, flags);

	  sprintf (stream_value, "%d", page_value);
	  pagefunc_set_variable (varname, stream_value);

	  {
	    PageEnv *environment = pagefunc_save_environment ();

	    stream_environment_level++;
	    if ((jump_again = setjmp (page_jmp_buffer)) == 0)
	      page_process_page_internal (body_code);

	    stream_environment_level--;

	    pagefunc_restore_environment (environment);
	  }

	  if (body_code != (PAGE *)NULL)
	    {
	      if (!jump_again && body_code->buffer)
		{
		  bprintf_insert (page, start, "%s", body_code->buffer);
		  *newstart = start + (body_code->bindex);
		}

	      page_free_page (body_code);
	    }

	  discard_stream (page_value);
	}
      else
	{
	  char *temp = mhtml_funargs (vars);

	  page_syserr ("<with-open-stream%s%s>: %s",
		       temp ? " " : "", temp ? temp : "",
		       (char *)strerror (errno));
	  if (temp) free (temp);
	}
    }

  if (filename) free (filename);
  if (varname) free (varname);
  if (jump_again) longjmp (page_jmp_buffer, 1);
}

static Stream *
get_stream_reference (Package *vars)
{
  Stream *stream = (Stream *)NULL;

  if (stream_environment_level != 0)
    {
      char *stream_ref = mhtml_evaluate_string (get_positional_arg (vars, 0));

      if (stream_ref)
	{
	  register int i;
	  char *rep = pagefunc_get_variable (stream_ref);

	  free (stream_ref);

	  /* The entire reference must be digits. */
	  if (!empty_string_p (rep))
	    {
	      for (i = 0; rep[i] != '\0'; i++);

	      if (i == strlen (rep))
		{
		  int stream_index = atoi (rep);

		  if ((stream_index < open_streams_index) &&
		      (stream_index > -1))
		    stream = open_streams[stream_index];
		}
	    }
	}
    }

  return (stream);
}

/* Gets called if/when we run out of time waiting for a known number
   of bytes that are available. */
static int operation_timed_out_p = 0;
static jmp_buf operation_jmp_buf;

static void
operation_timed_out (int sig)
{
  alarm (0);
  operation_timed_out_p = 1;
  longjmp (operation_jmp_buf, 1);
}

/* A 28.8 modem can deliver a maximum of 3.6k per second without
   taking compression into consideration.  A 14.4 can only deliver
   1.8k per second.  So, we will give the network 2 seconds to
   deliver each 1k of data, and then cushion that with a 10 second
   grace period.  Gee, isn't the Internet Fantastic? */
static void
set_io_timer (int length)
{
  char *minimum_data_transfer_rate = 
    pagefunc_get_variable ("mhtml::minimum-data-transfer-rate");
  int timeout, divisor = 0;

  if (minimum_data_transfer_rate != (char *)NULL)
    divisor = atoi (minimum_data_transfer_rate);

  if (divisor < 1)
    divisor = 512;

  timeout = length / 512;
  timeout += 10;

  operation_timed_out_p = 0;
  signal (SIGALRM, operation_timed_out);
  alarm (timeout);
}

static void
stream_write (Stream *stream, int length, char *bytes)
{
  char *write_from_loc = bytes;
  int bytes_written;

  if ((stream->flags & stream_MUST_SEEK) && (stream->last_op != op_WRITE))
    lseek (stream->fd, 0l, 1);

  set_io_timer (length);

  if (setjmp (operation_jmp_buf))
    {
      length = 0;
      stream->type = stream_CLOSED;
    }

  while (length != 0)
    {
      bytes_written = write (stream->fd, write_from_loc, length);

      if ((bytes_written == -1) || (operation_timed_out_p == 1))
	{
	  stream->type = stream_CLOSED;
	  return;
	}

      length -= bytes_written;
      write_from_loc += bytes_written;
    }

  alarm (0);
  signal (SIGALRM, SIG_DFL);
}

static int stream_last_read_object_size = 0;

static char *
stream_read (Stream *stream, char char_to_stop_at)
{
  char *buffer = (char *)NULL;
  int buffer_index = 0;
  int buffer_size = 0;
  int done = 0;
  char minbuf[512];
  int mindex = 0;
  int read_result = 0;

  if ((stream->flags & stream_MUST_SEEK) && (stream->last_op != op_READ))
    lseek (stream->fd, 0l, 1);

  if (stream->flags & stream_NOTIMEOUT)
    set_io_timer (999999);
  else
    set_io_timer (80);

  if (setjmp (operation_jmp_buf))
    {
      done = 1;
      stream->type = stream_CLOSED;
    }

  while (!done)
    {
      if (char_to_stop_at != '\0')
	{
	  if (mindex > 511)
	    {
	      if (buffer_index + mindex + 1 >= buffer_size)
		buffer = (char *)xrealloc (buffer, (buffer_size += mindex +2));

	      memmove (buffer + buffer_index, minbuf, mindex);
	      buffer_index += mindex;
	      buffer[buffer_index] = '\0';
	      mindex = 0;
	    }

	  read_result = read (stream->fd, minbuf + mindex, 1);
	}
      else
	{
	  /* Read as much as we can, as fast as we can.
	     For the case of file streams, this means get the length
	     of the file, and try to read that much. */
	  if (stream->type == stream_FILE)
	    {
	      struct stat finfo;

	      if (fstat (stream->fd, &finfo) == -1)
		read_result = -1;
	      else
		{
		  buffer = (char *)
		    xrealloc (buffer, (buffer_size = 1 + (int)finfo.st_size));
		  buffer[0] = '0';
		  read_result = read (stream->fd, buffer, buffer_size);
		  done = 1;

		  if (read_result > -1)
		    {
		      buffer_index = read_result;
		      buffer[buffer_index] = '\0';
		    }
		}
	    }
	  else if (stream->type == stream_TCP)
	    {
	      /* For TCP streams, we try hard to read in 10k chunks. */
	      int bits_size = 20 * 1024;

	      while (!done)
		{
		  if ((buffer_index + bits_size) >= buffer_size)
		    {
		      buffer = (char *)
			xrealloc (buffer, (buffer_size += bits_size));
		    }

		  read_result =
		    read (stream->fd, buffer + buffer_index, bits_size);

		  if (read_result > 0)
		    {
		      buffer_index += read_result;
		    }
		  else
		    done = 1;
		}
	    }
	}

      if ((read_result < 1) || (operation_timed_out_p == 1))
	{
	  done = 1;
	  stream->type = stream_CLOSED;
	}
      else
	{
	  if ((char_to_stop_at != 0) && (minbuf[mindex] == char_to_stop_at))
	    done = 1;
	  mindex++;
	}
    }

  alarm (0);
  signal (SIGALRM, SIG_DFL);

  if ((char_to_stop_at != '\0') && (mindex != 0))
    {
      if (buffer_index + mindex + 1 >= buffer_size)
	buffer = (char *)xrealloc (buffer, (buffer_index + mindex + 20));

      memmove (buffer + buffer_index, minbuf, mindex);
      buffer_index += mindex;
      buffer[buffer_index] = '\0';
    }

  stream_last_read_object_size = buffer_index;

  return (buffer);
}

static char *
stream_read_chunk (Stream *stream, int length)
{
  char *buffer = (char *)NULL;
  int buffer_index = 0;
  int bytes_left = length;
  int done = 0;

  if ((stream->flags & stream_MUST_SEEK) && (stream->last_op != op_READ))
    lseek (stream->fd, 0l, 1);

  buffer = (char *)xmalloc (length + 1);

  if (stream->flags & stream_NOTIMEOUT)
    set_io_timer (999999);
  else
    set_io_timer (length);

  if (setjmp (operation_jmp_buf))
    {
      done = 1;
      stream->type = stream_CLOSED;
    }

  while (!done)
    {
      int bytes_read = read (stream->fd, buffer + buffer_index, bytes_left);

      if (bytes_read > 0)
	{
	  buffer_index += bytes_read;
	  bytes_left -= bytes_left;
	}
      else
	{
	  done = 1;
	  stream->type = stream_CLOSED;
	}

      if (bytes_left == 0)
	done = 1;
    }

  alarm (0);
  signal (SIGALRM, SIG_DFL);
  stream_last_read_object_size = buffer_index;

  return (buffer);
}

static void
pf_stream_put (PFunArgs)
{
  Stream *stream = get_stream_reference (vars);

  if (stream && stream->type != stream_CLOSED)
    {
      char *put_text = mhtml_evaluate_string (get_positional_arg (vars, 1));

      if (put_text)
	{
	  stream_write (stream, strlen (put_text), put_text);
	  free (put_text);
	}
      stream->last_op = op_WRITE;
    }
}

static void
pf_stream_put_contents (PFunArgs)
{
  Stream *stream = get_stream_reference (vars);

  if (stream && stream->type != stream_CLOSED)
    {
      char *name = mhtml_evaluate_string (get_positional_arg (vars, 1));
      Symbol *sym = (name ? symbol_lookup (name) : (Symbol *)NULL);

      if (sym != (Symbol *)NULL)
	{
	  char *data = (char *)NULL;
	  int length = 0;

	  switch (sym->type)
	    {
	    case symtype_STRING:
	      if (sym->values_index)
		{
		  data = sym->values[0];
		  length = data ? (1 + strlen (data)) : 0;
		}
	      break;

	    case symtype_FUNCTION:
	      break;

	    case symtype_BINARY:
	      data = ((Datablock *)sym->values)->data;
	      length = ((Datablock *)sym->values)->length;
	      break;
	    }

	  if (length != 0)
	    {
	      stream->last_op = op_WRITE;
	      stream_write (stream, length, data);
	    }
	}

      if (name != (char *)NULL)
	free (name);
    }
}

static void
pf_stream_get (PFunArgs)
{
  Stream *stream = get_stream_reference (vars);
  char *stop_at = mhtml_evaluate_string (get_value (vars, "stop-at"));
  char *result = (char *)NULL;

  if (stream)
    {
      if (stream->type != stream_CLOSED)
	{
	  result = stream_read (stream, stop_at ? *stop_at : '\n');
	  stream->last_op = op_READ;
	}
    }

  if (result)
    {
      bprintf_insert (page, start, "%s", result);
      *newstart += strlen (result);
      free (result);
    }

  if (stop_at != (char *)NULL) free (stop_at);
}

static void
pf_stream_get_contents (PFunArgs)
{
  Stream *stream = get_stream_reference (vars);
  char *stop_at = mhtml_evaluate_string (get_value (vars, "stop-at"));
  char *chunk_size_arg = mhtml_evaluate_string (get_value (vars, "chunk-size"));
  char *name = mhtml_evaluate_string (get_positional_arg (vars, 1));
  char *result = (char *)NULL;
  Symbol *sym;

  /* We are replacing the value that this symbol already had, no
     matter what.  Get rid of the existing symbol. */
  if (name != (char *)NULL)
    {
      sym = symbol_remove (name);
      symbol_free (sym);
    }

  /* Now get the data from the stream. */
  if (stream)
    {
      if (stream->type != stream_CLOSED)
	{
	  if (empty_string_p (chunk_size_arg))
	    result = stream_read (stream, stop_at ? *stop_at : '\0');
	  else
	    {
	      int chunk_size = atoi (chunk_size_arg);

	      result = stream_read_chunk (stream, chunk_size);
	    }

	  stream->last_op = op_READ;
	}
    }

  /* If there was any data, create a new binary symbol to contain it. */
  if (result)
    {
      if (name != (char *)NULL)
	{
	  Datablock *block;

	  sym = symbol_intern (name);
	  block = datablock_create (result, stream_last_read_object_size);
	  sym->type = symtype_BINARY;
	  sym->values = (char **)block;
	}
      free (result);
    }

  if (name) free (name);
  if (chunk_size_arg) free (chunk_size_arg);
  if (stop_at) free (stop_at);
}

static void
pf_stream_readable (PFunArgs)
{
  Stream *stream = get_stream_reference (vars);
  char *delay = get_value (vars, "delay");
  int delay_in_seconds = delay ? atoi (delay) : 0;
  char *result = (char *)NULL;

  if ((stream != (Stream *)NULL) &&
      (stream->type != stream_CLOSED) &&
      ((stream->type == stream_TCP) ||
       (stream->mode == O_RDONLY) ||
       (stream->mode & O_RDWR)))
    {
      result = "true";

      /* The result may in fact be true.  On systems which support
	 select (), check explicity that the fd is readable. */
#if defined (FD_SET)
      {
	fd_set read_fds;
	struct timeval poll;
	int ready;

	FD_ZERO (&read_fds);
	FD_SET (stream->fd, &read_fds);
	poll.tv_sec = delay_in_seconds;
	poll.tv_usec = 0;
	ready = select (stream->fd + 1, &read_fds, (fd_set *)0, (fd_set *)0,
			&poll);

	if (ready < 1)
	  result = (char *)NULL;
      }
#endif /* FD_SET */
    }
  if (result)
    {
      bprintf_insert (page, start, "%s", result);
      *newstart += strlen (result);
    }
}

static void
pf_stream_writable (PFunArgs)
{
  Stream *stream = get_stream_reference (vars);
  char *delay = get_value (vars, "delay");
  int delay_in_seconds = delay ? atoi (delay) : 0;
  char *result = (char *)NULL;

  if ((stream != (Stream *)NULL) &&
      (stream->type != stream_CLOSED) &&
      ((stream->type == stream_TCP) ||
       (stream->mode & (O_APPEND | O_RDWR | O_WRONLY))))
    {
      result = "true";

      /* The result may in fact be true.  On systems which support
	 select (), check explicity that the fd is writeable. */
#if defined (FD_SET)
      {
	fd_set write_fds;
	struct timeval poll;
	int ready;

	FD_ZERO (&write_fds);
	FD_SET (stream->fd, &write_fds);
	poll.tv_sec = delay_in_seconds;
	poll.tv_usec = 0;
	ready = select (stream->fd + 1, (fd_set *)0, &write_fds, (fd_set *)0,
			&poll);

	if (ready < 1)
	  result = (char *)NULL;
      }
#endif /* FD_SET */
    }

  if (result)
    {
      bprintf_insert (page, start, "%s", result);
      *newstart += strlen (result);
    }
}

static void
pf_stream_shutdown (PFunArgs)
{
  Stream *stream = get_stream_reference (vars);

  if ((stream != (Stream *)NULL) && (stream->type != stream_CLOSED))
    shutdown (stream->fd, 2);
}
