/*
 * DANG_BEGIN_MODULE
 *
 *   This code is based on information in Ricard Ferraro's "Programmer's
 *   Guide to the EGA, VGA, and Super VGA Cards" which is a wonderful
 *   reference for those interested.
 *
 * Description:
 *   This is the code for the Paradise/Western Digital chipset.  It works
 *   on my laptop (Western Digital WD90C24CA -- a chip not entirely
 *   supported because the docs I have don't mention it :)
 *
 *   Driver original for DOSEMU by: Scott Buchholz (scottb@eecs.nwu.edu)
 *
 * DANG_END_MODULE
 *
 * $Date: $
 * $Source: $
 * $Revision: $
 * $State: $
 *
 * $Log: $
 */

#include "dosemu.h"
#include "base/port.h"
#include "base/detach.h"
#include "base/misc.h"
#include "env/libvga.h"
#include "vgaregs.h"

#include <sys/mman.h>

enum { PVGA1A, WD90C00, WD90C10, WD90C11, WD90C30, WD90C31, WD90C33 };

static const char *chipname[] = { "PVGA1A", "WD90C00", "WD90C10",
	"WD90C11", "WD90C30", "WD90C31", "WD90C33" };

static int paradise_chipset;

#define WD_PR0A      0+VGA_TOTAL_REGS
#define WD_PR0B      1+VGA_TOTAL_REGS
#define WD_PR1       2+VGA_TOTAL_REGS
#define WD_PR2       3+VGA_TOTAL_REGS
#define WD_PR3       4+VGA_TOTAL_REGS
#define WD_PR4       5+VGA_TOTAL_REGS
#define WD_PR5       6+VGA_TOTAL_REGS
#define WD_PR10      7+VGA_TOTAL_REGS
#define WD_PR11      8+VGA_TOTAL_REGS
#define WD_PR12      9+VGA_TOTAL_REGS
#define WD_PR13      10+VGA_TOTAL_REGS
#define WD_PR14      11+VGA_TOTAL_REGS
#define WD_PR15      12+VGA_TOTAL_REGS
#define WD_PR16      13+VGA_TOTAL_REGS
#define WD_PR17      14+VGA_TOTAL_REGS
#define WD_PR18      15+VGA_TOTAL_REGS
#define WD_PR19      16+VGA_TOTAL_REGS
#define WD_PR20      17+VGA_TOTAL_REGS
#define WD_PR21      18+VGA_TOTAL_REGS
#define WD_PR22      19+VGA_TOTAL_REGS
#define WD_PR23      20+VGA_TOTAL_REGS
#define WD_PR30      21+VGA_TOTAL_REGS
#define WD_PR31      22+VGA_TOTAL_REGS
#define WD_PR32      23+VGA_TOTAL_REGS
#define WD_PR33      24+VGA_TOTAL_REGS
#define WD_PR34      25+VGA_TOTAL_REGS
#define WD_PR35      26+VGA_TOTAL_REGS
#define WD_CURSOR    27+VGA_TOTAL_REGS

static void paradise_unlock(void)
{
  /* extensions on */
  outw(GRA_I, 0x050f);

  /* unlock PR10-PR17 */
  outw(0x3d2, 0x8529);

  /* unlock extended seq reg */
  outw(SEQ_I, 0x4806);
}

static void paradise_lock(void)
{
  /* extensions on */
  outw(GRA_I, 0x000f);

  /* unlock PR10-PR17 */
  outw(0x3d2, 0x0029);

  /* unlock extended seq reg */
  outw(SEQ_I, 0x0006);
}

static void paradise_getperm(void)
{
}

static void paradise_releaseperm(void)
{
}

static void paradise_setpage(int page)
{
  port_out(0x04, GRA_I);
  port_out(page, GRA_D);
}

