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

// =======================================================================
//  iface.cc:
//
//  this module defines the interface between the user and the z-machine.
//  it is responsible for all window and i/o stream manipulation.  note
//  that both of the classes contained herein (iface and stream_output)
//  are zscii_outputs, which means that a bfish can write decoded z-char
//  strings to them.
// =======================================================================

#include "zeal.h"
#include "error.h"
#include "iface.h"
#include "bfish.h"
#include "obj.h"
#include "machine.h"
#include "stdlite.h"

extern machine* m;

// =======================================================================
//  iface
// =======================================================================

// constructor
//
// should be called at startup, given a main window and an indication of
// the level of strictz warnings.
//
// responsible for setting up initial window layout.

iface::iface(winid_t main_win, int strictz_in)
  : lower_win(main_win), upper_win(NULL), status_win(NULL),
    script_file(NULL), cmd_file(NULL), select_win(LOWER),
    cur_x(1), cur_y(1), real_height(0), line_buf(NULL), line_buf_size(0),
    line_addr(0), ipt_addr(0), cont(NULL), now_blocked(false),
    script_flag(false), strictz(strictz_in)
{
    ASSERT(main_win != NULL);

    if (m->version() <= 3) {
        status_win = glk_window_open(lower_win,
                                     winmethod_Above | winmethod_Fixed,
                                     1, wintype_TextGrid, 0);
        ASSERT_MSG(status_win != NULL, "Error opening Glk window.");
    }
    
    // we waste one slot in the out_* arrays, but it makes life easier later
    for (uint i = 1; i <= NUM_STREAMS; i++) {
        out_stream[i] = NULL;
        out_select[i] = false;
    }

    strid_t s = glk_window_get_stream(lower_win);
    ASSERT(s != NULL);
    out_stream[1] = s;
    out_select[1] = true;

    set_text_style(ROMAN);

    out_mem_top = 0;
}

// destructor
//
// nuke all the windows.

iface::~iface()
{
    winid_t root = glk_window_get_root();

    if (root != NULL) {
        glk_window_close(root, NULL);
    }

    if (line_buf != NULL) {
        delete [] line_buf;
    }
}

// split_window
//
// adjust the split between upper and lower windows so that the upper
// window has the indicated number of lines.

void
iface::split_window(uint lines)
{
    if (lines == 0) {
        // if the upper window exists, destroy it
        if (upper_win != NULL) {
            glk_window_close(upper_win, NULL);
            upper_win = NULL;
        }
    } else {
        if (upper_win == NULL) {
            // if the upper window doesn't exist, create it
            upper_win = glk_window_open(lower_win,
                                        winmethod_Above | winmethod_Fixed,
                                        lines, wintype_TextGrid, 0);
            ASSERT_MSG(upper_win != NULL, "Error opening Glk window.");
            reset_cursor();
        } else {
            winid_t parent;
            glui32 method;
            glui32 height;
            winid_t keywin;

            parent = glk_window_get_parent(upper_win);
            ASSERT(parent != NULL);

            glk_window_get_arrangement(parent, &method, &height, &keywin);

            // we can only enlarge the upper window
            // (photopia breaks without this)
            if (lines > height) {
                glk_window_set_arrangement(parent,
                                           winmethod_Above | winmethod_Fixed,
                                           lines, NULL);
            }
        }
    }

    real_height = lines;
}

// set_window
//
// set the current window to the indicated window.

void
iface::set_window(win w)
{
    strid_t str = NULL;

    switch (w) {
        case LOWER:
            str = glk_window_get_stream(lower_win);
            break;
        case UPPER:
            str = glk_window_get_stream(upper_win);
            reset_cursor();
            break;
    }

    ASSERT(str != NULL);

    out_stream[1] = str;
    select_win = w;
}

// erase_window
//
// clear all contents of the indicated window.

void
iface::erase_window(win w)
{
    switch (w) {
        case LOWER:
            glk_window_clear(lower_win);
            break;
        case UPPER:
            glk_window_clear(upper_win);
            reset_cursor();
            break;
    }
}

// erase_lines
//
// erases from the cursor position to the end of the line.
// TODO: do this for the lower window?

void
iface::erase_line()
{
    if (select_win == UPPER) {
        uint save_x;
        uint save_y;
        glui32 max_x;

        glk_window_get_size(upper_win, &max_x, NULL);
        get_cursor(&save_x, &save_y);

        for (uint i = save_x; i <= max_x; i++) {
            put_char(' ');
        }

        set_cursor(save_x, save_y);
    }
}

