/*
 * Copyright (c) 2002 by The XFree86 Project, Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *  
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
 * THE XFREE86 PROJECT BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
 * OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 * Except as contained in this notice, the name of the XFree86 Project shall
 * not be used in advertising or otherwise to promote the sale, use or other
 * dealings in this Software without prior written authorization from the
 * XFree86 Project.
 *
 * Author: Paulo César Pereira de Andrade
 */

/* $XFree86: xc/programs/xedit/lisp/format.c,v 1.29tsi Exp $ */

#include "lisp/io.h"
#include "lisp/write.h"
#include "lisp/format.h"
#include <ctype.h>

#define MAXFMT			8
#define NOERROR			0

/* parse error codes */
#define PARSE_2MANYPARM		1	/* too many directive parameters */
#define PARSE_2MANYATS		2	/* more than one @ in directive */
#define PARSE_2MANYCOLS		3	/* more than one : in directive */
#define PARSE_NOARGSLEFT	4	/* no arguments left to format */
#define PARSE_BADFMTARG		5	/* argument is not an integer or char */
#define PARSE_BADDIRECTIVE	6	/* unknown format directive */
#define PARSE_BADINTEGER	7	/* bad integer representation */

/* merge error codes */
#define MERGE_2MANY		1	/* too many parameters to directive */
#define MERGE_NOCHAR		2	/* parameter must be a character */
#define MERGE_NOINT		3	/* parameter must be an integer */

/* generic error codes */
#define GENERIC_RADIX		1	/* radix not in range 2-36 */
#define GENERIC_NEGATIVE	2	/* parameter is negative */
#define GENERIC_BADSTRING	3	/* argument is not a string */
#define GENERIC_BADLIST		4	/* argument is not a list */

#define IF_SPECIFIED(arg)	(arg).specified ? &((arg).value) : NULL

#define UPANDOUT_NORMAL		1
#define UPANDOUT_COLLON		2
#define UPANDOUT_HASH		4	/* only useful inside a ~{ iteration
					 * forces loop finalization. */

#define ITERATION_NORMAL	1
#define ITERATION_LAST		2

/*
 * Types
 */
/* parameter to format */
typedef struct {
    unsigned int achar : 1;	/* value was specified as a character */
    unsigned int specified : 1;	/* set if value was specified */
    unsigned int offset : 30;	/* offset in format string, for error printing */
    int value;
} FmtArg;

/* information about format parameters */
typedef struct {
    unsigned int atsign : 1;	/* @ specified */
    unsigned int collon : 1;	/* : specified */
    unsigned int command : 8;	/* the format command */
    unsigned int count : 4;	/* number of arguments processed */
    unsigned int offset : 10;	/* offset in format string, for error printing */
    char *base, *format;
    FmtArg arguments[MAXFMT];
} FmtArgs;

/* used for combining default format parameter values */
typedef struct {
    int achar;
    int value;
} FmtDef;

/* number of default format parameter values and defaults */
typedef struct {
    int count;
    FmtDef defaults[MAXFMT];
} FmtDefs;

/* used on recursive calls to LispFormat */
typedef struct {
    FmtArgs args;
    LispObj *base_arguments;	/* pointer to first format argument */
    int total_arguments;	/* number of objects in base_arguments */
    char **format;		/* if need to update format string pointer */
    LispObj **object;		/* CAR(arguments), for plural check */
    LispObj **arguments;	/* current element of base_arguments */
    int *num_arguments;		/* number of arguments after arguments */
    int upandout;		/* information for recursive calls */
    int iteration;		/* only set if in ~:{... or ~:@{ and in the
				 * last argument list, hint for upandout */
} FmtInfo;

/*
 * Prototypes
 */
static void merge_arguments(FmtArgs*, const FmtDefs*, int*);
static char *parse_arguments(char*, FmtArgs*, int*, LispObj**, int*);
static void merge_error(FmtArgs*, int);
static void parse_error(FmtArgs*, int);
static void generic_error(FmtArgs*, int);
static void format_error(FmtArgs*, const char*);

static int format_object(LispObj*, LispObj*);

static void format_ascii(LispObj*, LispObj*, FmtArgs*);
static void format_in_radix(LispObj*, LispObj*, int, FmtArgs*);
static void format_radix_special(LispObj*, LispObj*, FmtArgs*);
static void format_roman(LispObj*, LispObj*, FmtArgs*);
static void format_english(LispObj*, LispObj*, FmtArgs*);
static void format_character(LispObj*, LispObj*, FmtArgs*);
static void format_fixed_float(LispObj*, LispObj*, FmtArgs*);
static void format_exponential_float(LispObj*, LispObj*, FmtArgs*);
static void format_general_float(LispObj*, LispObj*, FmtArgs*);
static void format_dollar_float(LispObj*, LispObj*, FmtArgs*);
static void format_tabulate(LispObj*, FmtArgs*);

static void format_goto(FmtInfo*);
static void format_indirection(LispObj*, LispObj*, FmtInfo*);

static void list_formats(FmtInfo*, int, char**, char***, int*, int*, int*, int*);
static void free_formats(char**, int);

static void format_case_conversion(LispObj*, FmtInfo*);
static void format_conditional(LispObj*, FmtInfo*);
static void format_iterate(LispObj*, FmtInfo*);
static void format_justify(LispObj*, FmtInfo*);

static void LispFormat(LispObj*, FmtInfo*);

/*
 * Initialization
 */
static const FmtDefs AsciiDefs = {
    4,
    {
	{0, 0},			/* mincol */
	{0, 1},			/* colinc */
	{0, 0},			/* minpad */
	{1, ' '},		/* padchar */
    },
};

static const FmtDefs IntegerDefs = {
    4,
    {
	{0, 0},			/* mincol */
	{1, ' '},		/* padchar */
	{1, ','},		/* commachar */
	{0, 3},			/* commainterval */
    },
};

static const FmtDefs RadixDefs = {
    5,
    {
	{0, 10},		/* radix */
	{0, 0},			/* mincol */
	{1, ' '},		/* padchar */
	{1, ','},		/* commachar */
	{0, 3},			/* commainterval */
    },
};

static const FmtDefs NoneDefs = {
    0,
};

static const FmtDefs FixedFloatDefs = {
    5,
    {
	{0, 0},			/* w */
	{0, 16},		/* d */
	{0, 0},			/* k */
	{1, '\0'},		/* overflowchar */
	{1, ' '},		/* padchar */
    },
};

static const FmtDefs ExponentialFloatDefs = {
    7,
    {
	{0, 0},			/* w */
	{0, 16},		/* d */
	{0, 0},			/* e */
	{0, 1},			/* k */
	{1, '\0'},		/* overflowchar */
	{1, ' '},		/* padchar */
	{1, 'E'},		/* exponentchar */
	/* XXX if/when more than one float format,
	 * should default to object type */
    },
};

static const FmtDefs DollarFloatDefs = {
    4,
    {
	{0, 2},			/* d */
	{0, 1},			/* n */
	{0, 0},			/* w */
	{1, ' '},		/* padchar */
    },
};

static const FmtDefs OneDefs = {
    1,
    {
	{0, 1},
    },
};

static const FmtDefs TabulateDefs = {
    2,
    {
	{0, 0},			/* colnum */
	{0, 1},			/* colinc */
    },
};

extern LispObj *Oprint_escape;

/*
 * Implementation
 */
