//
// VBE Interface class for PTC 2.0 C++ API
// Copyright (c) 1998 Jonathan Matthew (jmatthew@uq.net.au)
// The PTC 2.0 C++ API is (c) 1998 Glenn Fiedler (ptc@gaffer.org)
// This source code is licensed under the GNU LGPL
//

#include <stdio.h>
#include <conio.h>

#include "Types.h"
#include "Error.h"
#include "Format.h"
#include "tables.h"
#include "vbe.h"
#include "dpmi.h"
#include "near.h"

#ifdef __DJGPP__
#include <pc.h>
#endif

VBE::VBE(int xres, int yres, int format, int direct, int flags)
{
    int mode;
    if ((format < 0) || (format > 3))
        throw Error("illegal source format");

    lock_near_base();
    vesa_init();

    find_virtual_mode(&mode, format, xres, yres, flags);
    m_lfb = 0;
    // no protected mode interface before this
    m_pmode_interface = 0;
    m_pm_page_flip = 0;
    m_pm_set_bank = 0;
    m_pm_palette = 0;
    set_vid_mode(mode, xres, yres, flags);

    page_set_init();

    // behaviour when a direct mode was requested and an 8-bit mode
    // found:
    if (direct && (m_bpp == 1)) {

        // if it would be better to fall back to a fakemode, throw
        // an exception
        if ((format>0) && (xres < 320) && (yres < 200))
            throw Error("falling back to fakemode");

        // otherwise, use RGB332
        m_ptcformat = Format(8, 0xE0, 0x1C, 0x03);
        int32 paldata[256];
        for (int i=0; i<256; i++) {
            int r = (int)(((i&0xE0)>>5) * (255.0 / 7.0));
            int g = (int)(((i&0x1C)>>2) * (255.0 / 7.0));
            int b = (int)( (i&0x03)     * (255.0 / 3.0));

            paldata[i] = (r<<16) | (g<<8) | (b<<0);
        }
        palette((const int32 *)paldata);
    }

    // clear video memory
    clear_video_memory();

    unlock_near_base();
    m_mode = mode;
    m_init = 1;
    m_locked = 0;
}

VBE::~VBE()
{
    if (m_init) {
        set_vid_mode(3);        // text mode
        if (m_pmode_interface) {
            delete[] m_pmode_interface;
            m_pmode_interface = 0;
            m_pm_page_flip = 0;
            m_pm_set_bank = 0;
            m_pm_palette = 0;
        }
        dpmi.dos_free(&m_dosmem);
        m_init = 0;
    }
}

inline int VBE::vesa_error(int eax)
{
    int result = (eax & 0xFFFF);
    if (result != 0x4F) return 1;
    return 0;
}

int VBE::identify_pixel_format()
{
    if (m_mib->bits_per_pixel == 8) {
        m_format = 10;
        return 0;
    } else {
        for (int i=0; i<10; i++) {
            if ((m_mib->red_pos == pixel_format_table[i][0]) &&
                (m_mib->red_mask_size == pixel_format_table[i][1]) &&
                (m_mib->green_pos == pixel_format_table[i][2]) &&
                (m_mib->green_mask_size == pixel_format_table[i][3]) &&
                (m_mib->blue_pos == pixel_format_table[i][4]) &&
                (m_mib->blue_mask_size == pixel_format_table[i][5]) &&
                (m_mib->rsvd_pos == pixel_format_table[i][6]) &&
                (m_mib->rsvd_mask_size == pixel_format_table[i][7])) {
                    m_format = i;
                    return 0;
            }
        }
    }
    return 1;
}

