/*
Copyright (C) 1996-1997 Id Software, Inc.

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.

*/
//
// vid_ext.c: extended video modes
// in this implementation, VESA-specific DOS video stuff
//

// TODO: make dependencies on vid_vga.c explicit or eliminate them

#include <stdlib.h>
#include <dos.h>

#include "quakedef.h"
#include "d_local.h"
#include "dosisms.h"
#include "vid_dos.h"
#include <dpmi.h>

#define MODE_SUPPORTED_IN_HW  0x0001
#define COLOR_MODE        0x0008
#define GRAPHICS_MODE     0x0010
#define VGA_INCOMPATIBLE    0x0020
#define LINEAR_FRAME_BUFFER   0x0080

#define LINEAR_MODE       0x4000

#define VESA_DONT_WAIT_VSYNC  0   // when page flipping
#define VESA_WAIT_VSYNC     0x80

#define MAX_VESA_MODES      30  // we'll just take the first 30 if there
                  //  are more
typedef struct {
  int     pages[3];     // either 2 or 3 is valid
  int     vesamode;     // LINEAR_MODE set if linear mode
  void    *plinearmem;    // linear address of start of frame buffer
  qboolean  vga_incompatible;
} vesa_extra_t;

static vmode_t    vesa_modes[MAX_VESA_MODES] =
  {{NULL, NULL, "    ********* VESA modes *********    "}};
static vesa_extra_t vesa_extra[MAX_VESA_MODES];
static char     names[MAX_VESA_MODES][10];

extern regs_t regs;

static int    VID_currentpage;
static int    VID_displayedpage;
static int    *VID_pagelist;
static byte   *VID_membase;
static int    VID_banked;

typedef struct
{
  int modenum;
  int mode_attributes;
  int winasegment;
  int winbsegment;
  int bytes_per_scanline; // bytes per logical scanline (+16)
  int win; // window number (A=0, B=1)
  int win_size; // window size (+6)
  int granularity; // how finely i can set the window in vid mem (+4)
  int width, height; // displayed width and height (+18, +20)
  int bits_per_pixel; // er, better be 8, 15, 16, 24, or 32 (+25)
  int bytes_per_pixel; // er, better be 1, 2, or 4
  int memory_model; // and better be 4 or 6, packed or direct color (+27)
  int num_pages; // number of complete frame buffer pages (+29)
  int red_width; // the # of bits in the red component (+31)
  int red_pos; // the bit position of the red component (+32)
  int green_width; // etc.. (+33)
  int green_pos; // (+34)
  int blue_width; // (+35)
  int blue_pos; // (+36)
  int pptr;
  int pagesize;
  int numpages;
} modeinfo_t;

static modeinfo_t modeinfo;

// all bytes to avoid problems with compiler field packing
typedef struct vbeinfoblock_s {
     byte     VbeSignature[4];
     byte     VbeVersion[2];
     byte     OemStringPtr[4];
     byte     Capabilities[4];
     byte     VideoModePtr[4];
     byte     TotalMemory[2];
     byte     OemSoftwareRev[2];
     byte     OemVendorNamePtr[4];
     byte     OemProductNamePtr[4];
     byte     OemProductRevPtr[4];
     byte     Reserved[222];
     byte     OemData[256];
} vbeinfoblock_t;

static int  totalvidmem;
static byte *ppal;
qboolean  vsync_exists, de_exists;

qboolean VID_ExtraGetModeInfo(int modenum);
int VID_ExtraInitMode (viddef_t *vid, vmode_t *pcurrentmode);
void VID_ExtraSwapBuffers (viddef_t *vid, vmode_t *pcurrentmode,
  vrect_t *rects);


/*
================
VGA_BankedBeginDirectRect
================
*/
void VGA_BankedBeginDirectRect (viddef_t *lvid, struct vmode_s *pcurrentmode,
  int x, int y, byte *pbitmap, int width, int height)
{

  if (!lvid->direct)
    return;

  regs.x.ax = 0x4f05;
  regs.x.bx = 0;
  regs.x.dx = VID_displayedpage;
  dos_int86(0x10);

  VGA_BeginDirectRect (lvid, pcurrentmode, x, y, pbitmap, width, height);

  regs.x.ax = 0x4f05;
  regs.x.bx = 0;
  regs.x.dx = VID_currentpage;
  dos_int86(0x10);
}


