static char rcsid[] = "$Id: bag.c,v 1.1 1992/09/05 01:13:32 mike Exp $";

/* $Log: bag.c,v $
 * Revision 1.1  1992/09/05  01:13:32  mike
 * Initial revision
 *
 */

/*
 * bag.c
 * Routines to handle bags of data - the basic things used to move chunks of
 *   data around.
 * What is a bag:
 *   - A bag holds text, as a character stream or in rectanglar form.
 *   - A rectangle is N rows of M columns, each row having M characters (no
 *     short lines).
 *   - Bags are used to move text to/from buffers, files, etc.  They are the
 *     text transport mechanism.
 * Notes:
 *   - Bags are never deleted.  This makes it easy to ensure bad ids are
 *     always unique.  Other reasons follow.
 *   - Want to be able to use pointers to bags (internally) for speed and
 *     ease of use.  This means no dTables because realloc() can invalidate
 *     pointers.
 *   - Since bags can be created/used/freed both by ME2 and Mutt pgms, I
 *     don't want pgms to be able to free a bag and cause ME2 to core dump
 *     because a bag pointer is invalid.
 *   - Because bags can be created by Mutt programs and Mutt programmers can
 *     be sloppy or the program terminate abnormally, I need to be able to
 *     garbage collect bags.  This is a limited GC because the only time I
 *     know when a bag is garbage is when the "root" pgm has terminated and
 *     the programmer has marked the bag as collectable.  Oh well.  Anyway,
 *     I use a mortal or immortal bit for this.  Mortal means collectable
 *     and immortal means somebody has to explicitly free the bag to remove
 *     it.  I mix this bit into the bag type to save some space in the bag
 *     structure.  See the spec for MMgc_external_objects() for more info.
 */

/* Copyright 1990, 1991, 1992 Craig Durland
 *   Distributed under the terms of the GNU General Public License.
 *   Distributed "as is", without warranties of any kind, but comments,
 *     suggestions and bug reports are welcome.
 */

#include "me2.h"

	/* bag types */
#define BAG_OF_TEXT		0
#define BAG_OF_RECTANGLE	1
	/* If the immortal bit is 0, bag can be recycled. */
#define IMMORTAL_BAG		0x80

#define BAG_TYPE(bag)		((bag)->type & 0x0F)

#define MAX_BAGS	4096
#define RBLOCK		1024	/* bag growth block size */


extern char *malloc();

static int insert_rect();

/* ******************************************************************** */
/* ********** Bag implementation details ****************************** */
/* ******************************************************************** */

static Bag *first_bag = NULL, *freed_bags = NULL;

	/* Given a bag id, return a pointer to the bag that has that id */
Bag *id_to_bag(bag_id) int bag_id;
{
  register Bag *bag;

  for (bag = first_bag; bag; bag = bag->next)
    if (bag->id == bag_id) return bag;
  return NULL;
}

bag_type(bag) Bag *bag; { return BAG_TYPE(bag); }

void set_bag_type(bag, type) Bag *bag;
{
  bag->type = (bag->type & IMMORTAL_BAG) | type;
}

/*
 * Delete all of the text saved in a bag.
 * To avoid thrashing, only free the bag if it is indeed big.  This will
 *   keep reusing the same buffer until it is necessary to allocate more mem
 *   to the hold the text.
 */
void clear_bag(bag) register Bag *bag;
{
  if (bag->text.max_items > RBLOCK)
	{ free_dTable(&bag->text); INIT_dTable(&bag->text); }
  else reset_dTable(&bag->text);

  set_bag_type(bag, BAG_OF_TEXT);
}

   /* Allocate a bag and return a pointer to it.
    * If you need the bag id, its in the bag.
    * Look for a free bag before expanding the table.
    */