static void
merge_arguments(FmtArgs *arguments, const FmtDefs *defaults, int *code)
{
    int count;
    const FmtDef *defaul;
    FmtArg *argument;

    defaul = &(defaults->defaults[0]);
    argument = &(arguments->arguments[0]);
    for (count = 0; count < defaults->count; count++, argument++, defaul++) {
	if (count >= arguments->count)
	    argument->specified = 0;
	if (argument->specified) {
	    if (argument->achar != defaul->achar) {
		*code = defaul->achar ? MERGE_NOCHAR : MERGE_NOINT;
		arguments->offset = argument->offset;
		return;
	    }
	}
	else {
	    argument->specified = 0;
	    argument->achar = defaul->achar;
	    argument->value = defaul->value;
	}
    }

    /* check if extra arguments were provided */
    if (arguments->count > defaults->count)
	*code = MERGE_2MANY;
}

/* the pointer arguments may be null, useful when just testing/parsing
 * the directive parameters */
static char *
parse_arguments(char *format, FmtArgs *arguments,
		int *num_objects, LispObj **objects, int *code)
{
    int test;
    char *ptr;
    FmtArg *argument;
    unsigned int tmpcmd = 0;

    /* initialize */
    test = objects == NULL || code == NULL || num_objects == NULL;
    ptr = format;
    argument = &(arguments->arguments[0]);
    arguments->atsign = arguments->collon = arguments->command = 0;

    /* parse format parameters */
    for (arguments->count = 0;; arguments->count++) {
	arguments->offset = ptr - format + 1;
	if (arguments->count >= MAXFMT) {
	    if (!test)
		*code = PARSE_2MANYPARM;
	    return (ptr);
	}
	if (*ptr == '\'') {		/* character parameter value */
	    ++ptr;			/* skip ' */
	    argument->achar = argument->specified = 1;
	    argument->value = *ptr++;
	}
	else if (*ptr == ',') {		/* use default parameter value */
	    argument->achar = 0;
	    argument->specified = 0;
	    /* don't increment ptr, will be incremented below */
	}
	else if (*ptr == '#') {		/* number of arguments is value */
	    ++ptr;			/* skip # */
	    argument->achar = 0;
	    argument->specified = 1;
	    if (!test)
		argument->value = *num_objects;
	}
	else if (*ptr == 'v' ||
		 *ptr == 'V') {		/* format object argument is value */
	    LispObj *object;

	    ++ptr;			/* skip V */
	    if (!test) {
		if (!CONSP(*objects)) {
		    *code = PARSE_NOARGSLEFT;
		    return (ptr);
		}
		object = CAR((*objects));
		if (FIXNUMP(object)) {
		    argument->achar = 0;
		    argument->specified = 1;
		    argument->value = FIXNUM_VALUE(object);
		}
		else if (SCHARP(object)) {
		    argument->achar = argument->specified = 1;
		    argument->value = SCHAR_VALUE(object);
		}
		else {
		    *code = PARSE_BADFMTARG;
		    return (ptr);
		}
		*objects = CDR(*objects);
		--*num_objects;
	    }
	}
	else if (isdigit(*ptr) ||
		*ptr == '-' || *ptr == '+') {	/* integer parameter value */
	    int sign;

	    argument->achar = 0;
	    argument->specified = 1;
	    if (!isdigit(*ptr)) {
		sign = *ptr++ == '-';
	    }
	    else
		sign = 0;
	    if (!test && !isdigit(*ptr)) {
		*code = PARSE_BADINTEGER;
		return (ptr);
	    }
	    argument->value = *ptr++ - '0';
	    while (isdigit(*ptr)) {
		argument->value = (argument->value * 10) + (*ptr++ - '0');
		if (argument->value > 65536) {
		    if (!test) {
			*code = PARSE_BADINTEGER;
			return (ptr);
		    }
		}
	    }
	    if (sign)
		argument->value = -argument->value;
	}
	else				/* no more arguments to format */
	    break;

	if (*ptr == ',')
	    ++ptr;

	/* remember offset of format parameter, for better error printing */
	argument->offset = arguments->offset;
	argument++;
    }

    /* check for extra flags */
    for (;;) {
	if (*ptr == '@') {		/* check for special parameter atsign */
	    if (arguments->atsign) {
		if (!test) {
		    *code = PARSE_2MANYATS;
		    return (ptr);
		}
	    }
	    ++ptr;
	    ++arguments->offset;
	    arguments->atsign = 1;
	}
	else if (*ptr == ':') {		/* check for special parameter collon */
	    if (arguments->collon) {
		if (!test) {
		    *code = PARSE_2MANYCOLS;
		    return (ptr);
		}
	    }
	    ++ptr;
	    ++arguments->offset;
	    arguments->collon = 1;
	}
	else				/* next value is format command */
	    break;
    }

    if (!test)
	*code = NOERROR;
    arguments->command = *ptr++;
    tmpcmd = arguments->command;
    if (islower(tmpcmd))
	arguments->command = toupper(tmpcmd);
    ++arguments->offset;

    return (ptr);
}

static void
parse_error(FmtArgs *args, int code)
{
    static const char * const errors[] = {
	NULL,
	"too many parameters to directive",
	"too many @ parameters",
	"too many : parameters",
	"no arguments left to format",
	"argument is not a fixnum integer or a character",
	"unknown format directive",
	"parameter is not a fixnum integer",
    };

    format_error(args, errors[code]);
}

static void
merge_error(FmtArgs *args, int code)
{
    static const char * const errors[] = {
	NULL,
	"too many parameters to directive",
	"argument must be a character",
	"argument must be a fixnum integer",
    };

    format_error(args, errors[code]);
}

static void
generic_error(FmtArgs *args, int code)
{
    static const char * const errors[] = {
	NULL,
	"radix must be in the range 2 to 36, inclusive",
	"parameter must be positive",
	"argument must be a string",
	"argument must be a list",
    };

    format_error(args, errors[code]);
}

static void
format_error(FmtArgs *args, const char *str)
{
    char *message;
    int errorlen, formatlen;

    /* number of bytes of format to be printed */
    formatlen = (args->format - args->base) + args->offset;

    /* length of specific error message */
    errorlen = strlen(str) + 1;			/* plus '\n' */

    /* XXX allocate string with LispMalloc,
     * so that it will be freed in LispTopLevel */
    message = LispMalloc(formatlen + errorlen + 1);

    sprintf(message, "%s\n", str);
    memcpy(message + errorlen, args->base, formatlen);
    message[errorlen + formatlen] = '\0';

    LispDestroy("FORMAT: %s", message);
}

static int
format_object(LispObj *stream, LispObj *object)
{
    int length;

    length = LispWriteObject(stream, object);

    return (length);
}

