/*
Copyright (C) 1996-2001 Id Software, Inc.
Copyright (C) 2002-2009 John Fitzgibbons and others
Copyright (C) 2010-2014 QuakeSpasm developers

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.

*/
// console.c

#include <cstring>
#include <sys/types.h>
#include <time.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <ranges>
#include <sstream>
#ifdef _WIN32
#include <io.h>
#else
#include <unistd.h>
#endif
#include "quakedef.hpp"

int con_linewidth;

float con_cursorspeed = 4;

#define		CON_TEXTSIZE (1024 * 1024) //ericw -- was 65536. johnfitz -- new default size
#define		CON_MINSIZE  16384 //johnfitz -- old default, now the minimum size

int con_buffersize; //johnfitz -- user can now override default


int con_current; // where next message will be printed
int con_x; // offset in current line for next print
char *con_text = NULL;



#define	NUM_CON_TIMES 4
float con_times[NUM_CON_TIMES]; // realtime time the line was generated
// for transparent notify lines

int con_vislines;

bool con_debuglog = false;

extern int history_line;

constexpr std::size_t MAX_PRINT_MSG = 4096;


namespace console {
    bool initialized{false};
    bool forced_up{false}; // because no entities to refresh

    int total_lines{0}; // total lines in console scrollback
    int back_scroll{0}; // lines up from bottom to display

    std::string last_center_string{};

    namespace {
        convar notify_time{"con_notifytime", "3"}; //seconds
        convar log_center_print{"con_logcenterprint", "1"}; //johnfitz

        void _toggle_console() {
            toggle_console();
        }

        void _message_mode() {
            if (cls.state != ca_connected || cls.demoplayback)
                return;
            chat_team = false;
            key_dest = key_message;
        }

        void _message_mode2() {
            if (cls.state != ca_connected || cls.demoplayback)
                return;
            chat_team = true;
            key_dest = key_message;
        }

        void _clear() {
            if (con_text)
                std::memset(con_text, ' ', con_buffersize); //johnfitz -- con_buffersize replaces CON_TEXTSIZE
            console::back_scroll = 0; //johnfitz -- if console is empty, being scrolled up is confusing
        }

        void _dump() {
            int l, x;
            const char *line;
            FILE *f;
            char buffer[1024];
            char name[MAX_OSPATH];

            q_snprintf(name, sizeof(name), "%s/condump.txt", com_gamedir);
            COM_CreatePath(name);
            f = fopen(name, "w");
            if (!f) {
                info("ERROR: couldn't open file %s.\n", name);
                return;
            }

            // skip initial empty lines
            for (l = con_current - total_lines + 1; l <= con_current; l++) {
                line = con_text + (l % total_lines) * con_linewidth;
                for (x = 0; x < con_linewidth; x++)
                    if (line[x] != ' ')
                        break;
                if (x != con_linewidth)
                    break;
            }

            // write the remaining lines
            buffer[con_linewidth] = 0;
            for (; l <= con_current; l++) {
                line = con_text + (l % total_lines) * con_linewidth;
                strncpy(buffer, line, con_linewidth);
                for (x = con_linewidth - 1; x >= 0; x--) {
                    if (buffer[x] == ' ')
                        buffer[x] = 0;
                    else
                        break;
                }
                for (x = 0; buffer[x]; x++)
                    buffer[x] &= 0x7f;

                fprintf(f, "%s\n", buffer);
            }

            fclose(f);
            info("Dumped console text to %s.\n", name);
        }

        void print(const std::string_view text) {
            int y;
            int l;
            static int cr;
            int mask;

            std::istringstream ss{text};

            switch (ss.peek()) {
                case 1:
                    mask = 128;
                    S_LocalSound("misc/talk.wav"); // play talk wav
                    ss.get();
                    break;
                case 2:
                    mask = 128;
                    ss.get();
                    break;
                default:
                    mask = 0;
                    break;
            }

            bool boundary = true;

            while (!ss.eof()) {
                const int c = ss.peek();
                if (ss.eof()) {
                    break;
                }

                if (c <= ' ') {
                    boundary = true;
                } else if (boundary) {
                    // count word length
                    for (l = 0; l < con_linewidth; l++) {
                        if (ss.peek() <= ' ')
                            break;
                        ss.get();
                    }

                    for (auto i = l; i > 0; i--) {
                        ss.unget();
                    }

                    // word wrap
                    if (l != con_linewidth && (con_x + l > con_linewidth))
                        con_x = 0;

                    boundary = false;
                }

                ss.get();

                if (cr) {
                    con_current--;
                    cr = false;
                }

                if (!con_x) {
                    line_feed();
                    // mark time for transparent overlay
                    if (con_current >= 0)
                        con_times[con_current % NUM_CON_TIMES] = realtime;
                }

                switch (c) {
                    case '\n':
                        con_x = 0;
                        break;

                    case '\r':
                        con_x = 0;
                        cr = 1;
                        break;

                    default: // display character and advance
                        y = con_current % total_lines;
                        con_text[y * con_linewidth + con_x] = c | mask;
                        con_x++;
                        if (con_x >= con_linewidth)
                            con_x = 0;
                        break;
                }
            }
        }