int VBE::find_vid_mode(int *mode, int mt, int xres, int yres, int flags)
{
    DPMI::dpmi_regs dr;
    short *ml = m_modelist;
    int index = mt & 127;
    int bxres, byres;
    int mbpp;

    // set up best x,y res according to flags
    if (flags&1) {
        // flexible
        bxres = 0xffffff;
        byres = 0xffffff;
    } else {
        // not flexible
        bxres = xres;
        byres = yres;
    }

    *mode = 0;
    if ((mt < 0) || (index > 4)) return 1;
    while ((*ml != -1) && (*ml)) {

        // modeinfo call
        memset(&dr, 0, sizeof(DPMI::dpmi_regs));
        dr.l.eax = 0x4F01;
        dr.l.ecx = *ml;
        dr.l.es = m_dosmem.seg;
        dr.l.edi = 512;
        dpmi.dos_int(0x10, &dr, &dr);
        if (vesa_error(dr.l.eax)) {
            ml++;
            continue;
        }

        // check mode
        if (!identify_pixel_format()) {
            mbpp = pixel_format_table[m_format][8];

            if ((m_mib->x_res >= xres) && (m_mib->y_res >= yres) &&
                (m_mib->x_res <=bxres) && (m_mib->y_res <=byres) &&
                (m_mib->mem_model == vid_mode_model[index]) &&
                ((m_mib->mode_attr & 17) == 17) &&
                (mbpp == vid_mode_rbpp[index])) {
                if (mt & 128) {
                    // disallow linear modes if requested to
                    if ((flags&16) && ((m_mib->mode_attr & 128) == 128)) {
                        *mode = *ml;
                        bxres = m_mib->x_res;
                        byres = m_mib->y_res;
                    }
                } else {
                    // disallow banked modes if requested to
                    if ((flags&8) && ((m_mib->mode_attr & 64) == 0)) {
                        *mode = *ml;
                        bxres = m_mib->x_res;
                        byres = m_mib->y_res;
                    }
                }
            }
        }
        ml++;
    }

    if (!*mode) return 1;

    // get modeinfo again
    memset(&dr, 0, sizeof(DPMI::dpmi_regs));
    dr.l.eax = 0x4F01;
    dr.l.ecx = *mode;
    dr.l.es = m_dosmem.seg;
    dr.l.edi = 512;
    dpmi.dos_int(0x10, &dr, &dr);
    if (vesa_error(dr.l.eax)) {
        *mode = 0;
        return 1;
    }
    identify_pixel_format();

    return 0;
}

void VBE::find_virtual_mode(int *mode, int type, int xres, int yres, int flags)
{
    int i,mt;
    *mode = 0;

    // try for exact match first
    for (i=0; i<10; i++) {
        mt = vid_mode_search_order[type][i];
        if (!find_vid_mode(mode, mt, xres, yres, (flags&24))) break;
    }

    // if that failed, try flexible if it's allowed
    if (!(*mode) && (flags&1)) {
        for (i=0; i<9; i++) {
            mt = vid_mode_search_order[type][i];
            if (!find_vid_mode(mode, mt, xres, yres, (flags&24)+1)) break;
        }
    }

    // set things up if a mode has been found
    if (*mode) {
        m_width = xres;
        m_height = yres;
        if (mt & 128) {
            m_mode_type = 0;
            *mode |= 0x4000;  // lfb flag
        } else {
            m_mode_type = 1;
        }
        return;
    }
    throw Error("unable to find a video mode");
}

void VBE::vesa_init()
{
    DPMI::dpmi_regs dr;
    int *vpr;

    // get dos memory
    dpmi.dos_alloc(2048, &m_dosmem);
    vpr = (int *)adjust_near_pointer(m_dosmem.ptr);
    memset(vpr, 0, 1024);
    *vpr = 0x32454256; /* '2EBV' */              // Intel I hate you.

    // get vbe info
    memset(&dr, 0, sizeof(DPMI::dpmi_regs));
    dr.l.eax = 0x4F00;
    dr.l.es = m_dosmem.seg;
    dr.l.edi = 0;
    dpmi.dos_int(0x10, &dr, &dr);
    if (vesa_error(dr.l.eax)) {
        dpmi.dos_free(&m_dosmem);
        throw Error("unable to initialise vbe");
    }
    m_vib = (vbe_info_block *)adjust_near_pointer(m_dosmem.ptr);
    m_mib = (mode_info_block *)adjust_near_pointer((void *)
        ((char *)m_dosmem.ptr + 512));

    // check 'VESA'
    if (m_vib->vbe_sig != 0x41534556 /* 'ASEV' */) {     // Intel I hate you.
        dpmi.dos_free(&m_dosmem);
        throw Error("unable to initialise vbe");
    }

    // get version
    m_vbe_ver = 0;
    if (m_vib->vbe_maj_ver > 1) m_vbe_ver = 1;

    // get modelist pointer
    m_modelist = (short *)adjust_near_pointer((void *)
       (((m_vib->vid_mode_ptr&0xFFFF0000)>>12)+(m_vib->vid_mode_ptr&0xFFFF)));
}