Bag *alloc_bag(immortal) int immortal;
{
  static int num_bags = 0;

  register Bag *bag;

  if (freed_bags)		/* there are bags in the free pool */
  {
    bag = freed_bags;
    freed_bags = freed_bags->next;
  }
  else				/* malloc a bag */
  {
	/* don't alloc more than MAX_BAGS! */
    if (num_bags >= MAX_BAGS) return NULL;

	/* no bags in the pool, create one */
    if (!(bag = (Bag *)malloc(sizeof(Bag)))) return NULL;
    INIT_dTable(&bag->text);
    bag->id = num_bags;
    num_bags++;
  }
  bag->next = first_bag;
  first_bag = bag;

  bag->type = BAG_OF_TEXT;
  if (immortal) bag->type |= IMMORTAL_BAG;

  return bag;
}

   /* Free a bag and return it to the bag pool.
    * WARNING
    *   If free a freed bag, core dump city.  I'll let this slide because
    *     ME2 never frees bags, only pgms and they have to check first.
    * Notes:
    *   It is possible for a Mutt program to free the cut buffer.  I think
    *     everything would still work (no core dumps) but might act strange.
    */
void free_bag(bag) register Bag *bag;
{
  register Bag *bbag;

	/* clear_bag(bag); */
  free_dTable(&bag->text); INIT_dTable(&bag->text);
  bag->type = BAG_OF_TEXT;		/* not immortal */

  if (first_bag == bag) first_bag = bag->next;
  else
  {
    for (bbag = first_bag; bbag->next != bag; bbag = bbag->next) ;
    bbag->next = bag->next;
  }
  bag->next = freed_bags;
  freed_bags = bag;
}

    /* Garbage collect all bags.
     * This means freeing all bags not marked immortal.
     * See the spec for MMgc_external_objects() for when and why this is
     *   called.
     */
void gc_bags()
{
  register Bag *bp, *bw;

  for (bp = first_bag; bp; bp = bw)
  {
    bw = bp->next;	/* since pointer is gone after free_bag() */
    if (!(bp->type & IMMORTAL_BAG)) free_bag(bp);
  }
}

/*
 * Append z characters to bag, enlarging the bag if there isn't enough room.
 *   Always grow the bag in chunks, on the assumption that if you put
 *   something in you are going to put more stuff there later.
 * WARNING!!!
 *   On systems with 16bit ints (MS-DOS), having z be an int means a max bag
 *     size of 32k.  This is bogus but hard to change because dTables would
 *     have to change.
 * Returns:
 *   TRUE:   Everything went as expected
 *   FALSE:  Bag overflow.
 */
bag_append(bag,text,z) Bag *bag; char *text; int z;
{
  int j;

  j = sizeof_dTable(&bag->text);
  if (!xpand_dTable(&bag->text, z, RBLOCK, RBLOCK)) return FALSE;
  blkmov(&bag->text.table[j], text, z);

  return TRUE;
}

insert_bag(bag) Bag *bag;
{
  if (BAG_TYPE(bag) == BAG_OF_TEXT)
    return insert_block(bag->text.table, (int32)sizeof_dTable(&bag->text));
  return insert_rect(bag);
}

/* ******************************************************************** */
/* ****************** Rectangles ************************************** */
/* ******************************************************************** */

/* A rectangle is the rectanglar region outlined by a mark at the upper left
 *   corner and a mark at the lower right corner.  It is R rows by C
 *   columns.  Once the rectangle gets into a bag, each row has C characters
 *   in it (ie the short lines are padded with white space).
 * For now, I use the dot and the mark to outline rectangles because thats
 *   easy and I haven't yet seen a need to make these more general.  Also,
 *   insert_rect() works like insert_block().  Probably a short sighted
 *   view.
 */

   /* Copy a rectangle in the current buffer to a bag.
    * Marks are unmoved.
    * Input:
    *   bag:  Pointer to bag rectangle will be copied to.
    *   mark1, mark2: pointer to marks that mark the upper left and lower
    *     right corner of the rectangle.  Order not important.
    * Returns:  FALSE if not a region, else TRUE.
    * Notes:
    *   !!! I don't check for out of memory!
    */