static void paradise_saveregs(Bit16u *xregs)
{
  int i;

  paradise_unlock();

  xregs[WD_PR0A] = inGR(0x09);
  xregs[WD_PR0B] = inGR(0x0A);
  xregs[WD_PR1]  = inGR(0x0B);
  xregs[WD_PR2]  = inGR(0x0C);
  xregs[WD_PR3]  = inGR(0x0D);
  xregs[WD_PR4]  = inGR(0x0E);
  xregs[WD_PR5]  = inGR(0x0F);

  if (paradise_chipset >= WD90C00) {
    xregs[WD_PR10] = inCR(0x29);
    xregs[WD_PR11] = inCR(0x2A);
    xregs[WD_PR12] = inCR(0x2B);
    xregs[WD_PR13] = inCR(0x2C);
    xregs[WD_PR14] = inCR(0x2D);
    xregs[WD_PR15] = inCR(0x2E);
    xregs[WD_PR16] = inCR(0x2F);
    xregs[WD_PR17] = inCR(0x30);
    xregs[WD_PR18] = inCR(0x3E);
    xregs[WD_PR19] = inCR(0x3F);
  }
#ifndef CONFIG_RUN_AS_ROOT
  priv_on();
#endif
  iopl(3);
  for (i = 0; i < 16; i++)
    xregs[WD_CURSOR+i] = inw(0x23c2);
  iopl(0);
#ifndef CONFIG_RUN_AS_ROOT
  priv_off();
#endif
}

static void paradise_restoreregs(const Bit16u *xregs)
{
  int i;

  paradise_unlock();

  outbGR(0x09, xregs[WD_PR0A]);
  outbGR(0x0A, xregs[WD_PR0B]);
  outbGR(0x0B, xregs[WD_PR1]);
  outbGR(0x0C, xregs[WD_PR2]);
  outbGR(0x0D, xregs[WD_PR3]);
  outbGR(0x0E, xregs[WD_PR4]);
  outbGR(0x0F, xregs[WD_PR5]);

  if (paradise_chipset >= WD90C00) {
    outbCR(0x29, xregs[WD_PR10]);
    outbCR(0x2A, xregs[WD_PR11]);
    outbCR(0x2B, xregs[WD_PR12]);
    outbCR(0x2C, xregs[WD_PR13]);
    outbCR(0x2D, xregs[WD_PR14]);
    outbCR(0x2E, xregs[WD_PR15]);
    outbCR(0x2F, xregs[WD_PR16]);
    outbCR(0x30, xregs[WD_PR17]);
    outbCR(0x3E, xregs[WD_PR18]);
    outbCR(0x3F, xregs[WD_PR19]);
  }
#ifndef CONFIG_RUN_AS_ROOT
  priv_on();
#endif
  iopl(3);
  for (i = 0; i < 16; i++)
    outw(0x23c2, xregs[WD_CURSOR+i]);
  iopl(0);
#ifndef CONFIG_RUN_AS_ROOT
  priv_off();
#endif
}

static Bit8u paradise_inb(Bit32u port)
{
  if (own_console) {
    return port_safe_inb(port);
  }
  else {
    return 0xff;
  }
}

static void paradise_outb(Bit32u port, Bit8u value)
{
  if (own_console) {
    return port_safe_outb(port, value);
  }
  else {
  }
}

static inline void read_hardware_cursor(void)
{
  int i;
  for (i = 0; i < 15; i++)
    inw(0x23c2);
}

