/* MDSI Genius VHR emulation*/
#include <stdlib.h>
#include "ibm.h"
#include "device.h"
#include "io.h"
#include "mem.h"
#include "rom.h"
#include "timer.h"
#include "video.h"
#include "vid_genius.h"

#define GENIUS_XSIZE 728
#define GENIUS_YSIZE 1008

void updatewindowsize(int x, int y);

extern uint8_t fontdat8x12[256][16];

/* I'm at something of a disadvantage writing this emulation: I don't have an
 * MDSI Genius card, nor do I have the BIOS extension (VHRBIOS.SYS) that came
 * with it. What I do have are the GEM and Windows 1.04 drivers, plus a driver
 * for a later MCA version of the card. The latter can be found at
 * <http://files.mpoli.fi/hardware/DISPLAY/GENIUS/> and is necessary if you
 * want the Windows driver to work.
 *
 * This emulation appears to work correctly with:
 * The MCA drivers GMC_ANSI.SYS and INS_ANSI.SYS
 * The GEM driver SDGEN9.VGA
 * The Windows 1.04 driver GENIUS.DRV
 *
 * As far as I can see, the card uses a fixed resolution of 728x1008 pixels.
 * It has the following modes of operation:
 *
 * > MDA-compatible:      80x25 text, each character 9x15 pixels.
 * > CGA-compatible:      640x200 mono graphics
 * > Dual:                MDA text in the top half, CGA graphics in the bottom
 * > Native text:         80x66 text, each character 9x15 pixels.
 * > Native graphics:     728x1008 mono graphics.
 *
 * Under the covers, this seems to translate to:
 *  > Text framebuffer.     At B000:0000, 16k. Displayed if enable bit is set
 *                         in the MDA control register.
 *  > Graphics framebuffer. In native modes goes from A000:0000 to A000:FFFF
 *                         and B800:0000 to B800:FFFF. In CGA-compatible
 *                         mode only the section at B800:0000 to B800:7FFF
 *                         is visible. Displayed if enable bit is set in the
 *                         CGA control register.
 *
 * Two card-specific registers control text and graphics display:
 *
 *  03B0: Control register.
 * 	   Bit 0: Map all graphics framebuffer into memory.
 *         Bit 2: Unknown. Set by GMC /M; cleared by mode set or GMC /T.
 *         Bit 4: Set for CGA-compatible graphics, clear for native graphics.
 *         Bit 5: Set for black on white, clear for white on black.
 *
 *  03B1: Character height register.
 *        Bits 0-1: Character cell height (0 => 15, 1 => 14, 2 => 13, 3 => 12)
 *        Bit  4:   Set to double character cell height (scanlines are doubled)
 *        Bit  7:   Unknown, seems to be set for all modes except 80x66
 *
 *  Not having the card also means I don't have its font. According to the
 *  card brochure the font is an 8x12 bitmap in a 9x15 character cell. I
 *  therefore generated it by taking the MDA font, increasing graphics to
 *  16 pixels in height and reducing the height of characters so they fit
 *  in an 8x12 cell if necessary.
 */

typedef struct genius_t {
        mem_mapping_t mapping;

        uint8_t mda_crtc[32];   /* The 'CRTC' as the host PC sees it */
        int mda_crtcreg;        /* Current CRTC register */
        uint8_t genius_control; /* Native control register
                                 * I think bit 0 enables the full
                                 * framebuffer.
                                 */
        uint8_t genius_charh;   /* Native character height register:
                                 * 00h => chars are 15 pixels high
                                 * 81h => chars are 14 pixels high
                                 * 83h => chars are 12 pixels high
                                 * 90h => chars are 30 pixels high [15 x 2]
                                 * 93h => chars are 24 pixels high [12 x 2]
                                 */
        uint8_t genius_mode;    /* Current mode (see list at top of file) */
        uint8_t cga_ctrl;       /* Emulated CGA control register */
        uint8_t mda_ctrl;       /* Emulated MDA control register */
        uint8_t cga_colour;     /* Emulated CGA colour register (ignored) */

        uint8_t mda_stat; /* MDA status (IN 0x3BA) */
        uint8_t cga_stat; /* CGA status (IN 0x3DA) */

        int font;    /* Current font, 0 or 1 */
        int enabled; /* Display enabled, 0 or 1 */
        int detach;  /* Detach cursor, 0 or 1 */

        uint64_t dispontime, dispofftime;
        pc_timer_t timer;

        int linepos, displine;
        int vc;
        int dispon, blink;
        int vsynctime;

        uint8_t *vram;
} genius_t;

