// 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.

// =======================================================================
//  machine.cc:
//
//  this module defines the main machine class, which handles memory
//  references, local variables, global variables, initialization, and
//  other good stuff.
//
//  note that the implementation of machine is split between two files:
//  machine.cc and quetzal.cc
// =======================================================================

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

extern osdep* os;

// =======================================================================
//  game_state
// =======================================================================

// constructor
//
// the default constructor is private and only for use by machine.

game_state::game_state()
  : ver(0), pc(0), stack(NULL), stack_size(0),
    call_stack(NULL), call_stack_size(0), call_stack_ptr(0),
    memory(NULL), mem_size(0), dyn_size(0),
    is_halted(false), in_interrupt(false), cont(NULL)
{
}

// copy constructor
//
// the copy constructor is used to make copies of the game's dynamic state.
// note that many members are initialized improperly in the initialization
// list.

game_state::game_state(const game_state& orig)
  : ver(orig.ver), pc(orig.pc), stack(NULL), stack_size(0),
    call_stack(NULL), call_stack_size(0), call_stack_ptr(orig.call_stack_ptr),
    memory(NULL), mem_size(orig.dyn_size), dyn_size(orig.dyn_size),
    is_halted(orig.is_halted), in_interrupt(false), cont(NULL)
{
    uint i;

    // TODO: this could probably be handled more gracefully
    ASSERT_MSG(orig.in_interrupt == false && orig.cont == NULL,
               "Can't copy machine image in interrupted state.");

    call_stack_size = call_stack_ptr + 1;
    call_stack = new frame [call_stack_size];
    MEMCHECK(call_stack);
    for (i = 0; i < call_stack_size; i++) {
        call_stack[i] = orig.call_stack[i];
    }

    stack_size = call_stack[call_stack_ptr].stack_ptr + 1;
    stack = new uword [stack_size];
    MEMCHECK(stack);
    for (i = 0; i < stack_size; i++) {
        stack[i] = orig.stack[i];
    }

    memory = new ubyte [mem_size]; // note mem_size == dyn_size
    MEMCHECK(memory);
    memcpy(memory, orig.memory, mem_size);
}

// destructor
//
// duh.

game_state::~game_state()
{
    delete [] call_stack;
    delete [] stack;
    delete [] memory;

    if (cont != NULL) {
        cont->cancel();
    }
}

// operator=
//
// copy dynamic state from one machine to another.

game_state&
game_state::operator=(const game_state& orig)
{
    uint i;

    // TODO: this could probably be handled more gracefully
    ASSERT_MSG(orig.in_interrupt == false && orig.cont == NULL,
               "Can't copy machine image in interrupted state.");
    in_interrupt = false;
    cont = NULL;

    // boring data--nothing bad will happen here
    pc = orig.pc;
    is_halted = orig.is_halted;
    ver = orig.ver;

    // resize and copy call stack
    call_stack_ptr = orig.call_stack_ptr;
    if (call_stack_ptr >= call_stack_size) {
        delete [] call_stack;
        call_stack_size = call_stack_ptr + 1;
        call_stack = new frame [call_stack_size];
        MEMCHECK(call_stack);
    }
    for (i = 0; i <= call_stack_ptr; i++) {
        call_stack[i] = orig.call_stack[i];
    }

    // resize and copy stack
    uint stack_ptr = call_stack[call_stack_ptr].stack_ptr;
    if (stack_ptr >= stack_size) {
        delete [] stack;
        stack_size = stack_ptr + 1;
        stack = new uword [stack_size];
        MEMCHECK(stack);
    }
    for (i = 0; i <= stack_ptr; i++) {
        stack[i] = orig.stack[i];
    }

    // copy memory
    dyn_size = orig.dyn_size;
    if (dyn_size > mem_size) {
        delete [] memory;
        mem_size = dyn_size;
        memory = new ubyte [mem_size];
        MEMCHECK(memory);
    }
    memcpy(memory, orig.memory, dyn_size);

    return *this;
}

// =======================================================================
//  machine (main part)
// =======================================================================

// static data
//
// defines max size for stack and call stack.
// TODO: perhaps sometime in the future these will grow as needed.

const uint machine::MAX_STACK = 1000;
const uint machine::MAX_CALL_STACK = 1000;

// constructor
//
// takes a stream to a story file and initializes main memory.

