/*
 * fileio.c
 *
 * File manipulation routines. Should be generic.
 *
 */

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

#include <oslib/osfile.h>
#include <oslib/osfind.h>
#include <oslib/osgbpb.h>

#include "options.h"

#include "ztypes.h"
#include "fileio.h"
#include "osdepend.h"
#include "text.h"
#include "input.h"
#include "operand.h"
#include "screen.h"
#include "control.h"
#include "roinput.h"
#include "special.h"
#include "quetzal.h"
#include "unicutils.h"
#include "blorb.h"
#include "blorblow.h"

#define MAX_UNDO 20

/* Static data */

static FILE *gfp = NULL; /* Game file pointer */
static FILE *sfp = NULL; /* Script file pointer */
static FILE *rfp = NULL; /* Record file pointer */
FILE *bfp = NULL; /* Blorb file pointer */

/* Last used filename - for defaults */
static char save_name[FILENAME_SIZE];
static char script_name[FILENAME_SIZE];
static char record_name[FILENAME_SIZE];
static char aux_name[FILENAME_SIZE];

bb_map_t *blorb_map;
char *blorb_copyright, *blorb_author, *blorb_anno, *blorb_name_s;
zword_t *blorb_name;
unsigned blorb_release;

static unsigned zcode_offset, zcode_length;

static int undo_count, undo_valid;
int undo_slots, option_undo_slots=1;

int quetzal_started;
char *quetzal_to_load;

zword_t *undo_stack[MAX_UNDO];
zbyte_t *undo_datap[MAX_UNDO];

static int script_file_valid = FALSE;
static int filetype_identified = FALSE;
int zcode_in_blorb;

unsigned h_release;
unsigned h_checksum;

#define blorb_FORM 0x4D524F46 /* Little-endian */
#define blorb_IFRS 0x53524649 /* Little-endian */
#define blorb_C    0x28632920
#define blorb_AUTH 0x41555448
#define blorb_ANNO 0x414E4E4F
#define blorb_RelN 0x52656C4E
#define blorb_IFhd 0x49466864
#define blorb_SNam 0x534E616D
#define blorb_ZCOD 0x5A434F44

#define quetzal_FORM 0x4D524F46
#define quetzal_IFZS 0x535A4649

static void load_blorb_ascii(unsigned chunk, char **ptr)
{
    bb_err_t err;
    bb_result_t res;

    free(*ptr);
    *ptr = NULL;

    err = bb_load_chunk_by_type(blorb_map, bb_method_FilePos,
                                &res, chunk, 0);
    if (!err)
    {
        *ptr = (char *)malloc((size_t) res.length+1);
        if (!*ptr) return;
        fseek(bfp, res.data.startpos, SEEK_SET);
        if (fread(*ptr, 1, (size_t) res.length, bfp) < res.length)
        {
            free(*ptr); *ptr = NULL;
        }
        (*ptr)[res.length] = '\0';
    }
}

static void load_blorb_ucs2(unsigned chunk, zword_t **ptr, char **ptr2)
{
    bb_err_t err;
    bb_result_t res;
    int i;

    free(*ptr);
    *ptr = NULL;

    err = bb_load_chunk_by_type(blorb_map, bb_method_FilePos,
                                &res, chunk, 0);
    if (!err)
    {
        res.length/=2;
        *ptr = (zword_t *)calloc((size_t)res.length+1, sizeof(zword_t));
        if (!*ptr) return;
        fseek(bfp, res.data.startpos, SEEK_SET);
        for (i=0; i < res.length; i++)
            (*ptr)[i] = fget16(bfp);
        if (ferror(bfp))
        {
            free(*ptr); *ptr = NULL;
        }
        (*ptr)[res.length] = '\0';

        if (ptr2)
        {
            free(*ptr2);
            *ptr2 = malloc((size_t)res.length*3+1);
            unicode_string_to_system(*ptr2, *ptr, '?');
            *ptr2 = realloc(*ptr2, strlen(*ptr2) + 1);
        }
    }
}