copy_rect(bag, mark1,mark2) Bag *bag; Mark *mark1, *mark2;
{
  char c;
  int i, j, k, w, a = 0;
  Region region;
  Line *lp;

  if (get_rectangle(mark1,mark2, &region) == 0) return FALSE;

  clear_bag(bag);
  bag->width = region.width; bag->height = region.height;

  lp = region.mark.line;
  while (region.height--)
  {
    w = region.width; k = llength(lp);
    i = getgoal(lp,region.ulcol); j = getcol(lp,i);
    if (j != region.ulcol)	/* in middle of a tab or at end of line */
	a = region.ulcol -j;

    while (0 < w && i < k)
    {
      c = lgetc(lp,i);
      if (c == '\t')	/* expand tabs */
      {
	j = getcol(lp,i); j = NEXT_TAB_COLUMN(j) -j -a; a = 0;
	while (j-- && w--) bag_append(bag," ",1);
      }
      else { bag_append(bag,&c,1); w--; }
      i++;
    }
    while (0 < w--) bag_append(bag," ",1);	/* take care of short line */
    lp = lforw(lp);
  }

  set_bag_type(bag, BAG_OF_RECTANGLE);

  return TRUE;
}

   /* Insert a rectanglar bag at dot.
    * WARNING!  only call this with a bag that contains a rectangle!
    * Dot marks the upper left of the rectangle, dot is left there.
    * Mark is left at the lower right corner.
    * Notes:
    *   Doesn't work like insert_block().
    */
static int insert_rect(bag) Bag *bag;
{
  char *text;
  int col, w, h;

  set_mark(THE_MARK);
  col = getccol();
  w = bag->width; h = bag->height; text = bag->text.table;
  while (TRUE)
  {
    insert_text(text,w);
    if (--h == 0) break;
    text += w;
    next_line(1);
    the_dot->offset = getgoal(the_dot->line,col);
    to_col(col);	/* in case of tab or end of line */
  }
  swap_marks(THE_DOT,THE_MARK);
  return TRUE;
}

   /* Delete or clear rectangle.
    * Dot is left at the upper left corner of where the rectangle used to be.
    * Mark is left at the lower left corner of where the rectangle used to be.
    * Returns: FALSE if no region, else TRUE.
    */
erase_rect(delete)
{
  int j,cw,a,b,n,z,col,h;
  Region r;
  Line *lp;

  if (get_rectangle(id_to_mark(THE_DOT), id_to_mark(THE_MARK), &r) == 0)
	return FALSE;

  *the_dot = r.mark;	/* put dot at upper left corner of rectangle */
  set_mark(THE_MARK);
  cw = (col = r.ulcol) +r.width; h = r.height;
  while (TRUE)
  {
    z = TRUE; n = delete ? col : cw;
    lp = the_dot->line;
    the_dot->offset = a = getgoal(lp,col);
    b = getgoal(lp,cw);
    if (b == llength(lp)) z = FALSE;	/* short line */
    else	/* tab or at column cw */
    {
      j = getcol(lp,b);
      if (j < cw)		/* tab */
        if (delete) { j = getcol(lp,++b); n = j -r.width; }
	else n = j;
    }
    ldelete((int32)(b-a),FALSE);
    if (z) to_col(n);
    if (--h == 0) break;   /* leave dot at lower right corner of rectangle */
    next_line(1);
  }
	/* put dot at upper left corner of rectangle */
  swap_marks(THE_DOT,THE_MARK);

		/* ??? next 2 lines needed??? */
  the_dot->offset = getgoal(the_dot->line,col);
  to_col(col);

  return TRUE;
}


/* ******************************************************************** */
/* ************* Bag utilities **************************************** */
/* ******************************************************************** */

char *bag_text(bag) Bag *bag; { return bag->text.table; }

int bag_size(bag)   Bag *bag; { return sizeof_dTable(&bag->text); }

	/* Remove n character from the end of the bag */
void trim_bag(bag,n) Bag *bag;
{
  sizeof_dTable(&bag->text) -= n;
}

    /* Convert a bag to a string.
     * Gonna look a little funny if you convert a rectangle to a string.
     * NOTES
     *   This just adds a '\0' to the end of the bag so you can use it like
     *     a C string.  I remove the '\0' but it will stick around until
     *     overwritten ie the string is valid until the bag is written to.
     * WARNING
     *   You better do something with the string before the bag is munged
     *     again!  Or you will have a very long string or a seg violation.
     * Input:
     *   bag : pointer to a bag.
     * Returns:
     *   NULL : Out of memory.
     *   Pointer to a NULL terminated string.
     */
