/* This is -*- C -*- */
/* $Id: guppi-stream.c,v 1.17 2001/09/08 05:50:00 trow Exp $ */

/*
 * guppi-stream.c
 *
 * Copyright (C) 1999 EMC Capital Management, Inc.
 *
 * Developed by Jon Trowbridge <trow@gnu.org> and
 * Havoc Pennington <hp@pobox.com>.
 *
 * 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 <config.h>
#include "guppi-stream.h"

#include <sys/stat.h>
#include <string.h>

#include <gtk/gtksignal.h>
#include <guppi-convenient.h>


static GtkObjectClass *parent_class = NULL;

enum {
  PRELOAD,
  FULLY_PRELOADED,
  CHANGED_CODES,
  LAST_SIGNAL
};
static guint str_signals[LAST_SIGNAL] = { 0 };

static void
guppi_stream_finalize (GtkObject *obj)
{
  GuppiStream *gs = GUPPI_STREAM (obj);
  gint i;

  guppi_finalized (obj);

  if (gs->in) {
    guppi_file_close (gs->in);
    guppi_unref0 (gs->in);
  }

  if (gs->buffer) {
    for (i = 0; i <= gs->buffer_top; ++i) {
      guppi_free (gs->buffer[i]);
      guppi_free (gs->marked_buffer[i]);
      guppi_free (gs->sani_buffer[i]);
    }
    guppi_free (gs->buffer);
    guppi_free (gs->marked_buffer);
    guppi_free (gs->sani_buffer);
  }

  guppi_free (gs->current_line);
  guppi_free (gs->current_marked_line);
  guppi_free (gs->current_sani_line);

  guppi_free (gs->eol_comment);
  guppi_free (gs->ml_start_comment);
  guppi_free (gs->ml_end_comment);

  if (parent_class->finalize)
    parent_class->finalize (obj);
}

static void
guppi_stream_class_init (GuppiStreamClass *klass)
{
  GtkObjectClass *object_class = (GtkObjectClass *) klass;

  parent_class = gtk_type_class (GTK_TYPE_OBJECT);

  str_signals[PRELOAD] =
    gtk_signal_new ("preload",
		    GTK_RUN_FIRST,
		    object_class->type,
		    GTK_SIGNAL_OFFSET (GuppiStreamClass, preload),
		    gtk_marshal_NONE__NONE, GTK_TYPE_NONE, 0);

  str_signals[FULLY_PRELOADED] =
    gtk_signal_new ("fully_preloaded",
		    GTK_RUN_FIRST,
		    object_class->type,
		    GTK_SIGNAL_OFFSET (GuppiStreamClass, fully_preloaded),
		    gtk_marshal_NONE__NONE, GTK_TYPE_NONE, 0);

  str_signals[CHANGED_CODES] =
    gtk_signal_new ("changed_codes",
		    GTK_RUN_FIRST,
		    object_class->type,
		    GTK_SIGNAL_OFFSET (GuppiStreamClass, changed_codes),
		    gtk_marshal_NONE__NONE, GTK_TYPE_NONE, 0);

  gtk_object_class_add_signals (object_class, str_signals, LAST_SIGNAL);

  klass->preload = NULL;
  klass->fully_preloaded = NULL;
  klass->changed_codes = NULL;

  object_class->finalize = guppi_stream_finalize;
}

static void
guppi_stream_init (GuppiStream *gs)
{
  gs->in = NULL;

  gs->bad = FALSE;
  gs->buffering = TRUE;
  gs->eof = FALSE;


  gs->current_line_no = -1;
  gs->current_line = NULL;
  gs->current_marked_line = NULL;
  gs->current_sani_line = NULL;

  gs->last_visited_line_no = -1;

  gs->buffer_count = 0;
  gs->buffer_top = 0;
  gs->buffer = NULL;
  gs->marked_buffer = NULL;
  gs->sani_buffer = NULL;

  gs->total_size = -1;
  gs->buffered_size = 0;

  gs->in_ml_comment = FALSE;

  gs->eol_comment = guppi_strdup ("//");
  gs->ml_start_comment = guppi_strdup ("/*");
  gs->ml_end_comment = guppi_strdup ("*/");
  gs->start_quote = '"';
  gs->end_quote = '"';
  gs->escape = '~';
}

GtkType
guppi_stream_get_type (void)
{
  static GtkType guppi_stream_type = 0;
  if (!guppi_stream_type) {
    static const GtkTypeInfo guppi_stream_info = {
      "GuppiStream",
      sizeof (GuppiStream),
      sizeof (GuppiStreamClass),
      (GtkClassInitFunc) guppi_stream_class_init,
      (GtkObjectInitFunc) guppi_stream_init,
      NULL, NULL, (GtkClassInitFunc) NULL
    };
    guppi_stream_type = gtk_type_unique (GTK_TYPE_OBJECT, &guppi_stream_info);
  }
  return guppi_stream_type;
}

