// zeal - A portable Glk-based Z-code interpreter
// Copyright (C) 2000 Jeremy Condit <jcondit@eecs.harvard.edu>
// 
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
// 
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
// 
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

// =======================================================================
//  quetzal.cc:
//
//  this module contains all the machine routines used for reading/writing
//  quetzal saved games.
// =======================================================================

#include "zeal.h"
#include "error.h"
#include "machine.h"

// =======================================================================
//  machine (quetzal save/load)
// =======================================================================

// chars_to_int
//
// convert a 4-char string to an unsigned integer.  (multi-byte character
// constants annoy pedantic gcc.)

uint
machine::chars_to_int(const char* s)
{
    uint result = 0;
    for (uint i = 0; i < 4; i++) {
        result |= (s[i] << ((3 - i) * 8));
    }
    return result;
}

// write_file_int
//
// write int d to s using n bytes.

void
machine::write_file_int(strid_t s, uint d, uint n)
{
    for (uint i = n; i > 0; i--) {
        glk_put_char_stream(s, (d >> ((i - 1) * 8)) & 0xff);
    }
}

// write_header
//
// write the header chunk.

void
machine::write_header(strid_t s)
{
    write_file_int(s, chars_to_int("IFhd"));
    write_file_int(s, 13);

    write_file_word(s, read_word(HEADER_RELEASE));
    write_file_word(s, read_word(HEADER_SERIAL));
    write_file_word(s, read_word(HEADER_SERIAL + 2));
    write_file_word(s, read_word(HEADER_SERIAL + 4));
    write_file_word(s, read_word(HEADER_CHECKSUM));

    write_file_int(s, pc, 3);
    write_file_byte(s, 0);
}

// write_umem
//
// write the uncompressed memory chunk.

void
machine::write_umem(strid_t s)
{
    write_file_int(s, chars_to_int("UMem"));
    write_file_int(s, dyn_size);

    glk_put_buffer_stream(s, (char*)memory, dyn_size);

    if (dyn_size % 2 == 1) {
        write_file_byte(s, 0);
    }
}

// write_cmem
//
// write the run-length encoded memory chunk.

void
machine::write_cmem(strid_t s, game_state* initial)
{
    ASSERT(initial != NULL && initial->memory != NULL &&
           initial->dyn_size == dyn_size);

    glui32 length_pos = glk_stream_get_position(s);

    write_file_int(s, chars_to_int("CMem"));
    write_file_int(s, 0);

    ubyte zeros = 0;
    for (uint i = 0; i < dyn_size; i++) {
        ubyte b = memory[i] ^ initial->memory[i];
        if (b != 0) {
            if (zeros > 0) {
                write_file_byte(s, 0);
                write_file_byte(s, zeros - 1);
                zeros = 0;
            }
            write_file_byte(s, b);
        } else if (zeros == 255) {
            write_file_byte(s, 0);
            write_file_byte(s, 255);
            zeros = 0;
        } else {
            zeros++;
        }
    }

    glui32 cur_pos = glk_stream_get_position(s);
    glk_stream_set_position(s, length_pos + 4, seekmode_Start);
    write_file_int(s, cur_pos - length_pos - 8);
    glk_stream_set_position(s, cur_pos, seekmode_Start);

    if ((cur_pos - length_pos) % 2 == 1) {
        write_file_byte(s, 0);
    }
}

// write_frame
//
// write a single stack frame.

void
machine::write_frame(strid_t s, frame* fr, uint prev_stack_ptr)
{
    uint i;

    write_file_int(s, fr->return_addr, 3);

    ubyte flags = fr->num_locals;
    if (!fr->return_var_valid) {
        flags |= 0x10;
    }
    write_file_byte(s, flags);

    write_file_byte(s, fr->return_var);

    ubyte args = 0;
    for (i = 0; i < fr->num_args; i++) {
        args |= (1 << i);
    }
    write_file_byte(s, args);

    write_file_word(s, fr->stack_ptr - prev_stack_ptr);

    for (i = 0; i < fr->num_locals; i++) {
        write_file_word(s, fr->locals[i]);
    }

    for (i = prev_stack_ptr; i < fr->stack_ptr; i++) {
        write_file_word(s, stack[i]);
    }
}