static uint32_t genius_pal[4];

/* Mapping of attributes to colours, in MDA emulation mode */
static int mdacols[256][2][2];

void genius_recalctimings(genius_t *genius);
void genius_write(uint32_t addr, uint8_t val, void *p);
uint8_t genius_read(uint32_t addr, void *p);

void genius_out(uint16_t addr, uint8_t val, void *p) {
        genius_t *genius = (genius_t *)p;

        switch (addr) {
        case 0x3b0: /* Command / control register */
                genius->genius_control = val;
                if (val & 1) {
                        mem_mapping_set_addr(&genius->mapping, 0xa0000, 0x28000);
                } else {
                        mem_mapping_set_addr(&genius->mapping, 0xb0000, 0x10000);
                }

                break;

        case 0x3b1:
                genius->genius_charh = val;
                break;

                /* Emulated CRTC, register select */
        case 0x3b2:
        case 0x3b4:
        case 0x3b6:
        case 0x3d0:
        case 0x3d2:
        case 0x3d4:
        case 0x3d6:
                genius->mda_crtcreg = val & 31;
                break;

                /* Emulated CRTC, value */
        case 0x3b3:
        case 0x3b5:
        case 0x3b7:
        case 0x3d1:
        case 0x3d3:
        case 0x3d5:
        case 0x3d7:
                genius->mda_crtc[genius->mda_crtcreg] = val;
                genius_recalctimings(genius);
                return;

                /* Emulated MDA control register */
        case 0x3b8:
                genius->mda_ctrl = val;
                return;
                /* Emulated CGA control register */
        case 0x3D8:
                genius->cga_ctrl = val;
                return;
                /* Emulated CGA colour register */
        case 0x3D9:
                genius->cga_colour = val;
                return;
        }
}

uint8_t genius_in(uint16_t addr, void *p) {
        genius_t *genius = (genius_t *)p;

        switch (addr) {
        case 0x3b0:
        case 0x3b2:
        case 0x3b4:
        case 0x3b6:
        case 0x3d0:
        case 0x3d2:
        case 0x3d4:
        case 0x3d6:
                return genius->mda_crtcreg;
        case 0x3b1:
        case 0x3b3:
        case 0x3b5:
        case 0x3b7:
        case 0x3d1:
        case 0x3d3:
        case 0x3d5:
        case 0x3d7:
                return genius->mda_crtc[genius->mda_crtcreg];
        case 0x3b8:
                return genius->mda_ctrl;
        case 0x3d9:
                return genius->cga_colour;
        case 0x3ba:
                return genius->mda_stat;
        case 0x3d8:
                return genius->cga_ctrl;
        case 0x3da:
                return genius->cga_stat;
        }
        return 0xff;
}

void genius_write(uint32_t addr, uint8_t val, void *p) {
        genius_t *genius = (genius_t *)p;
        egawrites++;

        if (genius->genius_control & 1) {
                addr = addr % 0x28000;
        } else
        /* If hi-res memory is disabled, only visible in the B000 segment */
        {
                addr = (addr & 0xFFFF) + 0x10000;
        }
        genius->vram[addr] = val;
}

uint8_t genius_read(uint32_t addr, void *p) {
        genius_t *genius = (genius_t *)p;
        egareads++;

        if (genius->genius_control & 1) {
                addr = addr % 0x28000;
        } else
        /* If hi-res memory is disabled, only visible in the B000 segment */
        {
                addr = (addr & 0xFFFF) + 0x10000;
        }
        return genius->vram[addr];
}