static void
format_ascii(LispObj *stream, LispObj *object, FmtArgs *args)
{
    GC_ENTER();
    LispObj *string = NIL;
    int length = 0,
	atsign = args->atsign,
	collon = args->collon,
	mincol = args->arguments[0].value,
	colinc = args->arguments[1].value,
	minpad = args->arguments[2].value,
	padchar = args->arguments[3].value;

    /* check/correct arguments */
    if (mincol < 0)
	mincol = 0;
    if (colinc < 0)
	colinc = 1;
    if (minpad < 0)
	minpad = 0;
    /* XXX pachar can be the null character? */

    if (object == NIL)
	length = collon ? 2 : 3;	    /* () or NIL */

    /* left padding */
    if (atsign) {
	/* if length not yet known */
	if (object == NIL) {
	    string = LSTRINGSTREAM("", STREAM_READ | STREAM_WRITE, 0);
	    GC_PROTECT(string);
	    length = LispWriteObject(string, object);
	}

	/* output minpad characters at left */
	if (minpad) {
	    length += minpad;
	    LispWriteChars(stream, padchar, minpad);
	}

	if (colinc) {
	    /* puts colinc spaces at a time,
	     * until at least mincol chars out */
	    while (length < mincol) {
		LispWriteChars(stream, padchar, colinc);
		length += colinc;
	    }
	}
    }

    if (object == NIL) {
	if (collon)
	    LispWriteStr(stream, "()", 2);
	else
	    LispWriteStr(stream,  Snil->value, 3);
    }
    else {
	/* if string is not NIL, atsign was specified
	 * and object printed to string */
	if (string == NIL)
	    length = format_object(stream, object);
	else {
	    int size;
	    const char *str = LispGetSstring(SSTREAMP(string), &size);

	    LispWriteStr(stream, str, size);
	}
    }

    /* right padding */
    if (!atsign) {
	/* output minpad characters at left */
	if (minpad) {
	    length += minpad;
	    LispWriteChars(stream, padchar, minpad);
	}
	if (colinc) {
	    /* puts colinc spaces at a time,
	     * until at least mincol chars out */
	    while (length < mincol) {
		LispWriteChars(stream, padchar, colinc);
		length += colinc;
	    }
	}
    }

    GC_LEAVE();
}

/* assumes radix is 0 or in range 2 - 36 */
static void
format_in_radix(LispObj *stream, LispObj *object, int radix, FmtArgs *args)
{
    if (INTEGERP(object)) {
	int i, atsign, collon, mincol, padchar, commachar, commainterval;

	i = (radix == 0);
	atsign = args->atsign;
	collon = args->collon;
	if (radix == 0) {
	    radix = args->arguments[0].value;
	    if (radix < 2 || radix > 36) {
		args->offset = args->arguments[0].offset;
		generic_error(args, GENERIC_RADIX);
	    }
	}
	mincol = args->arguments[i++].value;
	padchar = args->arguments[i++].value;
	commachar = args->arguments[i++].value;
	commainterval = args->arguments[i++].value;

	LispFormatInteger(stream, object, radix, atsign, collon,
			  mincol, padchar, commachar, commainterval);
    }
    else
	format_object(stream, object);
}

static void
format_radix_special(LispObj *stream, LispObj *object, FmtArgs *args)
{
    if (FIXNUMP(object)) {
	if (args->atsign)
	    format_roman(stream, object, args);
	else
	    format_english(stream, object, args);
    }
    else
	format_object(stream, object);
}

static void
format_roman(LispObj *stream, LispObj *object, FmtArgs *args)
{
    long value = 0;
    int cando, new_roman = args->collon == 0;

    if (FIXNUMP(object)) {
	value = FIXNUM_VALUE(object);
	if (new_roman)
	    cando = value >= 1 && value <= 3999;
	else
	    cando = value >= 1 && value <= 4999;
    }
    else
	cando = 0;

    if (cando)
	LispFormatRomanInteger(stream, value, new_roman);
    else
	format_object(stream, object);
}

static void
format_english(LispObj *stream, LispObj *object, FmtArgs *args)
{
    int cando;
    long number = 0;

    if (FIXNUMP(object)) {
	number = FIXNUM_VALUE(object);
	cando = number >= -999999999 && number <= 999999999;
    }
    else
	cando = 0;

    if (cando)
	LispFormatEnglishInteger(stream, number, args->collon);
    else
	format_object(stream, object);
}

static void
format_character(LispObj *stream, LispObj *object, FmtArgs *args)
{
    if (SCHARP(object))
	LispFormatCharacter(stream, object, args->atsign, args->collon);
    else
	format_object(stream, object);
}

static void
format_fixed_float(LispObj *stream, LispObj *object, FmtArgs *args)
{
    if (FLOATP(object))
	LispFormatFixedFloat(stream, object, args->atsign,
			     args->arguments[0].value,
			     IF_SPECIFIED(args->arguments[1]),
			     args->arguments[2].value,
			     args->arguments[3].value,
			     args->arguments[4].value);
    else
	format_object(stream, object);
}

static void
format_exponential_float(LispObj *stream, LispObj *object, FmtArgs *args)
{
    if (FLOATP(object))
	LispFormatExponentialFloat(stream, object, args->atsign,
				   args->arguments[0].value,
				   IF_SPECIFIED(args->arguments[1]),
				   args->arguments[2].value,
				   args->arguments[3].value,
				   args->arguments[4].value,
				   args->arguments[5].value,
				   args->arguments[6].value);
    else
	format_object(stream, object);
}

static void
format_general_float(LispObj *stream, LispObj *object, FmtArgs *args)
{
    if (FLOATP(object))
	LispFormatGeneralFloat(stream, object, args->atsign,
				args->arguments[0].value,
				IF_SPECIFIED(args->arguments[1]),
				args->arguments[2].value,
				args->arguments[3].value,
				args->arguments[4].value,
				args->arguments[5].value,
				args->arguments[6].value);
    else
	format_object(stream, object);
}

static void
format_dollar_float(LispObj *stream, LispObj *object, FmtArgs *args)
{
    if (FLOATP(object))
	LispFormatDollarFloat(stream, object,
			      args->atsign, args->collon,
			      args->arguments[0].value,
			      args->arguments[1].value,
			      args->arguments[2].value,
			      args->arguments[3].value);
    else
	format_object(stream, object);
}

static void
format_tabulate(LispObj *stream, FmtArgs *args)
{
    int atsign = args->atsign,
	colnum = args->arguments[0].value,
	colinc = args->arguments[1].value,
	column;

    column = LispGetColumn(stream);

    if (atsign) {
	/* relative tabulation */
	if (colnum > 0) {
	    LispWriteChars(stream, ' ', colnum);
	    column += colnum;
	}
	/* tabulate until at a multiple of colinc */
	if (colinc > 0)
	    LispWriteChars(stream, ' ', colinc - (column % colinc));
    }
    else {
	/* if colinc not specified, just move to given column */
	if (colinc <= 0)
	    LispWriteChars(stream, ' ', column - colnum);
	else {
	    /* always output at least colinc spaces */
	    do {
		LispWriteChars(stream, ' ', colinc);
		colnum -= colinc;
	    } while (colnum > column);
	}
    }
}

static void
format_goto(FmtInfo *info)
{
    int count, num_arguments;
    LispObj *object, *arguments;

    /* number of arguments to ignore or goto offset */
    count = info->args.arguments[0].value;
    if (count < 0)
	generic_error(&(info->args), GENERIC_NEGATIVE);

    if (info->args.atsign) {
	/* absolute goto */

	/* if not specified, defaults to zero */
	if (!(info->args.arguments[0].specified))
	    count = 0;

	/* if offset too large */
	if (count > info->total_arguments)
	    parse_error(&(info->args), PARSE_NOARGSLEFT);
	else if (count != info->total_arguments - *(info->num_arguments)) {
	    /* calculate new parameters */
	    object = NIL;
	    arguments = info->base_arguments;
	    num_arguments = info->total_arguments - count;

	    for (; count > 0; count--, arguments = CDR(arguments))
		object = CAR(arguments);

	    /* update format information */
	    *(info->object) = object;
	    *(info->arguments) = arguments;
	    *(info->num_arguments) = num_arguments;
	}
    }
    else if (count) {
	/* relative goto, ignore or go back count arguments */

	/* prepare to update parameters */
	arguments = *(info->arguments);
	num_arguments = *(info->num_arguments);

	/* go back count arguments? */
	if (info->args.collon)
	    count = -count;

	num_arguments -= count;

	if (count > 0) {
	    if (count > *(info->num_arguments))
		parse_error(&(info->args), PARSE_NOARGSLEFT);

	    object = *(info->object);
	    for (; count > 0; count--, arguments = CDR(arguments))
		object = CAR(arguments);
	}
	else {		/* count < 0 */
	    if (info->total_arguments + count - *(info->num_arguments) < 0)
		parse_error(&(info->args), PARSE_NOARGSLEFT);

	    object = NIL;
	    arguments = info->base_arguments;
	    for (count = 0; count < info->total_arguments - num_arguments;
		count++, arguments = CDR(arguments))
		object = CAR(arguments);
	}

	/* update format parameters */
	*(info->object) = object;
	*(info->arguments) = arguments;
	*(info->num_arguments) = num_arguments;
    }
}

