/*
 * DOS Virtual Machine
 *
 * Copyright 1998 Ove Kåven
 *
 * This code hasn't been completely cleaned up yet.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/stat.h>

#include "wine/winbase16.h"
#include "winuser.h"
#include "winnt.h"
#include "msdos.h"
#include "file.h"
#include "miscemu.h"
#include "dosexe.h"
#include "dosmod.h"
#include "stackframe.h"
#include "debugtools.h"

DECLARE_DEBUG_CHANNEL(int)
DECLARE_DEBUG_CHANNEL(module)
DECLARE_DEBUG_CHANNEL(relay)

#ifdef MZ_SUPPORTED

#include <sys/vm86.h>
#include <sys/mman.h>

#define IF_CLR(ctx) EFL_reg(ctx) &= ~VIF_MASK
#define IF_ENABLED(ctx) (EFL_reg(ctx) & VIF_MASK)
#define SET_PEND(ctx) EFL_reg(ctx) |= VIP_MASK
#define CLR_PEND(ctx) EFL_reg(ctx) &= ~VIP_MASK
#define IS_PEND(ctx) (EFL_reg(ctx) & VIP_MASK)

#undef TRY_PICRETURN

extern void WINAPI REGS_FUNC(RtlRaiseException)( EXCEPTION_RECORD *rec,
                                                 CONTEXT *context );

static void do_exception( int signal, CONTEXT86 *context )
{
    EXCEPTION_RECORD rec;
    if ((signal == SIGTRAP) || (signal == SIGHUP))
    {
        rec.ExceptionCode  = EXCEPTION_BREAKPOINT;
        rec.ExceptionFlags = EXCEPTION_CONTINUABLE;
    }
    else
    {
        rec.ExceptionCode  = EXCEPTION_ILLEGAL_INSTRUCTION;  /* generic error */
        rec.ExceptionFlags = EH_NONCONTINUABLE;
    }
    rec.ExceptionRecord  = NULL;
    rec.ExceptionAddress = (LPVOID)EIP_reg(context);
    rec.NumberParameters = 0;
    REGS_FUNC(RtlRaiseException)( &rec, context );
}

static void DOSVM_Dump( LPDOSTASK lpDosTask, int fn, int sig,
                        struct vm86plus_struct*VM86 )
{
 unsigned iofs;
 BYTE*inst;
 int x;

 switch (VM86_TYPE(fn)) {
  case VM86_SIGNAL:
   printf("Trapped signal %d\n",sig); break;
  case VM86_UNKNOWN:
   printf("Trapped unhandled GPF\n"); break;
  case VM86_INTx:
   printf("Trapped INT %02x\n",VM86_ARG(fn)); break;
  case VM86_STI:
   printf("Trapped STI\n"); break;
  case VM86_PICRETURN:
   printf("Trapped due to pending PIC request\n"); break;
  case VM86_TRAP:
   printf("Trapped debug request\n"); break;
  default:
   printf("Trapped unknown VM86 type %d arg %d\n",VM86_TYPE(fn),VM86_ARG(fn)); break;
 }
#define REGS VM86->regs
 fprintf(stderr,"AX=%04lX CX=%04lX DX=%04lX BX=%04lX\n",REGS.eax,REGS.ecx,REGS.edx,REGS.ebx);
 fprintf(stderr,"SI=%04lX DI=%04lX SP=%04lX BP=%04lX\n",REGS.esi,REGS.edi,REGS.esp,REGS.ebp);
 fprintf(stderr,"CS=%04X DS=%04X ES=%04X SS=%04X\n",REGS.cs,REGS.ds,REGS.es,REGS.ss);
 fprintf(stderr,"IP=%04lX EFLAGS=%08lX\n",REGS.eip,REGS.eflags);

 iofs=((DWORD)REGS.cs<<4)+REGS.eip;
#undef REGS
 inst=(BYTE*)lpDosTask->img+iofs;
 printf("Opcodes:");
 for (x=0; x<8; x++) printf(" %02x",inst[x]);
 printf("\n");
}