machine::machine(strid_t s)
  : game_state() // default constructor doesn't allocate anything
{
    stack_size = MAX_STACK;
    stack = new uword [stack_size];
    MEMCHECK(stack);

    call_stack_size = MAX_CALL_STACK;
    call_stack = new frame [call_stack_size];
    MEMCHECK(call_stack);

    glk_stream_set_position(s, 0, seekmode_End);
    mem_size = glk_stream_get_position(s);
    ASSERT_MSG(mem_size > 0x40, "Input file too small.");
    glk_stream_set_position(s, 0, seekmode_Start);

    memory = new ubyte [mem_size];
    MEMCHECK(memory);
    ASSERT_MSG(glk_get_buffer_stream(s, (char*)memory, mem_size) == mem_size,
               "Error reading story file.");
    dyn_size = read_word(HEADER_STATIC_BASE);

    ver = read_byte(HEADER_VERSION);
    pc = read_word(HEADER_INIT_PC);
    ASSERT_MSG(ver >= 1 && ver <= 8, "Invalid machine version.");

    call_stack_ptr = 0;
    frame* fr = &call_stack[0];
    fr->return_addr = 0;
    fr->num_locals = 0;
    fr->num_args = 0;
    fr->stack_ptr = 0;
    fr->return_var = 0;
    fr->return_var_valid = true;
    fr->interrupt_start = false;

    is_halted = false;
    in_interrupt = false;
    cont = NULL;
}

// copy constructor
//
// there should be only one machine, ever.

machine::machine(const machine& orig)
{
    FATAL("Tried to copy a machine.");
}

// destructor
//
// game over.

machine::~machine()
{
}

// operator=
//
// there should be only one machine, ever.

machine&
machine::operator=(const machine& orig)
{
    FATAL("Tried to copy a machine.");
    return *this;
}

// halted
//
// indicates whether the machine has halted.  duh.

bool
machine::halted()
{
    return is_halted;
}

// interrupted
//
// indicates whether the machine is currently servicing a timer interrupt.

bool
machine::interrupted()
{
    return in_interrupt;
}

// version
//
// returns the machine version (1-8) expected by this game.

ubyte
machine::version()
{
    return ver;
}

// one_inst
//
// fetch, decode, and execute the next instruction.

void
machine::one_inst()
{
    inst_args args;
    inst_handler handler;
    unsigned char opcode = read_pc_opcode();

    os->debug_msg("pc: %x opcode: %x\n", pc - 1, opcode);

    for (int k = 0; k < 8; k++) {
        args.argv[k] = 0;
    }

    args.opcode = opcode;
    args.ext = 0;

    switch (opcode & 0xf0) {
        case 0x00: // long 2op sc sc
        case 0x10:
            args.argc = 2;
            args.argv[0] = read_pc_byte();
            args.argv[1] = read_pc_byte();
            handler = two_inst_table[opcode & 0x1f];
            break;
        case 0x20: // long 2op sc v
        case 0x30:
            args.argc = 2;
            args.argv[0] = read_pc_byte();
            args.argv[1] = read_pc_var();
            handler = two_inst_table[opcode & 0x1f];
            break;
        case 0x40: // long 2op v sc
        case 0x50:
            args.argc = 2;
            args.argv[0] = read_pc_var();
            args.argv[1] = read_pc_byte();
            handler = two_inst_table[opcode & 0x1f];
            break;
        case 0x60: // long 2op v v
        case 0x70:
            args.argc = 2;
            args.argv[0] = read_pc_var();
            args.argv[1] = read_pc_var();
            handler = two_inst_table[opcode & 0x1f];
            break;
        case 0x80: // short 1op lc
            args.argc = 1;
            args.argv[0] = read_pc_word();
            handler = one_inst_table[opcode & 0xf];
            break;
        case 0x90: // short 1op sc
            args.argc = 1;
            args.argv[0] = read_pc_byte();
            handler = one_inst_table[opcode & 0xf];
            break;
        case 0xa0: // short 1op v
            args.argc = 1;
            args.argv[0] = read_pc_var();
            handler = one_inst_table[opcode & 0xf];
            break;
        case 0xb0: // short 0op
            if (opcode != 0xbe) {
                args.argc = 0;
                handler = zero_inst_table[opcode & 0xf];
                break;
            }
            // else fall thru
        case 0xc0: // var 2op
        case 0xd0:
        case 0xe0: // var var
        case 0xf0:
            {
            args.argc = 0;

            ubyte inst = (opcode == 0xbe) ? read_pc_byte() : (opcode & 0x1f);
            ubyte var_bytes = (opcode == 0xec || opcode == 0xfa) ? 2 : 1;

            ubyte vars[2];
            int i, j;
            for (i = 0; i < var_bytes; i++) {
                vars[i] = read_pc_byte();
            }

            bool done = false;
            for (i = 0; i < var_bytes && !done; i++) {
                for (j = 0; j < 4 && !done; j++) {
                    int shift = (3 - j) * 2;
                    switch ((vars[i] & (0x3 << shift)) >> shift) {
                        case 0x0:
                            args.argv[args.argc++] = read_pc_word();
                            break;
                        case 0x1:
                            args.argv[args.argc++] = read_pc_byte();
                            break;
                        case 0x2:
                            args.argv[args.argc++] = read_pc_var();
                            break;
                        case 0x3:
                            done = true;
                            break;
                    }
                }
            }

            if (opcode == 0xbe) {
                handler = ext_inst_table[inst];
                args.ext = inst;
            } else if (opcode & 0x20) {
                handler = var_inst_table[inst];
            } else {
                handler = two_inst_table[inst];
            }
            }
            break;
        default:
            FATAL("Couldn't decode opcode.");
            break;
    }

    handler(&args);
}