char *bag_to_string(bag) Bag *bag;
{
  if (!bag_append(bag, "\0", 1)) return NULL;	/* NULL terminate string */
  trim_bag(bag,1);				/* just kidding */
  return bag_text(bag);
}

	/* Slide the end of the bag towards the front by n characters */
void slide_bag(bag,n) Bag *bag;
{
  char *text = bag->text.table;

  blkmov(text, text+n, bag_size(bag) -n);
  trim_bag(bag,n);
}



   /* Insert a block of text (n characters long) at dot.
    * Dot is left after the inserted characters.
    * Input:
    *   text:  Pointer to characters to be inserted.  Not a string - no \0
    *     expected.
    *   n:  Number of character of text to insert.
    * Returns:
    *   TRUE:  Everything when as expected.
    *   FALSE: out of memory, ???
    *     In this case, the undo buffer may be messed up.
    * Notes:
    *   I turn off the undo flag so that the various routines I call (like
    *     openline(), ldelete()) won't mess the undo buffer.
    *   Error checking is real weak.
    */
insert_block(text,n) char *text; int32 n;
{
  int s, undo_flag;
  int32 i, j;
  unsigned int size;		/* 16 bits OK here */
  Line *lp1, *lp2, *anchorline;

  if (n == 0) return TRUE;

  save_for_undo(BU_INSERT,n);
  undo_flag = do_undo; do_undo = FALSE;

  s = TRUE;

	/* split the current line at the cursor */
  if (!openline(FALSE,1))	/* leave dot on first line */
    { s = FALSE; goto done; }	/* undo may blotto'ed */

  i = j = 0;
  lp2 = the_dot->line;	  /* line containing dot. Insert new line after this */
  anchorline = lp2->l_next;  /* Second half of split line. This never moves */
  while (i < n)
  {
    while (j < n && text[j] != '\n') j++; size = j -i;
    if ((lp1 = lalloc((int)size,FALSE)) == NULL) { s = FALSE; break; }
    lp1->l_prev = lp2; lp2->l_next = lp1;		/* link in new line */
    blkmov((lp2 = lp1)->l_text, &text[i], size);
    i = ++j;
  }
  lp2->l_next = anchorline; anchorline->l_prev = lp2;  /* make the last link */

  ldelete((int32)1, FALSE);	/* rejoin split line to inserted line */

  the_dot->line = anchorline; the_dot->offset = 0;
  if (text[n-1] != '\n') delete_characters(-1, FALSE);

done:
  do_undo = undo_flag;

  return s;
}


#include <stdio.h>

   /* file_to_bag:  Copy the contents of a file to a bag.
    * The file is appended to the end of the bag.
    * Returns:  TRUE if everything worked, FALSE if problems.
    */
#define BSIZ 512
file_to_bag(file_name,bag) char *file_name; Bag *bag;
{
  char buf[BSIZ];
  FILE *fptr;
  int n;
  
  if ((fptr = fopen(file_name,"r")) == NULL)
    { mlwrite("Can't open %s",file_name); return FALSE; }

  while (n = fread(buf,1,BSIZ,fptr))	/* error checking ?!?*/
    if (!bag_append(bag,buf,n)) return FALSE;

  fclose(fptr);
  return TRUE;
}

   /* bag_to_file:  Copy the contents of a bag to a file.
    * The file is overwritten, old contents lost.
????????send in a append flag?
    * Returns:  TRUE if everything worked, FALSE if problems.
    */
bag_to_file(bag,file_name) Bag *bag; char *file_name;
{
  FILE *fptr;
  int s;

  if ((fptr = fopen(file_name,"w")) == NULL)
    { mlwrite("Can't open \"%s\"",file_name); return FALSE; }
  s = fwrite(bag->text.table,1,sizeof_dTable(&bag->text),fptr);
  fclose(fptr);
  if (sizeof_dTable(&bag->text) > s)
  {
    unlink(file_name);				/* clean up */
    mlwrite("Not enough disk space!");
    return FALSE;
  }
  return TRUE;
}