static void parse_blorb_info(void)
{
    /* Get all the copyright info etc */
    load_blorb_ascii(blorb_C, &blorb_copyright);
    load_blorb_ascii(blorb_ANNO, &blorb_anno);
    load_blorb_ascii(blorb_AUTH, &blorb_author);
    load_blorb_ucs2(blorb_SNam, &blorb_name, &blorb_name_s);
    blorb_release = bb_get_release_num(blorb_map);
}

/*
 * load_extra_blorb
 *
 * If main file isn't Blorb, find an auxiliary Blorb file
 */
void load_extra_blorb(void)
{
    char filename[1024];
    char *leaf;
    bb_err_t err;
    extern char StoryName[];

    if (blorb_map) return;

    leaf = strrchr(StoryName, '.')+1;
    strcpy(filename, StoryName);
    strcpy(strrchr(filename, '.')+1, "Blorb");
    bfp = fopen(filename, "rb");
    if (!bfp)
    {
        sprintf(strrchr(filename, '.')+1, "^.blorb.%s", leaf);
        bfp = fopen(filename, "rb");
    }
    if (!bfp)
    {
        sprintf(filename, "<Zip2000$Dir>.Resources.Blorb.%s", leaf);
        bfp = fopen(filename, "rb");
    }
    if (!bfp) return;

    err = bb_create_map(bfp, &blorb_map);
    if (err)
    {
        fclose(bfp);
        bfp = NULL;
        blorb_map = NULL;
        warning_lookup_1("BlorbErr", bb_err_to_string(err));
    }
    else
    {
        bb_zheader_t *zh = bb_get_zheader(blorb_map);
        if (zh)
        {
            if (zh->releasenum != h_release ||
                zh->checksum != h_checksum ||
                memcmp(zh->serialnum, datap+H_SERIAL_NUMBER, 6))
            {
                warning_lookup_1("WrongBlorb", filename);
            }
        }
        parse_blorb_info();
    }
}

/*
 * open_story
 *
 * Open game file for read.
 *
 */

void open_story(const char *storyname)
{
    if (!gfp)
    {
        unsigned head[3];

    	gfp = fopen(storyname, "rb");
    	if (gfp == NULL)
            fatal(strerror(errno));

        if (!filetype_identified)
        {
            if (fread(head, sizeof(unsigned), 3, gfp) < 3)
            {
                int error = ferror(gfp);
                fclose(gfp); gfp = NULL;
                if (error)
                    fatal(strerror(errno));
                else
                    fatal_lookup("GameErr");
                return;
            }

            if (head[0] == blorb_FORM && head[2] == blorb_IFRS)
            {
                bb_err_t err = bb_create_map(gfp, &blorb_map);
                bb_result_t res;

                if (err)
                {
                    fatal_lookup_1("BlorbErr", bb_err_to_string(err));
                    return;
                }

                err = bb_load_resource(blorb_map, bb_method_FilePos,
                                       &res, bb_ID_Exec, 0);
                if (err == bb_err_NotFound ||
                    err == bb_err_None &&
                         blorb_map->chunks[res.chunknum].type != blorb_ZCOD)
                {
                    fatal_lookup("NoGame");
                    return;
                }
                else if (err)
                {
                    fatal_lookup_1("BlorbErr", bb_err_to_string(err));
                    return;
                }

                bfp = gfp;
                zcode_in_blorb = TRUE;
                zcode_offset = (unsigned)res.data.startpos;
                zcode_length = (unsigned)res.length;

                parse_blorb_info();
            }
            else if (head[0] == quetzal_FORM && head[2] == quetzal_IFZS)
            {
                FILE *qfp = gfp;
                if (quetzal_started)
                    fatal_lookup("WhoseQ");

                quetzal_to_load = malloc(strlen(storyname)+1);
                if (!quetzal_to_load)
                        fatal_lookup("NoMem");
                strcpy(quetzal_to_load, storyname);

                gfp = NULL;
                quetzal_started = TRUE;
                rewind(qfp);
                restore_quetzal(qfp);
                fclose(qfp);

                return;
            }
            filetype_identified = TRUE;
        }
    }

    fseek(gfp, zcode_offset, SEEK_SET);
    clearerr(gfp);

}/* open_story */

