/*
 * 86Box    A hypervisor and IBM PC system emulator that specializes in
 *          running old operating systems and software designed for IBM
 *          PC systems and compatibles from 1981 through fairly recent
 *          system designs based on the PCI bus.
 *
 *          This file is part of the 86Box distribution.
 *
 *          Chips & Technologies 82C425 display controller emulation,
 *          with support for 640x200 LCD and SMARTMAP text contrast
 *          enhancement.
 *
 *          Relevant literature:
 *
 *          [1] Chips and Technologies, Inc., 82C425 CGA LCD/CRT Controller,
 *              Data Sheet, Revision No. 2.2, September 1991.
 *              <https://archive.org/download/82C425/82C425.pdf>
 *
 *          [2] Pleva et al., COLOR TO MONOCHROME CONVERSION,
 *              U.S. Patent 4,977,398, Dec. 11, 1990.
 *              <https://pimg-fpiw.uspto.gov/fdd/98/773/049/0.pdf>
 *
 *          Based on Toshiba T1000 plasma display emulation code.
 *
 * Authors: Fred N. van Kempen, <decwiz@yahoo.com>
 *          Miran Grca, <mgrca8@gmail.com>
 *          John Elliott, <jce@seasip.info>
 *          Lubomir Rintel, <lkundrak@v3.sk>
 *
 *          Copyright 2018-2019 Fred N. van Kempen.
 *          Copyright 2018-2019 Miran Grca.
 *          Copyright 2018-2019 John Elliott.
 *          Copyright 2021      Lubomir Rintel.
 *
 * 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.
 */
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <wchar.h>
#include <86box/86box.h>
#include <86box/device.h>
#include <86box/io.h>
#include <86box/mem.h>
#include <86box/timer.h>
#include "cpu.h"
#include <86box/video.h>
#include <86box/vid_cga.h>
#include <86box/plat_unused.h>

#define F82C425_XSIZE 640
#define F82C425_YSIZE 200

/* Mapping of attributes to colours */
static uint32_t smartmap[256][2];
static uint32_t colormap[4];

static video_timings_t timing_f82c425 = { .type = VIDEO_ISA, .write_b = 8, .write_w = 16, .write_l = 32, .read_b = 8, .read_w = 16, .read_l = 32 };

static uint8_t st_video_options;
static uint8_t st_enabled          = 1;
static int8_t  st_display_internal = -1;

void
f82c425_video_options_set(uint8_t options)
{
    st_video_options = options;
}

void
f82c425_video_enable(uint8_t enabled)
{
    st_enabled = enabled;
}

void
f82c425_display_set(uint8_t internal)
{
    st_display_internal = (int8_t) internal;
}

uint8_t
f82c425_display_get(void)
{
    return (uint8_t) st_display_internal;
}

typedef struct f82c425_t {
    mem_mapping_t mapping;
    cga_t         cga;
    uint8_t       crtcreg;

    uint64_t dispontime, dispofftime;

    int     linepos, displine;
    int     dispon;
    uint8_t video_options;

    uint8_t *vram;

    /* Registers specific to 82C425. */
    uint8_t ac_limit;
    uint8_t threshold;
    uint8_t shift;
    uint8_t hsync;
    uint8_t vsync_blink;
    uint8_t timing;
    uint8_t function;
} f82c425_t;

/* Convert IRGB representation to RGBI,
 * useful in SMARTMAP calculations. */
static inline uint8_t
f82c425_rgbi(uint8_t irgb)
{
    return ((irgb & 0x7) << 1) | (irgb >> 3);
}

/* Convert IRGB SMARTMAP output to a RGB representation of one of 4/8 grey
 * shades we'd see on an actual V86P display: with some bias toward lighter
 * shades and a backlight with yellow/green-ish tint. */
static inline uint32_t
f82c425_makecol(uint8_t rgbi, int gs4, int inv)
{
    uint8_t c;

    gs4 = 1 + !!gs4;
    if (!inv) {
        rgbi = 15 - rgbi;
    }
    c = 0x10 * gs4 * ((rgbi >> gs4) + 2);

#ifdef NO_BLUE
    return makecol(c, c + 0x08, c - 0x20);
#else
    return makecol(c, c + 0x08, 0x70);
#endif
}