// set_cursor
//
// change the position of the cursor in the upper window, which must be
// selected.

void
iface::set_cursor(uint x, uint y)
{
    ASSERT(x > 0 && y > 0);

    if (select_win == UPPER) {
        glk_window_move_cursor(upper_win, x - 1, y - 1);
        cur_x = x;
        cur_y = y;
    }
}

// get_cursor
//
// get the current position of the cursor in the upper window.

void
iface::get_cursor(uint* x, uint* y)
{
    if (x != NULL) {
        *x = cur_x;
    }
    
    if (y != NULL) {
        *y = cur_y;
    }
}

// set_text_style
//
// set text style according to user preferences.  we've set up user1 and
// user2 for use as bold and italic, and we're ignoring reverse video for
// now.
//
// we could probably use style_Emphasized for bold or italic, but this way
// we get to tell the library exactly what we want, from a clean slate.

void
iface::set_text_style(style s)
{
    glui32 new_style;

    switch (s) {
        case BOLD:
            new_style = style_User1;
            break;
        case ITALIC:
            new_style = style_User2;
            break;
        case FIXED:
            new_style = style_Preformatted;
            break;
        // TODO: reverse video
        case ROMAN:
        default:
            if (m->version() >= 3 && m->read_bit(HEADER_FLAGS2, 1) == true) {
                new_style = style_Preformatted;
            } else {
                new_style = style_Normal;
            }
            break;
    }

    glk_set_style_stream(out_stream[1], new_style);
}

// select
//
// select output stream id.  addr must be supplied for memory streams.

void
iface::select(uint id, address addr)
{
    ASSERT_MSG(id >= 1 && id <= NUM_STREAMS, "Invalid stream selected.");

    glui32 mode;

    switch (id) {
        case 1:
            // select the primary output stream
            out_select[id] = true;
            break;
        case 2:
            // select the transcript stream by creating a transcript file
            if (out_select[id]) {
                break;
            }
            mode = filemode_WriteAppend;
            if (script_file == NULL) {
                script_file = glk_fileref_create_by_prompt(
                                  fileusage_Transcript | fileusage_TextMode,
                                  filemode_Write, 0);
                mode = filemode_Write;
            }
            if (script_file != NULL) {
                out_stream[id] = glk_stream_open_file(script_file, mode, 0);
                if (out_stream[id] != NULL) {
                    glk_window_set_echo_stream(lower_win, out_stream[id]);
                    out_select[id] = true;
                    m->write_bit(HEADER_FLAGS2 + 1, 0, true);
                    script_flag = true;
                }
            }
            break;
        case 3:
            // select the memory stream by asking the machine for one
            ASSERT_MSG(addr != 0, "Invalid address for stream 3.");
            if (out_select[id]) {
                ASSERT_MSG(out_mem_top < MAX_STACK,
                           "Too many memory streams open.");
                out_mem_stack[out_mem_top++] = out_stream[id];
            }
            out_select[id] = true;
            out_stream[id] = m->open_mem_stream(addr);
            ASSERT_MSG(out_stream[id] != NULL,
                       "Failed to create memory stream.");
            break;
        case 4:
            // select the command transciprt stream by creating a
            // transcript file
            if (out_select[id]) {
                break;
            }
            mode = filemode_WriteAppend;
            if (cmd_file == NULL) {
                cmd_file = glk_fileref_create_by_prompt(
                               fileusage_Transcript | fileusage_TextMode,
                               filemode_Write, 0);
                mode = filemode_Write;
            }
            if (cmd_file != NULL) {
                out_stream[id] = glk_stream_open_file(cmd_file, mode, 0);
                if (out_stream[id] != NULL) {
                    out_select[id] = true;
                }
            }
            break;
    }
}

// deselect
//
// deselct the specified output stream.

