/*
 * quetzal.c
 *
 * Quetzal save file manipulation
 *
 */

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <ctype.h>

#include <oslib/osfile.h>

#include "options.h"

#include "ztypes.h"
#include "fileio.h"
#include "quetzal.h"
#include "osdepend.h"
#include "text.h"
#include "input.h"
#include "operand.h"
#include "control.h"
#include "special.h"
#include "unicutils.h"

extern char StoryName[];

static void fput16(zword_t w, FILE *f)
{
    fputc(w >> 8, f);
    fputc(w & 0xFF, f);
}

static void fput24(unsigned w, FILE *f)
{
    fputc((w >> 16) & 0xFF, f);
    fputc((w >> 8) & 0xFF, f);
    fputc(w & 0xFF, f);
}

static void fput32(unsigned int w, FILE *f)
{
    fputc(w >> 24, f);
    fputc((w >> 16) & 0xFF, f);
    fputc((w >> 8) & 0xFF, f);
    fputc(w & 0xFF, f);
}

unsigned fget16(FILE *f)
{
    unsigned w;

    w = fgetc(f);
    w = (w << 8) | fgetc(f);

    return w;
}

unsigned fget24(FILE *f)
{
    unsigned w;

    w = fgetc(f);
    w = (w << 8) | fgetc(f);
    w = (w << 8) | fgetc(f);

    return w;
}

unsigned fget32(FILE *f)
{
    unsigned w;

    w = fgetc(f);
    w = (w << 8) | fgetc(f);
    w = (w << 8) | fgetc(f);
    w = (w << 8) | fgetc(f);

    return w;
}

#define quetzal_FORM 0x464F524D
#define quetzal_IFZS 0x49465A53
#define quetzal_IntD 0x496E7444
#define quetzal_RISC 0x52495343
#define quetzal_____ 0x20202020
#define quetzal_2000 0x32303030
#define quetzal_SNam 0x534E616D
#define quetzal_IFhd 0x49466864
#define quetzal_CMem 0x434D656D
#define quetzal_UMem 0x554D656D
#define quetzal_Stks 0x53746B73

/*
 * save_quetzal
 *
 * Save game state to disk as a Quetzal file. Returns:
 *     0 = save failed
 *     1 = save succeeded
 *
 */