/* Saturating/non-saturating addition for SMARTMAP(see below). */
static inline int
f82c425_smartmap_add(int a, int b, int sat)
{
    int c = a + b;

    /* (SATURATING OR NON SATURATING) */
    if (sat) {
        if (c < 0)
            c = 0;
        else if (c > 15)
            c = 15;
    }

    return c & 0xf;
}

/* Calculate and cache mapping of CGA text color attribute to a
 * shade of gray enhanced via the SMARTMAP algorithm.
 *
 * This is a straightforward implementation of the algorithm as described
 * in U.S. Patent 4,977,398 [2]. The comments in capitals refer to portions
 * of a figure on page 4. */
static void
f82c425_smartmap(f82c425_t *f82c425)
{
    for (uint16_t i = 0; i < 256; i++) {
        uint8_t bg = f82c425_rgbi(i >> 4);
        uint8_t fg = f82c425_rgbi(i & 0xf);

        /* FIG._4. */
        if (abs(bg - fg) <= (f82c425->threshold & 0x0f)) {
            /* FOREGROUND=BACKGROUND */
            if (bg == fg) {
                /* SPECIAL CASE */
                if (f82c425->shift == 0xff) {
                    /* CHECK MOST SIGNIFICANT BIT */
                    if (fg & 0x8) {
                        /* FULL WHITE */
                        fg = bg = 15;
                    } else {
                        /* FULL BLACK */
                        fg = bg = 0;
                    }
                }
            } else {
                uint8_t sat = f82c425->threshold & 0x10;

                /* DETERMINE WHICH IS LIGHT */
                if (fg > bg) {
                    fg = f82c425_smartmap_add(fg, f82c425->shift & 0x0f, sat);
                    bg = f82c425_smartmap_add(bg, -(f82c425->shift >> 4), sat);
                } else {
                    fg = f82c425_smartmap_add(fg, -(f82c425->shift & 0x0f), sat);
                    bg = f82c425_smartmap_add(bg, f82c425->shift >> 4, sat);
                }
            }
        }

        smartmap[i][0] = f82c425_makecol(bg, f82c425->threshold & 0x20, f82c425->function & 0x80);
        smartmap[i][1] = f82c425_makecol(fg, f82c425->threshold & 0x20, f82c425->function & 0x80);
    }
}

/* Calculate mapping of 320x200 graphical mode colors. */
static void
f82c425_colormap(f82c425_t *f82c425)
{
    for (uint8_t i = 0; i < 4; i++)
        colormap[i] = f82c425_makecol(5 * i, 0, f82c425->function & 0x80);
}

static void
f82c425_out(uint16_t addr, uint8_t val, void *priv)
{
    f82c425_t *f82c425 = (f82c425_t *) priv;

    if (addr == 0x3d4)
        f82c425->crtcreg = val;

    if (((f82c425->function & 0x01) == 0) && ((f82c425->crtcreg != 0xdf) || (addr != 0x3d5)))
        return;

    if (addr != 0x3d5 || f82c425->crtcreg <= 31) {
        cga_out(addr, val, &f82c425->cga);
        return;
    }

    switch (f82c425->crtcreg) {
        case 0xd9:
            f82c425->ac_limit = val;
            break;
        case 0xda:
            f82c425->threshold = val;
            f82c425_smartmap(f82c425);
            break;
        case 0xdb:
            f82c425->shift = val;
            f82c425_smartmap(f82c425);
            break;
        case 0xdc:
            f82c425->hsync = val;
            break;
        case 0xdd:
            f82c425->vsync_blink = val;
            break;
        case 0xde:
            f82c425->timing = val;
            break;
        case 0xdf:
            f82c425->function = val;
            f82c425_smartmap(f82c425);
            f82c425_colormap(f82c425);
            break;

        default:
            break;
    }
}