        void center_print(int line_width, const char *fmt, ...) {
            va_list argptr;
            char msg[MAX_PRINT_MSG]; //the original message
            char line[MAX_PRINT_MSG]; //one line from the message
            char spaces[21]; //buffer for spaces
            char *src, *dst;
            int len, s;

            va_start(argptr, fmt);
            q_vsnprintf(msg, sizeof(msg), fmt, argptr);
            va_end(argptr);

            line_width = std::min(line_width, con_linewidth);
            for (src = msg; *src;) {
                dst = line;
                while (*src && *src != '\n')
                    *dst++ = *src++;
                *dst = 0;
                if (*src == '\n')
                    src++;

                len = strlen(line);
                if (len < line_width) {
                    s = (line_width - len) / 2;
                    memset(spaces, ' ', s);
                    spaces[s] = 0;
                    info("%s%s\n", spaces, line);
                } else
                    info("%s\n", line);
            }
        }
    }

    void init() {
        if (const auto i = common::check_param("-consize"); i.has_value() && i.value() < common::_argv.size() - 1) {
            con_buffersize = std::atoi(common::_argv[i.value() + 1].c_str()) * 1024;
            if (con_buffersize < CON_MINSIZE)
                con_buffersize = CON_MINSIZE;
        } else
            con_buffersize = CON_TEXTSIZE;
        //johnfitz

        con_text = (char *) Hunk_AllocName(con_buffersize, "context");
        //johnfitz -- con_buffersize replaces CON_TEXTSIZE
        std::memset(con_text, ' ', con_buffersize); //johnfitz -- con_buffersize replaces CON_TEXTSIZE
        con_linewidth = -1;

        //johnfitz -- no need to run Con_CheckResize here
        con_linewidth = 38;
        total_lines = con_buffersize / con_linewidth; //johnfitz -- con_buffersize replaces CON_TEXTSIZE
        console::back_scroll = 0;
        con_current = total_lines - 1;
        //johnfitz

        info("Console initialized.\n");

        notify_time.inscribe();
        log_center_print.inscribe(); //johnfitz

        command::add("toggleconsole", _toggle_console);
        command::add("messagemode", _message_mode);
        command::add("messagemode2", _message_mode2);
        command::add("clear", _clear);
        command::add("condump", _dump); //johnfitz
        initialized = true;
    }

    void line_feed() {
        //johnfitz -- improved scrolling
        if (console::back_scroll)
            console::back_scroll++;
        if (console::back_scroll > total_lines - (glheight >> 3) - 1)
            console::back_scroll = total_lines - (glheight >> 3) - 1;
        //johnfitz

        con_x = 0;
        con_current++;
        std::memset(&con_text[(con_current % total_lines) * con_linewidth], ' ', con_linewidth);
    }

    void info(const char *fmt, ...) {
        va_list argptr;
        char msg[MAX_PRINT_MSG];
        static bool inupdate;

        va_start(argptr, fmt);
        q_vsnprintf(msg, sizeof(msg), fmt, argptr);
        va_end(argptr);

        // also echo to debugging console
        Sys_Printf("%s", msg);

        // log all messages to file
        if (con_debuglog) {
            Con_DebugLog(msg);
        }

        if (!initialized) {
            return;
        }

        if (cls.state == ca_dedicated)
            return; // no graphics mode

        // write it to the scrollable buffer
        print(msg);

        // update the screen if the console is displayed
        if (cls.signon != SIGNONS && !scr_disabled_for_loading) {
            // protect against infinite loop if something in SCR_UpdateScreen calls
            // Con_Printd
            if (!inupdate) {
                inupdate = true;
                SCR_UpdateScreen();
                inupdate = false;
            }
        }
    }