#ifdef __WATCOMC__

// Watcom implementation
void biostextmode();
#pragma aux biostextmode = "mov ax, 3" "int 10h" modify [ax];

#else

// DJGPP implementation
inline void biostextmode()
{
    DPMI dpmi;
    DPMI::dpmi_regs dr;
    memset(&dr, 0, sizeof(DPMI::dpmi_regs));
    dr.l.eax = 3;
    dpmi.dos_int(0x10, &dr, &dr);
}

#endif

void VBE::set_vid_mode(int mode, int xres, int yres, int flags)
{
    DPMI::dpmi_regs dr;

    // unmap lfb if mapped
    if (m_lfb) {
#ifndef __DJGPP
        // for some reason, DJGPP doesn't like doing this.
        dpmi.unlock_mem((int)m_screen, m_vib->total_mem << 16);
#endif
        dpmi.unmap_linear_mem((int)m_screen);
        m_lfb = 0;
    }

    // if mode==3, just set text mode
    if (mode == 3) {
        biostextmode();
        return;
    }

    // set the mode
    memset(&dr, 0, sizeof(DPMI::dpmi_regs));
    dr.l.eax = 0x4F02;
    dr.l.ebx = mode & 0x7FFF;
    dpmi.dos_int(0x10, &dr, &dr);
    if (vesa_error(dr.l.eax)) {
        biostextmode();
        throw Error("unable to set video mode");
    }

    // get mode pitch
    memset(&dr, 0, sizeof(DPMI::dpmi_regs));
    dr.l.eax = 0x4F06;
    dr.l.ebx = 1;
    dpmi.dos_int(0x10, &dr, &dr);
    m_pitch = dr.w.bx;
    m_bpp = (pixel_format_table[m_format][8]+7)/8;

    // create PTC Format object
    if (m_bpp == 1) {
        m_ptcformat = Format(8);
    } else {
        m_ptcformat = Format(m_bpp*8,
            ((1 << m_mib->red_mask_size  )-1) << m_mib->red_pos,
            ((1 << m_mib->green_mask_size)-1) << m_mib->green_pos,
            ((1 << m_mib->blue_mask_size )-1) << m_mib->blue_pos);
    }

    // calculate space between centred pages
    m_blacksize = (m_mib->y_res - yres+1) / 2;

    // offset to centre image on screen
    int dispoffset = m_blacksize * m_pitch +
        ((m_mib->x_res/2 - xres/2) & (~3)) * m_bpp;

    // calculate number of pages available (max 3, anyway)
    int pn = (((m_mib->y_res - m_blacksize) * m_pitch) + 0xFFFF) & (~0xFFFF);
    int totalmem = m_vib->total_mem << 16;
    m_pages = (totalmem - dispoffset) / pn;
    if (m_pages > 3) m_pages = 3;
    else if (m_pages == 0) {
        // not enough memory for one page.
        biostextmode();
        throw Error("not enough video memory");
    }

    // relative offsets of pages
    m_page_addr[0] = dispoffset;
    m_page_addr[1] = pn + dispoffset;
    m_page_addr[2] = (2*pn) + dispoffset;
    // map lfb if necessary
    if (mode & 0x4000) {
        if (dpmi.map_linear_mem(m_mib->phys_base_ptr, totalmem,
            (int *)&m_screen)) {
            biostextmode();
            throw Error("unable to map lfb");
        }
#ifndef __DJGPP
        // for some reason, DJGPP doesn't like doing this.
        dpmi.lock_mem((int)m_screen, totalmem);
#endif
        // make page addresses into absolute offsets.
        for (int i=0; i<3; i++) {
            m_page_addr[i] += (int)m_screen;
        }

        m_lfb = 1;
    } else {
        m_screen = (char *)((m_mib->win_a_seg&0xFFFF) << 4);
        m_banksize = m_mib->win_size * 1024;
        m_bankinc = m_mib->win_size / m_mib->win_gran;
        m_granularity = m_mib->win_gran * 1024;

        // calculate starting banks for each page
        for (int i=0; i<3; i++) {
            m_page_bank[i][0] = ((int)m_page_addr[i]) / m_granularity;
            m_page_bank[i][1] = ((int)m_page_addr[i]) % m_granularity;
        }

        m_lfb = 0;
    }
    m_current_page = 0;

    // enable wide dac when in indexed mode and not disallowed
    m_wide_dac = 0;
    if (m_bpp == 1 && (m_vib->caps[0]&1) && (flags&2)) {
        // enable 8bit dac
        memset(&dr, 0, sizeof(DPMI::dpmi_regs));
        dr.l.eax = 0x4F08;
        dr.l.ebx = 0x0800;
        dpmi.dos_int(0x10, &dr, &dr);
        if ((!vesa_error(dr.l.eax)) && (dr.b.bh == 8)) {
            m_wide_dac = 1;
        }
    }

    // attempt to get protected mode interface
    m_pm_page_flip = 0;
    m_pm_set_bank = 0;
    m_pm_palette = 0;
    m_pmode_interface = 0;
    if (flags & 4) {        // if allowed
        memset(&dr, 0, sizeof(DPMI::dpmi_regs));
        dr.l.eax = 0x4f0a;
        dr.l.ebx = 0;
        dpmi.dos_int(0x10, &dr, &dr);
        if ((!vesa_error(dr.l.eax)) && (dr.w.cx != 0)) {
            short16 *table = (short16 *)adjust_near_pointer((void *)
               (((int32)dr.l.es << 4) + dr.w.di));
            m_pmode_interface = new char[dr.w.cx];
            memcpy(m_pmode_interface, table, dr.w.cx);

            if (table[0] != 0)
                m_pm_set_bank = (void *)(m_pmode_interface+table[0]);
            if (table[1] != 0)
                m_pm_page_flip = (void *)(m_pmode_interface+table[1]);
            if (table[2] != 0)
                m_pm_palette = (void *)(m_pmode_interface+table[2]);
        }
    }

}