/*
 * close_story
 *
 * Close game file if open.
 *
 */

void close_story (void)
{
    if (gfp != NULL && !zcode_in_blorb)
    {
        fclose (gfp);
        gfp=NULL;
    }

}/* close_story */

/*
 * get_story_byte
 *
 * Get next byte from game file.
 *
 */

int get_story_byte(void)
{
    return getc(gfp);
}

/*
 * get_story_bytes
 *
 * Get next len bytes from game file.
 *
 */

void get_story_bytes(void *dst, int len)
{
    fread(dst, 1, len, gfp);
}

/*
 * story_error
 *
 * Return error indicator for game file.
 *
 */

int story_error(void)
{
    return ferror(gfp);
}

/*
 * get_story_size
 *
 * Calculate the size of the game file. Only used for very old games that do not
 * have the game file size in the header.
 *
 */

extern char StoryName[];

unsigned int get_story_size(void)
{
    int length;

    if (zcode_in_blorb)
        length = zcode_length;
    else
        osfile_read(StoryName, 0, 0, &length, 0);

    /* Calculate length of file in game allocation units */

    length = (length + ((1 << story_shift) - 1)) >> story_shift;

    return length;

}

/*
 * read_game
 *
 * Read the whole game
 *
 */

void read_game(void *buffer, int len)
{
    int unread;
    os_fw f;
    os_error *e;

    f=osfind_openinw(osfind_ERROR_IF_ABSENT|osfind_ERROR_IF_DIR, StoryName, 0);

    e=xosgbpb_read_atw(f, buffer, len, zcode_offset, &unread);

    osfind_closew(f);

    if (e)
    	os_generate_error(e);

    if (unread > 0)
    	fatal_lookup("ShrtFile");
}

/*
 * read_page
 *
 * Read one game file page.
 *
 */

void read_from_story_file(void *buffer, long start, size_t len)
{
    fseek(gfp, zcode_offset + start, SEEK_SET);

    if (fread(buffer, 1, len, gfp) != len)
        fatal_lookup("GameErr");

}/* read_page */

zword_t file_checksum(void)
{
    static zword_t file_check;
    static int calculated;

    if (!calculated)
    {
        unsigned long file_size, i;
        unsigned checksum = 0;

        file_size = (unsigned long) h_file_size << size_shift;

        open_story(StoryName);

        fseek(gfp, 64L, SEEK_CUR);

        /* Sum all bytes in game file, except header bytes */

        for (i = 64; i < file_size; i++)
             checksum += getc(gfp);

        file_check = (zword_t) checksum;
        calculated = 1;
    }

    return file_check;
}

/*
 * verify
 *
 * Verify game ($verify verb). Add all bytes in game file except for bytes in
 * the game file header.
 *
 */

void z_verify(void)
{
    if (h_type < V3)
    {
    	warn_bad_use("verify");
    	conditional_jump(FALSE);
    	return;
    }

    /* Print version banner */

    if (h_type < V4)
    {
        write_string(msgs_lookup("B1"));
        z_print_num(h_interpreter);
        write_string(msgs_lookup("B2"));
        write_char(h_interpreter_version);
        write_string(msgs_lookup("B3"));
        z_new_line();
    }

    /* Make a conditional jump based on whether the checksum is equal */

    conditional_jump(file_checksum() == h_checksum);

}/* verify */

/*
 * piracy
 *
 * Branches if the game disc is believed to be genuine
 *
 */

void z_piracy(void)
{
    conditional_jump(TRUE);

}/* piracy */

/*
 * save_named
 *
 * Saves a chunk of game to disk. Returns:
 *     0 = save failed
 *     1 = save succeeded
 *
 */

