/* FrobTadsApplicationCurses class implementation.
 */
#include "common.h"

#include <string.h>
#include <stdlib.h>
#if HAVE_TIOCGWINSZ or HAVE_TIOCGSIZE
# if (not GWINSZ_IN_SYS_IOCTL or HAVE_TIOCGSIZE) and HAVE_TERMIOS_H
#  include <termios.h>
# endif
# if HAVE_SYS_IOCTL_H
#  include <sys/ioctl.h>
# endif
#endif

#include "frobtadsappcurses.h"
#include "colors.h"


/* Service routine.  Gets the terminal's size.
 * On success, stores the terminal dimensions in 'width' and
 * 'height' and returns true.  On failure, returns false and
 * 'width' nor 'height' are modified.
 */
static bool
getTermSize( unsigned& width, unsigned& height )
{
    // If the system has support for the TIOCGWINSZ or TIOCGSIZE
    // ioctl, use it to obtain the terminal's dimensions.
    int res = -1;
    unsigned w = 0;
    unsigned h = 0;
#if HAVE_TIOCGWINSZ
    winsize size;
    size.ws_col = size.ws_row = 0;
    res = ioctl(0, TIOCGWINSZ, &size);
    w = size.ws_col;
    h = size.ws_row;
#elif HAVE_TIOCGSIZE
    ttysize size;
    size.ts_cols = size.ts_lines = 0;
    res = ioctl(0, TIOCGSIZE, &size);
    w = size.ts_cols;
    h = size.ts_lines;
#endif
    if (res < 0) {
        // The call failed.
        return false;
    }
    if (w == 0 or h == 0) {
        // Kernel doesn't seem to know the terminal's size.     
        // Assume 80x24.
        width = 80;
        height = 24;
        return true;
    }
    width = w;
    height = h;
    return true;
}