// write_stacks
//
// write the entire stack.

void
machine::write_stacks(strid_t s)
{
    glui32 length_pos = glk_stream_get_position(s);

    write_file_int(s, chars_to_int("Stks"));
    write_file_int(s, 0);

    uint prev_stack_ptr = 0;
    for (uint i = 0; i <= call_stack_ptr; i++) {
        write_frame(s, &call_stack[i], prev_stack_ptr);
        prev_stack_ptr = call_stack[i].stack_ptr;
    }

    glui32 cur_pos = glk_stream_get_position(s);
    glk_stream_set_position(s, length_pos + 4, seekmode_Start);
    write_file_int(s, cur_pos - length_pos - 8);
    glk_stream_set_position(s, cur_pos, seekmode_Start);
}

// save_state
//
// saves current machine state to s.  initial state must be supplied for
// compressed memory image to be used.

bool
machine::save_state(strid_t s, game_state* initial)
{
    glk_stream_set_position(s, 0, seekmode_Start);

    write_file_int(s, chars_to_int("FORM"));
    write_file_int(s, 0);
    write_file_int(s, chars_to_int("IFZS"));

    write_header(s);
    if (initial != NULL) {
        write_cmem(s, initial);
    } else {
        write_umem(s);
    }
    write_stacks(s);

    glui32 cur_pos = glk_stream_get_position(s);
    glk_stream_set_position(s, 4, seekmode_Start);
    write_file_int(s, cur_pos - 8);

    return true;
}

// read_file_int
//
// reads an n-byte int from s.

uint
machine::read_file_int(strid_t s, uint n)
{
    uint result = 0;
    for (int i = n; i > 0; i--) {
        result |= (glk_get_char_stream(s) << ((i - 1) * 8));
    }
    return result;
}

// read_header
//
// reads the header chunk from s.  return value indicates whether it looks
// like a real saved game for this game.

bool
machine::read_header(strid_t s, uint /* length */, story_sig* sig)
{
    if (sig->release != read_file_word(s)) {
        return false;
    }
    for (uword i = 0; i < 3; i++) {
        if (sig->serial[i] != read_file_word(s)) {
            return false;
        }
    }
    if (sig->checksum != read_file_word(s)) {
        return false;
    }

    pc = read_file_int(s, 3);

    return true;
}

// read_umem
//
// reads an uncompressed memory chunk.

bool
machine::read_umem(strid_t s, uint length)
{
    if (length != dyn_size) {
        return false;
    }

    glk_get_buffer_stream(s, (char*)memory, dyn_size);

    return true;
}

// read_cmem
//
// reads a compressed memory chunk.  the error conditions could use a
// second look.

bool
machine::read_cmem(strid_t s, uint length, game_state* initial)
{
    ASSERT(initial != NULL && initial->memory != NULL &&
           initial->dyn_size == dyn_size);

    uint pos = 0;
    uint amt_read = 0;
    while (amt_read < length) {
        if (pos >= dyn_size) {
            return false;
        }
        ubyte b = read_file_byte(s);
        amt_read++;
        if (b != 0) {
            memory[pos] = b ^ initial->memory[pos];
            pos++;
        } else {
            if (amt_read >= length) {
                return false;
            }
            uint zeros = read_file_byte(s);
            amt_read++;
            for (uint j = 0; j < zeros + 1; j++) {
                memory[pos] = initial->memory[pos];
                pos++;
            }
        }
    }

    // the rest is assumed to be zeros
    for (; pos < dyn_size; pos++) {
        memory[pos] = initial->memory[pos];
    }

    return true;
}