void VBE::clear_video_memory()
{
    int totalmem = m_vib->total_mem << 16;
    if (m_mode_type) {
        int tbanks = totalmem / m_granularity;
        for (int b=0; b<tbanks; b++) {
            set_bank(b);
            memset(adjust_near_pointer(m_screen), 255, m_granularity);
        }
    } else {
        memset(adjust_near_pointer(m_screen), 0, totalmem);
    }
}


void VBE::page_set_init()
{
    int pn;
    if (!m_pitch) throw Error("large scale coder fuckup");

    pn = (((m_mib->y_res - m_blacksize) * m_pitch) + 0xFFFF) & (~0xFFFF);
    if (m_pm_page_flip) {
        pn >>= 2;
        m_page_cx[0] = 0;
        m_page_dx[0] = 0;
        m_page_cx[1] = pn & 0xffff;
        m_page_dx[1] = pn>>16;
        m_page_cx[2] = (pn*2) & 0xffff;
        m_page_dx[2] = (pn*2) >> 16;
    } else {
        m_page_cx[0] = 0;
        m_page_dx[0] = 0;
        m_page_cx[1] = (pn % m_pitch) / m_bpp;
        m_page_dx[1] = (pn / m_pitch);
        m_page_cx[2] = ((pn*2) % m_pitch) / m_bpp;
        m_page_dx[2] = ((pn*2) / m_pitch);
    }
}