FrobTadsApplicationCurses::FrobTadsApplicationCurses( const FrobOptions& opts )
    : FrobTadsApplication(opts)
{
    // Before starting curses, we set the LINES and COLUMNS env.
    // variables to a "maximum" value.  This is needed in case we're
    // running on a system with an old/broken curses implementation
    // (Solaris curses, for example).  This forces curses to
    // allocate enough internal memory for the windows in case the
    // terminal gets resized.  Note that we only do this if
    // detection of the terminal size is possible at all.
#if HAVE_PUTENV
    unsigned bogusX, bogusY; // Dummies.
    if (getTermSize(bogusX, bogusY)) {
        // The strings become part of the environment; they have to be
        // static.
        static char lines[] = "LINES=225";
        static char columns[] = "COLUMNS=300";
        putenv(lines);
        putenv(columns);
    }
#endif

    // Fire-up curses.
    initscr();

    // Make characters typed by the user immediately available to
    // the program.
    cbreak();

    // Tell courses not to echo user-input (TADS does the echoing).
    noecho();

    // Hide the cursor.  This avoids the "crazy cursor jumping"
    // problem on *really* slow terminals, as TADS constantly moves
    // the cursor all over the place.
    curs_set(0);

    // Tell courses not to try to translate the RETURN/ENTER key on
    // input (or else getch() and friends would never report that
    // key), and the '\n' character on output (since the osgen layer
    // handles newlines).
    nonl();

    // Initialize colors if we want color support and the terminal supports it.
    if ((opts.useColors and has_colors()) or opts.forceColors) {
        this->fColorsEnabled = true;
        start_color();
        // Assign terminal default foreground/background colors
        // to color number -1 / color pair 0.
#if HAVE_USE_DEFAULT_COLORS
        if (opts.defColors) {
            use_default_colors();
        }
#endif
        // Initialize the curses color-pairs we use.
        //
        // This is paranoia, but better to make sure that curses
        // will use the right colors.  The paranoid part of this
        // is that we assume that there might be some curses
        // version out there that does not use monotonically
        // increasing values for the COLOR_* symbols.
        init_pair(makeColorPair(FROB_BLACK,   FROB_BLACK), COLOR_BLACK,   COLOR_BLACK);
        init_pair(makeColorPair(FROB_RED,     FROB_BLACK), COLOR_RED,     COLOR_BLACK);
        init_pair(makeColorPair(FROB_GREEN,   FROB_BLACK), COLOR_GREEN,   COLOR_BLACK);
        init_pair(makeColorPair(FROB_YELLOW,  FROB_BLACK), COLOR_YELLOW,  COLOR_BLACK);
        init_pair(makeColorPair(FROB_BLUE,    FROB_BLACK), COLOR_BLUE,    COLOR_BLACK);
        init_pair(makeColorPair(FROB_MAGENTA, FROB_BLACK), COLOR_MAGENTA, COLOR_BLACK);
        init_pair(makeColorPair(FROB_CYAN,    FROB_BLACK), COLOR_CYAN,    COLOR_BLACK);
        // White on black is wired to color pair 0 by
        // definition; we can't and don't need to initialize it.
        //init_pair(makeColorPair(FROB_WHITE,   FROB_BLACK), COLOR_WHITE,   COLOR_BLACK);

        init_pair(makeColorPair(FROB_BLACK,   FROB_RED), COLOR_BLACK,   COLOR_RED);
        init_pair(makeColorPair(FROB_RED,     FROB_RED), COLOR_RED,     COLOR_RED);
        init_pair(makeColorPair(FROB_GREEN,   FROB_RED), COLOR_GREEN,   COLOR_RED);
        init_pair(makeColorPair(FROB_YELLOW,  FROB_RED), COLOR_YELLOW,  COLOR_RED);
        init_pair(makeColorPair(FROB_BLUE,    FROB_RED), COLOR_BLUE,    COLOR_RED);
        init_pair(makeColorPair(FROB_MAGENTA, FROB_RED), COLOR_MAGENTA, COLOR_RED);
        init_pair(makeColorPair(FROB_CYAN,    FROB_RED), COLOR_CYAN,    COLOR_RED);
        init_pair(makeColorPair(FROB_WHITE,   FROB_RED), COLOR_WHITE,   COLOR_RED);

        init_pair(makeColorPair(FROB_BLACK,   FROB_GREEN), COLOR_BLACK,   COLOR_GREEN);
        init_pair(makeColorPair(FROB_RED,     FROB_GREEN), COLOR_RED,     COLOR_GREEN);
        init_pair(makeColorPair(FROB_GREEN,   FROB_GREEN), COLOR_GREEN,   COLOR_GREEN);
        init_pair(makeColorPair(FROB_YELLOW,  FROB_GREEN), COLOR_YELLOW,  COLOR_GREEN);
        init_pair(makeColorPair(FROB_BLUE,    FROB_GREEN), COLOR_BLUE,    COLOR_GREEN);
        init_pair(makeColorPair(FROB_MAGENTA, FROB_GREEN), COLOR_MAGENTA, COLOR_GREEN);
        init_pair(makeColorPair(FROB_CYAN,    FROB_GREEN), COLOR_CYAN,    COLOR_GREEN);
        init_pair(makeColorPair(FROB_WHITE,   FROB_GREEN), COLOR_WHITE,   COLOR_GREEN);

        init_pair(makeColorPair(FROB_BLACK,   FROB_YELLOW), COLOR_BLACK,   COLOR_YELLOW);
        init_pair(makeColorPair(FROB_RED,     FROB_YELLOW), COLOR_RED,     COLOR_YELLOW);
        init_pair(makeColorPair(FROB_GREEN,   FROB_YELLOW), COLOR_GREEN,   COLOR_YELLOW);
        init_pair(makeColorPair(FROB_YELLOW,  FROB_YELLOW), COLOR_YELLOW,  COLOR_YELLOW);
        init_pair(makeColorPair(FROB_BLUE,    FROB_YELLOW), COLOR_BLUE,    COLOR_YELLOW);
        init_pair(makeColorPair(FROB_MAGENTA, FROB_YELLOW), COLOR_MAGENTA, COLOR_YELLOW);
        init_pair(makeColorPair(FROB_CYAN,    FROB_YELLOW), COLOR_CYAN,    COLOR_YELLOW);
        init_pair(makeColorPair(FROB_WHITE,   FROB_YELLOW), COLOR_WHITE,   COLOR_YELLOW);

        init_pair(makeColorPair(FROB_BLACK,   FROB_BLUE), COLOR_BLACK,   COLOR_BLUE);
        init_pair(makeColorPair(FROB_RED,     FROB_BLUE), COLOR_RED,     COLOR_BLUE);
        init_pair(makeColorPair(FROB_GREEN,   FROB_BLUE), COLOR_GREEN,   COLOR_BLUE);
        init_pair(makeColorPair(FROB_YELLOW,  FROB_BLUE), COLOR_YELLOW,  COLOR_BLUE);
        init_pair(makeColorPair(FROB_BLUE,    FROB_BLUE), COLOR_BLUE,    COLOR_BLUE);
        init_pair(makeColorPair(FROB_MAGENTA, FROB_BLUE), COLOR_MAGENTA, COLOR_BLUE);
        init_pair(makeColorPair(FROB_CYAN,    FROB_BLUE), COLOR_CYAN,    COLOR_BLUE);
        init_pair(makeColorPair(FROB_WHITE,   FROB_BLUE), COLOR_WHITE,   COLOR_BLUE);

        init_pair(makeColorPair(FROB_BLACK,   FROB_MAGENTA), COLOR_BLACK,   COLOR_MAGENTA);
        init_pair(makeColorPair(FROB_RED,     FROB_MAGENTA), COLOR_RED,     COLOR_MAGENTA);
        init_pair(makeColorPair(FROB_GREEN,   FROB_MAGENTA), COLOR_GREEN,   COLOR_MAGENTA);
        init_pair(makeColorPair(FROB_YELLOW,  FROB_MAGENTA), COLOR_YELLOW,  COLOR_MAGENTA);
        init_pair(makeColorPair(FROB_BLUE,    FROB_MAGENTA), COLOR_BLUE,    COLOR_MAGENTA);
        init_pair(makeColorPair(FROB_MAGENTA, FROB_MAGENTA), COLOR_MAGENTA, COLOR_MAGENTA);
        init_pair(makeColorPair(FROB_CYAN,    FROB_MAGENTA), COLOR_CYAN,    COLOR_MAGENTA);
        init_pair(makeColorPair(FROB_WHITE,   FROB_MAGENTA), COLOR_WHITE,   COLOR_MAGENTA);

        init_pair(makeColorPair(FROB_BLACK,   FROB_CYAN), COLOR_BLACK,   COLOR_CYAN);
        init_pair(makeColorPair(FROB_RED,     FROB_CYAN), COLOR_RED,     COLOR_CYAN);
        init_pair(makeColorPair(FROB_GREEN,   FROB_CYAN), COLOR_GREEN,   COLOR_CYAN);
        init_pair(makeColorPair(FROB_YELLOW,  FROB_CYAN), COLOR_YELLOW,  COLOR_CYAN);
        init_pair(makeColorPair(FROB_BLUE,    FROB_CYAN), COLOR_BLUE,    COLOR_CYAN);
        init_pair(makeColorPair(FROB_MAGENTA, FROB_CYAN), COLOR_MAGENTA, COLOR_CYAN);
        init_pair(makeColorPair(FROB_CYAN,    FROB_CYAN), COLOR_CYAN,    COLOR_CYAN);
        init_pair(makeColorPair(FROB_WHITE,   FROB_CYAN), COLOR_WHITE,   COLOR_CYAN);

        init_pair(makeColorPair(FROB_BLACK,   FROB_WHITE), COLOR_BLACK,   COLOR_WHITE);
        init_pair(makeColorPair(FROB_RED,     FROB_WHITE), COLOR_RED,     COLOR_WHITE);
        init_pair(makeColorPair(FROB_GREEN,   FROB_WHITE), COLOR_GREEN,   COLOR_WHITE);
        init_pair(makeColorPair(FROB_YELLOW,  FROB_WHITE), COLOR_YELLOW,  COLOR_WHITE);
        init_pair(makeColorPair(FROB_BLUE,    FROB_WHITE), COLOR_BLUE,    COLOR_WHITE);
        init_pair(makeColorPair(FROB_MAGENTA, FROB_WHITE), COLOR_MAGENTA, COLOR_WHITE);
        init_pair(makeColorPair(FROB_CYAN,    FROB_WHITE), COLOR_CYAN,    COLOR_WHITE);
        init_pair(makeColorPair(FROB_WHITE,   FROB_WHITE), COLOR_WHITE,   COLOR_WHITE);
    }
}