static void
format_indirection(LispObj *stream, LispObj *format, FmtInfo *info)
{
    char *string;
    LispObj *object;
    FmtInfo indirect_info;

    if (!STRINGP(format))
	generic_error(&(info->args), GENERIC_BADSTRING);
    string = THESTR(format);

    /* most information is the same */
    memcpy(&indirect_info, info, sizeof(FmtInfo));

    /* set new format string */
    indirect_info.args.base = indirect_info.args.format = string;
    indirect_info.format = &string;

    if (info->args.atsign) {
	/* use current arguments */

	/* do the indirect format */
	LispFormat(stream, &indirect_info);
    }
    else {
	/* next argument is the recursive call arguments */

	int num_arguments;

	/* it is valid to not have a list following string, as string may
	 * not have format directives */
	if (CONSP(*(indirect_info.arguments)))
	    object = CAR(*(indirect_info.arguments));
	else
	    object = NIL;

	if (!LISTP(object) || !CONSP(*(info->arguments)))
	    generic_error(&(info->args), GENERIC_BADLIST);

	/* update information now */
	*(info->object) = object;
	*(info->arguments) = CDR(*(info->arguments));
	*(info->num_arguments) -= 1;

	/* set arguments for recursive call */
	indirect_info.base_arguments = object;
	indirect_info.arguments = &object;
	for (num_arguments = 0; CONSP(object); object = CDR(object))
	    ++num_arguments;

	/* note that indirect_info.arguments is a pointer to "object",
	 * keep it pointing to the correct object */
	object = indirect_info.base_arguments;
	indirect_info.total_arguments = num_arguments;
	indirect_info.num_arguments = &num_arguments;

	/* do the indirect format */
	LispFormat(stream, &indirect_info);
    }
}

/* update pointers to a list of format strings:
 *	for '(' and '{' only one list is required
 *	for '[' and '<' more than one may be returned
 *	has_default is only meaningful for '[' and '<'
 *	comma_width and line_width are only meaningful to '<', and
 *	    only valid if has_default set
 * if the string is finished prematurely, LispDestroy is called
 * format_ptr is updated to the correct pointer in the "main" format string
 */
static void
list_formats(FmtInfo *info, int command, char **format_ptr,
	     char ***format_list, int *format_count, int *has_default,
	     int *comma_width, int *line_width)
{
    /* instead of processing the directives recursively, just separate the
     * input formats in separate strings, then see if one of then need to
     * be used */
    FmtArgs args;
    int counters[] = {  0,   0,   0,   0};
		    /* '[', '(', '{', '<' */
    char *format, *next_format, *start, **formats;
    int num_formats, format_index, separator, add_format;

    /* initialize */
    formats = NULL;
    num_formats = format_index = 0;
    if (has_default != NULL)
	*has_default = 0;
    if (comma_width != NULL)
	*comma_width = 0;
    if (line_width != NULL)
	*line_width = 0;
    format = start = next_format = *format_ptr;
    switch (command) {
	case '[': counters[0] = 1; format_index = 0; break;
	case '(': counters[1] = 1; format_index = 1; break;
	case '{': counters[2] = 1; format_index = 2; break;
	case '<': counters[3] = 1; format_index = 3; break;
    }

#define	LIST_FORMATS_ADD	1
#define	LIST_FORMATS_DONE	2

    /* fill list of format options to conditional */
    while (*format) {
	if (*format == '~') {
	    separator = add_format = 0;
	    args.format = format + 1;
	    next_format = parse_arguments(format + 1, &args, NULL, NULL, NULL);
	    switch (args.command) {
 		case '[': ++counters[0];    break;
		case ']': --counters[0];    break;
		case '(': ++counters[1];    break;
		case ')': --counters[1];    break;
		case '{': ++counters[2];    break;
		case '}': --counters[2];    break;
		case '<': ++counters[3];    break;
		case '>': --counters[3];    break;
		case ';': separator = 1;    break;
	    }

	    /* check if a new format string must be added */
	    if (separator && counters[format_index] == 1 &&
		(command == '[' || command == '<'))
		add_format = LIST_FORMATS_ADD;
	    else if (counters[format_index] == 0)
		add_format = LIST_FORMATS_DONE;

	    if (add_format) {
		int length = format - start;

		formats = LispRealloc(formats,
				      (num_formats + 1) * sizeof(char*));

		formats[num_formats] = LispMalloc(length + 1);
		strncpy(formats[num_formats], start, length);
		formats[num_formats][length] = '\0';
		++num_formats;
		/* loop finished? */
		if (add_format == LIST_FORMATS_DONE)
		    break;
		else if (command == '[' && has_default != NULL)
		    /* will be set only for the last parameter, what is
		     * expected, just don't warn about it in the incorrect
		     * place */
		    *has_default = args.collon != 0;
		else if (command == '<' && num_formats == 1) {
		    /* if the first parameter to '<', there may be overrides
		     * to comma-width and line-width */
		    if (args.collon && has_default != NULL) {
			*has_default = 1;
			if (comma_width != NULL &&
			    args.arguments[0].specified &&
			    !args.arguments[0].achar)
			    *comma_width = args.arguments[0].value;
			if (line_width != NULL &&
			    args.arguments[1].specified &&
			    !args.arguments[1].achar)
			    *line_width = args.arguments[1].value;
		    }
		}
		start = next_format;
	    }
	    format = next_format;
	}
	else
	    ++format;
    }

    /* check if format string did not finish prematurely */
    if (counters[format_index] != 0) {
	char error_message[64];

	sprintf(error_message, "expecting ~%c", command);
	format_error(&(info->args), error_message);
    }

    /* update pointers */
    *format_list = formats;
    *format_count = num_formats;
    *format_ptr = next_format;
}

static void
free_formats(char **formats, int num_formats)
{
    if (num_formats) {
	while (--num_formats >= 0)
	    LispFree(formats[num_formats]);
	LispFree(formats);
    }
}