void genius_recalctimings(genius_t *genius) {
        double disptime;
        double _dispontime, _dispofftime;

        disptime = 0x31;
        _dispontime = 0x28;
        _dispofftime = disptime - _dispontime;
        _dispontime *= MDACONST;
        _dispofftime *= MDACONST;
        genius->dispontime = (uint64_t)_dispontime;
        genius->dispofftime = (uint64_t)_dispofftime;
}

/* Draw a single line of the screen in either text mode */
void genius_textline(genius_t *genius, uint8_t background) {
        int x;
        int w = 80; /* 80 characters across */
        int cw = 9; /* Each character is 9 pixels wide */
        uint8_t chr, attr;
        uint8_t bitmap[2];
        int blink, c, row;
        int drawcursor, cursorline;
        uint16_t addr;
        uint8_t sc;
        int charh;
        uint16_t ma = (genius->mda_crtc[13] | (genius->mda_crtc[12] << 8)) & 0x3fff;
        uint16_t ca = (genius->mda_crtc[15] | (genius->mda_crtc[14] << 8)) & 0x3fff;
        unsigned char *framebuf = genius->vram + 0x10000;
        uint32_t col;

        /* Character height is 12-15 */
        charh = 15 - (genius->genius_charh & 3);
        if (genius->genius_charh & 0x10) {
                row = ((genius->displine >> 1) / charh);
                sc = ((genius->displine >> 1) % charh);
        } else {
                row = (genius->displine / charh);
                sc = (genius->displine % charh);
        }
        addr = ((ma & ~1) + row * w) * 2;

        ma += (row * w);

        if ((genius->mda_crtc[10] & 0x60) == 0x20) {
                cursorline = 0;
        } else {
                cursorline = ((genius->mda_crtc[10] & 0x1F) <= sc) && ((genius->mda_crtc[11] & 0x1F) >= sc);
        }

        for (x = 0; x < w; x++) {
                chr = framebuf[(addr + 2 * x) & 0x3FFF];
                attr = framebuf[(addr + 2 * x + 1) & 0x3FFF];
                drawcursor = ((ma == ca) && cursorline && genius->enabled && (genius->mda_ctrl & 8));

                switch (genius->mda_crtc[10] & 0x60) {
                case 0x00:
                        drawcursor = drawcursor && (genius->blink & 16);
                        break;
                case 0x60:
                        drawcursor = drawcursor && (genius->blink & 32);
                        break;
                }
                blink = ((genius->blink & 16) && (genius->mda_ctrl & 0x20) && (attr & 0x80) && !drawcursor);

                if (genius->mda_ctrl & 0x20)
                        attr &= 0x7F;
                /* MDA underline */
                if (sc == charh && ((attr & 7) == 1)) {
                        col = mdacols[attr][blink][1];

                        if (genius->genius_control & 0x20) {
                                col ^= 0xffffff;
                        }

                        for (c = 0; c < cw; c++) {
                                if (col != background)
                                        ((uint32_t *)buffer32->line[genius->displine])[(x * cw) + c] = col;
                        }
                } else /* Draw 8 pixels of character */
                {
                        bitmap[0] = fontdat8x12[chr][sc];
                        for (c = 0; c < 8; c++) {
                                col = mdacols[attr][blink][(bitmap[0] & (1 << (c ^ 7))) ? 1 : 0];
                                if (!(genius->enabled) || !(genius->mda_ctrl & 8))
                                        col = mdacols[0][0][0];

                                if (genius->genius_control & 0x20) {
                                        col ^= 0xffffff;
                                }
                                if (col != background) {
                                        ((uint32_t *)buffer32->line[genius->displine])[(x * cw) + c] = col;
                                }
                        }
                        /* The ninth pixel column... */
                        if ((chr & ~0x1f) == 0xc0) {
                                /* Echo column 8 for the graphics chars */
                                col = ((uint32_t *)buffer32->line[genius->displine])[(x * cw) + 7];
                                if (col != background)
                                        ((uint32_t *)buffer32->line[genius->displine])[(x * cw) + 8] = col;
                        } else /* Otherwise fill with background */
                        {
                                col = mdacols[attr][blink][0];
                                if (genius->genius_control & 0x20) {
                                        col ^= 0xffffff;
                                }
                                if (col != background)
                                        ((uint32_t *)buffer32->line[genius->displine])[(x * cw) + 8] = col;
                        }
                        if (drawcursor) {
                                for (c = 0; c < cw; c++)
                                        ((uint32_t *)buffer32->line[genius->displine])[(x * cw) + c] ^= mdacols[attr][0][1];
                        }
                        ++ma;
                }
        }
}