FrobTadsApplicationCurses::~FrobTadsApplicationCurses()
{
    // Enable the cursor.
    curs_set(0);

    // Flush any pending output so we won't lose any "thanks for
    // playing" messages when "pausing prior to exit" is disabled.
    if (not this->options.exitPause)
        this->fGameWindow->flush();

    // Shut down curses.
    endwin();
}


void
FrobTadsApplicationCurses::init()
{
    // Create the main window.  Make it fill the whole terminal.  We
    // initialize the dimensions to 0 in case getTermSize() is
    // unable to detect the actual size, since in curses/ncurses 0
    // for width and height actually means "fullscreen".
    unsigned width = 0;
    unsigned height = 0;
    bool sizeDetected = getTermSize(width, height);

    // Update the LINES/COLUMNS environment vars in case we're running inside
    // an old/broken terminal or are using an old/broken version of curses.
    // Only do this if we could determine the terminal dimensions and we're
    // not running inside an Emacs eterm; setting the env vars in an eterm
    // results in severe display problems.
#if HAVE_PUTENV
    const char* termEnv = getenv("TERM");
    if (sizeDetected and termEnv != 0 and strncmp(termEnv, "eterm", 5) != 0) {
        // The strings have to be static, since putenv() does not always copy
        // the string arguments but points the environment to the application's
        // address space.
        static char linesEnv[32];
        static char columnsEnv[32];
        char tmp[16];
        strcpy(linesEnv, "LINES=");
        strcpy(columnsEnv, "COLUMNS=");
        sprintf(tmp, "%u", height);
        strcat(linesEnv, tmp);
        sprintf(tmp, "%u", width);
        strcat(columnsEnv, tmp);
        // Normally, since the strings are part of the
        // environment, we would need to use putenv() only once;
        // changing the strings later would also change the
        // environment, since the strings are part of it.
        // However, not every system follows the standard, so we
        // need to putenv() the strings over and over again.
        putenv(linesEnv);
        putenv(columnsEnv);
        // Enforce the detected size upon curses.
        // TODO: Is this needed?  Or even allowed?  For now, we
        // assume it's neither.
        //COLS = width;
        //LINES = height;
    }
#endif

    // Reset curses, in case the terminal's size has changed.
    endwin();
    refresh();
    // Fixes problems with old curses versions.
    wclear(stdscr);

    // Create the window.
    this->fGameWindow = std::make_unique<FrobTadsWindow>(height, width, 0, 0);

    // Disable scrolling.
    this->fGameWindow->enableScrolling(false);

    // Tell the window to recognize keypad keys (function keys,
    // cursor-keys, etc.) during input.
    this->fGameWindow->keypadMode(true);

    // Enable 8-bit characters during input.
    this->fGameWindow->input8bit(true);

    // Create the buffer that we use to optimize output.
    this->fDispBuf = std::make_unique<chtype[]>(this->fGameWindow->width() + 1);

    // Explicitly mark the window as touched, to work around a
    // curses color and screen corruption issue.
    this->fGameWindow->touch();

    // Fix for curses color corruption problem.  The window is
    // cleared to a color (other than black and white) which contrasts
    // with the normal background color.
    if (sizeDetected) {
        int fillcolor;
        if (this->options.bgColor == FROB_BLUE) {
            fillcolor = ossgetcolor(OSGEN_COLOR_WHITE, OSGEN_COLOR_RED, 0, 0);
        } else {
            fillcolor = ossgetcolor(OSGEN_COLOR_WHITE, OSGEN_COLOR_BLUE, 0, 0);
        }
        ossclr(0, 0, height - 1, width - 1, fillcolor);
        this->fGameWindow->flush();
    }
}