static int DOSVM_Int( int vect, CONTEXT86 *context, LPDOSTASK lpDosTask )
{
 extern UINT16 DPMI_wrap_seg;

 if (vect==0x31) {
  if (CS_reg(context)==DPMI_wrap_seg) {
   /* exit from real-mode wrapper */
   return -1;
  }
  /* we could probably move some other dodgy stuff here too from dpmi.c */
 }
 INT_RealModeInterrupt(vect,context);
 return 0;
}

static void DOSVM_SimulateInt( int vect, CONTEXT86 *context, LPDOSTASK lpDosTask )
{
  FARPROC16 handler=INT_GetRMHandler(vect);

  if (SELECTOROF(handler)==0xf000) {
    /* if internal interrupt, call it directly */
    INT_RealModeInterrupt(vect,context);
  } else {
    WORD*stack=(WORD*)(V86BASE(context)+(((DWORD)SS_reg(context))<<4)+LOWORD(ESP_reg(context)));
    WORD flag=LOWORD(EFL_reg(context));

    if (IF_ENABLED(context)) flag|=IF_MASK;
    else flag&=~IF_MASK;

    *(--stack)=flag;
    *(--stack)=CS_reg(context);
    *(--stack)=LOWORD(EIP_reg(context));
    ESP_reg(context)-=6;
    CS_reg(context)=SELECTOROF(handler);
    EIP_reg(context)=OFFSETOF(handler);
    IF_CLR(context);
  }
}

#define SHOULD_PEND(x) \
  (x && ((!lpDosTask->current) || (x->priority < lpDosTask->current->priority)))

static void DOSVM_SendQueuedEvent(CONTEXT86 *context, LPDOSTASK lpDosTask)
{
  LPDOSEVENT event = lpDosTask->pending;

  if (SHOULD_PEND(event)) {
    /* remove from "pending" list */
    lpDosTask->pending = event->next;
    /* process event */
    if (event->irq>=0) {
      /* it's an IRQ, move it to "current" list */
      event->next = lpDosTask->current;
      lpDosTask->current = event;
      TRACE_(int)("dispatching IRQ %d\n",event->irq);
      /* note that if DOSVM_SimulateInt calls an internal interrupt directly,
       * lpDosTask->current might be cleared (and event freed) in this very call! */
      DOSVM_SimulateInt((event->irq<8)?(event->irq+8):(event->irq-8+0x70),context,lpDosTask);
    } else {
      /* callback event */
      TRACE_(int)("dispatching callback event\n");
      (*event->relay)(lpDosTask,context,event->data);
      free(event);
    }
  }
  if (!SHOULD_PEND(lpDosTask->pending)) {
    TRACE_(int)("clearing Pending flag\n");
    CLR_PEND(context);
  }
}

static void DOSVM_SendQueuedEvents(CONTEXT86 *context, LPDOSTASK lpDosTask)
{
  /* we will send all queued events as long as interrupts are enabled,
   * but IRQ events will disable interrupts again */
  while (IS_PEND(context) && IF_ENABLED(context))
    DOSVM_SendQueuedEvent(context,lpDosTask);
}

void DOSVM_QueueEvent( int irq, int priority, void (*relay)(LPDOSTASK,CONTEXT86*,void*), void *data)
{
  LPDOSTASK lpDosTask = MZ_Current();
  LPDOSEVENT event, cur, prev;

  if (lpDosTask) {
    event = malloc(sizeof(DOSEVENT));
    if (!event) {
      ERR_(int)("out of memory allocating event entry\n");
      return;
    }
    event->irq = irq; event->priority = priority;
    event->relay = relay; event->data = data;

    /* insert event into linked list, in order *after*
     * all earlier events of higher or equal priority */
    cur = lpDosTask->pending; prev = NULL;
    while (cur && cur->priority<=priority) {
      prev = cur;
      cur = cur->next;
    }
    event->next = cur;
    if (prev) prev->next = event;
    else lpDosTask->pending = event;
    
    /* get dosmod's attention to the new event, except for irq==0 where we already have it */
    if (irq && !lpDosTask->sig_sent) {
      TRACE_(int)("new event queued, signalling dosmod\n");
      kill(lpDosTask->task,SIGUSR2);
      lpDosTask->sig_sent++;
    } else {
      TRACE_(int)("new event queued\n");
    }
  }
}