/* Draw a line in the CGA 640x200 mode */
void genius_cgaline(genius_t *genius) {
        int x, c;
        uint32_t dat;
        uint32_t ink;
        uint32_t addr;

        ink = (genius->genius_control & 0x20) ? genius_pal[0] : genius_pal[3];
        /* We draw the CGA at row 600 */
        if (genius->displine < 600) {
                return;
        }
        addr = 0x18000 + 80 * ((genius->displine - 600) >> 2);
        if ((genius->displine - 600) & 2) {
                addr += 0x2000;
        }

        for (x = 0; x < 80; x++) {
                dat = genius->vram[addr];
                addr++;

                for (c = 0; c < 8; c++) {
                        if (dat & 0x80) {
                                ((uint32_t *)buffer32->line[genius->displine])[x * 8 + c] = ink;
                        }
                        dat = dat << 1;
                }
        }
}

/* Draw a line in the native high-resolution mode */
void genius_hiresline(genius_t *genius) {
        int x, c;
        uint32_t dat;
        uint32_t ink;
        uint32_t addr;

        ink = (genius->genius_control & 0x20) ? genius_pal[0] : genius_pal[3];
        /* The first 512 lines live at A0000 */
        if (genius->displine < 512) {
                addr = 128 * genius->displine;
        } else /* The second 496 live at B8000 */
        {
                addr = 0x18000 + 128 * (genius->displine - 512);
        }

        for (x = 0; x < 91; x++) {
                dat = genius->vram[addr];
                addr++;

                for (c = 0; c < 8; c++) {
                        if (dat & 0x80) {
                                ((uint32_t *)buffer32->line[genius->displine])[x * 8 + c] = ink;
                        }
                        dat = dat << 1;
                }
        }
}

void genius_poll(void *p) {
        genius_t *genius = (genius_t *)p;
        int x;
        uint32_t background;

        if (!genius->linepos) {
                timer_advance_u64(&genius->timer, genius->dispofftime);
                genius->cga_stat |= 1;
                genius->mda_stat |= 1;
                genius->linepos = 1;
                if (genius->dispon) {
                        if (genius->genius_control & 0x20) {
                                background = genius_pal[3];
                        } else {
                                background = genius_pal[0];
                        }
                        if (genius->displine == 0) {
                                video_wait_for_buffer();
                        }
                        /* Start off with a blank line */
                        for (x = 0; x < GENIUS_XSIZE; x++) {
                                ((uint32_t *)buffer32->line[genius->displine])[x] = background;
                        }
                        /* If graphics display enabled, draw graphics on top
                         * of the blanked line */
                        if (genius->cga_ctrl & 8) {
                                if (genius->genius_control & 8) {
                                        genius_cgaline(genius);
                                } else {
                                        genius_hiresline(genius);
                                }
                        }
                        /* If MDA display is enabled, draw MDA text on top
                         * of the lot */
                        if (genius->mda_ctrl & 8) {
                                genius_textline(genius, background);
                        }
                }
                genius->displine++;
                /* Hardcode a fixed refresh rate and VSYNC timing */
                if (genius->displine == 1008) /* Start of VSYNC */
                {
                        genius->cga_stat |= 8;
                        genius->dispon = 0;
                }
                if (genius->displine == 1040) /* End of VSYNC */
                {
                        genius->displine = 0;
                        genius->cga_stat &= ~8;
                        genius->dispon = 1;
                }
        } else {
                if (genius->dispon) {
                        genius->cga_stat &= ~1;
                        genius->mda_stat &= ~1;
                }
                timer_advance_u64(&genius->timer, genius->dispontime);
                genius->linepos = 0;

                if (genius->displine == 1008) {
                        /* Hardcode GENIUS_XSIZE * GENIUS_YSIZE window size */
                        if (GENIUS_XSIZE != xsize || GENIUS_YSIZE != ysize) {
                                xsize = GENIUS_XSIZE;
                                ysize = GENIUS_YSIZE;
                                if (xsize < 64)
                                        xsize = 656;
                                if (ysize < 32)
                                        ysize = 200;
                                updatewindowsize(xsize, ysize);
                        }
                        video_blit_memtoscreen(0, 0, 0, ysize, xsize, ysize);

                        frames++;
                        /* Fixed 728x1008 resolution */
                        video_res_x = GENIUS_XSIZE;
                        video_res_y = GENIUS_YSIZE;
                        video_bpp = 1;
                        genius->blink++;
                }
        }
}