static void
format_case_conversion(LispObj *stream, FmtInfo *info)
{
    GC_ENTER();
    LispObj *string;
    FmtInfo case_info;
    char *str, *ptr;
    char *format, *next_format, **formats;
    int atsign, collon, num_formats, length;

    atsign = info->args.atsign;
    collon = info->args.collon;

    /* output to a string, before case conversion */
    string = LSTRINGSTREAM("", STREAM_READ | STREAM_WRITE, 0);
    GC_PROTECT(string);

    /* most information is the same */
    memcpy(&case_info, info, sizeof(FmtInfo));

    /* list formats */
    next_format = *(info->format);
    list_formats(info, '(', &next_format, &formats, &num_formats,
		 NULL, NULL, NULL);

    /* set new format string */
    format = formats[0];
    case_info.args.base = case_info.args.format = format;
    case_info.format = &format;

    /* format text to string */
    LispFormat(string, &case_info);

    str = ptr = LispGetSstring(SSTREAMP(string), &length);

    /* do case conversion */
    if (!atsign && !collon) {
	/* convert all upercase to lowercase */
	for (; *ptr; ptr++) {
	    if (isupper(*ptr))
		*ptr = tolower(*ptr);
	}
    }
    else if (atsign && collon) {
	/* convert all lowercase to upercase */
	for (; *ptr; ptr++) {
	    if (islower(*ptr))
		*ptr = toupper(*ptr);
	}
    }
    else {
	int upper = 1;

	/* skip non-alphanumeric characters */
	for (; *ptr; ptr++)
	    if (isalnum(*ptr))
		break;

	/* capitalize words */
	for (; *ptr; ptr++) {
	    if (isalnum(*ptr)) {
		if (upper) {
		    if (islower(*ptr))
			*ptr = toupper(*ptr);
		    upper = 0;
		}
		else if (isupper(*ptr))
		    *ptr = tolower(*ptr);
	    }
	    else
		upper = collon;
		/* if collon, capitalize all words, else just first word */
	}
    }

    /* output case converted string */
    LispWriteStr(stream, str, length);

    /* temporary string stream is not necessary anymore */
    GC_LEAVE();

    /* free temporary memory */
    free_formats(formats, num_formats);

    /* this information always updated */
    *(info->format) = next_format;
}

static void
format_conditional(LispObj *stream, FmtInfo *info)
{
    LispObj *object, *arguments;
    char *format, *next_format, **formats;
    int choice, num_formats, has_default, num_arguments;

    /* save information that may change */
    object = *(info->object);
    arguments = *(info->arguments);
    num_arguments = *(info->num_arguments);

    /* initialize */
    choice = -1;
    next_format = *(info->format);

    /* list formats */
    list_formats(info, '[',
		 &next_format, &formats, &num_formats, &has_default, NULL, NULL);

    /* ~:[false;true] */
    if (info->args.collon) {
	/* one argument always consumed */
	if (!CONSP(arguments))
	    parse_error(&(info->args), PARSE_NOARGSLEFT);
	object = CAR(arguments);
	arguments = CDR(arguments);
	--num_arguments;
	choice = object == NIL ? 0 : 1;
    }
    /* ~@[true] */
    else if (info->args.atsign) {
	/* argument consumed only if nil, but one must be available */
	if (!CONSP(arguments))
	    parse_error(&(info->args), PARSE_NOARGSLEFT);
	if (CAR(arguments) != NIL)
	    choice = 0;
	else {
	    object = CAR(arguments);
	    arguments = CDR(arguments);
	    --num_arguments;
	}
    }
    /* ~n[...~] */
    else if (info->args.arguments[0].specified)
	/* no arguments consumed */
	choice = info->args.arguments[0].value;
    /* ~[...~] */
    else {
	/* one argument consumed, it is the index in the available formats */
	if (!CONSP(arguments))
	    parse_error(&(info->args), PARSE_NOARGSLEFT);
	object = CAR(arguments);
	arguments = CDR(arguments);
	--num_arguments;
	/* no error if it isn't a number? */
	if (FIXNUMP(object))
	    choice = FIXNUM_VALUE(object);
    }

    /* update anything that may have changed */
    *(info->object) = object;
    *(info->arguments) = arguments;
    *(info->num_arguments) = num_arguments;

    /* if choice is out of range check if there is a default choice */
    if (has_default && (choice < 0 || choice >= num_formats))
	choice = num_formats - 1;

    /* if one of the formats must be parsed */
    if (choice >= 0 && choice < num_formats) {
	FmtInfo conditional_info;

	/* most information is the same */
	memcpy(&conditional_info, info, sizeof(FmtInfo));

	/* set new format string */
	format = formats[choice];
	conditional_info.args.base = conditional_info.args.format = format;
	conditional_info.format = &format;

	/* do the conditional format */
	LispFormat(stream, &conditional_info);
    }

    /* free temporary memory */
    free_formats(formats, num_formats);

    /* this information always updated */
    *(info->format) = next_format;
}