static uint8_t
f82c425_in(uint16_t addr, void *priv)
{
    f82c425_t *f82c425 = (f82c425_t *) priv;

    if ((f82c425->function & 0x01) == 0)
        return 0xff;

    if (addr == 0x3d4)
        return f82c425->crtcreg;

    if (addr != 0x3d5 || f82c425->crtcreg <= 31)
        return cga_in(addr, &f82c425->cga);

    switch (f82c425->crtcreg) {
        case 0xd9:
            return f82c425->ac_limit;
        case 0xda:
            return f82c425->threshold;
        case 0xdb:
            return f82c425->shift;
        case 0xdc:
            return f82c425->hsync;
        case 0xdd:
            return f82c425->vsync_blink;
        case 0xde:
            return f82c425->timing;
        case 0xdf:
            return f82c425->function;

        default:
            break;
    }

    return 0xff;
}

static void
f82c425_write(uint32_t addr, uint8_t val, void *priv)
{
    f82c425_t *f82c425 = (f82c425_t *) priv;

    f82c425->vram[addr & 0x3fff] = val;
    cycles -= 4;
}

static uint8_t
f82c425_read(uint32_t addr, void *priv)
{
    const f82c425_t *f82c425 = (f82c425_t *) priv;

    cycles -= 4;

    return f82c425->vram[addr & 0x3fff];
}

static void
f82c425_recalctimings(f82c425_t *f82c425)
{
    double disptime;
    double _dispontime;
    double _dispofftime;

    if (f82c425->function & 0x08) {
        cga_recalctimings(&f82c425->cga);
        return;
    }

    disptime             = 651;
    _dispontime          = 640;
    _dispofftime         = disptime - _dispontime;
    f82c425->dispontime  = (uint64_t) (_dispontime * xt_cpu_multi);
    f82c425->dispofftime = (uint64_t) (_dispofftime * xt_cpu_multi);
}

/* Draw a row of text. */
static void
f82c425_text_row(f82c425_t *f82c425)
{
    uint32_t colors[2];
    int      c;
    uint8_t  chr;
    uint8_t  attr;
    int      drawcursor;
    int      cursorline;
    int      blink;
    uint16_t addr;
    uint8_t  scanline;
    uint16_t memaddr      = (f82c425->cga.crtc[CGA_CRTC_START_ADDR_LOW] | (f82c425->cga.crtc[CGA_CRTC_START_ADDR_HIGH] << 8)) & 0x3fff;
    uint16_t cursoraddr      = (f82c425->cga.crtc[0x0f] | (f82c425->cga.crtc[0x0e] << 8)) & 0x3fff;
    uint8_t  sl      = f82c425->cga.crtc[CGA_CRTC_MAX_SCANLINE_ADDR] + 1;
    int      columns = f82c425->cga.crtc[CGA_CRTC_HDISP];

    scanline   = (f82c425->displine) & 7;
    addr = ((memaddr & ~1) + (f82c425->displine >> 3) * columns) * 2;
    memaddr += (f82c425->displine >> 3) * columns;

    if ((f82c425->cga.crtc[CGA_CRTC_CURSOR_START] & 0x60) == 0x20) {
        cursorline = 0;
    } else {
        cursorline = ((f82c425->cga.crtc[CGA_CRTC_CURSOR_START] & 0x0F) <= scanline) && ((f82c425->cga.crtc[CGA_CRTC_CURSOR_END] & 0x0F) >= scanline);
    }

    for (int x = 0; x < columns; x++) {
        chr        = f82c425->vram[(addr + 2 * x) & 0x3FFF];
        attr       = f82c425->vram[(addr + 2 * x + 1) & 0x3FFF];
        drawcursor = ((memaddr == cursoraddr) && cursorline && (f82c425->cga.cgamode & CGA_MODE_FLAG_VIDEO_ENABLE) && (f82c425->cga.cgablink & 0x10));

        blink = ((f82c425->cga.cgablink & 0x10) && (f82c425->cga.cgamode & CGA_MODE_FLAG_BLINK) && (attr & 0x80) && !drawcursor);

        if (drawcursor) {
            colors[0] = smartmap[~attr & 0xff][0];
            colors[1] = smartmap[~attr & 0xff][1];
        } else {
            colors[0] = smartmap[attr][0];
            colors[1] = smartmap[attr][1];
        }

        if (blink)
            colors[1] = colors[0];

        if (f82c425->cga.cgamode & 0x01) {
            /* High resolution (80 cols) */
            for (c = 0; c < sl; c++) {
                (buffer32->line[f82c425->displine])[(x << 3) + c] = colors[(fontdat[chr][scanline] & (1 << (c ^ 7))) ? 1 : 0];
            }
        } else {
            /* Low resolution (40 columns, stretch pixels horizontally) */
            for (c = 0; c < sl; c++) {
                (buffer32->line[f82c425->displine])[(x << 4) + c * 2] = (buffer32->line[f82c425->displine])[(x << 4) + c * 2 + 1] = colors[(fontdat[chr][scanline] & (1 << (c ^ 7))) ? 1 : 0];
            }
        }

        ++memaddr;
    }
}