/*
================
VGA_BankedEndDirectRect
================
*/
void VGA_BankedEndDirectRect (viddef_t *lvid, struct vmode_s *pcurrentmode,
  int x, int y, int width, int height)
{

  if (!lvid->direct)
    return;

  regs.x.ax = 0x4f05;
  regs.x.bx = 0;
  regs.x.dx = VID_displayedpage;
  dos_int86(0x10);

  VGA_EndDirectRect (lvid, pcurrentmode, x, y, width, height);

  regs.x.ax = 0x4f05;
  regs.x.bx = 0;
  regs.x.dx = VID_currentpage;
  dos_int86(0x10);
}


/*
================
VID_SetVESAPalette
================
*/
void VID_SetVESAPalette (viddef_t *lvid, vmode_t *pcurrentmode,
  unsigned char *pal)
{
  int   i;
  byte  *pp;

  UNUSED(lvid);
  UNUSED(pcurrentmode);

  pp = ppal;

  for (i=0 ; i<256 ; i++)
  {
    pp[2] = pal[0] >> 2;
    pp[1] = pal[1] >> 2;
    pp[0] = pal[2] >> 2;
    pp += 4;
    pal += 3;
  }

  regs.x.ax = 0x4F09;
  regs.x.bx = 0;
  regs.x.cx = 256;
  regs.x.dx = 0;
  regs.x.es = ptr2real(ppal) >> 4;
  regs.x.di = ptr2real(ppal) & 0xf;
  dos_int86(0x10);

  if (regs.x.ax != 0x4f)
    Sys_Error ("Unable to load VESA palette\n");
}




/*
================
VID_ExtraFarToLinear
================
*/
void *VID_ExtraFarToLinear (void *ptr)
{
  int   temp;

  temp = (int)ptr;
  return real2ptr(((temp & 0xFFFF0000) >> 12) + (temp & 0xFFFF));
}


/*
================
VID_ExtraWaitDisplayEnable
================
*/
void VID_ExtraWaitDisplayEnable ()
{
  while ((inportb (0x3DA) & 0x01) == 1)
    ;
}


/*
================
VID_ExtraVidLookForState
================
*/
qboolean VID_ExtraVidLookForState (unsigned state, unsigned mask)
{
  int   i;
  double  starttime, time;

  starttime = Sys_FloatTime ();

  do
  {
    for (i=0 ; i<100000 ; i++)
    {
      if ((inportb (0x3DA) & mask) == state)
        return true;
    }

    time = Sys_FloatTime ();
  } while ((time - starttime) < 0.1);

  return false;
}


/*
================
VID_ExtraStateFound
================
*/
qboolean VID_ExtraStateFound (unsigned state)
{
  int   i, workingstate;

  workingstate = 0;

  for (i=0 ; i<10 ; i++)
  {
    if (!VID_ExtraVidLookForState(workingstate, state))
    {
      return false;
    }

    workingstate ^= state;
  }

  return true;
}