int save_quetzal(const char *filename)
{
    int status = 0;
    FILE *sfp;
    unsigned addr;
    int i;
    long length_pos, pos, length;
    zbyte_t byte;

    if ((sfp = fopen(filename, "wb")) == NULL)
        return 0;

    /* FORM header. Will fill in length later. */

    fput32(quetzal_FORM, sfp);
    fseek(sfp, 4, SEEK_CUR);
    fput32(quetzal_IFZS, sfp);

    /* Story file location chunk */

    fput32(quetzal_IntD, sfp);
    fput32((i=strlen(StoryName))+12, sfp);
    fput32(quetzal_RISC, sfp);
    fputc(2, sfp); /* flags */
    fputc(0, sfp); /* chunk type: 0 = location */
    fput16(0, sfp); /* reserved */
    fput32(quetzal_____, sfp);
    fwrite(StoryName, 1, (i+1)&~1, sfp);

    /* Story file name chunk */
    fput32(quetzal_SNam, sfp);
    if (blorb_name)
    {
        fput32((int) (length = strlen_u(blorb_name))*2, sfp);
        for (i = 0; i < length; i++)
            fput16(blorb_name[i], sfp);
    }
    else
    {
        extern const char *GameTitle(void);
        const char *t = GameTitle();
        fput32((int) (length = strlen(t))*2, sfp);
        for (i = 0; i < length; i++)
            fput16(system_to_unicode(t[i]), sfp);
    }

    /* Game header chunk */

    fput32(quetzal_IFhd, sfp);
    fput32(13, sfp);
    fput16(h_release, sfp);

    fwrite(datap+H_SERIAL_NUMBER, 1, 6, sfp);

    if (h_type >= V3)
        fput16(h_checksum, sfp);
    else
        fput16(file_checksum(), sfp);

    fput24(pc - datap, sfp);

    fputc(0, sfp); /* Pad byte */

    /* The compressed game data chunk */

    fput32(quetzal_CMem, sfp);
    length_pos = ftell(sfp);
    fput32(0, sfp); /* Come back to the length */

    open_story(StoryName);

    length=0;
    addr = 0;

    byte = datap[addr++] ^ get_story_byte();
    do
    {
        if (byte != 0)
        {
            fputc(byte, sfp);
            length++;
            byte = datap[addr++] ^ get_story_byte();
        }
        else
        {
            int len = 0;
            while (byte == 0 && addr < h_static_offset)
            {
                len++;
                byte = datap[addr++] ^ get_story_byte();
            }
            if (addr < h_static_offset) /* Don't need zeroes at end */
            {
                while (len > 256)
                {
                    fputc(0, sfp); fputc(255, sfp);
                    length+=2; len-=256;
                }
                if (len > 0)
                {
                    fputc(0, sfp); fputc(len-1, sfp);
                    length+=2;
                }
            }
        }
    }
    while (addr < h_static_offset);

    if (length & 1) fputc(0, sfp); /* Pad to even length */

    /* Go back and fill in the length */
    pos = ftell(sfp);
    fseek(sfp, length_pos, SEEK_SET);
    fput32((unsigned) length, sfp);
    fseek(sfp, pos, SEEK_SET);

    /* The stack chunk */

    fput32(quetzal_Stks, sfp);
    length_pos = ftell(sfp);
    fseek(sfp, 4, SEEK_CUR); /* Come back to the length */

    /*
     * This is a pain. We need to write from the top down, and we
     * have no way of stepping downwards. Therefore we have
     * to loop inefficiently.
     */

    for (int frame=(h_type == V6 ? 1 : 0); frame<=frame_number; frame++)
    {
        struct zframe *cfp = fp;
        unsigned cpc, flags, retvar, n, args, locals;

        n = (unsigned *) fp - sp;
        if (frame_number > 0);
            n -= locals = fp->locals;

        for (i=frame_number; i>frame; i--)
        {
            struct zframe *nfp = cfp->ret_fp;
            n = (unsigned *) nfp - (unsigned *) (cfp + 1);
            if (i==1)
                locals = 0;
            else
                n -= locals = nfp->locals;
            cfp = nfp;
        }

        if (frame==0)
        {
            cpc = flags = retvar = args = 0;
        }
        else
        {
            cpc = cfp->ret_pc - datap;
            flags = locals;
            if (cfp->type == PROCEDURE)
            {
                flags |= 0x10;
                retvar = 0;
            }
            else
            {
                retvar = get_byte_priv(cpc);
                cpc++;
            }
            args = (1 << cfp->args) - 1;
        }

        fput24(cpc, sfp);
        fputc(flags, sfp);
        fputc(retvar, sfp);
        fputc(args, sfp);
        fput16(n, sfp);
        n += locals;
        for (i=1; i<=n; i++)
            fput16(((unsigned *)cfp)[-i], sfp);

    }

    /* Go back and fill in the length */
    pos = ftell(sfp);
    fseek(sfp, length_pos, SEEK_SET);
    fput32((unsigned) (pos - length_pos - 4), sfp);
    fseek(sfp, pos, SEEK_SET);

    /* And the length of the FORM */
    fseek(sfp, 4, SEEK_SET);
    fput32((unsigned) (pos - 8), sfp);

    if (ferror(sfp) || story_error())
        status=0;
    else
        status=1;

    /* Close game file */

    close_story();
    if (fclose(sfp))
        status=0;
    else
        osfile_set_type(filename, osfile_TYPE_QUETZAL);

    return status;

}/* save */

static int handle_IFhd(FILE *sfp, int length, const zword_t *snam)
{
    if (length < 13)
    {
        info_lookup("BadQuetzal");
        return 0;
    }

    unsigned release, checksum, wanted_checksum;
    char serial[6];

    release = fget16(sfp);
    fread(serial, 1, 6, sfp);
    checksum = fget16(sfp);

    wanted_checksum = h_type < V3 ? file_checksum() : h_checksum;

    if (release != h_release || checksum != wanted_checksum ||
        memcmp(serial, datap+H_SERIAL_NUMBER, 6))
    {
        if (snam)
        {
            char snam_s[strlen_u(snam)+1];
            bool name_matches;
            unicode_string_to_system(snam_s, snam, '?');
            if (blorb_name)
                name_matches = strcmp_u(snam, blorb_name) == 0;
            else
            {
                extern const char *GameTitle(void);
                name_matches = strcmp(snam_s, GameTitle()) == 0;
            }
            info_lookup_1(name_matches ? "BadSaveSame" : "BadSaveDiff",
                          snam_s);
            return 0;
        }

        info_lookup("BadSave");
        return 0;
    }

    pc = datap + fget24(sfp);

    return 1;
}

static int handle_UMem(FILE *sfp, int length)
{
    if (length != h_static_offset)
        return 0;

    fread(datap, 1, length, sfp);
    return 1;
}

static int handle_CMem(FILE *sfp, int length)
{
    int addr = 0;

    open_story(StoryName);

    do
    {
        int byte = fgetc(sfp);
        length--;

        if (byte == 0)
        {
            if (length < 1)
                goto bad;
            byte = fgetc(sfp);
            length--;
            get_story_bytes(datap + addr, byte + 1);
            addr += byte+1;
        }
        else
        {
            byte ^= get_story_byte();
            set_byte_priv(addr, byte);
            addr++;
        }
    } while (addr < h_static_offset && length > 0);

    if (addr < h_static_offset)
        get_story_bytes(datap + addr, h_static_offset - addr);
    else if (length > 0)
    {
        info_lookup("BadQuetzal");
        fseek(sfp, length, SEEK_CUR);
    }

    return 1;

  bad:
    info_lookup("BadQuetzal");
    close_story();
    return 0;
}