static int save_named(int table, int bytes, int name, int prompt)
{
    char new_save_name[FILENAME_SIZE];
    int status = 0;
    int len;
    FILE *tfp=NULL;

    len=get_byte(name);
    memcpy(new_save_name, datap+name+1, len);
    new_save_name[len]='\0';

    if (get_file_name(new_save_name, aux_name, GAME_AUXSAVE, prompt)==0)
    {
        const char *s = os_file_supplied();

        tfp=fopen(new_save_name, "wb");

        if (tfp)
        {
            if (fwrite(datap+table, 1, bytes, tfp) == bytes)
                status=1;

            fclose(tfp);
        }

        os_file_saved(status);

        if (status==1 && !s)
            strcpy(aux_name, new_save_name);
    }

    return status;

}/* save_named */

/*
 * save
 *
 * Save game state to disk. Returns:
 *     0 = save failed
 *     1 = save succeeded
 *
 */

void z_save(int count, unsigned *operand)
{
    char new_save_name[FILENAME_SIZE];
    int status = 0;

    if (count > 0)
    	status=save_named(operand[0], operand[1], operand[2], count>=4 ? operand[3] : -1);
    else
    {
        const char *s;

        /* Get the file name */

        if (get_file_name(new_save_name, save_name, GAME_SAVE, -1) != 0)
            goto finished;

        s = os_file_supplied();

        status = save_quetzal(new_save_name);

        os_file_saved(status);

        if (status == 1 && !s)
            strcpy(save_name, new_save_name);
    }

  finished:

    /* Return result of save to Z-code */

    if (h_type < V4)
        conditional_jump(status != 0);
    else
        store_operand(status);

}/* save */

/*
 * restore_named                                    KJB
 *
 * Restores a chunk of game from disk. Returns:
 *     0 = restore failed
 *     otherwise number of bytes loaded
 *
 */

static int restore_named(int table, int bytes, int name, int prompt)
{
    char new_save_name[FILENAME_SIZE];
    int status = 0;
    int len;
    FILE *tfp=NULL;

    len=get_byte(name);
    memcpy(new_save_name, datap+name+1, len);
    new_save_name[len]='\0';

    if (get_file_name(new_save_name, aux_name, GAME_AUXLOAD, prompt)==0)
    {
        const char *s = os_file_supplied();

        tfp=fopen(new_save_name, "rb");

        if (tfp)
        {
            status=fread(datap+table, 1, bytes, tfp);
            fclose(tfp);
        }

        os_file_loaded();

        if (!s) output_new_line();
    }

    return status;

}/* restore_named */

/*
 * Restore the game from a named file. Returns:
 *     0 = failed
 */
static int restore_game(const char *new_save_name)
{
    int status = 0;
    FILE *sfp;
    int i;
    int scripting_flag;

    if ((sfp = fopen(new_save_name, "rb")) == NULL)
    {
        os_file_loaded();
 	goto finished;
    }

    /* Make a note of the scripting flag */
    scripting_flag = get_word_priv(H_FLAGS) & SCRIPTING_FLAG;

    i = restore_quetzal(sfp);
    fclose(sfp);

    os_file_loaded();
    if (i == -1)
        fatal_lookup("BadLoad");
    else if (i == 0)
    {
        goto finished;
    }
    else if (i == -2)
    {
        info_lookup("BadQuetzal");
        goto finished;
    }

    /* Restore scripting if necessary */
    if (scripting_flag)
        set_word_priv(H_FLAGS, get_word(H_FLAGS) | SCRIPTING_FLAG);

    /* Reset the status region (this is just for Seastalker) */

    if (h_type == V3)
    {
        z_split_window(0);
        blank_status_line();
    }

    /* Save the new name as the default file name */

    strcpy(save_name, new_save_name);

    /* Restore some relevant stuff */

    restart_header();

    /* Indicate success */

    status = 2;

  finished:

    return status;
}

/*
 * restore
 *
 * Restore game state from disk. Returns:
 *     0 = restore failed
 *     2 = restore succeeded
 *
 */