#define CV CP(eax,EAX); CP(ecx,ECX); CP(edx,EDX); CP(ebx,EBX); \
           CP(esi,ESI); CP(edi,EDI); CP(esp,ESP); CP(ebp,EBP); \
           CP(cs,CS); CP(ds,DS); CP(es,ES); \
           CP(ss,SS); CP(fs,FS); CP(gs,GS); \
           CP(eip,EIP); CP(eflags,EFL)

static int DOSVM_Process( LPDOSTASK lpDosTask, int fn, int sig,
                          struct vm86plus_struct*VM86 )
{
 CONTEXT86 context;
 int ret=0;

#define CP(x,y) y##_reg(&context) = VM86->regs.x
  CV;
#undef CP
 if (VM86_TYPE(fn)==VM86_UNKNOWN) {
  ret=INSTR_EmulateInstruction(&context);
#define CP(x,y) VM86->regs.x = y##_reg(&context)
  CV;
#undef CP
  if (ret) return 0;
  ret=0;
 }
 (void*)V86BASE(&context)=lpDosTask->img;
#ifdef TRY_PICRETURN
 if (VM86->vm86plus.force_return_for_pic) {
   SET_PEND(&context);
 }
#else
 /* linux doesn't preserve pending flag on return */
 if (SHOULD_PEND(lpDosTask->pending)) {
   SET_PEND(&context);
 }
#endif

 switch (VM86_TYPE(fn)) {
  case VM86_SIGNAL:
   TRACE_(int)("DOS module caught signal %d\n",sig);
   if ((sig==SIGALRM) || (sig==SIGUSR2)) {
     if (sig==SIGALRM) {
       DOSVM_QueueEvent(0,DOS_PRIORITY_REALTIME,NULL,NULL);
     }
     if (lpDosTask->pending) {
       TRACE_(int)("setting Pending flag, interrupts are currently %s\n",
                 IF_ENABLED(&context) ? "enabled" : "disabled");
       SET_PEND(&context);
       DOSVM_SendQueuedEvents(&context,lpDosTask);
     } else {
       TRACE_(int)("no events are pending, clearing Pending flag\n");
       CLR_PEND(&context);
     }
     if (sig==SIGUSR2) lpDosTask->sig_sent--;
   }
   else if ((sig==SIGHUP) || (sig==SIGILL) || (sig==SIGSEGV)) {
       do_exception( sig, &context );
   } else {
    DOSVM_Dump(lpDosTask,fn,sig,VM86);
    ret=-1;
   }
   break;
  case VM86_UNKNOWN: /* unhandled GPF */
   DOSVM_Dump(lpDosTask,fn,sig,VM86);
   do_exception( SIGSEGV, &context );
   break;
  case VM86_INTx:
   if (TRACE_ON(relay))
    DPRINTF("Call DOS int 0x%02x (EAX=%08lx) ret=%04lx:%04lx\n",VM86_ARG(fn),context.Eax,context.SegCs,context.Eip);
   ret=DOSVM_Int(VM86_ARG(fn),&context,lpDosTask);
   if (TRACE_ON(relay))
    DPRINTF("Ret  DOS int 0x%02x (EAX=%08lx) ret=%04lx:%04lx\n",VM86_ARG(fn),context.Eax,context.SegCs,context.Eip);
   break;
  case VM86_STI:
  case VM86_PICRETURN:
    TRACE_(int)("DOS task enabled interrupts with events pending, sending events\n");
    DOSVM_SendQueuedEvents(&context,lpDosTask);
    break;
  case VM86_TRAP:
   do_exception( SIGTRAP, &context );
   break;
  default:
   DOSVM_Dump(lpDosTask,fn,sig,VM86);
   ret=-1;
 }

#define CP(x,y) VM86->regs.x = y##_reg(&context)
 CV;
#undef CP
#ifdef TRY_PICRETURN
 VM86->vm86plus.force_return_for_pic = IS_PEND(&context) ? 1 : 0;
 CLR_PEND(&context);
#endif
 return ret;
}