GuppiStream *
guppi_stream_new (GuppiFile *f)
{
  GuppiStream *gs;
  struct stat stat;

  g_return_val_if_fail (f != NULL, NULL);

  gs = GUPPI_STREAM (guppi_type_new (guppi_stream_get_type ()));

  gs->in = f;
  guppi_ref (gs->in);

  if (guppi_file_stat (gs->in, &stat) == 0) {
    gs->total_size = stat.st_size;
  }

  return gs;
}

GuppiStream *
guppi_stream_open_file (const gchar *filename)
{
  GuppiFile *in;
  GuppiStream *gs;

  g_return_val_if_fail (filename != NULL, NULL);

  in = guppi_file_open (filename);
  if (in == NULL)
    return NULL;

  gs = guppi_stream_new (in);
  guppi_unref (in);

  return gs;
}

/**********************************************************************/

static gboolean
guppi_stream_mark_line (GuppiStream *gs,
			const gchar *line, gchar *buffer, gint buffer_len)
{
  gboolean injected_escape, in_comment, in_quote, punch_in;
  gint p = 0, i, j;
  gint eol_comment_len;
  gint ml_start_comment_len;
  gint ml_end_comment_len;

  g_return_val_if_fail (gs != NULL, FALSE);
  g_return_val_if_fail (line != NULL, FALSE);
  g_return_val_if_fail (buffer_len > 0, FALSE);

  injected_escape = FALSE;
  eol_comment_len = gs->eol_comment ? strlen (gs->eol_comment) : 0;
  ml_start_comment_len =
    gs->ml_start_comment ? strlen (gs->ml_start_comment) : 0;
  ml_end_comment_len = gs->ml_end_comment ? strlen (gs->ml_end_comment) : 0;

  if (gs->in_ml_comment) {
    buffer[0] = gs->escape;
    buffer[1] = 'C';
    p += 2;
    injected_escape = TRUE;
  }
  in_comment = gs->in_ml_comment;
  in_quote = FALSE;

  i = 0;
  while (line[i]) {

    punch_in = TRUE;

    if (line[i] == gs->escape) {

      buffer[p] = gs->escape;
      ++p;
      injected_escape = TRUE;

    } else if (in_quote) {

      in_quote = line[i] != gs->end_quote;

    } else if (line[i] == gs->start_quote) {

      in_quote = TRUE;

    } else if (!in_quote &&
	       !in_comment &&
	       eol_comment_len &&
	       !strncmp (gs->eol_comment, line + i, eol_comment_len)) {

      /* We've hit an end-of-line-comment, so adjust accordingly */
      buffer[p] = gs->escape;
      buffer[p + 1] = 'L';
      p += 2;
      while (line[i])
	buffer[p++] = line[i++];
      injected_escape = TRUE;
      punch_in = FALSE;

    } else if (!in_quote &&
	       !in_comment &&
	       ml_start_comment_len &&
	       !strncmp (gs->ml_start_comment, line + i,
			 ml_start_comment_len)) {

      buffer[p] = gs->escape;
      buffer[p + 1] = 'C';
      p += 2;
      in_comment = TRUE;
      injected_escape = TRUE;

    } else if (in_comment &&
	       !in_quote &&
	       ml_end_comment_len &&
	       !strncmp (gs->ml_end_comment, line + i, ml_end_comment_len)) {

      for (j = 0; j < ml_end_comment_len; ++j)
	buffer[p++] = line[i++];

      buffer[p] = gs->escape;
      buffer[p + 1] = 'c';
      p += 2;
      in_comment = FALSE;
      injected_escape = TRUE;
      punch_in = FALSE;
    }

    if (punch_in) {
      buffer[p] = line[i];
      ++p;
      ++i;
    }
  }

  gs->in_ml_comment = in_comment;

  buffer[p] = '\0';

  /* Return whether or not we did anything */
  return injected_escape;
}