void z_restore(int count, unsigned *operand)
{
    char new_save_name[FILENAME_SIZE];
    int status = 0;

    if (count > 0)
    	status=restore_named(operand[0], operand[1], operand[2], count>=4 ? operand[3] : -1);
    else
    {
        /* Get the file name */

        if (get_file_name (new_save_name, save_name, GAME_RESTORE, -1) == 0)
        {
            const char *s = os_file_supplied();

            /* Do the restore operation */
            status = restore_game(new_save_name);

            /* Finally spit a new-line out if it was a drag in. Couldn't
             * do it before because we didn't want to poll.
             */
            if (s) output_new_line();
        }
    }

    /* Return result of restore to Z-code */

    if (h_type < V4)
        conditional_jump(status != 0);
    else
        store_operand(status);

}/* restore */

void restore_drag(const char *filename)
{
    int status = restore_game(filename);

    if (status == 2)
    {
        if (h_type < V4)
            conditional_jump(TRUE);
        else
            store_operand(2);

        reset_line_counts();

        restore_longjmp();
    }
}

/*
 * save_undo
 *
 * Save the current Z machine state in memory for a future undo. Returns:
 *    -1 = feature unavailable
 *     0 = save failed
 *     1 = save succeeded
 *
 */

void z_save_undo(void)
{
    if (undo_slots == 0)
    {
        store_operand(-1);
        return;
    }

    if (undo_count == undo_slots)
    	undo_count = 0;

    /* Our stack is really 32-bit, so we can get away with this :-) */
    stack[0] = (unsigned) pc;
    stack[1] = (unsigned) fp;
    stack[2] = (unsigned) sp;
    stack[3] = frame_number;

    /* Save the undo data and return success */

    memcpy(undo_stack[undo_count], stack, STACK_SIZE * sizeof stack[0]);
    memcpy(undo_datap[undo_count], datap, h_static_offset);

    /* Adjust undo counters */

    if (++undo_count == undo_slots)
	undo_count = 0;
    if (++undo_valid > undo_slots)
	undo_valid = undo_slots;

    store_operand(1);

}/* save_undo */

/*
 * restore_undo
 *
 * Restore the current Z machine state from memory. Returns:
 *    -1 = feature unavailable
 *     0 = restore failed
 *     2 = restore succeeded
 *
 */

void z_restore_undo(void)
{
    /* Check if undo is available first */

    if (undo_slots == 0)
    {
        store_operand(-1);
        return;
    }

    if (undo_valid == 0)
    {
        store_operand(0);
        return;
    }

    if (undo_count == 0)
    	undo_count = undo_slots;


    memcpy(stack, undo_stack[undo_count-1], STACK_SIZE * sizeof stack[0]);
    memcpy(datap, undo_datap[undo_count-1], h_static_offset);

    frame_number = stack[3];
    sp = (unsigned *) stack[2];
    fp = (struct zframe *) stack[1];
    pc = (zbyte_t *) stack[0];

    /* if something */
    undo_count--;
    undo_valid--;

    store_operand(2);

}/* restore_undo */

/*
 * open_script
 *
 * Open the scripting file.
 *
 */

void open_script(void)
{
    char new_script_name[FILENAME_SIZE];

    /* Open scripting file if closed */

    if (scripting == OFF)
    {
        if (script_file_valid == TRUE)
        {
            sfp = fopen (script_name, "a");

            /* Turn on scripting if open succeeded */

            if (sfp != NULL)
            {
                /* Make it line buffered */
                setvbuf(sfp, NULL, _IOLBF, 256);

                scripting = ON;
            }
            else
                output_line(msgs_lookup_u("ScriptErr"));
        }
        else
        {
            /* Get scripting file name and record it */

            if (get_file_name (new_script_name, script_name, GAME_SCRIPT, -1) == 0)
            {
                const char *s = os_file_supplied();

                /* Open scripting file */

                sfp = fopen (new_script_name, "w");

                /* Turn on scripting if open succeeded */

                if (sfp != NULL)
                {
                    script_file_valid = TRUE;

                    /* Make file name the default name */

                    if (!s) strcpy (script_name, new_script_name);

                    /* Turn on scripting */

                    scripting = ON;
                }
                else
                    output_line(msgs_lookup_u("ScriptErr"));

                os_file_saved(scripting);
            }
        }
    }

    /* Set the scripting flag in the game file flags */

    if (scripting == ON)
        set_word_priv(H_FLAGS, get_word_priv(H_FLAGS) | SCRIPTING_FLAG);
    else
        set_word_priv(H_FLAGS, get_word_priv(H_FLAGS) & (~SCRIPTING_FLAG));

}/* open_script */