    void warn(const char *fmt, ...) {
        va_list argptr;
        char msg[MAX_PRINT_MSG];

        va_start(argptr, fmt);
        std::vsnprintf(msg, sizeof(msg), fmt, argptr);
        va_end(argptr);

        safe_print("\x02Warning: ");
        info("%s", msg);
    }

    void dev_warn(const char *fmt, ...) {
        va_list argptr;
        char msg[MAX_PRINT_MSG];

        if (!developer.value)
            return; // don't confuse non-developers with techie stuff...

        va_start(argptr, fmt);
        q_vsnprintf(msg, sizeof(msg), fmt, argptr);
        va_end(argptr);

        safe_print("\x02Warning: ");
        info("%s", msg);
    }

    void debug(const char *fmt, ...) {
        va_list argptr;
        char msg[MAX_PRINT_MSG];

        if (developer.value == 0)
            return; // don't confuse non-developers with techie stuff...

        va_start(argptr, fmt);
        q_vsnprintf(msg, sizeof(msg), fmt, argptr);
        va_end(argptr);

        safe_print("%s", msg); //johnfitz -- was console::info
    }

    void trace(const char *fmt, ...) {
        va_list argptr;
        char msg[MAX_PRINT_MSG];

        if (developer.value >= 2) {
            va_start(argptr, fmt);
            q_vsnprintf(msg, sizeof(msg), fmt, argptr);
            va_end(argptr);
            info("%s", msg);
        }
    }

    void safe_print(const char *fmt, ...) {
        va_list argptr;
        char msg[MAX_PRINT_MSG];
        int temp;

        va_start(argptr, fmt);
        q_vsnprintf(msg, sizeof(msg), fmt, argptr);
        va_end(argptr);

        temp = scr_disabled_for_loading;
        scr_disabled_for_loading = true;
        info("%s", msg);
        scr_disabled_for_loading = temp;
    }

    void toggle_console() {
        if (key_dest == key_console/* || (key_dest == key_game && con_forcedup)*/) {
            key_lines[edit_line][1] = 0; // clear any typing
            key_linepos = 1;
            console::back_scroll = 0; //johnfitz -- toggleconsole should return you to the bottom of the scrollback
            history_line = edit_line; //johnfitz -- it should also return you to the bottom of the command history

            if (cls.state == ca_connected) {
                IN_Activate();
                key_dest = key_game;
            } else {
                M_Menu_Main_f();
            }
        } else {
            IN_Deactivate(modestate == MS_WINDOWED);
            key_dest = key_console;
        }

        SCR_EndLoadingPlaque();
        memset(con_times, 0, sizeof(con_times));
    }

    void check_resize() {
        int i, j;

        const int width = (vid.conwidth >> 3) - 2; //johnfitz -- use vid.conwidth instead of vid.width

        if (width == con_linewidth)
            return;

        const int oldwidth = con_linewidth;
        con_linewidth = width;
        const int oldtotallines = total_lines;
        total_lines = con_buffersize / con_linewidth; //johnfitz -- con_buffersize replaces CON_TEXTSIZE
        int numlines = oldtotallines;

        if (total_lines < numlines)
            numlines = total_lines;

        int numchars = oldwidth;

        if (con_linewidth < numchars)
            numchars = con_linewidth;

        const int mark = Hunk_LowMark(); //johnfitz
        char *tbuf = (char *) Hunk_Alloc(con_buffersize); //johnfitz

        std::memcpy(tbuf, con_text, con_buffersize); //johnfitz -- con_buffersize replaces CON_TEXTSIZE
        std::memset(con_text, ' ', con_buffersize); //johnfitz -- con_buffersize replaces CON_TEXTSIZE

        for (i = 0; i < numlines; i++) {
            for (j = 0; j < numchars; j++) {
                con_text[(total_lines - 1 - i) * con_linewidth + j] =
                        tbuf[((con_current - i + oldtotallines) % oldtotallines) * oldwidth + j];
            }
        }

        Hunk_FreeToLowMark(mark); //johnfitz

        Con_ClearNotify();

        back_scroll = 0;
        con_current = total_lines - 1;
    }
}