void DOSVM_ProcessMessage(LPDOSTASK lpDosTask,MSG *msg)
{
  BYTE scan = 0;

  fprintf(stderr,"got message %04x, wparam=%08x, lparam=%08lx\n",msg->message,msg->wParam,msg->lParam);
  if ((msg->message>=WM_MOUSEFIRST)&&
      (msg->message<=WM_MOUSELAST)) {
    INT_Int33Message(msg->message,msg->wParam,msg->lParam);
  } else {
    switch (msg->message) {
    case WM_KEYUP:
      scan = 0x80;
    case WM_KEYDOWN:
      scan |= (msg->lParam >> 16) & 0x7f;

      /* check whether extended bit is set,
       * and if so, queue the extension prefix */
      if (msg->lParam & 0x1000000) {
	/* FIXME: some keys (function keys) have
	 * extended bit set even when they shouldn't,
	 * should check for them */
	INT_Int09SendScan(0xE0);
      }
      INT_Int09SendScan(scan);
      break;
    }
  }
}

void DOSVM_Wait( int read_pipe, HANDLE hObject )
{
  LPDOSTASK lpDosTask = MZ_Current();
  MSG msg;
  DWORD waitret;
  BOOL got_msg = FALSE;

  do {
    /* check for messages (waste time before the response check below) */
    while (PeekMessageA(&msg,0,0,0,PM_REMOVE|PM_NOYIELD)) {
      /* got a message */
      DOSVM_ProcessMessage(lpDosTask,&msg);
      /* we don't need a TranslateMessage here */
      DispatchMessageA(&msg);
      got_msg = TRUE;
    }
    if (read_pipe == -1) {
      if (got_msg) break;
    } else {
      fd_set readfds;
      struct timeval timeout={0,0};
      /* quick check for response from dosmod
       * (faster than doing the full blocking wait, if data already available) */
      FD_ZERO(&readfds); FD_SET(read_pipe,&readfds);
      if (select(read_pipe+1,&readfds,NULL,NULL,&timeout)>0)
	break;
    }
    /* check for data from win32 console device */

    /* nothing yet, block while waiting for something to do */
    waitret=MsgWaitForMultipleObjects(1,&hObject,FALSE,INFINITE,QS_ALLINPUT);
    if (waitret==(DWORD)-1) {
      ERR_(module)("dosvm wait error=%ld\n",GetLastError());
    }
    if (read_pipe != -1) {
      if (waitret==WAIT_OBJECT_0) break;
    }
  } while (TRUE);
}