/*
 * close_script
 *
 * Close the scripting file.
 *
 */

void close_script(void)
{
    /* Close scripting file if open */

    if (scripting == ON)
    {
        fclose (sfp);

        /* Turn off scripting */

        scripting = OFF;
    }

    /* Set the scripting flag in the game file flags */

    if (scripting == OFF)
        set_word_priv(H_FLAGS, get_word_priv(H_FLAGS) & (~SCRIPTING_FLAG));
    else
        set_word_priv(H_FLAGS, get_word_priv(H_FLAGS) | SCRIPTING_FLAG);

}/* close_script */

static void script_c(int c)
{
    int c2;

    if (c < 32 && c != '\n')
        return;

    c2 = unicode_to_system(c, 0);
    if (c2)
        putc(c2, sfp);
    else
        fprintf(sfp, "[U+%04X]", c);
}

/*
 * script_backup
 *
 * Remove the last n characters from the transscript file.
 *
 */

void script_backup(int n)
{
    fseek(sfp, -(long)n, SEEK_CUR);
}

/*
 * script_char
 *
 * Write one character to scripting file.
 *
 * Check the state of the scripting flag first. Older games only set the
 * scripting flag in the game flags instead of calling the z_output_stream
 * function. This is because they expect a physically attached printer that
 * doesn't need opening like a file.
 */

void script_char(int c)
{
    /* Check the state of the scripting flag in the game flags. If it is on
       then check to see if the scripting file is open as well */

    if ((get_word_priv(H_FLAGS) & SCRIPTING_FLAG) != 0 && scripting == OFF)
        open_script ();

    /* Check the state of the scripting flag in the game flags. If it is off
       then check to see if the scripting file is closed as well */

    else if ((get_word_priv(H_FLAGS) & SCRIPTING_FLAG) == 0 && scripting == ON)
        close_script ();

    /* If scripting file is open, we are in the text window and the character is
       printable then write the character */

    if (scripting == ON && scripting_disable == OFF)
        script_c(c);

}/* script_char */

/*
 * script_string
 *
 * Write a string to the scripting file.
 *
 */

void script_string (const zword_t *s)
{
    if ((get_word_priv(H_FLAGS) & SCRIPTING_FLAG) != 0 && scripting == OFF)
        open_script();
    else if ((get_word_priv(H_FLAGS) & SCRIPTING_FLAG) == 0 && scripting == ON)
        close_script();

    /* Write string */

    if (scripting == ON && scripting_disable == OFF)
        while (*s)
            script_c(*s++);

}/* script_string */

/*
 * script_line
 *
 * Write a string followed by a new line to the scripting file.
 *
 */

void script_line(const zword_t *s)
{
    /* Write string */

    script_string(s);

    /* Write new line */

    script_new_line();

}/* script_line */

/*
 * script_new_line
 *
 * Write a new line to the scripting file.
 *
 */

void script_new_line(void)
{
    script_char ('\n');

}/* script_new_line */

/*
 * open_record
 *
 * Turn on recording of all input to an output file.
 *
 */

void open_record (void)
{
    char new_record_name[FILENAME_SIZE];

    /* If recording or playback is already on then complain */

    if (recording == ON || replaying == ON)
        output_line(msgs_lookup_u("Recording"));
    else
    {
        /* Get recording file name */

        if (get_file_name(new_record_name, record_name, GAME_RECORD, -1) == 0)
        {
            const char *s = os_file_supplied();

            /* Open recording file */

            rfp = fopen(new_record_name, "w");

            /* Turn on recording if open succeeded */

            if (rfp != NULL)
            {
                /* Make it line buffered */

                setvbuf(rfp, NULL, _IOLBF, 256);

                /* Make file name the default name */

                if (!s) strcpy(record_name, new_record_name);

                /* Set recording on */

                recording = ON;
            }
            else
                output_line(msgs_lookup_u("RecFail"));

            os_file_saved(recording);
        }
    }

}/* open_record */