/*
================
VID_InitExtra
================
*/
void VID_InitExtra (void)
{
  int       nummodes;
  short     *pmodenums;
  vbeinfoblock_t  *pinfoblock;
  __dpmi_meminfo  phys_mem_info;

  pinfoblock = dos_getmemory(sizeof(vbeinfoblock_t));

  *(long *)pinfoblock->VbeSignature = 'V' + ('B'<<8) + ('E'<<16) + ('2'<<24);

// see if VESA support is available
  regs.x.ax = 0x4f00;
  regs.x.es = ptr2real(pinfoblock) >> 4;
  regs.x.di = ptr2real(pinfoblock) & 0xf;
  dos_int86(0x10);

  if (regs.x.ax != 0x4f)
    return;   // no VESA support

  if (pinfoblock->VbeVersion[1] < 0x02)
    return;   // not VESA 2.0 or greater

  Con_Printf ("VESA 2.0 compliant adapter:\n%s\n",
        VID_ExtraFarToLinear (*(byte **)&pinfoblock->OemStringPtr[0]));

  totalvidmem = *(unsigned short *)&pinfoblock->TotalMemory[0] << 16;

  pmodenums = (short *)
      VID_ExtraFarToLinear (*(byte **)&pinfoblock->VideoModePtr[0]);

// find 8 bit modes until we either run out of space or run out of modes
  nummodes = 0;

  while ((*pmodenums != -1) && (nummodes < MAX_VESA_MODES))
  {
    if (VID_ExtraGetModeInfo (*pmodenums))
    {
      vesa_modes[nummodes].pnext = &vesa_modes[nummodes+1];
      if (modeinfo.width > 999)
      {
        if (modeinfo.height > 999)
        {
          sprintf (&names[nummodes][0], "%4dx%4d", modeinfo.width,
               modeinfo.height);
          names[nummodes][9] = 0;
        }
        else
        {
          sprintf (&names[nummodes][0], "%4dx%3d", modeinfo.width,
               modeinfo.height);
          names[nummodes][8] = 0;
        }
      }
      else
      {
        if (modeinfo.height > 999)
        {
          sprintf (&names[nummodes][0], "%3dx%4d", modeinfo.width,
               modeinfo.height);
          names[nummodes][8] = 0;
        }
        else
        {
          sprintf (&names[nummodes][0], "%3dx%3d", modeinfo.width,
               modeinfo.height);
          names[nummodes][7] = 0;
        }
      }

      vesa_modes[nummodes].name = &names[nummodes][0];
      vesa_modes[nummodes].width = modeinfo.width;
      vesa_modes[nummodes].height = modeinfo.height;
      vesa_modes[nummodes].aspect =
          ((float)modeinfo.height / (float)modeinfo.width) *
          (320.0 / 240.0);
      vesa_modes[nummodes].rowbytes = modeinfo.bytes_per_scanline;
      vesa_modes[nummodes].planar = 0;
      vesa_modes[nummodes].pextradata = &vesa_extra[nummodes];
      vesa_modes[nummodes].setmode = VID_ExtraInitMode;
      vesa_modes[nummodes].swapbuffers = VID_ExtraSwapBuffers;
      vesa_modes[nummodes].setpalette = VID_SetVESAPalette;

      if (modeinfo.mode_attributes & LINEAR_FRAME_BUFFER)
      {
      // add linear bit to mode for linear modes
        vesa_extra[nummodes].vesamode = modeinfo.modenum | LINEAR_MODE;
        vesa_extra[nummodes].pages[0] = 0;
        vesa_extra[nummodes].pages[1] = modeinfo.pagesize;
        vesa_extra[nummodes].pages[2] = modeinfo.pagesize * 2;
        vesa_modes[nummodes].numpages = modeinfo.numpages;

        vesa_modes[nummodes].begindirectrect = VGA_BeginDirectRect;
        vesa_modes[nummodes].enddirectrect = VGA_EndDirectRect;

        phys_mem_info.address = (int)modeinfo.pptr;
        phys_mem_info.size = 0x400000;

        if (__dpmi_physical_address_mapping(&phys_mem_info))
          goto NextMode;

        vesa_extra[nummodes].plinearmem =
             real2ptr (phys_mem_info.address);
      }
      else
      {
      // banked at 0xA0000
        vesa_extra[nummodes].vesamode = modeinfo.modenum;
        vesa_extra[nummodes].pages[0] = 0;
        vesa_extra[nummodes].plinearmem =
            real2ptr(modeinfo.winasegment<<4);

        vesa_modes[nummodes].begindirectrect =
            VGA_BankedBeginDirectRect;
        vesa_modes[nummodes].enddirectrect = VGA_BankedEndDirectRect;
        vesa_extra[nummodes].pages[1] = modeinfo.pagesize;
        vesa_extra[nummodes].pages[2] = modeinfo.pagesize * 2;
        vesa_modes[nummodes].numpages = modeinfo.numpages;
      }

      vesa_extra[nummodes].vga_incompatible =
          modeinfo.mode_attributes & VGA_INCOMPATIBLE;

      nummodes++;
    }
NextMode:
    pmodenums++;
  }

// add the VESA modes at the start of the mode list (if there are any)
  if (nummodes)
  {
    vesa_modes[nummodes-1].pnext = pvidmodes;
    pvidmodes = &vesa_modes[0];
    numvidmodes += nummodes;
    ppal = dos_getmemory(256*4);
  }

  dos_freememory(pinfoblock);
}