/*
================
Con_Quakebar -- johnfitz -- returns a bar of the desired length, but never wider than the console

includes a newline, unless len >= con_linewidth.
================
*/
const char *Con_Quakebar(int len) {
    static char bar[42];
    int i;

    len = std::min(len, (int) sizeof(bar) - 2);
    len = std::min(len, con_linewidth);

    bar[0] = '\35';
    for (i = 1; i < len - 1; i++)
        bar[i] = '\36';
    bar[len - 1] = '\37';

    if (len < con_linewidth) {
        bar[len] = '\n';
        bar[len + 1] = 0;
    } else
        bar[len] = 0;

    return bar;
}

/*
================
Con_ClearNotify
================
*/
void Con_ClearNotify(void) {
    int i;

    for (i = 0; i < NUM_CON_TIMES; i++)
        con_times[i] = 0;
}



// borrowed from uhexen2 by S.A. for new procs, LOG_Init, LOG_Close

static char logfilename[MAX_OSPATH]; // current logfile name
static int log_fd = -1; // log file descriptor

/*
================
Con_DebugLog
================
*/
void Con_DebugLog(const char *msg) {
    if (log_fd == -1)
        return;

    if (write(log_fd, msg, strlen(msg)) < 0) {
        close(log_fd);
        log_fd = -1;
        con_debuglog = false;
        fprintf(stderr, "Error writing to log file\n");
    }
}

/*
==================
Con_LogCenterPrint -- johnfitz -- echo centerprint message to the console
==================
*/
void Con_LogCenterPrint(const char *str) {
    if (str == console::last_center_string)
        return; //ignore duplicates

    if (cl.gametype == GAME_DEATHMATCH && console::log_center_print.value != 2)
        return; //don't log in deathmatch

    console::last_center_string = str;

    if (console::log_center_print.value != 0) {
        console::info("%s", Con_Quakebar(40));
        console::center_print(40, "%s\n", str);
        console::info("%s", Con_Quakebar(40));
        Con_ClearNotify();
    }
}

/*
==============================================================================

	TAB COMPLETION

==============================================================================
*/

//johnfitz -- tab completion stuff
//unique defs
char key_tabpartial[MAXCMDLINE];

typedef struct tab_s {
    const char *name;
    const char *type;
    struct tab_s *next;
    struct tab_s *prev;
} tab_t;

tab_t *tablist;

//defs from elsewhere
extern bool keydown[256];
#define	MAX_ALIAS_NAME	32

/*
============
AddToTabList -- johnfitz

tablist is a doubly-linked loop, alphabetized by name
============
*/

// bash_partial is the string that can be expanded,
// aka Linux Bash shell. -- S.A.
static char bash_partial[80];
static bool bash_singlematch;

void AddToTabList(const char *name, const char *type) {
    tab_t *t, *insert;
    char *i_bash;
    const char *i_name;

    if (!*bash_partial) {
        strncpy(bash_partial, name, 79);
        bash_partial[79] = '\0';
    } else {
        bash_singlematch = 0;
        // find max common between bash_partial and name
        i_bash = bash_partial;
        i_name = name;
        while (*i_bash && (*i_bash == *i_name)) {
            i_bash++;
            i_name++;
        }
        *i_bash = 0;
    }

    t = (tab_t *) Hunk_Alloc(sizeof(tab_t));
    t->name = name;
    t->type = type;

    if (!tablist) //create list
    {
        tablist = t;
        t->next = t;
        t->prev = t;
    } else if (strcmp(name, tablist->name) < 0) //insert at front
    {
        t->next = tablist;
        t->prev = tablist->prev;
        t->next->prev = t;
        t->prev->next = t;
        tablist = t;
    } else //insert later
    {
        insert = tablist;
        do {
            if (strcmp(name, insert->name) < 0)
                break;
            insert = insert->next;
        } while (insert != tablist);

        t->next = insert;
        t->prev = insert->prev;
        t->next->prev = t;
        t->prev->next = t;
    }
}

typedef struct arg_completion_type_s {
    const char *command;
    filelist_item_t **filelist;
} arg_completion_type_t;

static const arg_completion_type_t arg_completion_types[] =
{
    {"map ", &extralevels},
    {"changelevel ", &extralevels},
    {"game ", &modlist},
    {"record ", &demolist},
    {"playdemo ", &demolist},
    {"timedemo ", &demolist}
};

static constexpr int num_arg_completion_types = std::size(arg_completion_types);