/*
 * record_char
 *
 * Write a single character (a plain character or a function key) to
 * the command file.
 *
 */

static void record_char(int u, int force_encoding)
{
    if (force_encoding || u < 32)
    {
        fprintf(rfp, "[%d]", u);
    }
    else
    {
        int c = unicode_to_system(u, -1);
        if (c != 127 && c != '[')
            fputc(c, rfp);
        else
            fprintf(rfp, "[U+%04X]", u);
    }

}/* record_char */


extern int mouse_x, mouse_y;

/*
 * record_line
 *
 * Write a string (Unicode) followed by a new line to the recording file.
 *
 */
void record_line(const zword_t *buffer, int terminator)
{
    /* Copy input line to command file */

    while (*buffer)
    	record_char(*buffer++, FALSE);

    if (terminator != zscii_NEWLINE)
    {
	record_char(terminator, 1);
	if (terminator == zscii_CLICK || terminator == zscii_DBLCLICK)
	{
	    record_char(mouse_x, 1);
	    record_char(mouse_y, 1);
	}
    }

    /* Write a newline */

    fputc('\n', rfp);

    /* Stop recording if problems occur */

    if (ferror (rfp) != 0)
	close_record ();

}/* record_line */

/*
 * record_key
 *
 * Write a key (ZSCII) followed by a new line to the recording file.
 *
 */

void record_key(int key)
{
    /* Write key */

    int u = zscii_to_unicode(key);

    if (u == 0xFFFD)
        record_char (key, 1);
    else
        record_char (u, 0);

    if (key == zscii_CLICK || key == zscii_DBLCLICK)
    {
	record_char(mouse_x, 1);
	record_char(mouse_y, 1);
    }

    /* Write a newline */

    fputc('\n', rfp);

    /* Stop recording if problems occur */

    if (ferror(rfp) != 0)
	close_record();

}/* record_key */

/*
 * close_record
 *
 * Turn off recording of all input to an output file.
 *
 */

void close_record(void)
{
    /* Close recording file */

    if (rfp != NULL)
    {
        fclose(rfp);
        rfp = NULL;
    }

    /* Set recording and replaying off */

    recording = OFF;
    replaying = OFF;

}/* close_record */

/*
 * input_stream
 *
 * Take input from command file instead of keyboard.
 *
 */

void z_input_stream(int stream)
{
    char new_record_name[FILENAME_SIZE];

    if (stream == 0)
    {
        if (rfp)
        {
            fclose(rfp);
            rfp=NULL;
        }
        replaying=OFF;
        return;
    }

    /* If recording or replaying is already on then complain */

    if (recording == ON || replaying == ON)
        output_line(msgs_lookup_u("Recording"));
    else
    {
        /* Get recording file name */

        if (get_file_name(new_record_name, record_name, GAME_PLAYBACK, -1) == 0)
        {
            /* Open recording file */

            rfp = fopen(new_record_name, "r");
            os_file_loaded();

            /* Turn on recording if open succeeded */

            if (rfp != NULL)
            {
                /* Make file name the default name */

                strcpy(record_name, new_record_name);

                /* Set replaying on */

                replaying = ON;
            }
            else
                output_line(msgs_lookup_u("RecordErr"));
        }
    }

}/* input_stream */

/*
 * playback_char
 *
 * Get a single character (a plain character or a function key) from
 * the command file.
 *
 */

static int playback_char(int force_encoding)
{
    int code;
    int c;

    /* Literal characters are all characters except '[' */

    c = getc(rfp);
    if (c == EOF) return -1;

    if (c != '[' && force_encoding == 0)
	return unicode_to_zscii(system_to_unicode(c), '?');

    /* Read an encoded character */

    if (c != '[')
	return -1;

    c = getc(rfp);
    if (c == 'U')
    {
        if (getc(rfp) != '+')
            return -1;
        code = 0;
        do
        {
            c = getc(rfp);
            if (isxdigit(c))
            {
                if (c <= '9')
                    code = 16 * code + c - '0';
                else if (c <= 'F')
                    code = 16 * code + c - 'A' + 10;
                else
                    code = 16 * code + c - 'a' + 10;
            }
        }
        while (isxdigit(c));

        if (c != ']')
            return -1;

        return unicode_to_zscii(code, '?');
    }
    else
        ungetc(c, rfp);

    code = 0;
    do
    {
	c = getc(rfp);
	if (isdigit (c))
	    code = 10 * code + c - '0';
    }
    while (isdigit(c));

    if (c != ']')
	return -1;

    /* Return character */

    return code;

}/* playback_char */