/*
================
VID_ExtraGetModeInfo
================
*/
qboolean VID_ExtraGetModeInfo(int modenum)
{
  char  *infobuf;
  int   numimagepages;

  infobuf = dos_getmemory(256);

  regs.x.ax = 0x4f01;
  regs.x.cx = modenum;
  regs.x.es = ptr2real(infobuf) >> 4;
  regs.x.di = ptr2real(infobuf) & 0xf;
  dos_int86(0x10);
  if (regs.x.ax != 0x4f)
  {
    return false;
  }
  else
  {
    modeinfo.modenum = modenum;
    modeinfo.bits_per_pixel = *(char*)(infobuf+25);
    modeinfo.bytes_per_pixel = (modeinfo.bits_per_pixel+1)/8;
    modeinfo.width = *(short*)(infobuf+18);
    modeinfo.height = *(short*)(infobuf+20);

  // we do only 8-bpp in software
    if ((modeinfo.bits_per_pixel != 8) ||
      (modeinfo.bytes_per_pixel != 1) ||
      (modeinfo.width > MAXWIDTH) ||
      (modeinfo.height > MAXHEIGHT))
    {
      return false;
    }

    modeinfo.mode_attributes = *(short*)infobuf;

  // we only want color graphics modes that are supported by the hardware
    if ((modeinfo.mode_attributes &
       (MODE_SUPPORTED_IN_HW | COLOR_MODE | GRAPHICS_MODE)) !=
      (MODE_SUPPORTED_IN_HW | COLOR_MODE | GRAPHICS_MODE))
    {
      return false;
    }

  // we only work with linear frame buffers, except for 320x200, which can
  // effectively be linear when banked at 0xA000
    if (!(modeinfo.mode_attributes & LINEAR_FRAME_BUFFER))
    {
      if ((modeinfo.width != 320) || (modeinfo.height != 200))
        return false;
    }

    modeinfo.bytes_per_scanline = *(short*)(infobuf+16);

    modeinfo.pagesize = modeinfo.bytes_per_scanline * modeinfo.height;

    if (modeinfo.pagesize > totalvidmem)
      return false;

  // force to one page if the adapter reports it doesn't support more pages
  // than that, no matter how much memory it has--it may not have hardware
  // support for page flipping
    numimagepages = *(unsigned char *)(infobuf+29);

    if (numimagepages <= 0)
    {
    // wrong, but there seems to be an ATI VESA driver that reports 0
      modeinfo.numpages = 1;
    }
    else if (numimagepages < 3)
    {
      modeinfo.numpages = numimagepages;
    }
    else
    {
      modeinfo.numpages = 3;
    }

    if (*(char*)(infobuf+2) & 5)
    {
      modeinfo.winasegment = *(unsigned short*)(infobuf+8);
      modeinfo.win = 0;
    }
    else if (*(char*)(infobuf+3) & 5)
    {
      modeinfo.winbsegment = *(unsigned short*)(infobuf+8);
      modeinfo.win = 1;
    }
    modeinfo.granularity = *(short*)(infobuf+4) * 1024;
    modeinfo.win_size = *(short*)(infobuf+6) * 1024;
    modeinfo.bits_per_pixel = *(char*)(infobuf+25);
    modeinfo.bytes_per_pixel = (modeinfo.bits_per_pixel+1)/8;
    modeinfo.memory_model = *(unsigned char*)(infobuf+27);
    modeinfo.num_pages = *(char*)(infobuf+29) + 1;

    modeinfo.red_width = *(char*)(infobuf+31);
    modeinfo.red_pos = *(char*)(infobuf+32);
    modeinfo.green_width = *(char*)(infobuf+33);
    modeinfo.green_pos = *(char*)(infobuf+34);
    modeinfo.blue_width = *(char*)(infobuf+35);
    modeinfo.blue_pos = *(char*)(infobuf+36);

    modeinfo.pptr = *(long *)(infobuf+40);

#if 0
    printf("VID: (VESA) info for mode 0x%x\n", modeinfo.modenum);
    printf("  mode attrib = 0x%0x\n", modeinfo.mode_attributes);
    printf("  win a attrib = 0x%0x\n", *(unsigned char*)(infobuf+2));
    printf("  win b attrib = 0x%0x\n", *(unsigned char*)(infobuf+3));
    printf("  win a seg 0x%0x\n", (int) modeinfo.winasegment);
    printf("  win b seg 0x%0x\n", (int) modeinfo.winbsegment);
    printf("  bytes per scanline = %d\n",
        modeinfo.bytes_per_scanline);
    printf("  width = %d, height = %d\n", modeinfo.width,
        modeinfo.height);
    printf("  win = %c\n", 'A' + modeinfo.win);
    printf("  win granularity = %d\n", modeinfo.granularity);
    printf("  win size = %d\n", modeinfo.win_size);
    printf("  bits per pixel = %d\n", modeinfo.bits_per_pixel);
    printf("  bytes per pixel = %d\n", modeinfo.bytes_per_pixel);
    printf("  memory model = 0x%x\n", modeinfo.memory_model);
    printf("  num pages = %d\n", modeinfo.num_pages);
    printf("  red width = %d\n", modeinfo.red_width);
    printf("  red pos = %d\n", modeinfo.red_pos);
    printf("  green width = %d\n", modeinfo.green_width);
    printf("  green pos = %d\n", modeinfo.green_pos);
    printf("  blue width = %d\n", modeinfo.blue_width);
    printf("  blue pos = %d\n", modeinfo.blue_pos);
    printf("  phys mem = %x\n", modeinfo.pptr);
#endif
  }

  dos_freememory(infobuf);

  return true;
}