static void
format_iterate(LispObj *stream, FmtInfo *info)
{
    FmtInfo iterate_info;
    LispObj *object, *arguments, *iarguments, *iobject;
    char *format, *next_format, *loop_format, **formats;
    int num_arguments, iterate, iterate_max, has_max, has_min, inum_arguments,
	num_formats;

    /* save information that may change */
    object = *(info->object);
    arguments = *(info->arguments);
    num_arguments = *(info->num_arguments);

    /* initialize */
    iterate = has_min = 0;
    next_format = *(info->format);

    /* if has_max set, iterate at most iterate_max times */
    has_max = info->args.arguments[0].specified;
    iterate_max = info->args.arguments[0].value;

    /* list formats */
    list_formats(info, '{', &next_format, &formats, &num_formats,
		 NULL, NULL, NULL);
    loop_format = formats[0];

    /* most information is the same */
    memcpy(&iterate_info, info, sizeof(FmtInfo));

    /* ~{...~} */
    if (!info->args.atsign && !info->args.collon) {
	/* next argument is the argument list for the iteration */

	/* fetch argument list, must exist */
	if (!CONSP(arguments))
	    parse_error(&(info->args), PARSE_NOARGSLEFT);
	iarguments = object = CAR(arguments);
	object = CAR(arguments);
	arguments = CDR(arguments);
	--num_arguments;

	inum_arguments = 0;
	if (CONSP(object)) {
	    /* count arguments to format */
	    for (iobject = object; CONSP(iobject); iobject = CDR(iobject))
		++inum_arguments;
	}
	else if (object != NIL)
	    generic_error(&(info->args), GENERIC_BADLIST);

	iobject = NIL;

	/* set new arguments to recursive calls */
	iarguments = object;
	iterate_info.base_arguments = iarguments;
	iterate_info.total_arguments = inum_arguments;
	iterate_info.object = &iobject;
	iterate_info.arguments = &iarguments;
	iterate_info.num_arguments = &inum_arguments;

	/* iterate */
	for (;; iterate++) {
	    /* if maximum iterations done or all arguments consumed */
	    if (has_max && iterate > iterate_max)
		break;
	    else if (inum_arguments == 0 && (!has_min || iterate > 0))
		break;

	    format = loop_format;

	    /* set new format string */
	    iterate_info.args.base = iterate_info.args.format = format;
	    iterate_info.format = &format;

	    /* information for possible ~^, in this case ~:^ is a noop */
	    iterate_info.iteration = ITERATION_NORMAL;

	    /* do the format */
	    LispFormat(stream, &iterate_info);

	    /* check for forced loop break */
	    if (iterate_info.upandout & UPANDOUT_HASH)
		break;
	}
    }
    /* ~:@{...~} */
    else if (info->args.atsign && info->args.collon) {
	/* every following argument is the argument list for the iteration */

	/* iterate */
	for (;; iterate++) {
	    /* if maximum iterations done or all arguments consumed */
	    if (has_max && iterate > iterate_max)
		break;
	    else if (num_arguments == 0 && (!has_min || iterate > 0))
		break;

	    /* fetch argument list, must exist */
	    if (!CONSP(arguments))
		parse_error(&(info->args), PARSE_NOARGSLEFT);
	    iarguments = object = CAR(arguments);
	    object = CAR(arguments);
	    arguments = CDR(arguments);
	    --num_arguments;

	    inum_arguments = 0;
	    if (CONSP(object)) {
		/* count arguments to format */
		for (iobject = object; CONSP(iobject); iobject = CDR(iobject))
		    ++inum_arguments;
	    }
	    else if (object != NIL)
		generic_error(&(info->args), GENERIC_BADLIST);

	    iobject = NIL;

	    /* set new arguments to recursive calls */
	    iarguments = object;
	    iterate_info.base_arguments = iarguments;
	    iterate_info.total_arguments = inum_arguments;
	    iterate_info.object = &iobject;
	    iterate_info.arguments = &iarguments;
	    iterate_info.num_arguments = &inum_arguments;

	    format = loop_format;

	    /* set new format string */
	    iterate_info.args.base = iterate_info.args.format = format;
	    iterate_info.format = &format;

	    /* information for possible ~^ */
	    iterate_info.iteration =
		num_arguments > 0 ? ITERATION_NORMAL : ITERATION_LAST;

	    /* do the format */
	    LispFormat(stream, &iterate_info);

	    /* check for forced loop break */
	    if (iterate_info.upandout & UPANDOUT_HASH)
		break;
	}
    }
    /* ~:{...~} */
    else if (info->args.collon) {
	/* next argument is a list of lists */

	LispObj *sarguments, *sobject;
	int snum_arguments;

	/* fetch argument list, must exist */
	if (!CONSP(arguments))
	    parse_error(&(info->args), PARSE_NOARGSLEFT);
	sarguments = object = CAR(arguments);
	object = CAR(arguments);
	arguments = CDR(arguments);
	--num_arguments;

	snum_arguments = 0;
	if (CONSP(object)) {
	    /* count arguments to format */
	    for (sobject = object; CONSP(sobject); sobject = CDR(sobject))
		++snum_arguments;
	}
	else
	    generic_error(&(info->args), GENERIC_BADLIST);

	/* iterate */
	for (;; iterate++) {
	    /* if maximum iterations done or all arguments consumed */
	    if (has_max && iterate > iterate_max)
		break;
	    else if (snum_arguments == 0 && (!has_min || iterate > 0))
		break;

	    /* fetch argument list, must exist */
	    if (!CONSP(sarguments))
		parse_error(&(info->args), PARSE_NOARGSLEFT);
	    iarguments = sobject = CAR(sarguments);
	    sobject = CAR(sarguments);
	    sarguments = CDR(sarguments);
	    --snum_arguments;

	    inum_arguments = 0;
	    if (CONSP(object)) {
		/* count arguments to format */
		for (iobject = sobject; CONSP(iobject); iobject = CDR(iobject))
		    ++inum_arguments;
	    }
	    else if (sobject != NIL)
		generic_error(&(info->args), GENERIC_BADLIST);

	    iobject = NIL;

	    /* set new arguments to recursive calls */
	    iarguments = sobject;
	    iterate_info.base_arguments = iarguments;
	    iterate_info.total_arguments = inum_arguments;
	    iterate_info.object = &iobject;
	    iterate_info.arguments = &iarguments;
	    iterate_info.num_arguments = &inum_arguments;

	    format = loop_format;

	    /* set new format string */
	    iterate_info.args.base = iterate_info.args.format = format;
	    iterate_info.format = &format;

	    /* information for possible ~^ */
	    iterate_info.iteration =
		snum_arguments > 0 ? ITERATION_NORMAL : ITERATION_LAST;

	    /* do the format */
	    LispFormat(stream, &iterate_info);

	    /* check for forced loop break */
	    if (iterate_info.upandout & UPANDOUT_HASH)
		break;
	}
    }
    /* ~@{...~} */
    else if (info->args.atsign) {
	/* current argument list is used */

	/* set new arguments to recursive calls */
	iterate_info.base_arguments = info->base_arguments;
	iterate_info.total_arguments = info->total_arguments;
	iterate_info.object = &object;
	iterate_info.arguments = &arguments;
	iterate_info.num_arguments = &num_arguments;

	for (;; iterate++) {
	    /* if maximum iterations done or all arguments consumed */
	    if (has_max && iterate > iterate_max)
		break;
	    else if (num_arguments == 0 && (!has_min || iterate > 0))
		break;

	    format = loop_format;

	    /* set new format string */
	    iterate_info.args.base = iterate_info.args.format = format;
	    iterate_info.format = &format;

	    /* information for possible ~^, in this case ~:^ is a noop */
	    iterate_info.iteration = ITERATION_NORMAL;

	    /* do the format */
	    LispFormat(stream, &iterate_info);

	    /* check for forced loop break */
	    if (iterate_info.upandout & UPANDOUT_HASH)
		break;
	}
    }

    /* free temporary memory */
    free_formats(formats, num_formats);

    /* update anything that may have changed */
    *(info->object) = object;
    *(info->arguments) = arguments;
    *(info->num_arguments) = num_arguments;

    /* this information always updated */
    *(info->format) = next_format;
}