// halt
//
// we're done!

void
machine::halt()
{
    is_halted = true;
}

// read_bit
//
// read which'th bit from byte at addr.

bool
machine::read_bit(address addr, ubyte which)
{
    return ((read_byte(addr) & (1 << which)) != 0);
}

// write_bit
//
// write which'th bit to byte at addr.

void
machine::write_bit(address addr, ubyte which, bool bit)
{
    write_byte(addr, bit ? (read_byte(addr) | (1 << which))
                         : (read_byte(addr) & ~(1 << which)));
}

// read_byte
//
// read a byte from the game's memory.

ubyte
machine::read_byte(address addr)
{
    check_read(addr);

    return memory[addr];
}

// write_byte
//
// write a byte to the game's memory.

void
machine::write_byte(address addr, ubyte value)
{
    check_write(addr);

    memory[addr] = value;
}

// read_word
//
// read a word from the game's memory.

uword
machine::read_word(address addr)
{
    check_read(addr);

    return (memory[addr] << 8) | memory[addr + 1];
}

// write_word
//
// write a word to the game's memory.

void
machine::write_word(address addr, uword value)
{
    check_write(addr + 1);

    memory[addr] = (value & 0xff00) >> 8;
    memory[addr + 1] = (value & 0xff);
}

// read_var
//
// read a variable.  0 means the stack (implicit pop), 0x1-0x10 means a
// local variable, and 0x11-0xff means a global variable.

uword
machine::read_var(ubyte var)
{
    uword result;

    if (var == 0) {
        result = stack[--(call_stack[call_stack_ptr].stack_ptr)];
        os->debug_msg("\t\treading  SP -> %04x\n", result);
    } else if (var < 0x10) {
        result = call_stack[call_stack_ptr].locals[var - 1];
        os->debug_msg("\t\treading L%02x -> %04x\n", var - 1, result);
    } else {
        result = read_word(read_word(HEADER_GLOBAL_BASE) + (2 * (var - 0x10)));
        os->debug_msg("\t\treading G%02x -> %04x\n", var - 0x10, result);
    }

    return result;
}

// write_var
//
// write a variable.  0 means the stack (implicit push), 0x1-0x10 means a
// local variable, and 0x11-0xff means a global variable.

void
machine::write_var(ubyte var, uword value)
{
    if (var == 0) {
        ASSERT_MSG(call_stack[call_stack_ptr].stack_ptr < MAX_STACK - 1,
                   "Stack overflow.");
        stack[(call_stack[call_stack_ptr].stack_ptr)++] = value;
        os->debug_msg("\t\tsetting  SP <- %04x\n", value);
    } else if (var < 0x10) {
        call_stack[call_stack_ptr].locals[var - 1] = value;
        os->debug_msg("\t\tsetting L%02x <- %04x\n", var - 1, value);
    } else {
        write_word(read_word(HEADER_GLOBAL_BASE) + (2 * (var - 0x10)), value);
        os->debug_msg("\t\tsetting G%02x <- %04x\n", var - 0x10, value);
    }
}

// read_pc_*
//
// same as all those other read_* functions, except they read from the pc
// and then increment it.

ubyte
machine::read_pc_opcode()
{
    return read_byte(pc++);
}