/*
================
VID_ExtraInitMode
================
*/
int VID_ExtraInitMode (viddef_t *lvid, vmode_t *pcurrentmode)
{
  vesa_extra_t  *pextra;
  int       pageoffset;

  pextra = pcurrentmode->pextradata;

  if (vid_nopageflip.value)
    lvid->numpages = 1;
  else
    lvid->numpages = pcurrentmode->numpages;

// clean up any old vid buffer lying around, alloc new if needed
  if (!VGA_FreeAndAllocVidbuffer (lvid, lvid->numpages == 1))
    return -1;  // memory alloc failed

// clear the screen and wait for the next frame. VGA_pcurmode, which
// VGA_ClearVideoMem relies on, is guaranteed to be set because mode 0 is
// always the first mode set in a session
  if (VGA_pcurmode)
    VGA_ClearVideoMem (VGA_pcurmode->planar);

// set the mode
  regs.x.ax = 0x4f02;
  regs.x.bx = pextra->vesamode;
  dos_int86(0x10);

  if (regs.x.ax != 0x4f)
    return 0;

  VID_banked = !(pextra->vesamode & LINEAR_MODE);
  VID_membase = pextra->plinearmem;
  VGA_width = lvid->width;
  VGA_height = lvid->height;
  VGA_rowbytes = lvid->rowbytes;

  lvid->colormap = host_colormap;

  VID_pagelist = &pextra->pages[0];

// wait for display enable by default only when triple-buffering on a VGA-
// compatible machine that actually has a functioning display enable status
  vsync_exists = VID_ExtraStateFound (0x08);
  de_exists = VID_ExtraStateFound (0x01);

  if (!pextra->vga_incompatible  &&
    (lvid->numpages == 3)      &&
    de_exists                  &&
    (_vid_wait_override.value == 0.0))
  {
    Cvar_SetValue ("vid_wait", (float)VID_WAIT_DISPLAY_ENABLE);

    VID_displayedpage = 0;
    VID_currentpage = 1;
  }
  else
  {
    if ((lvid->numpages == 1) && (_vid_wait_override.value == 0.0))
    {
      Cvar_SetValue ("vid_wait", (float)VID_WAIT_NONE);
      VID_displayedpage = VID_currentpage = 0;
    }
    else
    {
      Cvar_SetValue ("vid_wait", (float)VID_WAIT_VSYNC);

      VID_displayedpage = 0;

      if (lvid->numpages > 1)
        VID_currentpage = 1;
      else
        VID_currentpage = 0;
    }
  }

// TODO: really should be a call to a function
  pageoffset = VID_pagelist[VID_displayedpage];

  regs.x.ax = 0x4f07;
  regs.x.bx = 0x80; // wait for vsync so we know page 0 is visible
  regs.x.cx = pageoffset % VGA_rowbytes;
  regs.x.dx = pageoffset / VGA_rowbytes;
  dos_int86(0x10);

  if (VID_banked)
  {
    regs.x.ax = 0x4f05;
    regs.x.bx = 0;
    regs.x.dx = VID_currentpage;
    dos_int86(0x10);

    VGA_pagebase = VID_membase;
  }
  else
  {
    VGA_pagebase = VID_membase + VID_pagelist[VID_currentpage];
  }

  if (lvid->numpages > 1)
  {
    lvid->buffer = VGA_pagebase;
    lvid->conbuffer = lvid->buffer;
  }
  else
  {
    lvid->rowbytes = lvid->width;
  }

  lvid->direct = VGA_pagebase;
  lvid->conrowbytes = lvid->rowbytes;
  lvid->conwidth = lvid->width;
  lvid->conheight = lvid->height;

  lvid->maxwarpwidth = WARP_WIDTH;
  lvid->maxwarpheight = WARP_HEIGHT;

  VGA_pcurmode = pcurrentmode;

  D_InitCaches (vid_surfcache, vid_surfcachesize);

  return 1;
}


