/* s_vcsa.cpp -- Linux /dev/vcsa screen driver

   This file is part of the UPX executable compressor.

   Copyright (C) Markus Franz Xaver Johannes Oberhumer
   Copyright (C) Laszlo Molnar
   All Rights Reserved.

   UPX and the UCL library are free software; you can redistribute them
   and/or modify them 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; see the file COPYING.
   If not, write to the Free Software Foundation, Inc.,
   59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.

   Markus F.X.J. Oberhumer              Laszlo Molnar
   <markus@oberhumer.com>               <ezerotven+github@gmail.com>
 */

#include "../conf.h"

#if (USE_SCREEN) && (USE_SCREEN_VCSA)

#include "screen.h"

#define this self

#define mask_fg 0x0f
#define mask_bg 0xf0

/* #define USE_SCROLLBACK 1 */

/*************************************************************************
// direct screen access ( /dev/vcsaNN )
**************************************************************************/

#include <sys/ioctl.h>
#include <sys/select.h>
#include <termios.h>
#if defined(__linux__)
#include <linux/kd.h>
#include <linux/kdev_t.h>
#include <linux/major.h>
#endif

#define Cell upx_uint16_t

struct screen_data_t {
    int fd;
    int mode;
    int page;
    int cols;
    int rows;
    int cursor_x;
    int cursor_y;
    int scroll_counter;
    unsigned char attr;
    unsigned char init_attr;
    unsigned char map[256];
    Cell empty_line[256];
#if USE_SCROLLBACK
    /* scrollback buffer */
    Cell sb_buf[32][256];
    int sb_size;
    int sb_base;
    int sb_sp;
#endif /* USE_SCROLLBACK */
};

#if USE_SCROLLBACK
static __inline__ void sb_add(screen_t *this, int *val, int inc) {
    *val = (*val + inc) & (this->data->sb_size - 1);
}

static void sb_push(screen_t *this, const Cell *line, int len) {
    memcpy(this->data->sb_buf[this->data->sb_sp], line, len);
    sb_add(this, &this->data->sb_sp, 1);
    if (this->data->sb_sp == this->data->sb_base)
        sb_add(this, &this->data->sb_base, 1);
}

static const Cell *sb_pop(screen_t *this) {
    if (this->data->sb_sp == this->data->sb_base)
        return nullptr;
    sb_add(this, &this->data->sb_sp, -1);
    return this->data->sb_buf[this->data->sb_sp];
}
#endif /* USE_SCROLLBACK */

static void refresh(screen_t *this) { UNUSED(this); }

static __inline__ Cell make_cell(screen_t *this, int ch, int attr) {
    return ((attr & 0xff) << 8) | (this->data->map[ch & 0xff] & 0xff);
}

static int getMode(const screen_t *this) { return this->data->mode; }

static int getPage(const screen_t *this) { return this->data->page; }

static int getRows(const screen_t *this) { return this->data->rows; }

static int getCols(const screen_t *this) { return this->data->cols; }

static int isMono(const screen_t *this) {
    /* FIXME */
    UNUSED(this);
    return 0;
}

static int getFg(const screen_t *this) { return this->data->attr & mask_fg; }

static int getBg(const screen_t *this) { return this->data->attr & mask_bg; }

static void setFg(screen_t *this, int fg) {
    this->data->attr = (this->data->attr & mask_bg) | (fg & mask_fg);
}

static void setBg(screen_t *this, int bg) {
    this->data->attr = (this->data->attr & mask_fg) | (bg & mask_bg);
}

/* private */
static int gotoxy(screen_t *this, int x, int y) {
    if (x >= 0 && y >= 0 && x < this->data->cols && y < this->data->rows) {
        if (lseek(this->data->fd, 4 + (x + y * this->data->cols) * 2, SEEK_SET) != -1) {
            return 0;
        }
    }
    return -1;
}

static void setCursor(screen_t *this, int x, int y) {
    if (gotoxy(this, x, y) == 0) {
        unsigned char b[2] = {x, y};
        if (lseek(this->data->fd, 2, SEEK_SET) != -1)
            write(this->data->fd, b, 2);
        this->data->cursor_x = x;
        this->data->cursor_y = y;
    }
}

static void getCursor(const screen_t *this, int *x, int *y) {
    int cx = this->data->cursor_x;
    int cy = this->data->cursor_y;
#if 1
    if (lseek(this->data->fd, 2, SEEK_SET) != -1) {
        unsigned char b[2];
        if (read(this->data->fd, b, 2) == 2) {
            if (b[0] < this->data->cols && b[1] < this->data->rows) {
                cx = b[0];
                cy = b[1];
            }
        }
    }
#endif
    if (x)
        *x = cx;
    if (y)
        *y = cy;
}

static void putCharAttr(screen_t *this, int ch, int attr, int x, int y) {
    Cell a = make_cell(this, ch, attr);

    if (gotoxy(this, x, y) == 0)
        write(this->data->fd, &a, 2);
}