static void
format_justify(LispObj *stream, FmtInfo *info)
{
    GC_ENTER();
    FmtInfo justify_info;
    char **formats, *format, *next_format;
    const char *str;
    LispObj *string, *strings = NIL, *cons;
    int atsign = info->args.atsign,
	collon = info->args.collon,
	mincol = info->args.arguments[0].value,
	colinc = info->args.arguments[1].value,
	minpad = info->args.arguments[2].value,
	padchar = info->args.arguments[3].value;
    int i, k, total_length, length, padding, num_formats, has_default,
	comma_width, line_width, size, extra;

    next_format = *(info->format);

    /* list formats */
    list_formats(info, '<', &next_format, &formats, &num_formats,
		 &has_default, &comma_width, &line_width);

    /* initialize list of strings streams */
    if (num_formats) {
	string = LSTRINGSTREAM("", STREAM_READ | STREAM_WRITE, 0);
	strings = cons = CONS(string, NIL);
	GC_PROTECT(strings);
	for (i = 1; i < num_formats; i++) {
	    string = LSTRINGSTREAM("", STREAM_READ | STREAM_WRITE, 0);
	    RPLACD(cons, CONS(string, NIL));
	    cons = CDR(cons);
	}
    }

    /* most information is the same */
    memcpy(&justify_info, info, sizeof(FmtInfo));

    /* loop formating strings */
    for (i = 0, cons = strings; i < num_formats; i++, cons = CDR(cons)) {
	/* set new format string */
	format = formats[i];
	justify_info.args.base = justify_info.args.format = format;
	justify_info.format = &format;

	/* format string, maybe consuming arguments */
	LispFormat(CAR(cons), &justify_info);

	/* if format was aborted, it is discarded */
	if (justify_info.upandout)
	    RPLACA(cons, NIL);
	/* check if the entire "main" iteration must be aborted */
	if (justify_info.upandout & UPANDOUT_COLLON) {
	    for (cons = CDR(cons); i < num_formats; i++, cons = CDR(cons))
		RPLACA(cons, NIL);
	    break;
	}
    }

    /* free temporary format strings */
    free_formats(formats, num_formats);

    /* remove aborted formats */
	/* first remove leading discarded formats */
    if (CAR(strings) == NIL) {
	while (CAR(strings) == NIL) {
	    strings = CDR(strings);
	    --num_formats;
	}
	/* keep strings gc protected, discarding first entries */
	lisp__data.protect.objects[gc__protect] = strings;
    }
	/* now remove intermediary discarded formats */
    cons = strings;
    while (CONSP(cons)) {
	if (CONSP(CDR(cons)) && CAR(CDR(cons)) == NIL) {
	    RPLACD(cons, CDR(CDR(cons)));
	    --num_formats;
	}
	else
	    cons = CDR(cons);
    }

    /* calculate total length required for output */
    if (has_default)
	cons = CDR(strings);	/* if has_defaults, strings is surely a list */
    else
	cons = strings;
    for (total_length = 0; CONSP(cons); cons = CDR(cons))
	total_length += SSTREAMP(CAR(cons))->length;

    /* initialize pointer to string streams */
    if (has_default)
	cons = CDR(strings);
    else
	cons = strings;

    /* check if padding will need to be printed */
    extra = 0;
    padding = mincol - total_length;
    if (padding < 0)
	k = padding = 0;
    else {
	int num_fields = num_formats - (has_default != 0);

	if (num_fields > 1) {
	    /* check if padding is distributed in num_fields or
	     * num_fields - 1 steps */
	    if (!collon)
		--num_fields;
	}

	if (num_fields)
	    k = padding / num_fields;
	else
	    k = padding;

	if (k <= 0)
	    k = colinc;
	else if (colinc)
	    k = k + (k % colinc);
	extra = mincol - (num_fields * k + total_length);
	if (extra < 0)
	    extra = 0;
    }
    if (padding && k < minpad) {
	k = minpad;
	if (colinc)
	    k = k + (k % colinc);
    }

    /* first check for the special case of only one string being justified */
    if (num_formats - has_default == 1) {
	if (has_default && line_width > 0 && comma_width >= 0 &&
	    total_length + comma_width > line_width) {
	    str = LispGetSstring(SSTREAMP(CAR(strings)), &size);
	    LispWriteStr(stream, str, size);
	}
	string = has_default ? CAR(CDR(strings)) : CAR(strings);
	/* check if need left padding */
	if (k && !atsign) {
	    LispWriteChars(stream, padchar, k);
	    k = 0;
	}
	/* check for centralizing text */
	else if (k && atsign && collon) {
	    LispWriteChars(stream, padchar, k / 2 + ((k / 2) & 1));
	    k -= k / 2;
	}
	str = LispGetSstring(SSTREAMP(string), &size);
	LispWriteStr(stream, str, size);
	/* if any padding remaining */
	if (k)
	    LispWriteChars(stream, padchar, k);
    }
    else {
	LispObj *result;
	int last, spaces_before, padout;

	/* if has default, need to check output length */
	if (has_default && line_width > 0 && comma_width >= 0) {
	    result = LSTRINGSTREAM("", STREAM_READ | STREAM_WRITE, 0);
	    GC_PROTECT(result);
	}
	/* else write directly to stream */
	else
	    result = stream;

	/* loop printing justified text */
	    /* padout controls padding for cases where padding is
	     * is separated in n-1 chunks, where n is the number of
	     * formatted strings.
	     */
	for (i = padout = 0; CONSP(cons); i++, cons = CDR(cons), --extra) {
	    string = CAR(cons);
	    last = !CONSP(CDR(cons));

	    spaces_before = (i != 0 || collon) && (!last || !atsign);

	    if (!spaces_before) {
		/* check for special case */
		if (last && atsign && collon && padding > 0) {
		    int spaces;

		    spaces = minpad > colinc ? minpad : colinc;
		    LispWriteChars(result, padchar, spaces + (extra > 0));
		    k -= spaces;
		}
		str = LispGetSstring(SSTREAMP(string), &size);
		LispWriteStr(result, str, size);
		padout = 0;
	    }
	    if (!padout)
		LispWriteChars(result, padchar, k + (extra > 0));
	    padout = k;
	    /* if not first string, or if left padding specified */
	    if (spaces_before) {
		str = LispGetSstring(SSTREAMP(string), &size);
		LispWriteStr(result, str, size);
		padout = 0;
	    }
	    padding -= k;
	}

	if (has_default && line_width > 0 && comma_width >= 0) {
	    length = SSTREAMP(result)->length + LispGetColumn(stream);

	    /* if current line is too large */
	    if (has_default && length + comma_width > line_width) {
		str = LispGetSstring(SSTREAMP(CAR(strings)), &size);
		LispWriteStr(stream, str, size);
	    }

	    /* write result to stream */
	    str = LispGetSstring(SSTREAMP(result), &size);
	    LispWriteStr(stream, str, size);
	}
    }

    /* unprotect string streams from GC */
    GC_LEAVE();

    /* this information always updated */
    *(info->format) = next_format;
}