static Boolean paradise_init(void)
{
#define BIT_ON_MASK   0x0020
#define BIT_OFF_MASK  0xffdf
  emu_iodev_t  io_device;
  Bit16u       old_value, new_value, value, wd90c10;

  io_device.read_portb   = paradise_inb;
  io_device.write_portb  = paradise_outb;
  io_device.read_portw   = NULL;
  io_device.write_portw  = NULL;
  io_device.handler_name = "PARADISE";
  io_device.start_addr   = 0x23c0;
  io_device.end_addr     = 0x23cf;
  io_device.irq          = EMU_NO_IRQ;
  port_register_handler(io_device);

  paradise_unlock();

  /* check for a PVGA1a */
  paradise_chipset = PVGA1A;
  old_value = inCR(0x2b);
  outbCR(0x2b, 0xaa);
  new_value = inCR(0x2b);
  outbCR(0x2b, old_value);
  if (new_value != 0xaa)
    return TRUE;                 /* found a PVGA1a */

  paradise_chipset = WD90C00;    /* assume it's a wd90c00 */

  old_value = inSR(0x12);
  outbSR(0x12, old_value & 0xbf);
  new_value = inSR(0x12) & 0x40;
  if (new_value != 0) {
    outbSR(0x12, old_value);
    return TRUE;                /* found a WD90C00 */
  }

  outbSR(0x12, old_value | 0x40);
  new_value = inSR(0x12);
  if (new_value == 0) {
    outbSR(0x12, old_value);
    return TRUE;                /* found a WD90C00 */
  }

  paradise_chipset = WD90C10;    /* assume it's a wd90c10 */

  outbSR(0x12, old_value);

  wd90c10 = 0;
  old_value = inSR(0x10);
  outbSR(0x10, old_value & 0xfb);
  new_value = inSR(0x10) & 0x04;
  if (new_value != 0)
    wd90c10 = 1;

  outbSR(0x10, old_value | 0x04);
  new_value = inSR(0x10) & 0x04;
  if (new_value == 0)
    wd90c10 = 1;

  outbSR(0x10, old_value);
  if (wd90c10)
    return TRUE;

  outbSR(0x06, 0x48);
  old_value = inSR(0x12);
  value = (old_value & 0xfc) + 0x01;
  outbSR(0x12, value);
  new_value = inSR(0x12) & 0x03;
  outbSR(0x12, old_value);
  outbSR(0x06, 0x00);

  if (new_value != 0x01) {
    paradise_chipset = WD90C11;
    return TRUE;
  }

  outw(0x23c0, 0x02);
  old_value = inw(0x23c2);
  read_hardware_cursor();
  value = old_value & BIT_OFF_MASK;
  outw(0x23c2, value);
  new_value = inw(0x23c2);
  read_hardware_cursor();
  new_value &= BIT_ON_MASK;
  if (new_value != 0) {
    outw(0x23c2, old_value);
    paradise_chipset = WD90C30;
    return TRUE;
  }

  value = (old_value & BIT_OFF_MASK) | BIT_ON_MASK;
  outw(0x23c2, value);
  new_value = inw(0x23c2);
  read_hardware_cursor();
  new_value &= BIT_ON_MASK;
  outw(0x23c2, old_value);
  if (new_value != BIT_ON_MASK) {
    paradise_chipset = WD90C30;
    return TRUE;
  }
  new_value = inw(0x23ce);

  if (new_value == 0xffff)
    paradise_chipset = WD90C31;
  else
    paradise_chipset = WD90C33;

  return TRUE;

#undef BIT_ON_MASK
#undef BIT_OFF_MASK
}

static Boolean paradise_test(void)
{
  Boolean  result = FALSE;
  Bit8u   *vga_bios;

  vga_bios = (Bit8u *)mmap(0, 4096, PROT_READ, MAP_SHARED, mem_fd, 0xc0000);
  if ((off_t)vga_bios <= 0) {
    v_printf("VGA: Paradise mmap failed!\n");
    return FALSE;
  }

  /* ID a Paradise chip (second check is for AST) */
  if (!strncmp("VGA=", (vga_bios+0x7D), 4) ||
      !strncmp("(c)",  (vga_bios+0x7D), 3))
    result = TRUE;

  munmap((caddr_t) vga_bios, 4096);

  /* My Paradise ROM is located at 0xe0000 */
  if (!result) {
    vga_bios = (Bit8u *)mmap(0, 4096, PROT_READ, MAP_SHARED, mem_fd, 0xe0000);
    if ((off_t)vga_bios <= 0) {
      v_printf("VGA: Paradise mmap failed!\n");
      return FALSE;
    }

    /* ID a Paradise chip (second check is for AST) */
    if (!strncmp("VGA=", (vga_bios+0x7D), 4) ||
	!strncmp("(c)",  (vga_bios+0x7D), 3))
      result = TRUE;

    munmap((caddr_t) vga_bios, 4096);
  }

  if (result && (result = paradise_init()))
    v_printf("PARADISE: detected a %s chip\n", chipname[paradise_chipset]);
  return result;
}


DriverSpecs paradise_driverspecs = {
	"Paradise",		/* driver title */
	paradise_saveregs,	/* saveregs */
	paradise_restoreregs,	/* restoreregs */
	paradise_test,		/* test */
	paradise_init,		/* init */
	paradise_setpage,	/* setpage */
	paradise_getperm,	/* get perms */
	paradise_releaseperm,	/* release perms */
	paradise_inb,		/* inb */
	paradise_outb		/* outb */
};