#ifdef __WATCOMC__

int protected_mode_vesa_call(int ebx, int ecx, int edx, int edi, void *function);
#pragma aux protected_mode_vesa_call \
    parm [ebx] [ecx] [edx] [edi] [esi] \
    = "call esi" \
    value [eax];

#endif



void VBE::page_flip(int page, int wvr)
{
    if ((page != m_current_page) && (page >= 0) && (page < m_pages)) {
        if (m_pm_page_flip) {
#ifdef __WATCOMC__
            protected_mode_vesa_call(wvr?0x80:0,m_page_cx[page],
                m_page_dx[page],0,m_pm_page_flip);
#else
            // DJGPP call
            __asm__ __volatile__("call *%%eax" : : "a"(m_pm_page_flip), "b"(wvr?0x80:0), "c"(m_page_cx[page]), "d"(m_page_dx[page]) : "memory","cc");
#endif
            m_current_page = page;
            return;
        }
        DPMI::dpmi_regs dr;
        memset(&dr, 0, sizeof(DPMI::dpmi_regs));
        dr.l.ecx = m_page_cx[page];
        dr.l.edx = m_page_dx[page];

        if (wvr && m_vbe_ver) dr.l.ebx = 0x80;

        dr.l.eax = 0x4F07;
        dpmi.dos_int(0x10, &dr, &dr);
        m_current_page = page;

        if (wvr && (!m_vbe_ver)) {
            while (inp(0x3DA) & 8) {}
            while (!(inp(0x3DA) & 8)) {}
        }
    }
}

void VBE::set_bank(int bank)
{
    if (m_pm_set_bank) {
#ifdef __WATCOMC__
        protected_mode_vesa_call(0, 0, bank, 0, m_pm_set_bank);
#else
        // DJGPP call
        __asm__ __volatile__("call *%%eax" : : "a"(m_pm_set_bank), "b"(0), "d"(bank) : "memory","cc");
#endif
        return;
    }
    DPMI::dpmi_regs dr;
    memset(&dr, 0, sizeof(DPMI::dpmi_regs));
    dr.l.eax = 0x4F05;
    dr.l.ebx = 0;
    dr.l.edx = bank;
    dpmi.dos_int(0x10, &dr, &dr);
}

void VBE::banked_area_load(char *src, int top, int left, int bottom,
                           int right, int destpage, int wvr)
{

    int xcopy = (right-left) * m_bpp;
    int dskip = m_pitch - xcopy;
    int sskip = (m_width - (right-left)) * m_bpp;
    int cbank, bankpos;

    // ensure valid dest page
    if ((destpage < 0) || (destpage > 2)) return;
    lock_near_base();

    // adjust screen pointer
    char *dest = (char *)adjust_near_pointer((void *)m_screen);

    // calculate first bank and bankpos
    int destaddr = m_page_addr[destpage] + (top*m_pitch) + (left*m_bpp);
    cbank = destaddr / m_granularity;
    bankpos = destaddr % m_granularity;
    set_bank(cbank);

    // move src to start of area
    src += (top * m_width * m_bpp) + (left * m_bpp);

    for (int lines = bottom; lines > top; lines--) {
        // copy xcopy bytes
        int nextpos = bankpos + xcopy;
        if (nextpos >= m_banksize) {
            int over = nextpos - m_banksize;
            memcpy(dest + bankpos, src, xcopy - over);
            src += xcopy - over;
            cbank += m_bankinc;
            set_bank(cbank);
            memcpy(dest, src, over);
            src += over;
            bankpos = over;
        } else {
            memcpy(dest+bankpos, src, xcopy);
            src += xcopy;
            bankpos = nextpos;
        }

        // skip dskip bytes
        src += sskip;
        nextpos = bankpos + dskip;
        if (nextpos >= m_banksize) {
            cbank += m_bankinc;
            set_bank(cbank);
            bankpos = nextpos - m_banksize;
        } else {
            bankpos = nextpos;
        }
    }
    page_flip(destpage, wvr);
    unlock_near_base();
}