static void
LispFormat(LispObj *stream, FmtInfo *info)
{
    FmtArgs *args;
    const FmtDefs *defs = NULL;
    LispObj *object, *arguments;
    char stk[256], *format, *next_format;
    int length, num_arguments, code, need_update, need_argument, hash, head;

    /* arguments that will be updated on function exit */
    format = *(info->format);
    object = *(info->object);
    arguments = *(info->arguments);
    num_arguments = *(info->num_arguments);

    /* initialize */
    length = 0;
    args = &(info->args);
    info->upandout = 0;

    while (*format) {
	if (*format == '~') {
	    /* flush non formatted characters */
	    if (length) {
		LispWriteStr(stream, stk, length);
		length = 0;
	    }

	    need_argument = need_update = hash = 0;

	    /* parse parameters */
	    args->format = format + 1;
	    next_format = parse_arguments(format + 1, args, &num_arguments,
					  &arguments, &code);
	    if (code != NOERROR)
		parse_error(args, code);

	    /* check parameters */
	    switch (args->command) {
		case 'A': case 'S':
		    defs = &AsciiDefs;
		    break;
		case 'B': case 'O': case 'D': case 'X':
		    defs = &IntegerDefs;
		    break;
		case 'R':
		    defs = &RadixDefs;
		    break;
		case 'P': case 'C':
		    defs = &NoneDefs;
		    break;
		case 'F':
		    defs = &FixedFloatDefs;
		    break;
		case 'E': case 'G':
		    defs = &ExponentialFloatDefs;
		    break;
		case '$':
		    defs = &DollarFloatDefs;
		    break;
		case '%': case '&': case '|': case '~': case '\n':
		    defs = &OneDefs;
		    break;
		case 'T':
		    defs = &TabulateDefs;
		    break;
		case '*':
		    defs = &OneDefs;
		    break;
		case '?': case '(':
		    defs = &NoneDefs;
		    break;
		case ')':
		    /* this is never seen, processed in format_case_conversion */
		    format_error(args, "no match for directive ~)");
		case '[':
		    defs = &OneDefs;
		    break;
		case ']':
		    /* this is never seen, processed in format_conditional */
		    format_error(args, "no match for directive ~]");
		case '{':
		    defs = &OneDefs;
		    break;
		case '}':
		    /* this is never seen, processed in format_iterate */
		    format_error(args, "no match for directive ~}");
		case '<':
		    defs = &AsciiDefs;
		    break;
		case '>':
		    /* this is never seen, processed in format_justify */
		    format_error(args, "no match for directive ~>");
		case ';':
		    /* this is never seen here */
		    format_error(args, "misplaced directive ~;");
		case '#':
		    /* special handling for ~#^ */
		    if (*next_format == '^') {
			++next_format;
			hash = 1;
			defs = &NoneDefs;
			args->command = '^';
			break;
		    }
		    parse_error(args, PARSE_BADDIRECTIVE);
		case '^':
		    defs = &NoneDefs;
		    break;
		default:
		    parse_error(args, PARSE_BADDIRECTIVE);
		    break;
	    }
	    merge_arguments(args, defs, &code);
	    if (code != NOERROR)
		merge_error(args, code);

	    /* check if an argument is required by directive */
	    switch (args->command) {
		case 'A': case 'S':
		case 'B': case 'O': case 'D': case 'X': case 'R':
		    need_argument = 1;
		    break;
		case 'P':
		    /* if collon specified, plural is the last print argument */
		    need_argument = !args->collon;
		    break;
		case 'C':
		    need_argument = 1;
		    break;
		case 'F': case 'E': case 'G': case '$':
		    need_argument = 1;
		    break;
		case '%': case '&': case '|': case '~': case '\n':
		    break;
		case 'T':
		    break;
		case '*':			/* check arguments below */
		    need_update = 1;
		    break;
		case '?':
		    need_argument = need_update = 1;
		    break;
		case '(': case '[': case '{': case '<':
		    need_update = 1;
		    break;
		case '^':
		    break;
	    }
	    if (need_argument) {
		if (!CONSP(arguments))
		    parse_error(args, PARSE_NOARGSLEFT);
		object = CAR(arguments);
		arguments = CDR(arguments);
		--num_arguments;
	    }

	    /* will do recursive calls that change info */
	    if (need_update) {
		*(info->format) = next_format;
		*(info->object) = object;
		*(info->arguments) = arguments;
		*(info->num_arguments) = num_arguments;
	    }

	    /* everything seens fine, print the format directive */
	    switch (args->command) {
		case 'A':
		    head = lisp__data.env.length;
		    LispAddVar(Oprint_escape, NIL);
		    ++lisp__data.env.head;
		    format_ascii(stream, object, args);
		    lisp__data.env.head = lisp__data.env.length = head;
		    break;
		case 'S':
		    head = lisp__data.env.length;
		    LispAddVar(Oprint_escape, T);
		    ++lisp__data.env.head;
		    format_ascii(stream, object, args);
		    lisp__data.env.head = lisp__data.env.length = head;
		    break;
		case 'B':
		    format_in_radix(stream, object, 2, args);
		    break;
		case 'O':
		    format_in_radix(stream, object, 8, args);
		    break;
		case 'D':
		    format_in_radix(stream, object, 10, args);
		    break;
		case 'X':
		    format_in_radix(stream, object, 16, args);
		    break;
		case 'R':
		    /* if a single argument specified */
		    if (args->count)
			format_in_radix(stream, object, 0, args);
		    else
			format_radix_special(stream, object, args);
		    break;
		case 'P':
		    if (args->atsign) {
			if (FIXNUMP(object) && FIXNUM_VALUE(object) == 1)
			    LispWriteChar(stream, 'y');
			else
			    LispWriteStr(stream, "ies", 3);
		    }
		    else if (!FIXNUMP(object) || FIXNUM_VALUE(object) != 1)
			LispWriteChar(stream, 's');
		    break;
		case 'C':
		    format_character(stream, object, args);
		    break;
		case 'F':
		    format_fixed_float(stream, object, args);
		    break;
		case 'E':
		    format_exponential_float(stream, object, args);
		    break;
		case 'G':
		    format_general_float(stream, object, args);
		    break;
		case '$':
		    format_dollar_float(stream, object, args);
		    break;
		case '&':
		    if (LispGetColumn(stream) == 0)
			--args->arguments[0].value;
		case '%':
		    LispWriteChars(stream, '\n', args->arguments[0].value);
		    break;
		case '|':
		    LispWriteChars(stream, '\f', args->arguments[0].value);
		    break;
		case '~':
		    LispWriteChars(stream, '~', args->arguments[0].value);
		    break;
		case '\n':
		    if (!args->collon) {
			if (args->atsign)
			    LispWriteChar(stream, '\n');
			/* ignore newline and following spaces */
			while (*next_format && isspace(*next_format))
			    ++next_format;
		    }
		    break;
		case 'T':
		    format_tabulate(stream, args);
		    break;
		case '*':
		    format_goto(info);
		    break;
		case '?':
		    format_indirection(stream, object, info);
		    need_update = 1;
		    break;
		case '(':
		    format_case_conversion(stream, info);
		    /* next_format if far from what is set now */
		    next_format = *(info->format);
		    break;
		case '[':
		    format_conditional(stream, info);
		    /* next_format if far from what is set now */
		    next_format = *(info->format);
		    break;
		case '{':
		    format_iterate(stream, info);
		    /* next_format if far from what is set now */
		    next_format = *(info->format);
		    break;
		case '<':
		    format_justify(stream, info);
		    /* next_format if far from what is set now */
		    next_format = *(info->format);
		    break;
		case '^':
		    if (args->collon) {
			if (hash && num_arguments == 0) {
			    info->upandout = UPANDOUT_HASH;
			    goto format_up_and_out;
			}
			if (info->iteration &&
			    info->iteration == ITERATION_NORMAL)
			/* not exactly an error, but in this case,
			 * command is ignored */
			    break;
			info->upandout = UPANDOUT_COLLON;
			goto format_up_and_out;
		    }
		    else if (num_arguments == 0) {
			info->upandout = UPANDOUT_NORMAL;
			goto format_up_and_out;
		    }
		    break;
	    }

	    if (need_update) {
		object = *(info->object);
		arguments = *(info->arguments);
		num_arguments = *(info->num_arguments);
	    }

	    format = next_format;
	}
	else {
	    if (length >= sizeof(stk)) {
		LispWriteStr(stream, stk, length);
		length = 0;
	    }
	    stk[length++] = *format++;
	}
    }

    /* flush any peding output */
    if (length)
	LispWriteStr(stream, stk, length);

format_up_and_out:
    /* update for recursive call */
    *(info->format) = format;
    *(info->object) = object;
    *(info->arguments) = arguments;
    *(info->num_arguments) = num_arguments;
}

LispObj *
Lisp_Format(LispBuiltin *builtin)
/*
 format destination control-string &rest arguments
 */
{
    GC_ENTER();
    FmtInfo info;
    LispObj *object;
    char *control_string;
    int num_arguments;

    LispObj *stream, *format, *arguments;

    arguments = ARGUMENT(2);
    format = ARGUMENT(1);
    stream = ARGUMENT(0);

    /* check format and stream */
    CHECK_STRING(format);
    if (stream == NIL) {	/* return a string */
	stream = LSTRINGSTREAM("", STREAM_READ | STREAM_WRITE, 0);
	GC_PROTECT(stream);
    }
    else if (stream == T ||	/* print directly to *standard-output* */
	     stream == STANDARD_OUTPUT)
	stream = NIL;
    else {
	CHECK_STREAM(stream);
	if (!stream->data.stream.writable)
	    LispDestroy("%s: stream %s is not writable",
			STRFUN(builtin), STROBJ(stream));
    }

    /* count number of arguments */
    for (object = arguments, num_arguments = 0; CONSP(object);
	 object = CDR(object), num_arguments++)
	;

    /* initialize plural/argument info */
    object = NIL;

    /* the format string */
    control_string = THESTR(format);

    /* arguments to recursive calls */
    info.args.base = control_string;
    info.base_arguments = arguments;
    info.total_arguments = num_arguments;
    info.format = &control_string;
    info.object = &object;
    info.arguments = &arguments;
    info.num_arguments = &num_arguments;
    info.iteration = 0;

    /* format arguments */
    LispFormat(stream, &info);

    /* if printing to stdout */
    if (stream == NIL)
	LispFflush(Stdout);
    /* else if printing to string-stream, return a string */
    else if (stream->data.stream.type == LispStreamString) {
	int length;
	const char *string;

	string = LispGetSstring(SSTREAMP(stream), &length);
	stream = LSTRING(string, length);
    }

    GC_LEAVE();

    return (stream);
}