void
iface::deselect(uint id)
{
    ASSERT_MSG(id >= 1 && id <= NUM_STREAMS, "Invalid stream deselected.");

    out_select[id] = false;

    switch (id) {
        case 2:
            // close and clean up a transcript stream
            if (out_stream[id] != NULL) {
                glk_stream_close(out_stream[id], NULL);
                out_stream[id] = NULL;
            }
            glk_window_set_echo_stream(lower_win, NULL);
            m->write_bit(HEADER_FLAGS2 + 1, 0, false);
            script_flag = false;
            break;
        case 3:
            // close and clean up a memory stream
            m->close_mem_stream(out_stream[id]);

            if (out_mem_top > 0) {
                out_stream[id] = out_mem_stack[--out_mem_top];
                out_select[id] = true;
            }
            break;
        case 4:
            // close and clean up a command transcript stream
            if (out_stream[id] != NULL) {
                glk_stream_close(out_stream[id], NULL);
                out_stream[id] = NULL;
            }
            break;
    }
}

// put_char
//
// sends a character to the appropriate conglomeration of output streams.

void
iface::put_char(ubyte z)
{
    if (z == 0) {
        // do nothing
        return;
    }

    if (out_select[3]) {
        glk_put_char_stream(out_stream[3], z);
    } else if (out_select[1]) {
        ubyte l = zscii_to_latin(z);
        glk_put_char_stream(out_stream[1], l);
        adjust_cursor(l);
    } else if (out_select[2]) {
        glk_put_char_stream(out_stream[2], zscii_to_latin(z));
    }
}

// put_error
//
// sends a printf-style error message to the lower window.

void
iface::put_error(char* s, ...)
{
    if (lower_win != NULL) {
        char buf[500];

        va_list args;
        va_start(args, s);
        vsnprintf_lite(buf, 500, s, args);
        va_end(args);

        strid_t s = glk_window_get_stream(lower_win);
        ASSERT(s != NULL);

        glk_put_string_stream(s, "[");
        glk_put_string_stream(s, buf);
        glk_put_string_stream(s, "]\n");
    }
}

// put_warning
//
// sends a warning to the lower window, according to the strictz level
// defined.  the warned parameter is used to save state about whether the
// user has been warned about this kind of error before.

bool
iface::put_warning(char* warning, bool* warned)
{
    bool fatal = false;

    switch (strictz) {
        case 0:
            // do nothing
            break;
        case 1:
            if (!*warned) {
                put_error("StrictZ Warning: %s Future occurrences ignored.",
                          warning);
                *warned = true;
            }
            break;
        case 2:
            put_error("StrictZ Warning: %s", warning);
            break;
        case 3:
        default:
            put_error("StrictZ Warning: %s Halting.", warning);
            fatal = true;
            break;
    }

    return fatal;
}

/*
void
iface::put_debug(const char* format, ...)
{
    if (debug_str != NULL) {
        char buf[500];

        va_list args;
        va_start(args, s);
        vsnprintf_lite(buf, 500, s, args);
        va_end(args);

        glk_put_string_stream(debug_str, buf);
    }
}
*/

// read_zscii
//
// read a single zscii character from the user.

void
iface::read_zscii(timer_info* timer, input_continue* cont_in)
{
    ASSERT_MSG(!blocked(), "Tried to post a second request for input!");

    if (m->version() == 3) {
        show_status();
    }

    // request input
    glk_request_char_event(lower_win);

    // set timer
    if (timer != NULL) {
        glk_request_timer_events((glui32) (timer->interval * 100));
        ipt_addr = timer->addr;
    }

    // wait for input to arrive
    ASSERT(cont == NULL);
    cont = cont_in;
    now_blocked = true;
}

// read_zscii_line
//
// read a line of zscii characters from the user.

void
iface::read_zscii_line(address addr, uword n,
                       timer_info* timer, input_continue* cont_in)
{
    ASSERT_MSG(!blocked(), "Tried to post a second request for input!");

    if (m->version() == 3) {
        show_status();
    }

    // grow input buffer if necessary
    if (line_buf_size < n + 1) {
        line_buf_size = n + 1;
        delete [] line_buf;
        line_buf = new char [line_buf_size];
        MEMCHECK(line_buf);
    }

    // request input
    glk_request_line_event(lower_win, line_buf, line_buf_size, 0);
    line_addr = addr;

    // set timer
    if (timer != NULL) {
        glk_request_timer_events((glui32) (timer->interval * 100));
        ipt_addr = timer->addr;
    }

    // wait for input to arrive
    ASSERT(cont == NULL);
    cont = cont_in;
    now_blocked = true;
}

// blocked
//
// are we currently waiting for an input event?

bool
iface::blocked()
{
    return now_blocked;
}

// timer_continue
//
// handles completion of timer interrupt.