void VBE::banked_load(char *src, int destpage, int wvr)
{

    int xcopy = m_width * m_bpp;
    int xskip = m_pitch - xcopy;
    int cbank, bankpos;

    // ensure valid dest page
    if ((destpage < 0) || (destpage > 2)) return;

    lock_near_base();
    char *dest = (char *)adjust_near_pointer((void *)m_screen);

    // set first bank
    cbank = m_page_bank[destpage][0];
    set_bank(cbank);
    bankpos = m_page_bank[destpage][1];

    for (int lines = 0; lines < m_height; lines++) {
        // copy xcopy bytes
        int nextpos = bankpos + xcopy;
        if (nextpos >= m_banksize) {
            int over = nextpos - m_banksize;
            memcpy(dest + bankpos, src, xcopy-over);
            src += xcopy - over;
            cbank += m_bankinc;
            set_bank(cbank);
            memcpy(dest, src, over);
            src += over;
            bankpos = over;
        } else {
            memcpy(dest+bankpos, src, xcopy);
            src += xcopy;
            bankpos = nextpos;
        }

        // skip xskip
        nextpos = bankpos + xskip;
        if (nextpos >= m_banksize) {
            cbank += m_bankinc;
            set_bank(cbank);
            bankpos = nextpos - m_banksize;
        } else {
            bankpos = nextpos;
        }
    }
    page_flip(destpage, wvr);
    unlock_near_base();
}

void *VBE::lock(int page)
{
    if ((page >= 0) && (page < m_pages) && (!m_locked)) {
        lock_near_base();
        m_locked = 1;
        return adjust_near_pointer((void *)m_page_addr[page]);
    }
    else throw Error("illegal page lock");
}

void VBE::unlock()
{
    if (m_locked) {
        unlock_near_base();
        m_locked = 0;
    }
}

Format &VBE::format()
{
    return m_ptcformat;
}

void VBE::palette(const int32 palette[])
{
    if (m_vbe_ver) {
        // VBE > 1.x palette set
        lock_near_base();

        DPMI::dpmi_regs dr;
        int32 *d;
        int32 *dest = (int32 *)adjust_near_pointer(m_dosmem.ptr) + 256;

        if (m_wide_dac) {
            if (!m_pm_palette)
                memcpy(dest, palette, 1024);
            else
                dest = (int32 *)palette;
        } else {
            d = dest;
            for (int i=0; i<256; i++)
                *d++ = (((*palette++)>>2)&0x003F3F3F);
        }

        if (m_pm_palette) {
#ifdef __WATCOMC__
            protected_mode_vesa_call(0, 256, 0, (int32)dest, m_pm_palette);
#else
            __asm__ __volatile__("mov %%edx, %%edi \
                                  push %%edi \
                                  call *%%eax \
                                  pop %%edi" \
            : : "a"(m_pm_palette), "c"(256), "d"(dest) : "memory","cc");
#endif
        } else {
            memset(&dr, 0, sizeof(DPMI::dpmi_regs));
            dr.l.eax = 0x4F09;
            dr.l.ecx = 256;
            dr.l.es = m_dosmem.seg;
            dr.l.edi = 1024;
            dpmi.dos_int(0x10, &dr, &dr);
        }
        unlock_near_base();
    } else {
        // VBE 1.x palette set (direct VGA palette set)
        char8 *pal = (char8 *)palette;
        outp(0x3c8, 0);
        if (m_wide_dac) {
            for (int i=0; i<256; i++) {
                outp(0x3c9, pal[2]);
                outp(0x3c9, pal[1]);
                outp(0x3c9, pal[0]);
                pal+=4;
            }
        } else {
            for (int i=0; i<256; i++) {
                outp(0x3c9, pal[2]>>2);
                outp(0x3c9, pal[1]>>2);
                outp(0x3c9, pal[0]>>2);
                pal+=4;
            }
        }
    }
}