static gboolean
guppi_stream_sanitize_line (GuppiStream *gs,
			    const gchar *line, gchar *sani, gint sani_len)
{
  gint i = 0, j = 0;
  gboolean seen_escape;
  gboolean pending = FALSE, in_comment = FALSE, done = FALSE;

  g_return_val_if_fail (gs != NULL, FALSE);
  g_return_val_if_fail (line != NULL, FALSE);
  g_return_val_if_fail (sani != NULL, FALSE);
  g_return_val_if_fail (sani_len > 0, FALSE);

  seen_escape = FALSE;
  i = 0;
  while (line[i] && !seen_escape) {
    if (line[i] == gs->escape)
      seen_escape = TRUE;
    ++i;
  }

  if (!seen_escape)
    return FALSE;

  i = 0;
  while (line[i] && !done) {

    if (pending) {

      if (line[i] == gs->escape) {
	if (!in_comment)
	  sani[j++] = gs->escape;
      } else if (line[i] == 'C')
	in_comment = TRUE;
      else if (line[i] == 'c')
	in_comment = FALSE;
      else if (line[i] == 'L')
	done = TRUE;
      else
	g_assert_not_reached ();

      pending = FALSE;

    } else {

      if (line[i] == gs->escape)
	pending = TRUE;
      else if (!in_comment)
	sani[j++] = line[i];

    }

    ++i;

  }
  sani[j] = '\0';

  return TRUE;
}

/**********************************************************************/

static void
guppi_stream_changed (GuppiStream *gs)
{
  const gint buflen = 1024;
  gchar mbuf[1024];
  gchar sbuf[1024];
  gboolean did_mark, did_sani;
  gint i;

  g_return_if_fail (gs != NULL);
  g_assert (gs->buffering);

  /* Reset state */
  gs->in_ml_comment = FALSE;

  /* Regenerate buffered marked/sani lines and reset state */
  for (i = 0; i <= gs->buffer_top; ++i) {
    guppi_free (gs->marked_buffer[i]);
    guppi_free (gs->sani_buffer[i]);

    did_mark = guppi_stream_mark_line (gs, gs->buffer[i], mbuf, buflen);
    did_sani = did_mark
      && guppi_stream_sanitize_line (gs, mbuf, sbuf, buflen);

    gs->marked_buffer[i] = did_mark ? guppi_strdup (mbuf) : NULL;
    gs->sani_buffer[i] = did_sani ? guppi_strdup (sbuf) : NULL;
  }


  gtk_signal_emit (GTK_OBJECT (gs), str_signals[CHANGED_CODES]);
}

gboolean
guppi_stream_sequential_mode (GuppiStream *gs)
{
  g_return_val_if_fail (gs != NULL, FALSE);

  return !gs->buffering;
}

void
guppi_stream_set_sequential_mode (GuppiStream *gs)
{
  g_return_if_fail (gs != NULL);
  g_return_if_fail (gs->buffering);

  gs->buffering = FALSE;
}

const gchar *
guppi_stream_eol_comment (const GuppiStream *gs)
{
  g_return_val_if_fail (gs != NULL, NULL);
  return gs->eol_comment;
}

void
guppi_stream_set_eol_comment (GuppiStream *gs, const gchar *x)
{
  g_return_if_fail (gs != NULL);

  if (x == NULL && gs->eol_comment == NULL)
    return;

  if ((x == NULL && gs->eol_comment != NULL) ||
      (x != NULL && gs->eol_comment == NULL) ||
      (strcmp (x, gs->eol_comment) != 0)) {
    guppi_free (gs->eol_comment);
    gs->eol_comment = guppi_strdup (x);
    guppi_stream_changed (gs);
  }
}

const gchar *
guppi_stream_ml_comment_start (const GuppiStream *gs)
{
  g_return_val_if_fail (gs != NULL, NULL);

  return gs->ml_start_comment;
}

const gchar *
guppi_stream_ml_comment_end (const GuppiStream *gs)
{
  g_return_val_if_fail (gs != NULL, NULL);
  return gs->ml_end_comment;
}

void
guppi_stream_set_ml_comment_start (GuppiStream *gs, const gchar *x)
{
  g_return_if_fail (gs != NULL);

  if (x == NULL && gs->ml_start_comment == NULL)
    return;

  if ((x == NULL && gs->ml_start_comment != NULL) ||
      (x != NULL && gs->ml_start_comment == NULL) ||
      (strcmp (x, gs->ml_start_comment) != 0)) {
    guppi_free (gs->ml_start_comment);
    gs->ml_start_comment = guppi_strdup (x);
    guppi_stream_changed (gs);
  }
}

void
guppi_stream_set_ml_comment_end (GuppiStream *gs, const gchar *x)
{
  g_return_if_fail (gs != NULL);

  if (x == NULL && gs->ml_end_comment == NULL)
    return;

  if ((x == NULL && gs->ml_end_comment != NULL) ||
      (x != NULL && gs->ml_end_comment == NULL) ||
      (strcmp (x, gs->ml_end_comment) != 0)) {
    guppi_free (gs->ml_end_comment);
    gs->ml_end_comment = guppi_strdup (x);
    guppi_stream_changed (gs);
  }
}