class timer_continue : public interrupt_continue {
    public:
        virtual void execute(uword value);
        virtual void cancel();

        iface* timer_io;
};

void
timer_continue::execute(uword value)
{
    if (value != 0) {
        timer_io->finish_read(false, 0);
    }
    // TODO: redraw input so far

    delete this;
}

void
timer_continue::cancel()
{
    delete this;
}

// process_event
//
// handle an event returned by glk_select.  basically, we need to complete
// any input-reading or timer interrupt event we have waiting.

void
iface::process_event(event_t* ev)
{
    ASSERT(ev != NULL);

    switch (ev->type) {
        case evtype_LineInput:
            {
            ASSERT(ev->val1 <= MAX_UWORD);

            // send to command transcript if necessary
            if (out_select[4]) {
                glk_put_buffer_stream(out_stream[4], line_buf, ev->val1);
                glk_put_char_stream(out_stream[4], '\n');
            }

            // return input
            for (glui32 i = 0; i < ev->val1; i++) {
                m->write_byte(line_addr + i, latin_to_zscii(line_buf[i]));
            }

            finish_read(true, (uword) ev->val1);
            }
            break;
        case evtype_CharInput:
            {
            ubyte c = latin_to_zscii(ev->val1);

            // send to command transcript if necessary
            if (out_select[4]) {
                glk_put_char_stream(out_stream[4], c);
            }

            finish_read(true, (uword) c);
            }
            break;
        case evtype_Timer:
            {
            timer_continue* cont = new timer_continue;
            MEMCHECK(cont);
            cont->timer_io = this;
            m->interrupt(ipt_addr, cont);
            }
            break;
        default:
            FATAL("Unexpected event detected.");
            break;
    }
}

// finish_read
//
// finish the outstanding read request, cancelling if success is false.

void
iface::finish_read(bool success, uword value)
{
    sync_arrangement();

    if (cont != NULL) {
        if (success) {
            cont->execute(value);
        } else {
            cont->cancel();
        }
        cont = NULL;
    }

    glk_cancel_char_event(lower_win);
    glk_cancel_line_event(lower_win, NULL);
    glk_request_timer_events(0);
    now_blocked = false;
}

// show_status
//
// force zeal to update the status bar, for versions <= 3.

void
iface::show_status()
{
    ASSERT(m->version() <= 3);

    glk_window_clear(status_win);

    glui32 width;
    glk_window_get_size(status_win, &width, NULL);

    glui32 namewidth = width - 32;

    // add the current location at the beginning if we have room
    if (namewidth >= 3) {
        char tmpstr[20];
        char* name = new char [namewidth + 1];
        MEMCHECK(name);

        // use a glk memory stream to get the current room name
        strid_t s = glk_stream_open_memory(name, namewidth + 1,
                                           filemode_Write, NULL);

        stream_output sout(s);
        address addr = object(m->read_var(0x10)).get_name();

        bfish_addr(addr).print_string(&sout);

        stream_result_t result;
        glk_stream_close(s, &result);

        if (result.writecount > namewidth) {
            for (int i = 1; i <= 3; i++) {
                name[namewidth - i] = '.';
            }
            name[namewidth] = 0;
        } else {
            name[result.writecount] = 0;
        }

        // write the status line to the status window
        strid_t ws = glk_window_get_stream(status_win);
        glk_window_move_cursor(status_win, 1, 0);
        glk_put_string_stream(ws, name);

        if (m->version() == 3 && (m->read_byte(HEADER_FLAGS1) & 0x2)) {
            // print current time for time games
            uword hours = m->read_var(0x11);
            uword hours_print = hours % 12;
            uword minutes_print = m->read_var(0x12) % 60;

            if (hours_print == 0) {
                hours_print = 12;
            }

            glk_window_move_cursor(status_win, width - 28, 0);
            glk_put_string_stream(ws, "Time: ");
            snprintf_lite(tmpstr, 20, "%2d:%02d", hours_print, minutes_print);
            glk_put_string_stream(ws, tmpstr);
            glk_put_string_stream(ws, (hours < 12) ? " am" : " pm");
        } else {
            // print score and moves for "normal" games
            glk_window_move_cursor(status_win, width - 28, 0);
            glk_put_string_stream(ws, "Score: ");
            snprintf_lite(tmpstr, 20, "%d", m->read_var(0x11));
            glk_put_string_stream(ws, tmpstr);

            glk_window_move_cursor(status_win, width - 15, 0);
            glk_put_string_stream(ws, "Moves: ");
            snprintf_lite(tmpstr, 20, "%d", m->read_var(0x12));
            glk_put_string_stream(ws, tmpstr);
        }

        delete [] name;
    }
}