/*
============
FindCompletion -- stevenaaus
============
*/
const char *FindCompletion(const char *partial, filelist_item_t *filelist, int *nummatches_out) {
    static char matched[40];
    char *i_matched, *i_name;
    filelist_item_t *file;
    int init, match, plen;

    memset(matched, 0, sizeof(matched));
    plen = strlen(partial);
    match = 0;

    for (file = filelist, init = 0; file; file = file->next) {
        if (!strncmp(file->name, partial, plen)) {
            if (init == 0) {
                init = 1;
                strncpy(matched, file->name, sizeof(matched) - 1);
                matched[sizeof(matched) - 1] = '\0';
            } else {
                // find max common
                i_matched = matched;
                i_name = file->name;
                while (*i_matched && (*i_matched == *i_name)) {
                    i_matched++;
                    i_name++;
                }
                *i_matched = 0;
            }
            match++;
        }
    }

    *nummatches_out = match;

    if (match > 1) {
        for (file = filelist; file; file = file->next) {
            if (!strncmp(file->name, partial, plen))
                console::safe_print("   %s\n", file->name);
        }
        console::safe_print("\n");
    }

    return matched;
}

/*
============
BuildTabList -- johnfitz
============
*/
void BuildTabList(const char *partial) {
    int len;

    tablist = NULL;
    len = strlen(partial);

    bash_partial[0] = 0;
    bash_singlematch = 1;

    for (const auto cvar: convar::get_variables())
        if (!std::strncmp(partial, cvar->name.c_str(), len))
            AddToTabList(cvar->name.c_str(), "cvar");

    for (const auto &ccmd: command::get_commands())
        if (!std::strncmp(partial, ccmd.name.c_str(), len))
            AddToTabList(ccmd.name.c_str(), "command");

    for (const auto &alias: command::get_aliases())
        if (!std::strncmp(partial, alias.name.c_str(), len))
            AddToTabList(alias.name.c_str(), "alias");
}

/*
============
Con_TabComplete -- johnfitz
============
*/
void Con_TabComplete(void) {
    char partial[MAXCMDLINE];
    const char *match;
    static char *c;
    tab_t *t;
    int mark, i, j;

    // if editline is empty, return
    if (key_lines[edit_line][1] == 0)
        return;

    // get partial string (space -> cursor)
    if (!key_tabpartial[0]) //first time through, find new insert point. (Otherwise, use previous.)
    {
        //work back from cursor until you find a space, quote, semicolon, or prompt
        c = key_lines[edit_line] + key_linepos - 1; //start one space left of cursor
        while (*c != ' ' && *c != '\"' && *c != ';' && c != key_lines[edit_line])
            c--;
        c++; //start 1 char after the separator we just found
    }
    for (i = 0; c + i < key_lines[edit_line] + key_linepos; i++)
        partial[i] = c[i];
    partial[i] = 0;

    // Map autocomplete function -- S.A
    // Since we don't have argument completion, this hack will do for now...
    for (j = 0; j < num_arg_completion_types; j++) {
        // arg_completion contains a command we can complete the arguments
        // for (like "map ") and a list of all the maps.
        arg_completion_type_t arg_completion = arg_completion_types[j];
        const char *command_name = arg_completion.command;

        if (!strncmp(key_lines[edit_line] + 1, command_name, strlen(command_name))) {
            int nummatches = 0;
            const char *matched_map = FindCompletion(partial, *arg_completion.filelist, &nummatches);
            if (!*matched_map)
                return;
            q_strlcpy(partial, matched_map, MAXCMDLINE);
            *c = '\0';
            q_strlcat(key_lines[edit_line], partial, MAXCMDLINE);
            key_linepos = c - key_lines[edit_line] + std::strlen(matched_map); //set new cursor position
            if (key_linepos >= MAXCMDLINE)
                key_linepos = MAXCMDLINE - 1;
            // if only one match, append a space
            if (key_linepos < MAXCMDLINE - 1 &&
                key_lines[edit_line][key_linepos] == 0 && (nummatches == 1)) {
                key_lines[edit_line][key_linepos] = ' ';
                key_linepos++;
                key_lines[edit_line][key_linepos] = 0;
            }
            c = key_lines[edit_line] + key_linepos;
            return;
        }
    }

    //if partial is empty, return
    if (partial[0] == 0)
        return;

    //trim trailing space becuase it screws up string comparisons
    if (i > 0 && partial[i - 1] == ' ')
        partial[i - 1] = 0;

    // find a match
    mark = Hunk_LowMark();
    if (!key_tabpartial[0]) //first time through
    {
        q_strlcpy(key_tabpartial, partial, MAXCMDLINE);
        BuildTabList(key_tabpartial);

        if (!tablist)
            return;

        // print list if length > 1
        if (tablist->next != tablist) {
            t = tablist;
            console::safe_print("\n");
            do {
                console::safe_print("   %s (%s)\n", t->name, t->type);
                t = t->next;
            } while (t != tablist);
            console::safe_print("\n");
        }

        //	match = tablist->name;
        // First time, just show maximum matching chars -- S.A.
        match = bash_partial;
    } else {
        BuildTabList(key_tabpartial);

        if (!tablist)
            return;

        //find current match -- can't save a pointer because the list will be rebuilt each time
        t = tablist;
        match = keydown[K_SHIFT] ? t->prev->name : t->name;
        do {
            if (!std::strcmp(t->name, partial)) {
                match = keydown[K_SHIFT] ? t->prev->name : t->next->name;
                break;
            }
            t = t->next;
        } while (t != tablist);
    }
    Hunk_FreeToLowMark(mark); //it's okay to free it here because match is a pointer to persistent data

    // insert new match into edit line
    q_strlcpy(partial, match, MAXCMDLINE); //first copy match string
    q_strlcat(partial, key_lines[edit_line] + key_linepos, MAXCMDLINE); //then add chars after cursor
    *c = '\0'; //now copy all of this into edit line
    q_strlcat(key_lines[edit_line], partial, MAXCMDLINE);
    key_linepos = c - key_lines[edit_line] + std::strlen(match); //set new cursor position
    if (key_linepos >= MAXCMDLINE)
        key_linepos = MAXCMDLINE - 1;

    // if cursor is at end of string, let's append a space to make life easier
    if (key_linepos < MAXCMDLINE - 1 &&
        key_lines[edit_line][key_linepos] == 0 && bash_singlematch) {
        key_lines[edit_line][key_linepos] = ' ';
        key_linepos++;
        key_lines[edit_line][key_linepos] = 0;
        // S.A.: the map argument completion (may be in combination with the bash-style
        // display behavior changes, causes weirdness when completing the arguments for
        // the changelevel command. the line below "fixes" it, although I'm not sure about
        // the reason, yet, neither do I know any possible side effects of it:
        c = key_lines[edit_line] + key_linepos;
    }
}