void *genius_init() {
        int c;
        genius_t *genius = malloc(sizeof(genius_t));
        memset(genius, 0, sizeof(genius_t));

        /* 160k video RAM */
        genius->vram = malloc(0x28000);

        timer_add(&genius->timer, genius_poll, genius, 1);

        /* Occupy memory between 0xB0000 and 0xBFFFF (moves to 0xA0000 in
         * high-resolution modes)  */
        mem_mapping_add(&genius->mapping, 0xb0000, 0x10000, genius_read, NULL, NULL, genius_write, NULL, NULL, NULL, 0, genius);
        /* Respond to both MDA and CGA I/O ports */
        io_sethandler(0x03b0, 0x000C, genius_in, NULL, NULL, genius_out, NULL, NULL, genius);
        io_sethandler(0x03d0, 0x0010, genius_in, NULL, NULL, genius_out, NULL, NULL, genius);

        genius_pal[0] = makecol(0x00, 0x00, 0x00);
        genius_pal[1] = makecol(0x55, 0x55, 0x55);
        genius_pal[2] = makecol(0xaa, 0xaa, 0xaa);
        genius_pal[3] = makecol(0xff, 0xff, 0xff);

        /* MDA attributes */
        /* I don't know if the Genius's MDA emulation actually does
         * emulate bright / non-bright. For the time being pretend it does. */
        for (c = 0; c < 256; c++) {
                mdacols[c][0][0] = mdacols[c][1][0] = mdacols[c][1][1] = genius_pal[0];
                if (c & 8)
                        mdacols[c][0][1] = genius_pal[3];
                else
                        mdacols[c][0][1] = genius_pal[2];
        }
        mdacols[0x70][0][1] = genius_pal[0];
        mdacols[0x70][0][0] = mdacols[0x70][1][0] = mdacols[0x70][1][1] = genius_pal[3];
        mdacols[0xF0][0][1] = genius_pal[0];
        mdacols[0xF0][0][0] = mdacols[0xF0][1][0] = mdacols[0xF0][1][1] = genius_pal[3];
        mdacols[0x78][0][1] = genius_pal[2];
        mdacols[0x78][0][0] = mdacols[0x78][1][0] = mdacols[0x78][1][1] = genius_pal[3];
        mdacols[0xF8][0][1] = genius_pal[2];
        mdacols[0xF8][0][0] = mdacols[0xF8][1][0] = mdacols[0xF8][1][1] = genius_pal[3];
        mdacols[0x00][0][1] = mdacols[0x00][1][1] = genius_pal[0];
        mdacols[0x08][0][1] = mdacols[0x08][1][1] = genius_pal[0];
        mdacols[0x80][0][1] = mdacols[0x80][1][1] = genius_pal[0];
        mdacols[0x88][0][1] = mdacols[0x88][1][1] = genius_pal[0];

        /* Start off in 80x25 text mode */
        genius->cga_stat = 0xF4;
        genius->genius_mode = 2;
        genius->enabled = 1;
        genius->genius_charh = 0x90; /* Native character height register */
        return genius;
}

void genius_close(void *p) {
        genius_t *genius = (genius_t *)p;

        free(genius->vram);
        free(genius);
}

static int genius_available() { return rom_present("8x12.bin"); }

void genius_speed_changed(void *p) {
        genius_t *genius = (genius_t *)p;

        genius_recalctimings(genius);
}

device_t genius_device = {"Genius VHR", 0, genius_init, genius_close, genius_available, genius_speed_changed, NULL, NULL};