int DOSVM_Enter( CONTEXT86 *context )
{
 LPDOSTASK lpDosTask = MZ_Current();
 struct vm86plus_struct VM86;
 int stat,len,sig;

 if (!lpDosTask) {
  /* MZ_CreateProcess or MZ_AllocDPMITask should have been called first */
  ERR_(module)("dosmod has not been initialized!");
  return -1;
 }

 if (context) {
#define CP(x,y) VM86.regs.x = y##_reg(context)
  CV;
#undef CP
  if (VM86.regs.eflags & IF_MASK)
    VM86.regs.eflags |= VIF_MASK;
 } else {
/* initial setup */
  /* allocate standard DOS handles */
  FILE_InitProcessDosHandles(); 
  /* registers */
  memset(&VM86,0,sizeof(VM86));
  VM86.regs.cs=lpDosTask->init_cs;
  VM86.regs.eip=lpDosTask->init_ip;
  VM86.regs.ss=lpDosTask->init_ss;
  VM86.regs.esp=lpDosTask->init_sp;
  VM86.regs.ds=lpDosTask->psp_seg;
  VM86.regs.es=lpDosTask->psp_seg;
  VM86.regs.eflags=VIF_MASK;
  /* hmm, what else do we need? */
 }

 /* main exchange loop */
 do {
  stat = VM86_ENTER;
  errno = 0;
  /* transmit VM86 structure to dosmod task */
  if (write(lpDosTask->write_pipe,&stat,sizeof(stat))!=sizeof(stat)) {
   ERR_(module)("dosmod sync lost, errno=%d, fd=%d, pid=%d\n",errno,lpDosTask->write_pipe,getpid());
   return -1;
  }
  if (write(lpDosTask->write_pipe,&VM86,sizeof(VM86))!=sizeof(VM86)) {
   ERR_(module)("dosmod sync lost, errno=%d\n",errno);
   return -1;
  }
  /* wait for response, doing other things in the meantime */
  DOSVM_Wait(lpDosTask->read_pipe, lpDosTask->hReadPipe);
  /* read response */
  while (1) {
    if ((len=read(lpDosTask->read_pipe,&stat,sizeof(stat)))==sizeof(stat)) break;
    if (((errno==EINTR)||(errno==EAGAIN))&&(len<=0)) {
     WARN_(module)("rereading dosmod return code due to errno=%d, result=%d\n",errno,len);
     continue;
    }
    ERR_(module)("dosmod sync lost reading return code, errno=%d, result=%d\n",errno,len);
    return -1;
  }
  TRACE_(module)("dosmod return code=%d\n",stat);
  while (1) {
    if ((len=read(lpDosTask->read_pipe,&VM86,sizeof(VM86)))==sizeof(VM86)) break;
    if (((errno==EINTR)||(errno==EAGAIN))&&(len<=0)) {
     WARN_(module)("rereading dosmod VM86 structure due to errno=%d, result=%d\n",errno,len);
     continue;
    }
    ERR_(module)("dosmod sync lost reading VM86 structure, errno=%d, result=%d\n",errno,len);
    return -1;
  }
  if ((stat&0xff)==DOSMOD_SIGNAL) {
    while (1) {
      if ((len=read(lpDosTask->read_pipe,&sig,sizeof(sig)))==sizeof(sig)) break;
      if (((errno==EINTR)||(errno==EAGAIN))&&(len<=0)) {
	WARN_(module)("rereading dosmod signal due to errno=%d, result=%d\n",errno,len);
	continue;
      }
      ERR_(module)("dosmod sync lost reading signal, errno=%d, result=%d\n",errno,len);
      return -1;
    } while (0);
  } else sig=0;
  /* got response */
 } while (DOSVM_Process(lpDosTask,stat,sig,&VM86)>=0);

 if (context) {
#define CP(x,y) y##_reg(context) = VM86.regs.x
  CV;
#undef CP
 }
 return 0;
}

void DOSVM_PIC_ioport_out( WORD port, BYTE val)
{
  LPDOSTASK lpDosTask = MZ_Current();
  LPDOSEVENT event;

  if (lpDosTask) {
    if ((port==0x20) && (val==0x20)) {
      if (lpDosTask->current) {
	/* EOI (End Of Interrupt) */
	TRACE_(int)("received EOI for current IRQ, clearing\n");
	event = lpDosTask->current;
	lpDosTask->current = event->next;
	if (event->relay)
	(*event->relay)(lpDosTask,NULL,event->data);
	free(event);

	if (lpDosTask->pending &&
	    !lpDosTask->sig_sent) {
	  /* another event is pending, which we should probably
	   * be able to process now, so tell dosmod about it */
	  TRACE_(int)("another event pending, signalling dosmod\n");
	  kill(lpDosTask->task,SIGUSR2);
	  lpDosTask->sig_sent++;
	}
      } else {
	WARN_(int)("EOI without active IRQ\n");
      }
    } else {
      FIXME_(int)("unrecognized PIC command %02x\n",val);
    }
  }
}