/*
==============================================================================

DRAWING

==============================================================================
*/

/*
================
Con_DrawNotify

Draws the last few lines of output transparently over the game top
================
*/
void Con_DrawNotify(void) {
    int i, x, v;
    const char *text;
    float time;

    GL_SetCanvas(CANVAS_CONSOLE); //johnfitz
    v = vid.conheight; //johnfitz

    for (i = con_current - NUM_CON_TIMES + 1; i <= con_current; i++) {
        if (i < 0)
            continue;
        time = con_times[i % NUM_CON_TIMES];
        if (time == 0)
            continue;
        time = realtime - time;
        if (time > console::notify_time.value)
            continue;
        text = con_text + (i % console::total_lines) * con_linewidth;

        clearnotify = 0;

        for (x = 0; x < con_linewidth; x++)
            Draw_Character((x + 1) << 3, v, text[x]);

        v += 8;

        scr_tileclear_updates = 0; //johnfitz
    }

    if (key_dest == key_message) {
        clearnotify = 0;

        if (chat_team) {
            Draw_String(8, v, "say_team:");
            x = 11;
        } else {
            Draw_String(8, v, "say:");
            x = 6;
        }

        text = Key_GetChatBuffer();
        i = Key_GetChatMsgLen();
        if (i > con_linewidth - x - 1)
            text += i - con_linewidth + x + 1;

        while (*text) {
            Draw_Character(x << 3, v, *text);
            x++;
            text++;
        }

        Draw_Character(x << 3, v, 10 + ((int) (realtime * con_cursorspeed) & 1));
        v += 8;

        scr_tileclear_updates = 0; //johnfitz
    }
}

/*
================
Con_DrawInput -- johnfitz -- modified to allow insert editing

The input line scrolls horizontally if typing goes beyond the right edge
================
*/
extern qpic_t *pic_ovr, *pic_ins; //johnfitz -- new cursor handling