ubyte
machine::read_pc_byte()
{
    return read_byte(pc++);
}

uword
machine::read_pc_word()
{
    uword result = read_word(pc);
    pc += 2;
    return result;
}

uword
machine::read_pc_var()
{
    return read_var(read_pc_byte());
}

// read_pc_branch
//
// gets information about a conditional branch from under the pc.

branch_info
machine::read_pc_branch()
{
    branch_info result;

    ubyte first = read_pc_byte();

    result.on_true = (first & 0x80) != 0;

    if ((first & 0x40) != 0) {
        result.offset = (ubyte)(first & 0x3f);
    } else {
        ubyte second = read_pc_byte();
        uword combine = ((first & 0x3f) << 8) | second;

        if ((first & 0x20) != 0) {
            combine |= 0xc000; // sign-extend
        }

        result.offset = combine;
    }

    return result;
}

// call_routine
//
// used by call instructions to push a new call stack frame, etc...
// arguments are address of routine, arguments, and a boolean to indicate
// whether the result is to be stored in a variable.

void
machine::call_routine(address addr, ubyte argc, uword* argv,
                      bool store_return)
{
    addr = packed_to_byte(addr, true);

    if (addr == 0x59f0) {
        addr = 0x59f0;
    }

    os->debug_msg("\t\t\t\tcalling addr %04x\n", addr);

    if (addr == 0) {
        if (store_return) {
            store_value(0);
        }
    } else {
        ubyte return_var = store_return ? read_pc_byte() : 0;

        call_stack_ptr++;
        ASSERT_MSG(call_stack_ptr < MAX_CALL_STACK, "Call stack overflow.");
        frame* fr = &call_stack[call_stack_ptr];

        fr->return_addr = pc;
        pc = addr;
        fr->stack_ptr = call_stack[call_stack_ptr - 1].stack_ptr;
        fr->return_var = return_var;
        fr->return_var_valid = store_return;
        fr->interrupt_start = false;
        fr->num_locals = read_pc_byte();
        fr->num_args = argc;

        ASSERT(fr->num_locals < 16);
        os->debug_msg("\t\t\t\tlocals: %d\n", fr->num_locals);

        for (int i = 0; i < fr->num_locals; i++) {
            fr->locals[i] = (version() <= 4) ? read_pc_word() : 0;
            if (i < argc) {
                fr->locals[i] = argv[i];
                os->debug_msg("\t\t\t\tinit var L%02x <- %04x\n", i, argv[i]);
            }
        }
    }
}

// interrupt
//
// execute a timer interrupt at the given address by calling the routine
// and setting appropriate flags.

void
machine::interrupt(address addr, interrupt_continue* cont_in)
{
    ASSERT(!in_interrupt);

    call_routine(addr, 0, NULL, false);
    call_stack[call_stack_ptr].interrupt_start = true;
    in_interrupt = true;
    cont = cont_in;
}

// store_value
//
// used by instructions that store a value in a variable as a result.

void
machine::store_value(uword value)
{
    write_var(read_pc_byte(), value);
}

// return_value
//
// used by instructions that return from a routine call.  pops call stack
// frame, etc...

void
machine::return_value(uword value)
{
    frame* fr = &call_stack[call_stack_ptr];

    pc = fr->return_addr;
    call_stack_ptr--;
    os->debug_msg("\t\t\t\treturning to pc %04x\n", pc);

    if (fr->return_var_valid) {
        write_var(fr->return_var, value);
    }

    if (fr->interrupt_start) {
        in_interrupt = false;
        if (cont != NULL) {
            cont->execute(value);
            cont = NULL;
        }
    }
}

// branch
//
// used by instructions that do a conditional branch.

void
machine::branch(bool condition)
{
    branch_info info = read_pc_branch();

    if (condition == info.on_true) {
        if (info.offset == 0 || info.offset == 1) {
            return_value(info.offset);
        } else {
            pc += info.offset - 2;
            os->debug_msg("\t\t\t\tbranching to pc %04x\n", pc);
        }
    }
}

// jump
//
// used by instructions that do an unconditional jump.

void
machine::jump(uint amt)
{
    pc += amt;
    os->debug_msg("\t\t\t\tjumping to pc %04x\n", pc);
}

// set_header
//
// set all those annoying little flags in the game's header.  this could
// use a little more work.