/*
 * playback_line
 *
 * Get a line of input from the command file.
 *
 */

int playback_line(int max_size, zword_t *buffer)
{
    int length;
    int terminator;
    int c;

    /* Remove initial input from screen */

    input_blank(buffer, input_get_left(buffer));

    /* Read input buffer */

    length = 0;

    for (;;)
    {
	c = playback_char (0);
	if (c == -1)
	{
	    terminator = -1;
	    break;
	}

	/* Insert key 13 if the current character is newline */

	if (c == '\n')
	{
	    ungetc('\n', rfp);
	    c = zscii_NEWLINE;
	}

	/* Check for terminating key */

	if (c==0 || c==zscii_NEWLINE || end_key(c))
	{
	    terminator = c;
	    if (terminator == zscii_CLICK || terminator == zscii_DBLCLICK)
	    {
		mouse_x = playback_char (1);
		mouse_y = playback_char (1);
		if (hx_present(HX_CLICK_Y))
		{
		    set_word(h_extension_table+HX_CLICK_X, mouse_x);
		    set_word(h_extension_table+HX_CLICK_Y, mouse_y);
		}
	    }
	    break;
	}

	/* Add character to the buffer */

	if (length < max_size)
	    buffer[length++] = zscii_to_unicode(c);
    }

    buffer[length] = '\0';

    /* Next character should be newline */

    if (getc(rfp) != '\n')
	terminator = -1;

    /* Stop playback if problems occur */

    if (terminator == -1)
	close_record();

    /* Display input line on screen */

    output_string(buffer);

    /* Return terminating key value */

    return terminator;

}/* playback_line */


/*
 * playback_key
 *
 * Get a key from the command file.
 *
 */

int playback_key(void)
{
    int key;

    /* Read key */

    key = playback_char(0);

    if (key == zscii_CLICK || key == zscii_DBLCLICK)
    {
	mouse_x = playback_char (1);
	mouse_y = playback_char (1);
	if (hx_present(HX_CLICK_Y))
	{
	    set_word(h_extension_table+HX_CLICK_X, mouse_x);
	    set_word(h_extension_table+HX_CLICK_Y, mouse_y);
	}
    }

    /* Next character should be newline */

    if (getc(rfp) != '\n')
	key = -1;

    /* Stop playback if problems occur */

    if (key == -1)
	close_record();

    /* Return key value */

    return key;

}/* playback_key */

/*
 * autostart_quetzal
 *
 * Load the double-clicked Quetzal file (at last).
 */
void autostart_quetzal(void)
{
    if (quetzal_to_load)
    {
        FILE *sfp = fopen(quetzal_to_load, "rb");
        int i;
        char *p;

        quetzal_started = FALSE;

        if (!sfp)
            fatal_lookup("BadLoad");
        i = restore_quetzal(sfp);
        if (i != 1)
            fatal_lookup("BadLoad");

        fclose(sfp);

        /* Hackery doe-dah */
        p = getenv("Zip2000Save$Path");
        if (p && strncmp(quetzal_to_load, p, strlen(p)) == 0)
        {
            memmove(quetzal_to_load + 12, quetzal_to_load + strlen(p),
                    strlen(quetzal_to_load) + 1 - strlen(p));
            memcpy(quetzal_to_load, "Zip2000Save:", 12);
        }

        strcpy(save_name, quetzal_to_load);

        free(quetzal_to_load);
        quetzal_to_load = NULL;

        if (h_type < V4)
            conditional_jump(TRUE);
        else
            store_operand(2);
    }
}