void DOSVM_SetTimer( unsigned ticks )
{
  LPDOSTASK lpDosTask = MZ_Current();
  int stat=DOSMOD_SET_TIMER;
  struct timeval tim;

  if (lpDosTask) {
    /* the PC clocks ticks at 1193180 Hz */
    tim.tv_sec=0;
    tim.tv_usec=((unsigned long long)ticks*1000000)/1193180;
    /* sanity check */
    if (!tim.tv_usec) tim.tv_usec=1;

    if (write(lpDosTask->write_pipe,&stat,sizeof(stat))!=sizeof(stat)) {
      ERR_(module)("dosmod sync lost, errno=%d\n",errno);
      return;
    }
    if (write(lpDosTask->write_pipe,&tim,sizeof(tim))!=sizeof(tim)) {
      ERR_(module)("dosmod sync lost, errno=%d\n",errno);
      return;
    }
    /* there's no return */
  }
}

unsigned DOSVM_GetTimer( void )
{
  LPDOSTASK lpDosTask = MZ_Current();
  int stat=DOSMOD_GET_TIMER;
  struct timeval tim;

  if (lpDosTask) {
    if (write(lpDosTask->write_pipe,&stat,sizeof(stat))!=sizeof(stat)) {
      ERR_(module)("dosmod sync lost, errno=%d\n",errno);
      return 0;
    }
    /* read response */
    while (1) {
      if (read(lpDosTask->read_pipe,&tim,sizeof(tim))==sizeof(tim)) break;
      if ((errno==EINTR)||(errno==EAGAIN)) continue;
      ERR_(module)("dosmod sync lost, errno=%d\n",errno);
      return 0;
    }
    return ((unsigned long long)tim.tv_usec*1193180)/1000000;
  }
  return 0;
}

void DOSVM_SetSystemData( int id, void *data )
{
  LPDOSTASK lpDosTask = MZ_Current();
  DOSSYSTEM *sys, *prev;

  if (lpDosTask) {
    sys = lpDosTask->sys;
    prev = NULL;
    while (sys && (sys->id != id)) {
      prev = sys;
      sys = sys->next;
    }
    if (sys) {
      free(sys->data);
      sys->data = data;
    } else {
      sys = malloc(sizeof(DOSSYSTEM));
      sys->id = id;
      sys->data = data;
      sys->next = NULL;
      if (prev) prev->next = sys;
      else lpDosTask->sys = sys;
    }
  } else free(data);
}

void* DOSVM_GetSystemData( int id )
{
  LPDOSTASK lpDosTask = MZ_Current();
  DOSSYSTEM *sys;

  if (lpDosTask) {
    sys = lpDosTask->sys;
    while (sys && (sys->id != id))
      sys = sys->next;
    if (sys)
      return sys->data;
  }
  return NULL;
}

#else /* !MZ_SUPPORTED */

int DOSVM_Enter( CONTEXT86 *context )
{
 ERR_(module)("DOS realmode not supported on this architecture!\n");
 return -1;
}

void DOSVM_Wait( int read_pipe, HANDLE hObject) {}
void DOSVM_PIC_ioport_out( WORD port, BYTE val) {}
void DOSVM_SetTimer( unsigned ticks ) {}
unsigned DOSVM_GetTimer( void ) { return 0; }
void DOSVM_SetSystemData( int id, void *data ) { free(data); }
void* DOSVM_GetSystemData( int id ) { return NULL; }
void DOSVM_QueueEvent( int irq, int priority, void (*relay)(LPDOSTASK,CONTEXT86*,void*), void *data) {}

#endif
