/* quetzal.c
 *
 * routines to handle QUETZAL save format
 */

#include <stdio.h>
#include <unistd.h>
#include "ztypes.h"

/* You may want to define these as getc and putc, but then the code gets
 * quite big (especially for put_c).
 */
#define get_c fgetc
#define put_c fputc

#ifndef SEEK_CUR
#define SEEK_CUR 1
#endif

#if defined(USE_QUETZAL)    /* don't compile anything otherwise */

typedef unsigned long ul_t;

#define makeid(a,b,c,d) ((ul_t)(((a)<<24) | ((b)<<16) | ((c)<<8) | (d)))

/* IDs of chunks we understand */
#define ID_FORM makeid('F','O','R','M')
#define ID_IFZS makeid('I','F','Z','S')
#define ID_IFhd makeid('I','F','h','d')
#define ID_UMem makeid('U','M','e','m')
#define ID_CMem makeid('C','M','e','m')
#define ID_Stks makeid('S','t','k','s')
#define ID_ANNO makeid('A','N','N','O')

/* macros to write QUETZAL files */
#define write_byte(fp,b) (put_c (b,fp) != EOF)
#define write_bytx(fp,b) write_byte(fp,(b) & 0xFF)
#define write_word(fp,w) \
    (write_bytx(fp,(w)>> 8) && write_bytx(fp,(w)))
#define write_long(fp,l) \
    (write_bytx(fp,(l)>>24) && write_bytx(fp,(l)>>16) && \
     write_bytx(fp,(l)>> 8) && write_bytx(fp,(l)))
#define write_chnk(fp,id,len) \
    (write_long(fp,id)      && write_long(fp,len))
#define write_run(fp,run) \
    (write_byte(fp,0)       && write_byte(fp,(run)))

/* save_quetzal
 *
 * attempt to save game in QUETZAL format; return TRUE on success
 */

#if defined(__STDC__)
int save_quetzal (FILE *sfp, FILE *gfp, long zoffset)
#else
int save_quetzal (sfp,gfp,zoffset)
    FILE *sfp,*gfp;
    long zoffset;