// read_frame
//
// reads a single stack frame from the stream.

uint
machine::read_frame(strid_t s, frame* fr, uint prev_stack_ptr)
{
    uint i;

    fr->return_addr = read_file_int(s, 3);

    ubyte flags = read_file_byte(s);
    fr->return_var_valid = !(flags & 0x10);
    fr->num_locals = (flags & 0x0f);

    fr->return_var = read_file_byte(s);

    ubyte args = read_file_byte(s);
    for (i = 0; i < 8; i++) {
        if (args & (1 << i)) {
            fr->num_args = i + 1;
        }
    }

    fr->stack_ptr = prev_stack_ptr + read_file_word(s);
    ASSERT_MSG(fr->stack_ptr < MAX_STACK, "Stack overflow.");

    for (i = 0; i < fr->num_locals; i++) {
        fr->locals[i] = read_file_word(s);
    }

    for (i = prev_stack_ptr; i < fr->stack_ptr; i++) {
        stack[i] = read_file_word(s);
    }

    return 8 + (2 * fr->num_locals) + (2 * (fr->stack_ptr - prev_stack_ptr));
}

// read_stacks
//
// reads the entire stack from the file.

bool
machine::read_stacks(strid_t s, uint length)
{
    uint prev_stack_ptr = 0;
    call_stack_ptr = 0;
    while (length > 0) {
        ASSERT_MSG(call_stack_ptr < MAX_CALL_STACK, "Call stack overflow.");
        length -= read_frame(s, &call_stack[call_stack_ptr], prev_stack_ptr);
        prev_stack_ptr = call_stack[call_stack_ptr].stack_ptr;
        call_stack_ptr++;
    }
    call_stack_ptr--;

    return true;
}

// load_state
//
// read in a saved game.  initial state must be supplied for compressed
// memory image to be read.
//
// note that we overwrite the current memory image, so after a certain
// point we must either succeed or halt.  in a future version, i may
// provide the option of loading into a copy of the current machine so
// that the load can be aborted at any time.

bool
machine::load_state(strid_t s, game_state* initial)
{
    static uint ifhd_id = chars_to_int("IFhd");
    static uint cmem_id = chars_to_int("CMem");
    static uint umem_id = chars_to_int("UMem");
    static uint stks_id = chars_to_int("Stks");

    glk_stream_set_position(s, 0, seekmode_Start);

    if (read_file_int(s) != chars_to_int("FORM")) {
        return false;
    }

    uint length = read_file_int(s);

    if (read_file_int(s) != chars_to_int("IFZS")) {
        return false;
    }

    // get the current info about this game so we can check it against the
    // header we receive.
    story_sig sig;
    sig.release = read_word(HEADER_RELEASE);
    sig.checksum = read_word(HEADER_CHECKSUM);
    for (uword i = 0; i < 3; i++) {
        sig.serial[i] = read_word(HEADER_SERIAL + (2 * i));
    }

    // TODO: check for errors in number of chunks of each type
    uint amt_read = 0;
    bool success = true;
    while (success && amt_read < length) {
        uint type = read_file_int(s);
        uint chunk_length = read_file_int(s);

        if (type == ifhd_id) {
            success = read_header(s, chunk_length, &sig);
        } else if (type == cmem_id) {
            ASSERT_MSG(initial != NULL, "Can't find initial game state!");
            success = read_cmem(s, chunk_length, initial);
        } else if (type == umem_id) {
            success = read_umem(s, chunk_length);
        } else if (type == stks_id) {
            success = read_stacks(s, chunk_length);
        }

        amt_read += chunk_length + 8;
        if (chunk_length % 2 == 1) {
            amt_read -= 1;
            read_file_byte(s);
        }
    }

    // we've stomped all over memory by now; we succeeded or we bail
    ASSERT(success);

    set_header();

    return success;
}