void Con_DrawInput(void) {
    int i, ofs;

    if (key_dest != key_console && !console::forced_up)
        return; // don't draw anything

    // prestep if horizontally scrolling
    if (key_linepos >= con_linewidth)
        ofs = 1 + key_linepos - con_linewidth;
    else
        ofs = 0;

    // draw input string
    for (i = 0; key_lines[edit_line][i + ofs] && i < con_linewidth; i++)
        Draw_Character((i + 1) << 3, vid.conheight - 16, key_lines[edit_line][i + ofs]);

    // johnfitz -- new cursor handling
    if (!((int) ((realtime - key_blinktime) * con_cursorspeed) & 1)) {
        i = key_linepos - ofs;
        Draw_Pic((i + 1) << 3, vid.conheight - 16, key_insert ? pic_ins : pic_ovr);
    }
}

/*
================
Con_DrawConsole -- johnfitz -- heavy revision

Draws the console with the solid background
The typing input line at the bottom should only be drawn if typing is allowed
================
*/
void Con_DrawConsole(int lines, bool drawinput) {
    int i, x, y, j, sb, rows;
    const char *text;
    char ver[32];

    if (lines <= 0)
        return;

    con_vislines = lines * vid.conheight / glheight;
    GL_SetCanvas(CANVAS_CONSOLE);

    // draw the background
    Draw_ConsoleBackground();

    // draw the buffer text
    rows = (con_vislines + 7) / 8;
    y = vid.conheight - rows * 8;
    rows -= 2; //for input and version lines
    sb = (console::back_scroll) ? 2 : 0;

    for (i = con_current - rows + 1; i <= con_current - sb; i++, y += 8) {
        j = i - console::back_scroll;
        if (j < 0)
            j = 0;
        text = con_text + (j % console::total_lines) * con_linewidth;

        for (x = 0; x < con_linewidth; x++)
            Draw_Character((x + 1) << 3, y, text[x]);
    }

    // draw scrollback arrows
    if (console::back_scroll) {
        y += 8; // blank line
        for (x = 0; x < con_linewidth; x += 4)
            Draw_Character((x + 1) << 3, y, '^');
        y += 8;
    }

    // draw the input prompt, user text, and cursor
    if (drawinput)
        Con_DrawInput();

    //draw version number in bottom right
    y += 8;
    q_snprintf(ver, sizeof(ver), "QuakeSpasm " QUAKESPASM_VER_STRING);
    for (x = 0; x < (int) strlen(ver); x++)
        Draw_Character((con_linewidth - strlen(ver) + x + 2) << 3, y, ver[x] /*+ 128*/);
}


/*
==================
Con_NotifyBox
==================
*/
void Con_NotifyBox(const char *text) {
    double t1, t2;
    int lastkey, lastchar;

    // during startup for sound / cd warnings
    console::info("\n\n%s", Con_Quakebar(40)); //johnfitz
    console::info("%s", text);
    console::info("Press a key.\n");
    console::info("%s", Con_Quakebar(40)); //johnfitz

    IN_Deactivate(modestate == MS_WINDOWED);
    key_dest = key_console;

    Key_BeginInputGrab();
    do {
        t1 = Sys_DoubleTime();
        SCR_UpdateScreen();
        Sys_SendKeyEvents();
        Key_GetGrabbedInput(&lastkey, &lastchar);
        Sys_Sleep(16);
        t2 = Sys_DoubleTime();
        realtime += t2 - t1; // make the cursor blink
    } while (lastkey == -1 && lastchar == -1);
    Key_EndInputGrab();

    console::info("\n");
    IN_Activate();
    key_dest = key_game;
    realtime = 0; // put the cursor back to invisible
}


void LOG_Init(quakeparms_t *parms) {
    time_t inittime;
    char session[24];

    if (!common::check_param("-condebug").has_value())
        return;

    inittime = time(NULL);
    strftime(session, sizeof(session), "%m/%d/%Y %H:%M:%S", localtime(&inittime));
    q_snprintf(logfilename, sizeof(logfilename), "%s/qconsole.log", parms->basedir);

    //	unlink (logfilename);

    log_fd = open(logfilename, O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if (log_fd == -1) {
        fprintf(stderr, "Error: Unable to create log file %s\n", logfilename);
        return;
    }

    con_debuglog = true;
    Con_DebugLog(va("LOG started on: %s \n", session));
}

void LOG_Close(void) {
    if (log_fd == -1)
        return;
    close(log_fd);
    log_fd = -1;
}