void
machine::set_header()
{
    ubyte flags1;
    if (ver <= 3) {
        flags1 = read_byte(HEADER_FLAGS1);
        flags1 &= 0x8f; // 10001111
        flags1 |= 0x20; // 00100000
        // TODO: bit 6 should be obtained from glk
    } else {
        flags1 = 0x9c; // 10011100
    }
    write_byte(HEADER_FLAGS1, flags1);

    uword flags2 = read_word(HEADER_FLAGS2);
    flags2 &= 0xfffe; // 11111111 11111110
    if (ver >= 3) {
        flags2 &= 0xfffd; // 11111111 11111101
    }
    if (ver >= 5) {
        flags2 &= 0xfe47; // 11111110 01000111
    }
    write_word(HEADER_FLAGS2, flags2);

    glui32 width;
    glui32 height;
    glk_window_get_size(glk_window_get_root(), &width, &height);

    if (ver >= 4) {
        // we can't really predict the machine
        write_byte(HEADER_INT_NUM, 0);
        write_byte(HEADER_INT_VER, 'A');

        ASSERT(width <= MAX_UBYTE);
        ASSERT(height <= MAX_UBYTE);
        write_byte(HEADER_SCREEN_WIDTH, (ubyte) width);
        write_byte(HEADER_SCREEN_HEIGHT, (ubyte) height);
    }

    if (ver >= 5) {
        ASSERT(width <= MAX_UWORD);
        ASSERT(height <= MAX_UWORD);
        write_word(HEADER_SCREEN_WIDTH2, (uword) width);
        write_word(HEADER_SCREEN_HEIGHT2, (uword) height);
        write_byte(HEADER_FONT_WIDTH, 1);
        write_byte(HEADER_FONT_HEIGHT, 1);

        // TODO: get this from interpreter
        // currently, color not supported
        write_byte(HEADER_BG_COLOR, 0);
        write_byte(HEADER_FG_COLOR, 0);

        // we're not perfectly compliant with any standard
        write_word(HEADER_REV_NUMBER, 0);
    }
}

// packed_to_byte
//
// translate a packed address to a byte address.  routine indicates
// whether it's a routine or a string address, which matters in version 7.

address
machine::packed_to_byte(address addr, bool routine)
{
    if (ver <= 3) {
        return addr * 2;
    } else if (ver <= 5) {
        return addr * 4;
    } else if (ver == 7) {
        address base = read_word(routine ? HEADER_ROUTINE_OFFSET
                                         : HEADER_STRING_OFFSET);
        return (addr * 4) + (base * 8);
    } else if (ver == 8) {
        return addr * 8;
    } else {
        FATAL("Invalid machine version.");
        return 0;
    }
}

// get_num_args
//
// gets the number of arguments to the routine currently executing.

ubyte
machine::get_num_args()
{
    return call_stack[call_stack_ptr].num_args;
}

// open_mem_stream
//
// produces a glk memory stream to some point in the game's memory,
// bounded by the size of the game's memory.

strid_t
machine::open_mem_stream(address addr)
{
    return glk_stream_open_memory((char*)(memory + addr + 2),
                                  mem_size - addr - 2, filemode_Write, addr);
}

// close_mem_stream
//
// closes a glk memory stream opened by the above.

void
machine::close_mem_stream(strid_t s)
{
    address addr;
    stream_result_t result;

    addr = glk_stream_get_rock(s);
    glk_stream_close(s, &result);
    ASSERT(result.writecount <= MAX_UWORD);
    write_word(addr, (uword) result.writecount);
}

// check_read
//
// validates a read from memory.

void
machine::check_read(address addr)
{
    if (addr >= mem_size) {
        FATAL("Attempt to read from invalid address.");
    }
}

// check_write
//
// validates a write to memory.

void
machine::check_write(address addr)
{
    if (addr >= read_word(HEADER_STATIC_BASE)) {
        FATAL("Attempt to write to static memory or beyond.");
    }
}

// verify
//
// computes a checksum on the contents of memory and compares it against
// the one contained in the game's header.

bool
machine::verify()
{
    address len = read_word(HEADER_LENGTH);

    if (ver >= 6) {
        len *= 8;
    } else if (ver >= 4) {
        len *= 4;
    } else {
        len *= 2;
    }

    if (mem_size < len) {
        return false;
    }

    uword sum = 0;
    for (address addr = 0x40; addr < len; addr++) {
        sum += read_byte(addr);
    }

    return (sum == read_word(HEADER_CHECKSUM));
}