/* Draw a line in CGA 640x200 mode */
static void
f82c425_cgaline6(f82c425_t *f82c425)
{
    uint8_t  dat;
    uint16_t addr;

    uint16_t memaddr = (f82c425->cga.crtc[CGA_CRTC_START_ADDR_LOW] | (f82c425->cga.crtc[CGA_CRTC_START_ADDR_HIGH] << 8)) & 0x3fff;

    addr = ((f82c425->displine) & 1) * 0x2000 + (f82c425->displine >> 1) * 80 + ((memaddr & ~1) << 1);

    for (uint8_t x  = 0; x < 80; x++) {
        dat = f82c425->vram[addr & 0x3FFF];
        addr++;

        for (uint8_t c = 0; c < 8; c++) {
            (buffer32->line[f82c425->displine])[x * 8 + c] = colormap[dat & 0x80 ? 3 : 0];

            dat = dat << 1;
        }
    }
}

/* Draw a line in CGA 320x200 mode. */
static void
f82c425_cgaline4(f82c425_t *f82c425)
{
    uint8_t  dat;
    uint8_t  pattern;
    uint16_t addr;

    uint16_t memaddr = (f82c425->cga.crtc[CGA_CRTC_START_ADDR_LOW] | (f82c425->cga.crtc[CGA_CRTC_START_ADDR_HIGH] << 8)) & 0x3fff;
    addr        = ((f82c425->displine) & 1) * 0x2000 + (f82c425->displine >> 1) * 80 + ((memaddr & ~1) << 1);

    for (uint8_t x = 0; x < 80; x++) {
        dat = f82c425->vram[addr & 0x3FFF];
        addr++;

        for (uint8_t c = 0; c < 4; c++) {
            pattern = (dat & 0xC0) >> 6;
            if (!(f82c425->cga.cgamode & 0x08))
                pattern = 0;

            (buffer32->line[f82c425->displine])[x * 8 + 2 * c] = (buffer32->line[f82c425->displine])[x * 8 + 2 * c + 1] = colormap[pattern & 3];

            dat = dat << 2;
        }
    }
}