static int handle_Stks(FILE *sfp, int length)
{
    unsigned retpc, flags, var, args, eval, locals, proc;
    struct zframe *ofp;
    bool dummy = (h_type != V6);

    sp = stack + STACK_SIZE;
    fp = (struct zframe *) sp;
    frame_number = 0;

    while (length)
    {
        if (length < 8)
            goto bad;

        retpc = fget24(sfp);
        flags = fgetc(sfp);
        var = fgetc(sfp);
        args = fgetc(sfp);
        eval = fget16(sfp);
        length -= 8;

             if (args >= 0x40) args = 7;
        else if (args >= 0x20) args = 6;
        else if (args >= 0x10) args = 5;
        else if (args >= 0x08) args = 4;
        else if (args >= 0x04) args = 3;
        else if (args >= 0x02) args = 2;

        locals = flags & 0x0F;
        proc = flags & 0x10;

        if (!dummy)
        {
            if (!proc)
                retpc--;

            if (sp - (sizeof *fp/sizeof *sp) - locals < stack)
                return 0;

            ofp = fp;
            sp -= ZFRAME_STACK;
            fp = (struct zframe *) sp;
            fp->ret_pc = datap + retpc;
            fp->ret_fp = ofp;
            fp->locals = locals;
            fp->type = proc ? PROCEDURE : FUNCTION;
            fp->args = args;
            frame_number++;

            while (locals)
            {
                *--sp = fget16(sfp);
                locals--;
                length-=2;
            }
        }

        if (sp - eval < stack)
            return 0;

        while (eval)
        {
            *--sp = fget16(sfp);
            eval--;
            length-=2;
        }

        dummy = 0;
    }

    return 1;

  bad:
    info_lookup("BadQuetzal");
    return 0;
}

/*
 * restore_quetzal
 *
 * Restore a game state from disk as a Quetzal file. Returns:
 *     -2 = not a Quetzal file
 *     -1 = restore failed (fatally)
 *      0 = restore failed
 *      1 = restore succeeded
 *
 */

int restore_quetzal(FILE *sfp)
{
    unsigned chunk;
    int total_length, length;
    int had_IFhd=0, had_Mem=0, had_Stks=0;
    zbyte_t *old_pc = pc;
    zword_t *snam = NULL;

    if (fget32(sfp) != quetzal_FORM)
        goto notquetzal;

    total_length = fget32(sfp);

    if (fget32(sfp) != quetzal_IFZS)
        goto notquetzal;

    while (!(had_Mem && had_Stks) && total_length >= 0 && !ferror(sfp))
    {
        chunk = fget32(sfp);
        length = fget32(sfp);
        total_length -= 8;

        if (chunk == quetzal_IFhd && !had_IFhd)
        {
            if (quetzal_started)
                fatal_lookup("WhoseQ");

            if (!handle_IFhd(sfp, length, snam))
                goto bad;

            had_IFhd = 1;
        }
        else if (chunk == quetzal_UMem)
        {
            if (!had_IFhd || !handle_UMem(sfp, length))
                goto bad;

            had_Mem = 1;
        }
        else if (chunk == quetzal_CMem)
        {
            if (!had_IFhd || !handle_CMem(sfp, length))
                goto bad;

            had_Mem = 1;
        }
        else if (chunk == quetzal_Stks)
        {
            if (!had_IFhd || !handle_Stks(sfp, length))
                goto bad;

            had_Stks = 1;
        }
        else if (chunk == quetzal_SNam)
        {
            if (length & 1)
                goto bad;
            snam = malloc(length+2);
            if (snam)
            {
                int i;
                for (i=0; i<(length>>1); i++)
                    snam[i] = fget16(sfp);
                snam[length>>1] = 0;
            }
            else
                fseek(sfp, length, SEEK_CUR);
        }
        else if (chunk == quetzal_IntD && quetzal_started)
        {
            unsigned os, conts, interp;

            if (length < 12)
                goto bad;

            os = fget32(sfp);
            fgetc(sfp);
            conts = fgetc(sfp);
            fget16(sfp);
            interp = fget32(sfp);

            if (os == quetzal_RISC && conts == 0 && interp == quetzal_____)
            {
                fread(StoryName, length - 12, 1, sfp);
                StoryName[length-12] = '\0';
                open_story(StoryName);
                return 1;
            }
            else
                fseek(sfp, length - 12L, SEEK_CUR);
        }
        else
            fseek(sfp, length, SEEK_CUR);

        total_length -= length;
        if (length & 1)
        {
            fgetc(sfp);
            total_length--;
        }
    }

    free(snam);

    if (ferror(sfp) || !had_Mem || !had_Stks)
        return -1;

    return 1;

  bad:
    pc = old_pc;
    rewind(sfp);
    free(snam);
    return had_IFhd ? -1 : 0;

  notquetzal:
    rewind(sfp);
    free(snam);
    return -2;
}