gchar
guppi_stream_quote_start (const GuppiStream *gs)
{
  g_return_val_if_fail (gs != NULL, '\0');
  return gs->start_quote;
}

gchar
guppi_stream_quote_end (const GuppiStream *gs)
{
  g_return_val_if_fail (gs != NULL, '\0');
  return gs->end_quote;
}

void
guppi_stream_set_quote_start (GuppiStream *gs, gchar x)
{
  g_return_if_fail (gs != NULL);

  gs->start_quote = x;
  guppi_stream_changed (gs);
}

void
guppi_stream_set_quote_end (GuppiStream *gs, gchar x)
{
  g_return_if_fail (gs != NULL);

  gs->end_quote = x;
  guppi_stream_changed (gs);
}

gchar
guppi_stream_escape (const GuppiStream *gs)
{
  g_return_val_if_fail (gs != NULL, '\0');

  return gs->escape;
}

const gchar *
guppi_stream_source (GuppiStream *gs)
{
  g_return_val_if_fail (gs != NULL, NULL);
  return guppi_file_filename (gs->in);
}

gint
guppi_stream_number_of_lines (GuppiStream *gs)
{
  g_return_val_if_fail (gs != NULL, -1);

  return gs->buffering && gs->eof ? 1 + gs->current_line_no : -1;
}

gint
guppi_stream_estimated_number_of_lines (GuppiStream *gs)
{
  g_return_val_if_fail (gs != NULL, -1);

  if (gs->buffering && gs->eof)
    return 1 + gs->current_line_no;

  if (gs->total_size > 0) {
    if (gs->current_line > 0 && gs->buffered_size > 0)
      return (gint) (gs->total_size /
		     (gs->buffered_size / (1.0 + gs->current_line_no)));
    else
      return gs->total_size / 60;	/* totally arbitrary */
  }

  return -1;
}

int
guppi_stream_number_of_preloaded_lines (GuppiStream *gs)
{
  g_return_val_if_fail (gs != NULL, -1);

  return gs->buffering ? gs->buffer_top : -1;
}

/* Ensure that the buffer is big enough to accomodate at least minimum_count
   lines. */
static void
guppi_stream_grow_buffers (GuppiStream *gs, gint minimum_count)
{
  gint new_count;
  gchar **new_buffer;

  g_return_if_fail (gs != NULL);
  g_return_if_fail (minimum_count > 0);

  if (gs->buffer_count >= minimum_count)
    return;

  /* Find the new size for our buffers */
  new_count = gs->buffer_count ? gs->buffer_count : 500;
  while (new_count < minimum_count)
    new_count *= 2;

  /* Grow our three buffers (standard, marked, sanitized) */
  new_buffer = guppi_new0 (gchar *, new_count);
  memcpy (new_buffer, gs->buffer, gs->buffer_count * sizeof (gchar *));
  guppi_free (gs->buffer);
  gs->buffer = new_buffer;

  new_buffer = guppi_new0 (gchar *, new_count);
  memcpy (new_buffer, gs->marked_buffer, gs->buffer_count * sizeof (gchar *));
  guppi_free (gs->marked_buffer);
  gs->marked_buffer = new_buffer;

  new_buffer = guppi_new0 (gchar *, new_count);
  memcpy (new_buffer, gs->sani_buffer, gs->buffer_count * sizeof (gchar *));
  guppi_free (gs->sani_buffer);
  gs->sani_buffer = new_buffer;

  gs->buffer_count = new_count;
}

static void
guppi_stream_add_to_buffer (GuppiStream *gs,
			    const gchar *line,
			    const gchar *marked_line,
			    const gchar *sani_line)
{
  gint i;

  g_return_if_fail (gs != NULL);
  g_return_if_fail (gs->buffering);

  i = gs->current_line_no;
  guppi_stream_grow_buffers (gs, 1 + i);

  gs->buffer[i] = guppi_strdup (line);
  gs->marked_buffer[i] = guppi_strdup (marked_line);
  gs->sani_buffer[i] = guppi_strdup (sani_line);

  gs->buffered_size += strlen (line) + 1;
  gs->buffer_top = gs->current_line_no;
}


/**********************************************************************/