void
FrobTadsApplicationCurses::clear( int top, int left, int bottom, int right, int attrs )
{
    const chtype c = attrs | ' ';
    for (int y = top; y <= bottom; ++y) {
        for (int x = left; x <= right; ++x) {
            this->fGameWindow->printChar(y, x, c);
        }
    }
}


void
FrobTadsApplicationCurses::scrollRegionUp( int top, int left, int bottom, int right, int attrs )
{
    // Old curses versions might break when using overlapping
    // windows, so we do the scrolling by hand.
    for (int y = bottom; y > top; --y) {
        for (int x = left; x <= right; ++x) {
            const chtype c = this->fGameWindow->charAt(y-1, x);
            this->fGameWindow->printChar(y, x, c);
        }
    }

    // Clear the last line.
    const chtype c = attrs | ' ';
    for (int i = left; i <= right; ++i) this->fGameWindow->printChar(top, i, c);

    // If soft-scrolling is enabled, update the display so that we
    // have a visible scrolling-effect.
    if (this->options.softScroll) this->fGameWindow->flush();
}


void
FrobTadsApplicationCurses::scrollRegionDown( int top, int left, int bottom, int right, int attrs )
{
    // This works like scrollRegionUp(), just the other way around.
    for (int y = top; y < bottom; ++y) {
        for (int x = left; x <= right; ++x) {
            this->fGameWindow->printChar(y, x, this->fGameWindow->charAt(y+1, x));
        }
    }
    const chtype c = attrs | ' ';
    for (int i = left; i <= right; ++i) this->fGameWindow->printChar(bottom, i, c);
    if (this->options.softScroll) this->fGameWindow->flush();
}