static void putChar(screen_t *this, int ch, int x, int y) {
    putCharAttr(this, ch, this->data->attr, x, y);
}

static void putStringAttr(screen_t *this, const char *s, int attr, int x, int y) {
    assert((int) strlen(s) <= 256);
    assert(x + (int) strlen(s) <= this->data->cols);
    while (*s)
        putCharAttr(this, *s++, attr, x++, y);
}

static void putString(screen_t *this, const char *s, int x, int y) {
    putStringAttr(this, s, this->data->attr, x, y);
}

/* private */
static void getChar(screen_t *this, int *ch, int *attr, int x, int y) {
    upx_uint16_t a;

    if (gotoxy(this, x, y) == 0 && read(this->data->fd, &a, 2) == 2) {
        if (ch)
            *ch = a & 0xff;
        if (attr)
            *attr = (a >> 8) & 0xff;
    }
}

/* private */
static int init_scrnmap(screen_t *this, int fd) {
    int scrnmap_done = 0;
    int i;

#if 1 && defined(GIO_UNISCRNMAP) && defined(E_TABSZ)
    if (!scrnmap_done) {
        upx_uint16_t scrnmap[E_TABSZ];
        if (ioctl(fd, GIO_UNISCRNMAP, scrnmap) == 0) {
            for (i = 0; i < E_TABSZ; i++)
                this->data->map[scrnmap[i] & 0xff] = i;
            scrnmap_done = 1;
        }
    }
#endif
#if 1 && defined(GIO_SCRNMAP) && defined(E_TABSZ)
    if (!scrnmap_done) {
        unsigned char scrnmap[E_TABSZ];
        if (ioctl(fd, GIO_SCRNMAP, scrnmap) == 0) {
            for (i = 0; i < E_TABSZ; i++)
                this->data->map[scrnmap[i] & 0xff] = i;
            scrnmap_done = 1;
        }
    }
#endif

    return scrnmap_done;
}

static int init(screen_t *this, int fd) {
    struct stat st;

    if (!this || !this->data)
        return -1;

    this->data->fd = -1;
    this->data->mode = -1;
    this->data->page = 0;
#if USE_SCROLLBACK
    this->data->sb_size = 32;
    this->data->sb_base = 0;
    this->data->sb_sp = 0;
#endif
    if (fd < 0 || !acc_isatty(fd))
        return -1;
    if (fstat(fd, &st) != 0)
        return -1;

/* check if we are running in a virtual console */
#if defined(MINOR) && defined(MAJOR) && defined(TTY_MAJOR)
    if (MAJOR(st.st_rdev) == TTY_MAJOR) {
        char vc_name[64];
        unsigned char vc_data[4];
        int i;
        int attr;
        Cell a;

        upx_safe_snprintf(vc_name, sizeof(vc_name), "/dev/vcsa%d", (int) MINOR(st.st_rdev));
        this->data->fd = open(vc_name, O_RDWR);
        if (this->data->fd == -1) {
            upx_safe_snprintf(vc_name, sizeof(vc_name), "/dev/vcc/a%d", (int) MINOR(st.st_rdev));
            this->data->fd = open(vc_name, O_RDWR);
        }
        if (this->data->fd != -1) {
            if (read(this->data->fd, vc_data, 4) == 4) {
                this->data->mode = 3;
                this->data->rows = vc_data[0];
                this->data->cols = vc_data[1];
                this->data->cursor_x = vc_data[2];
                this->data->cursor_y = vc_data[3];

                for (i = 0; i < 256; i++)
                    this->data->map[i] = i;
                i = init_scrnmap(this, this->data->fd) || init_scrnmap(this, STDIN_FILENO);

                getChar(this, nullptr, &attr, this->data->cursor_x, this->data->cursor_y);
                this->data->init_attr = attr;
                this->data->attr = attr;
                a = make_cell(this, ' ', attr);
                for (i = 0; i < 256; i++)
                    this->data->empty_line[i] = a;
            } else {
                close(this->data->fd);
                this->data->fd = -1;
            }
        }
    }
#endif

    if (this->data->mode < 0)
        return -1;

    return 0;
}

static void finalize(screen_t *this) {
    if (this->data->fd != -1)
        (void) close(this->data->fd);
}

static void updateLineN(screen_t *this, const void *line, int y, int len) {
    if (len > 0 && len <= 2 * this->data->cols && gotoxy(this, 0, y) == 0) {
        int i;
        unsigned char new_line[len];
        unsigned char *l1 = new_line;
        const unsigned char *l2 = (const unsigned char *) line;

        for (i = 0; i < len; i += 2) {
            *l1++ = *l2++;
            *l1++ = this->data->map[*l2++];
        }
        write(this->data->fd, new_line, len);
    }
}