// check_transcript
//
// we need to keep output stream 2 in sync with the corresponding header
// bit, so this should be called often enough to ensure that output stream
// 2 can be selected/deselected by setting/clearing this header bit.

void
iface::check_transcript()
{
    bool flag = (m->read_word(HEADER_FLAGS2) & 0x1) != 0;

    if (script_flag != flag) {
        if (flag) {
            select(2);
        } else {
            deselect(2);
        }
    }
}

// zscii_to_latin
//
// convert a zscii character to latin for output to a glk stream.

ubyte
iface::zscii_to_latin(ubyte z)
{
    if (z == 0) {
        FATAL("Cannot translate zscii 0.");
        return 0;
    } else if (z == 9) {
        return ' '; // technically only v6, but zork seems to need it
    } else if (z == 13) {
        return '\n';
    } else if (z >= 32 && z <= 126) {
        return z;
    } else if (z >= 155 && z <= 251) {
        FATAL("Extra characters not supported.");
        return 0;
    } else {
        FATAL("Invalid zscii character specified.");
        return 0;
    }
}

// latin_to_zscii
//
// convert a latin character retrieved from glk to a zscii character that
// will make zeal happy.

ubyte
iface::latin_to_zscii(glsi32 c)
{
    if (c >= 32 && c <= 126) {
        return (ubyte)c;
    } else {
        switch (c) {
            case keycode_Delete:
                return 8;
            case keycode_Return:
                return 13;
            case keycode_Escape:
                return 27;
            case keycode_Up:
                return 129;
            case keycode_Down:
                return 130;
            case keycode_Left:
                return 131;
            case keycode_Right:
                return 132;
            case keycode_Func1:
                return 133;
            case keycode_Func2:
                return 134;
            case keycode_Func3:
                return 135;
            case keycode_Func4:
                return 136;
            case keycode_Func5:
                return 137;
            case keycode_Func6:
                return 138;
            case keycode_Func7:
                return 139;
            case keycode_Func8:
                return 140;
            case keycode_Func9:
                return 141;
            case keycode_Func10:
                return 142;
            case keycode_Func11:
                return 143;
            case keycode_Func12:
                return 144;
            default:
                FATAL("Unrecognized input character.");
                return 0;
        }
    }
}

// reset_cursor
//
// move the cursor back to the upper left corner of the upper window.

void
iface::reset_cursor()
{
    if (upper_win != NULL) {
        glk_window_move_cursor(upper_win, 0, 0);
        cur_x = 1;
        cur_y = 1;
    }
}

// adjust_cursor
//
// adjust the stored cursor coordinates to compensate for outputting the
// specified character.

void
iface::adjust_cursor(ubyte c)
{
    if (select_win == UPPER) {
        glui32 len;
        glui32 max_x;

        glk_gestalt_ext(gestalt_CharOutput, c, &len, 1);
        glk_window_get_size(upper_win, &max_x, NULL);

        cur_x += len;

        // wrap if necessary
        if (cur_x > max_x) {
            cur_x %= max_x;
            cur_y++;
        }
    }
}

// sync_arrangement
//
// make sure the upper window is the height that the machine expects.
// (we may have delayed updates until after player input.)

void
iface::sync_arrangement()
{
    if (upper_win != NULL) {
        glui32 height;

        glk_window_get_size(upper_win, NULL, &height);

        if (real_height < height) {
            ASSERT(real_height > 0);

            glk_window_set_arrangement(glk_window_get_parent(upper_win),
                                       winmethod_Above | winmethod_Fixed,
                                       real_height, NULL);
        }
    }
}

// =======================================================================
//  stream_output
// =======================================================================

// constructor/destructor
//
// save the stream we're given.  whee!  this is a generic zscii_output
// class for use with a single glk stream.

stream_output::stream_output(strid_t s_in)
  : s(s_in)
{
}

stream_output::~stream_output()
{
}

// put_char
//
// send the character to the stream.  duh.

void
stream_output::put_char(ubyte z)
{
    glk_put_char_stream(s, z);
}