#endif
{
    ul_t ifzslen = 0, cmemlen = 0, stkslen = 0, tmp_pc;
    int c;
    zword_t i, j, n, init_fp, tmp_fp, nstk, nvars, args;
    zword_t frames[STACK_SIZE/4+1];
    zbyte_t var;
    long cmempos,stkspos;

    /* write IFZS header */
    if (!write_chnk(sfp,ID_FORM,0))            return FALSE;
    if (!write_long(sfp,ID_IFZS))            return FALSE;

    /* write IFhd chunk */
    if (!write_chnk(sfp,ID_IFhd,13))            return FALSE;
    if (!write_word(sfp,h_version))            return FALSE;
    for (i=0; i<6; ++i)
        if(!write_byte(sfp,get_byte(H_RELEASE_DATE+i)))    return FALSE;
    if (!write_word(sfp,h_checksum))            return FALSE;
    if (!write_long(sfp,pc<<8)) /* includes pad byte */    return FALSE;

    /* write CMem chunk */
    /* j is current run length */
    if ((cmempos = ftell(sfp)) < 0)            return FALSE;
    if (!write_chnk(sfp,ID_CMem,0))            return FALSE;
    fseek (gfp, zoffset, SEEK_SET);
    for (i=0, j=0, cmemlen=0; i < h_restart_size; ++i) {
        if ((c = get_c (gfp)) == EOF)            return FALSE;
        c ^= get_byte (i);
        if(c == 0)
            ++j;
        else {
            /* write any run there may be */
            if (j > 0) {
                for (; j > 0x100; j -= 0x100) {
                    if (!write_run(sfp,0xFF))        return FALSE;
                    cmemlen += 2;
                }
                if (!write_run(sfp,j-1))        return FALSE;
                cmemlen += 2;
                j = 0;
            }
            /* write this byte */
            if (!write_byte(sfp,c))            return FALSE;
            ++cmemlen;
        }
    }
    /* there may be a run here, which we ignore */
    if (cmemlen & 1)    /* chunk length must be even */
        if (!write_byte(sfp,0))                return FALSE;

    /* write Stks chunk */
    if ((stkspos = ftell(sfp)) < 0)            return FALSE;
    if (!write_chnk(sfp,ID_Stks,0))            return FALSE;

    /* frames is a list of FPs, most recent first */
    frames[0] = sp-5;    /* what FP would be if we did a call now */
    for (init_fp=fp, n=0; init_fp <= STACK_SIZE-5; init_fp=stack[init_fp+2]) {
	frames[++n] = init_fp;
    }
    init_fp = frames[n] + 4;

    if (h_type != 6) {    /* write a dummy frame for stack used before first call */
        for (i = 0; i < 6; ++i)
            if (!write_byte(sfp,0))            return FALSE;
        nstk = STACK_SIZE - 1 - init_fp;
        if (!write_word(sfp,nstk))            return FALSE;
        for(i = STACK_SIZE-1; i > init_fp; --i)
            if (!write_word(sfp,stack[i]))        return FALSE;
        stkslen = 8 + 2*nstk;
        }
    for (i=n; i>0; --i) {
        /* write out one stack frame.
         *
         *  tmp_fp : FP when this frame was current
         *  tmp_pc : PC on return from this frame, plus 000pvvvv
         *  nvars  : number of local vars for this frame
         *  args   : argument mask for this frame
         *  nstk   : words of evaluation stack used for this frame
         *  var    : variable to store result
         */
        tmp_fp = frames[i];
        nvars  = (stack[tmp_fp+1] & VARS_MASK) >> VAR_SHIFT;
        args   = stack[tmp_fp+1] & ARGS_MASK;
        nstk   = tmp_fp - frames[i-1] - nvars - 4;
        tmp_pc = stack[tmp_fp+3] + (ul_t) stack[tmp_fp+4]*PAGE_SIZE;
        switch(stack[tmp_fp+1] & TYPE_MASK) {
            case FUNCTION:
                var = read_data_byte (&tmp_pc);    /* also increments tmp_pc */
                tmp_pc = (tmp_pc << 8) | nvars;
                break;
            case PROCEDURE:
                var = 0;
                tmp_pc = (tmp_pc << 8) | 0x10 | nvars; /* set procedure flag */
                break;
         /* case ASYNC: */
            default:
                output_line ("Illegal Z-machine operation: can't save while in interrupt.");
                return FALSE;
            }
        if (args != 0)
            args = (1 << args)-1;    /* make args into bitmap */
        if (!write_long(sfp,tmp_pc))            return FALSE;
        if (!write_byte(sfp,var))            return FALSE;
        if (!write_byte(sfp,args))            return FALSE;
        if (!write_word(sfp,nstk))            return FALSE;
        for (j=0; j<nvars+nstk; ++j, --tmp_fp)
            if (!write_word (sfp, stack[tmp_fp]))       return FALSE;
        stkslen += 8 + 2 * (nvars + nstk);
    }

    /* fill in lengths for variable-sized chunks */
    ifzslen = 3*8 + 4 + 14 + cmemlen + stkslen;
    if (cmemlen & 1)
        ++ifzslen;
    (void) fseek (sfp,   (long)4, SEEK_SET);
    if (!write_long(sfp,ifzslen))            return FALSE;
    (void) fseek (sfp, cmempos+4, SEEK_SET);
    if (!write_long(sfp,cmemlen))            return FALSE;
    (void) fseek (sfp, stkspos+4, SEEK_SET);
    if (!write_long(sfp,stkslen))            return FALSE;

    return TRUE;
}

/* read_word
 *
 * attempt to read a word; return TRUE on success
 */

#if defined(__STDC__)
static int read_word (FILE *fp, zword_t *result)
#else
static int read_word (fp,result)
    FILE *fp;
    zword_t *result;
#endif
{
    int a,b;
    if ((a = get_c (fp)) == EOF) return FALSE;
    if ((b = get_c (fp)) == EOF) return FALSE;
    *result = ((zword_t) a << 8) | b;
    return TRUE;
}

/* read_long
 *
 * attempt to read a longword; return TRUE on success
 */

#if defined(__STDC__)
static int read_long(FILE *fp, ul_t *result)
#else
static int read_long(fp,result)
    FILE *fp;
    ul_t *result;
#endif
{
    int a,b,c,d;
    if ((a = get_c (fp)) == EOF) return FALSE;
    if ((b = get_c (fp)) == EOF) return FALSE;
    if ((c = get_c (fp)) == EOF) return FALSE;
    if ((d = get_c (fp)) == EOF) return FALSE;
    *result = ((ul_t) a << 24) | ((ul_t) b << 16) | ((ul_t) c << 8) | d;
    return TRUE;
}

/* restore_quetzal
 *
 * attempt to restore game in QUETZAL format; return TRUE on success
 */