/*
================
VID_ExtraSwapBuffers
================
*/
void VID_ExtraSwapBuffers (viddef_t *lvid, vmode_t *pcurrentmode,
  vrect_t *rects)
{
  int pageoffset;

  UNUSED(rects);
  UNUSED(pcurrentmode);

  pageoffset = VID_pagelist[VID_currentpage];

// display the newly finished page
  if (lvid->numpages > 1)
  {
  // page flipped
    regs.x.ax = 0x4f07;
  
    if (vid_wait.value != VID_WAIT_VSYNC)
    {
      if ((vid_wait.value == VID_WAIT_DISPLAY_ENABLE) && de_exists)
        VID_ExtraWaitDisplayEnable ();
  
      regs.x.bx = VESA_DONT_WAIT_VSYNC;
    }
    else
    {
      regs.x.bx = VESA_WAIT_VSYNC;  // double buffered has to wait
    }

    regs.x.cx = pageoffset % VGA_rowbytes;
    regs.x.dx = pageoffset / VGA_rowbytes;
    dos_int86(0x10);
  
    VID_displayedpage = VID_currentpage;
    if (++VID_currentpage >= lvid->numpages)
      VID_currentpage = 0;
  
  //
  // set the new write window if this is a banked mode; otherwise, set the
  // new address to which to write
  //
    if (VID_banked)
    {
      regs.x.ax = 0x4f05;
      regs.x.bx = 0;
      regs.x.dx = VID_currentpage;
      dos_int86(0x10);
    }
    else
    {
      lvid->direct = lvid->buffer;  // direct drawing goes to the
                      //  currently displayed page
      lvid->buffer = VID_membase + VID_pagelist[VID_currentpage];
      lvid->conbuffer = lvid->buffer;
    }
  
    VGA_pagebase = lvid->buffer;
  }
  else
  {
  // non-page-flipped
    if (vsync_exists && (vid_wait.value == VID_WAIT_VSYNC))
    {
      VGA_WaitVsync ();
    }

    while (rects)
    {
      VGA_UpdateLinearScreen (
          lvid->buffer + rects->x + (rects->y * lvid->rowbytes),
          VGA_pagebase + rects->x + (rects->y * VGA_rowbytes),
          rects->width,
          rects->height,
          lvid->rowbytes,
          VGA_rowbytes);

      rects = rects->pnext;
    }
  }
}