int
FrobTadsApplicationCurses::getRawChar( bool cursorVisible, int timeout )
{
    // The character we read.
    int c;

    // Tell osgen to check if it should redraw the screen.
    osssb_redraw_if_needed();

    // Some curses versions don't automatically update the display
    // prior to input, so we must explicitly request it.
    this->fGameWindow->flush();

    // Prepare for timed input.
    this->fGameWindow->setTimeout(timeout);
    this->fRemainingTimeout = timeout;

    // Enable the cursor, if needed.  We won't enable it if the
    // timeout is very small, even if the caller requested it.
    // "Small" means under 1 second, which is what most terminals
    // use for a cursor blink.
    if (cursorVisible and (timeout > 1000 or timeout < 0)) curs_set(1);

    // On some systems, ncurses is configured to supply its own
    // SIGWINCH handler.  If this is the case, getch() returns
    // KEY_RESIZE when the terminal has been resized.  We can't rely
    // on this behavior to handle screen resizes, but we must still
    // recognize this return code; here we simply ignore it.  Note
    // that curses doesn't know anything about KEY_RESIZE; it's an
    // ncurses thing.  We only check for it if it is defined.
#ifdef KEY_RESIZE
    while ((c = this->fGameWindow->getChar()) == KEY_RESIZE)
        ;
#else
    c = this->fGameWindow->getChar();
#endif
    this->fRemainingTimeout = 0;

    // Some terminals don't return KEY_BACKSPACE for the
    // backspace-key, but some opaque platform-specific code.  If
    // this is the case, map it to KEY_BACKSPACE.
    if (c == erasechar()) c = KEY_BACKSPACE;

    // Similarly for KEY_DL.
    if (c == killchar()) c = KEY_DL;

    curs_set(0);
    return c;
}