#define GOT_HEADER    0x01
#define GOT_STACK    0x02
#define GOT_MEMORY    0x04
#define GOT_NONE    0x00
#define GOT_ALL        0x07
#define GOT_ERROR    0x80

#if defined(__STDC__)
int restore_quetzal (FILE *sfp, FILE *gfp, long zoffset)
#else
int restore_quetzal (sfp,gfp,zoffset)
    FILE *sfp,*gfp;
    long zoffset;
#endif
{
    ul_t ifzslen, currlen, tmpl;
    zword_t i, tmpw;
    zbyte_t skip, progress = GOT_NONE;
    int x, y;

    /* check for IFZS file */
    if (!read_long (sfp,&tmpl) || !read_long (sfp,&ifzslen))    return FALSE;
    if (!read_long (sfp,&currlen))                return FALSE;
    if (tmpl != ID_FORM || currlen != ID_IFZS) {
        output_line("This is not a saved game file!");
        return FALSE;
    }
    if ((ifzslen & 1) || ifzslen<4)                return FALSE;
    ifzslen -= 4;

    /* read a chunk and process it */
    while (ifzslen > 0) {
        /* read chunk header */
        if (ifzslen < 8)                    return FALSE;
        if (!read_long(sfp,&tmpl) || !read_long(sfp,&currlen))    return FALSE;
        ifzslen -= 8;

        /* body of chunk */
        if (ifzslen < currlen)                    return FALSE;
        skip = currlen & 1;
        ifzslen -= currlen+skip;
        switch (tmpl) {
            case ID_IFhd:
                if (progress & GOT_HEADER) {
                    output_line ("Save file has two IFhd chunks!");
                    return FALSE;
                }
                progress |= GOT_HEADER;
                if (currlen < 13 || !read_word (sfp,&i))    return FALSE;
                if (i != h_version)
                progress = GOT_ERROR;
                for (i=H_RELEASE_DATE; i<H_RELEASE_DATE+6; ++i) {
                    if ((x = get_c (sfp)) == EOF)        return FALSE;
                    if (x != get_byte (i))
                        progress = GOT_ERROR;
                }
                if (!read_word (sfp,&i))            return FALSE;
                if (i != h_checksum)
                    progress = GOT_ERROR;
                if (progress == GOT_ERROR) {
                    output_line ("File was not saved from this story!");
                    return FALSE;
                }
                if ((x = get_c (sfp)) == EOF)            return FALSE;
                pc = x << 16;
                if ((x = get_c (sfp)) == EOF)            return FALSE;
                pc |= x << 8;
                if ((x = get_c (sfp)) == EOF)            return FALSE;
                pc |= x;
                for (i=13; i<currlen; ++i)
                    (void) get_c (sfp);    /* skip rest of chunk */
                break;
            case ID_Stks:
                if (progress & GOT_STACK) {
                    output_line ("File contains two stack chunks!");
                    break;
                }
                progress |= GOT_STACK;
                sp = STACK_SIZE;
                if (h_type != 6) {
                    /* dummy stack frame for stack used before call */
                    if (currlen < 8)                return FALSE;
                    for (i=0; i<6; ++i)
                        if (get_c (sfp) != 0)            return FALSE;
                    if (!read_word (sfp,&tmpw))            return FALSE;
                    currlen -= 8;
                    if (currlen < tmpw*2)            return FALSE;
                    for (i=0; i<tmpw; ++i)
                        if (!read_word(sfp,stack+(--sp)))    return FALSE;
                    currlen -= tmpw*2;
                    }
                for (fp=STACK_SIZE-1, frame_count=0; currlen > 0; currlen-=8) {
                    if (currlen < 8)                return FALSE;
                    if (sp<4) {
                        output_line ("error: this save-file has too much stack, and I can't cope.");
                        return FALSE;
                    }
                    /* read PC, procedure flag, and arg count */
                    if (!read_long(sfp,&tmpl))            return FALSE;
                    y = (zword_t) tmpl & 0x0F;
                    tmpw = y << VAR_SHIFT;    /* number of variables */
                    /* read result variable  */
                    if ((x = get_c (sfp)) == EOF)        return FALSE;

                    if (tmpl & 0x10) {
                        tmpw |= PROCEDURE;
                        tmpl >>= 8;
                        }
                    else {
                        tmpw |= FUNCTION;
                        tmpl >>= 8;
                        --tmpl;
                        /* sanity check on result variable */
                        if (read_data_byte (&tmpl) != (zbyte_t) x) {
                            output_line ("error: wrong variable number on stack (wrong story file?).");
                            return FALSE;
                        }
                        --tmpl;    /* read_data_byte increments it */
                    }
                    stack[--sp] = (zword_t) (tmpl / PAGE_SIZE);
                    stack[--sp] = (zword_t) (tmpl % PAGE_SIZE);
                    stack[--sp] = fp;

                    if ((x = get_c (sfp)) == EOF)        return FALSE;
                    ++x;    /* hopefully x now contains a single set bit */

                    for (i=0; i<8; ++i)
                        if (x & (1<<i)) break;
                    if (x ^ (1<<i))    /* if more than 1 bit set */ {
                        output_line ("error: this game uses incomplete argument lists (which I can't handle).");
                        return FALSE;
                    }
                    tmpw |= i;
                    stack[--sp] = tmpw;
                    fp = sp - 1;    /* FP for next frame */
                    if (!read_word(sfp,&tmpw))            return FALSE;
                    tmpw += y;    /* local vars plus eval stack used */
                    if (tmpw>=sp) {
                        output_line ("error: this save-file uses more stack than I can cope with.");
                        return FALSE;
                    }
                    if (currlen < tmpw*2)            return FALSE;
                    for (i=0; i<tmpw; ++i)
                        if (!read_word(sfp,stack+(--sp)))    return FALSE;
                    currlen -= tmpw*2;
                    }
                break;
	    /* case ID_ANNO:
                set_format_mode (ON);
                for (; currlen > 0; --currlen) {
		    if ((x = get_c (sfp)) == EOF)        return FALSE;
		    else write_char (x);
		}
		write_char ((char) 13);
                break; */
            case ID_CMem:
                if (!(progress & GOT_MEMORY)) {
		    fseek (gfp, zoffset, SEEK_SET); 
                    i=0;    /* bytes written to data area */
                    for (; currlen > 0; --currlen) {
                        if ((x = get_c (sfp)) == EOF)        return FALSE;
                        if (x == 0) /* start run */ {
                            if (currlen < 2) {
                                output_line ("File contains bogus CMem chunk");
                                for (; currlen > 0; --currlen)
                                    (void) get_c (sfp);    /* skip rest */
                                currlen = 1;
                                i = 0xFFFF;
                                break;    /* keep going in case there's a UMem */
                            }
                            --currlen;
                            if ((x = get_c (sfp)) == EOF)    return FALSE;
                            for (; x>=0 && i<h_restart_size; --x, ++i)
                                if ((y = get_c (gfp)) == EOF)    return FALSE;
                                else set_byte (i,y);
                        }
                        else        /* not a run */ {
                            if ((y = get_c (gfp)) == EOF)        return FALSE;
                            set_byte (i,x^y);
                            ++i;
                        }
                        if (i>h_restart_size) {
                            output_line ("warning: CMem chunk too long!");
                            for (; currlen > 1; --currlen)
                                (void) get_c (sfp);        /* skip rest */
                            break;        /* keep going in case there's a UMem */
                        }
                    }
                    /* if chunk is short, assume a run */
                    for (; i<h_restart_size; ++i)
                        if ((y = get_c (gfp)) == EOF)        return FALSE;
                        else set_byte (i,y);
                    if (currlen == 0)
                        progress |= GOT_MEMORY;    /* only if succeeded */
                    break;
                    }
                /* Fall thru (to default) if already got memory */
            case ID_UMem:
                if (!(progress & GOT_MEMORY)) {
                    if (currlen == h_restart_size) {
                        if (fread(datap,h_restart_size,1,sfp)==1) {
                            progress |= GOT_MEMORY;    /* only if succeeded */
                            break;
                        }
                    }
                    else output_line ("warning: UMem chunk wrong size!");
                        /* and fall thru into default */
                }
                /* Fall thru (to default) if already got memory */
            default:
                (void) fseek (sfp, currlen, SEEK_CUR);    /* skip chunk */
                break;
        }
        if (skip)
            (void) get_c (sfp);    /* skip pad byte */
    }

    if (!(progress & GOT_HEADER))
        output_line ("error: no header chunk in file.");
    if (!(progress & GOT_STACK))
        output_line ("error: no stack chunk in file.");
    if (!(progress & GOT_MEMORY))
        output_line ("error: no memory chunk in file.");
    return (progress == GOT_ALL);
}

#endif    /* defined(USE_QUETZAL) */