static void
f82c425_poll(void *priv)
{
    f82c425_t *f82c425 = (f82c425_t *) priv;

    if (f82c425->video_options != st_video_options || !!(f82c425->function & 1) != st_enabled) {
        f82c425->video_options = st_video_options;
        f82c425->function &= ~1;
        f82c425->function |= st_enabled ? 1 : 0;

        if (f82c425->function & 0x01)
            mem_mapping_enable(&f82c425->mapping);
        else
            mem_mapping_disable(&f82c425->mapping);
    }
    /* Switch between internal LCD and external CRT display. */
    if (st_display_internal != -1 && st_display_internal != !!(f82c425->function & 0x08)) {
        if (st_display_internal) {
            f82c425->function &= ~0x08;
            f82c425->timing &= ~0x20;
        } else {
            f82c425->function |= 0x08;
            f82c425->timing |= 0x20;
        }
        f82c425_recalctimings(f82c425);
    }

    if (f82c425->function & 0x08) {
        cga_poll(&f82c425->cga);
        return;
    }

    if (!f82c425->linepos) {
        timer_advance_u64(&f82c425->cga.timer, f82c425->dispofftime);
        f82c425->cga.cgastat |= 1;
        f82c425->linepos = 1;
        if (f82c425->dispon) {
            if (f82c425->displine == 0) {
                video_wait_for_buffer();
            }

            switch (f82c425->cga.cgamode & 0x13) {
                case 0x12:
                    f82c425_cgaline6(f82c425);
                    break;
                case 0x02:
                    f82c425_cgaline4(f82c425);
                    break;
                case 0x00:
                case 0x01:
                    f82c425_text_row(f82c425);
                    break;

                default:
                    break;
            }
        }
        f82c425->displine++;

        /* Hardcode a fixed refresh rate and VSYNC timing */
        if (f82c425->displine >= 216) {
            /* End of VSYNC */
            f82c425->displine = 0;
            f82c425->cga.cgastat &= ~8;
            f82c425->dispon = 1;
        } else if (f82c425->displine == (f82c425->cga.crtc[CGA_CRTC_MAX_SCANLINE_ADDR] + 1) * f82c425->cga.crtc[CGA_CRTC_VDISP]) {
            /* Start of VSYNC */
            f82c425->cga.cgastat |= 8;
            f82c425->dispon = 0;
        }
    } else {
        if (f82c425->dispon)
            f82c425->cga.cgastat &= ~1;
        timer_advance_u64(&f82c425->cga.timer, f82c425->dispontime);
        f82c425->linepos = 0;

        if (f82c425->displine == 200) {
            /* Hardcode 640x200 window size */
            if ((F82C425_XSIZE != xsize) || (F82C425_YSIZE != ysize) || video_force_resize_get()) {
                xsize = F82C425_XSIZE;
                ysize = F82C425_YSIZE;
                set_screen_size(xsize, ysize);

                if (video_force_resize_get())
                    video_force_resize_set(0);
            }
            video_blit_memtoscreen(0, 0, xsize, ysize);
            frames++;

            /* Fixed 640x200 resolution */
            video_res_x = F82C425_XSIZE;
            video_res_y = F82C425_YSIZE;

            switch (f82c425->cga.cgamode & 0x12) {
                case 0x12:
                    video_bpp = 1;
                    break;
                case 0x02:
                    video_bpp = 2;
                    break;
                default:
                    video_bpp = 0;
            }

            f82c425->cga.cgablink++;
        }
    }
}

static void *
f82c425_init(UNUSED(const device_t *info))
{
    f82c425_t *f82c425 = malloc(sizeof(f82c425_t));

    memset(f82c425, 0, sizeof(f82c425_t));
    cga_init(&f82c425->cga);
    video_inform(VIDEO_FLAG_TYPE_CGA, &timing_f82c425);

    /* Initialize registers that don't default to zero. */
    f82c425->hsync       = 0x40;
    f82c425->vsync_blink = 0x72;

    /* 16k video RAM */
    f82c425->vram = malloc(0x4000);

    timer_set_callback(&f82c425->cga.timer, f82c425_poll);
    timer_set_p(&f82c425->cga.timer, f82c425);

    /* Occupy memory between 0xB8000 and 0xBFFFF */
    mem_mapping_add(&f82c425->mapping, 0xb8000, 0x8000, f82c425_read, NULL, NULL, f82c425_write, NULL, NULL, NULL, 0, f82c425);
    /* Respond to CGA I/O ports */
    io_sethandler(0x03d0, 0x000c, f82c425_in, NULL, NULL, f82c425_out, NULL, NULL, f82c425);

    /* Initialize color maps for text & graphic modes */
    f82c425_smartmap(f82c425);
    f82c425_colormap(f82c425);

    /* Start off in 80x25 text mode */
    f82c425->cga.cgastat   = 0xF4;
    f82c425->cga.vram      = f82c425->vram;
    f82c425->video_options = 0x01;

    return f82c425;
}

static void
f82c425_close(void *priv)
{
    f82c425_t *f82c425 = (f82c425_t *) priv;

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

static void
f82c425_speed_changed(void *priv)
{
    f82c425_t *f82c425 = (f82c425_t *) priv;

    f82c425_recalctimings(f82c425);
}

const device_t f82c425_video_device = {
    .name          = "82C425 CGA LCD/CRT Controller",
    .internal_name = "f82c425_video",
    .flags         = 0,
    .local         = 0,
    .init          = f82c425_init,
    .close         = f82c425_close,
    .reset         = NULL,
    .available     = NULL,
    .speed_changed = f82c425_speed_changed,
    .force_redraw  = NULL,
    .config        = NULL
};