static void
guppi_stream_get_lines_through (GuppiStream *gs, gint line_no)
{
  const gint buflen = 1024;
  gchar buf[1024];
  gchar mbuf[1024];
  gchar sbuf[1024];
  gboolean did_mark, did_sani;
  gboolean did_something = FALSE;

  g_return_if_fail (gs != NULL);
  g_return_if_fail (line_no >= 0);
  g_return_if_fail (!gs->bad);

  if (gs->eof)
    return;

  while (!gs->eof && gs->current_line_no < line_no) {

    if (guppi_file_gets (gs->in, buf, buflen)) {

      g_strchomp (buf);
      did_something = TRUE;
      ++gs->current_line_no;
      did_mark = guppi_stream_mark_line (gs, buf, mbuf, buflen);
      did_sani = did_mark &&
	guppi_stream_sanitize_line (gs, mbuf, sbuf, buflen);

      if (gs->buffering)
	guppi_stream_add_to_buffer (gs,
				    buf,
				    did_mark ? mbuf : NULL,
				    did_sani ? sbuf : NULL);

      if (gs->current_line_no == line_no) {

	guppi_free (gs->current_line);
	gs->current_line = guppi_strdup (buf);

	guppi_free (gs->current_marked_line);
	gs->current_marked_line = did_mark ? guppi_strdup (mbuf) : NULL;

	guppi_free (gs->current_sani_line);
	gs->current_sani_line = did_sani ? guppi_strdup (sbuf) : NULL;
      }
    } else {
      gs->eof = TRUE;
      /*
         guppi_file_close(gs->in);
         gtk_object_unref(GTK_OBJECT(gs->in));
         gs->in = NULL;
       */
      if (gs->buffering) {
	gtk_signal_emit (GTK_OBJECT (gs), str_signals[FULLY_PRELOADED]);
	if (gs->total_size < 0)
	  gs->total_size = gs->buffered_size;
      }
    }
  }

  if (did_something)
    gtk_signal_emit (GTK_OBJECT (gs), str_signals[PRELOAD]);
}

static void
guppi_stream_line_get_triple (GuppiStream *gs, gint line_no,
			      const gchar ** line,
			      const gchar ** marked, const gchar ** sani)
{
  g_return_if_fail (gs != NULL);
  g_return_if_fail (line_no >= 0);
  g_return_if_fail (!gs->bad);

  if (!gs->buffering) {
    if (line_no < gs->last_visited_line_no) {
      g_warning ("buffering violation: %d/%d", line_no, gs->current_line_no);
      return;
    }
    gs->last_visited_line_no = line_no;
  }

  if (line_no <= gs->buffer_top) {
    if (line)
      *line = gs->buffer[line_no];
    if (marked) {
      *marked = gs->marked_buffer[line_no];
      if (*marked == NULL)
	*marked = gs->buffer[line_no];
    }
    if (sani) {
      *sani = gs->sani_buffer[line_no];
      if (*sani == NULL)
	*sani = gs->buffer[line_no];
    }
    return;
  }

  if (gs->current_line_no < line_no)
    guppi_stream_get_lines_through (gs, line_no);


  if (gs->current_line_no == line_no) {
    if (line)
      *line = gs->current_line;
    if (marked) {
      *marked = gs->current_marked_line;
      if (*marked == NULL)
	*marked = gs->current_line;
    }
    if (sani) {
      *sani = gs->current_sani_line;
      if (*sani == NULL)
	*sani = gs->current_line;
    }

    return;
  }

  if (line)
    *line = NULL;
  if (marked)
    *marked = NULL;
  if (sani)
    *sani = NULL;
}

const gchar *
guppi_stream_get_line (GuppiStream *gs, gint line_no)
{
  const gchar *x;

  guppi_stream_line_get_triple (gs, line_no, &x, NULL, NULL);

  return x;
}

const gchar *
guppi_stream_get_marked_line (GuppiStream *gs, gint line_no)
{
  const gchar *x;

  guppi_stream_line_get_triple (gs, line_no, NULL, &x, NULL);

  return x;
}

const gchar *
guppi_stream_get_sanitized_line (GuppiStream *gs, gint line_no)
{
  const gchar *x;

  guppi_stream_line_get_triple (gs, line_no, NULL, NULL, &x);

  return x;
}

/**********************************************************************/

void
guppi_stream_load_some_lines (GuppiStream *gs)
{
  g_return_if_fail (gs != NULL);
  if (gs->current_line_no < 0)
    guppi_stream_get_lines_through (gs, 250);
}

void
guppi_stream_load_more_lines (GuppiStream *gs)
{
  g_return_if_fail (gs != NULL);
  guppi_stream_get_lines_through (gs, gs->current_line_no + 250);
}




/* $Id: guppi-stream.c,v 1.17 2001/09/08 05:50:00 trow Exp $ */


syntax highlighted by Code2HTML, v. 0.9.1