static void clearLine(screen_t *this, int y) {
    if (gotoxy(this, 0, y) == 0)
        write(this->data->fd, this->data->empty_line, 2 * this->data->cols);
}

static void clear(screen_t *this) {
    int y;

    for (y = 0; y < this->data->rows; y++)
        clearLine(this, y);
}

static int scrollUp(screen_t *this, int lines) {
    int sr = this->data->rows;
    int sc = this->data->cols;
    int y;

    if (lines <= 0 || lines > sr)
        return 0;

#if USE_SCROLLBACK
    /* copy to scrollback buffer */
    for (y = 0; y < lines; y++) {
        Cell buf[sc];
        gotoxy(this, 0, y);
        read(this->data->fd, buf, sizeof(buf));
        sb_push(this, buf, sizeof(buf));
    }
#endif

    /* move screen up */
    if (lines < sr) {
        Cell buf[(sr - lines) * sc];
        gotoxy(this, 0, lines);
        read(this->data->fd, buf, sizeof(buf));
        gotoxy(this, 0, 0);
        write(this->data->fd, buf, sizeof(buf));
    }

    /* fill in blank lines at bottom */
    for (y = sr - lines; y < sr; y++)
        clearLine(this, y);

    this->data->scroll_counter += lines;
    return lines;
}

static int scrollDown(screen_t *this, int lines) {
    int sr = this->data->rows;
    int sc = this->data->cols;
    int y;

    if (lines <= 0 || lines > sr)
        return 0;

    /* move screen down */
    if (lines < sr) {
        Cell buf[(sr - lines) * sc];
        gotoxy(this, 0, 0);
        read(this->data->fd, buf, sizeof(buf));
        gotoxy(this, 0, lines);
        write(this->data->fd, buf, sizeof(buf));
    }

    /* copy top lines from scrollback buffer */
    for (y = lines; --y >= 0;) {
#if USE_SCROLLBACK
        const Cell *buf = sb_pop(this);
        if (buf == nullptr)
            clearLine(this, y);
        else
            updateLineN(this, buf, y, sc * 2);
#else
        clearLine(this, y);
#endif
    }

    this->data->scroll_counter -= lines;
    return lines;
}

static int getScrollCounter(const screen_t *this) { return this->data->scroll_counter; }

static int getCursorShape(const screen_t *this) {
    UNUSED(this);
    return 0;
}

static void setCursorShape(screen_t *this, int shape) {
    UNUSED(this);
    UNUSED(shape);
}

static int kbhit(screen_t *this) {
    const int fd = STDIN_FILENO;
    const unsigned usec = 0;
    struct timeval tv;
    fd_set fds;

    UNUSED(this);
    FD_ZERO(&fds);
    FD_SET(fd, &fds);
    tv.tv_sec = usec / 1000000;
    tv.tv_usec = usec % 1000000;
    return (select(fd + 1, &fds, nullptr, nullptr, &tv) > 0);
}

static int intro(screen_t *this, void (*show_frames)(screen_t *)) {
    int shape;
    struct termios term_old, term_new;
    int term_r;

    if ((this->data->init_attr & mask_bg) != BG_BLACK)
        return 0;

    term_r = tcgetattr(STDIN_FILENO, &term_old);
    if (term_r == 0) {
        term_new = term_old;
        term_new.c_lflag &= ~(ISIG | ICANON | ECHO);
        tcsetattr(STDIN_FILENO, TCSANOW, &term_new);
    }

    shape = getCursorShape(this);
    setCursorShape(this, 0x2000);
    show_frames(this);
    if (this->data->rows > 24)
        setCursor(this, this->data->cursor_x, this->data->cursor_y + 1);
    setCursorShape(this, shape);

    while (kbhit(this))
        (void) fgetc(stdin);
    if (term_r == 0)
        tcsetattr(STDIN_FILENO, TCSANOW, &term_old);

    return 1;
}

static const screen_t driver = {sobject_destroy,
                                finalize,
                                nullptr, /* atExit */
                                init,
                                refresh,
                                getMode,
                                getPage,
                                getRows,
                                getCols,
                                isMono,
                                getFg,
                                getBg,
                                getCursor,
                                getCursorShape,
                                setFg,
                                setBg,
                                setCursor,
                                setCursorShape,
                                nullptr, /* hideCursor */
                                putChar,
                                putCharAttr,
                                putString,
                                putStringAttr,
                                clear,
                                clearLine,
                                updateLineN,
                                scrollUp,
                                scrollDown,
                                getScrollCounter,
                                kbhit,
                                intro,
                                (struct screen_data_t *) nullptr};

/* public constructor */
screen_t *screen_vcsa_construct(void) { return sobject_construct(&driver, sizeof(*driver.data)); }

#endif /* (USE_SCREEN) && (USE_SCREEN_VCSA) */

/* vim:set ts=4 sw=4 et: */
