#if 0
gcc -s -O2 -o ./glkrun glkrun.c libglkterm.a -lncurses
exit
#endif

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "glk.h"
#include "glkstart.h"
#include "gi_dispa.h"

typedef struct {
  glui32 array_id;
  char retain;
  char pass_out;
  unsigned char type;
  char need_free;
  unsigned char objclass;
} ArgMode;

static int fdr,fdw;
static void**registry;
static int nregistry;
static gluniversal_t disp_args[68];
static ArgMode argmode[68];
static int narg;
static char endian;
static glui32 func;

glkunix_argumentlist_t glkunix_arguments[]={
  {"",glkunix_arg_ValueFollows,"External program to run"},
  {NULL,glkunix_arg_End,NULL},
};

int glkunix_startup_code(glkunix_startup_t*data) {
  int pfd[4];
  int pid;
  int i;
  char**argv1=malloc(data->argc*sizeof(char*));
  if(data->argc<2 || !argv1) return FALSE;
  for(i=0;i<data->argc-1;i++) argv1[i]=data->argv[i+1];
  argv1[data->argc-1]=0;
  if(dup2(0,3)==-1 || dup2(0,4)==-1) return FALSE;
  if(pipe(pfd) || pipe(pfd+2)) return FALSE;
  if(dup2(pfd[1],3)==-1 || dup2(pfd[2],4)==-1) return FALSE;
  close(pfd[1]);
  close(pfd[2]);
  pid=fork();
  if(pid==-1) return FALSE;
  if(!pid) {
    close(pfd[0]);
    close(pfd[3]);
    execvp(data->argv[1],argv1);
    _exit(1);
  }
  fdr=pfd[0];
  fdw=pfd[3];
  close(3);
  close(4);
  return TRUE;
}

static void x_read(unsigned char*buf,int n) {
  int i;
  while(n) {
    i=read(fdr,buf,n);
    if(i<=0) glk_exit();
    buf+=i;
    n-=i;
  }
}

static void x_write(const unsigned char*buf,int n) {
  int i;
  while(n) {
    i=write(fdw,buf,n);
    if(i<=0) glk_exit();
    buf+=i;
    n-=i;
  }
}

static gidispatch_rock_t my_vm_reg_object(void*obj,glui32 objclass) {
  unsigned char buf[5];
  glui32 n=0;
  while(n<nregistry) {
    if(!registry[n]) goto found;
    n++;
  }
  registry=realloc(registry,(nregistry=n+1)*sizeof(void*));
  if(!registry) glk_exit();
  found:
  registry[n]=obj;
  *buf=objclass+'a';
  buf[1]=n>>24; buf[2]=n>>16; buf[3]=n>>8; buf[4]=n;
  x_write(buf,5);
  return (gidispatch_rock_t){num:n};
}

static void my_vm_unreg_object(void*obj,glui32 objclass,gidispatch_rock_t objrock) {
  unsigned char buf[5];
  glui32 n=objrock.num;
  registry[n]=0;
  *buf='$';
  buf[1]=n>>24; buf[2]=n>>16; buf[3]=n>>8; buf[4]=n;
  x_write(buf,5);
}

static void send_array(void*array,glui32 len,char typecode) {
  unsigned char buf[4];
  glui32*arr;
  void**oarr;
  glui32 i,n;
  if(typecode=='C') {
    buf[0]=len>>24; buf[1]=len>>16; buf[2]=len>>8; buf[3]=len;
    x_write(buf,4);
    x_write(array,len);
  } else if(typecode=='I') {
    buf[0]=len>>22; buf[1]=len>>14; buf[2]=len>>6; buf[3]=len<<2;
    x_write(buf,4);
    arr=array;
    for(i=0;i<len;i++) {
      buf[0]=arr[i]>>24; buf[1]=arr[i]>>16; buf[2]=arr[i]>>8; buf[3]=arr[i];
      x_write(buf,4);
    }
  } else if(typecode>='a' && typecode<='z') {
    buf[0]=len>>22; buf[1]=len>>14; buf[2]=len>>6; buf[3]=len<<2;
    x_write(buf,4);
    oarr=array;
    for(i=0;i<len;i++) {
      n=oarr[i]?gidispatch_get_objrock(oarr[i],typecode-'a').num:0xFFFFFFFF;
      buf[0]=n>>24; buf[1]=n>>16; buf[2]=n>>8; buf[3]=n;
      x_write(buf,4);
    }
    typecode='Q';
  }
  x_write(&typecode,1);
}

static gidispatch_rock_t my_vm_reg_array(void*array,glui32 len,char*typecode) {
  unsigned char buf[5];
  glui32 i,n;
  *buf='+';
  for(i=0;i<narg;i++) {
    if(argmode[i].retain && disp_args[i].array==array && argmode[i].need_free) {
      n=argmode[i].array_id;
      buf[1]=n>>24; buf[2]=n>>16; buf[3]=n>>8; buf[4]=n;
      argmode[i].need_free=0;
      x_write(buf,5);
      return (gidispatch_rock_t){num:argmode[i].array_id};
    }
  }
  return (gidispatch_rock_t){num:-1};
}

static void my_vm_unreg_array(void*array,glui32 len,char*typecode,gidispatch_rock_t objrock) {
  unsigned char buf[5];
  glui32 n=objrock.num;
  *buf='-';
  buf[1]=n>>24; buf[2]=n>>16; buf[3]=n>>8; buf[4]=n;
  x_write(buf,5);
  send_array(array,len,typecode[4]);
  free(array);
}

static void check_endian(void) {
  volatile glui32 x=256;
  endian=((char*)&x)[1];
}

static void*read_string(int m) {
  unsigned char*s=malloc(16);
  glui32 n=0;
  int i;
  if(!s) glk_exit();
  for(;;) {
    x_read(s+n,m);
    if(!s[n]) {
      if(m==1) return s;
      if(!s[n+1] && !s[n+2] && !s[n+3]) return s;
    }
    if(m==4 && endian) {
      i=s[0];
      s[0]=s[3];
      s[3]=i;
      i=s[1];
      s[1]=s[2];
      s[2]=i;
    }
    n+=m;
    if(!(n&7)) {
      s=realloc(s,n+16);
      if(!s) glk_exit();
    }
  }
}

static void*read_array_C(glui32 len) {
  unsigned char*s=malloc(len);
  if(len && !s) glk_exit();
  x_read(s,len);
  return s;
}

static void*read_array_I(glui32 len) {
  unsigned char*s=malloc(len<<2);
  glui32 n;
  int i;
  if(len && !s) glk_exit();
  x_read(s,len);
  if(endian) {
    for(n=0;n<len<<2;n+=4) {
      i=s[n+0];
      s[n+0]=s[n+3];
      s[n+3]=i;
      i=s[n+1];
      s[n+1]=s[n+2];
      s[n+2]=i;
    }
  }
  return s;
}

static void*read_array_Q(glui32 len) {
  void**s=malloc(len*sizeof(void*));
  glui32 n;
  unsigned char buf[4];
  if(len && !s) glk_exit();
  for(n=0;n<len;n++) {
    x_read(buf,4);
    if((buf[0]&buf[1]&buf[2]&buf[3])==255) s[n]=0;
    else s[n]=registry[(buf[0]<<24)|(buf[1]<<16)|(buf[2]<<8)|buf[3]];
  }
  return s;
}

void glk_main(void) {
  unsigned char buf[5];
  unsigned char t;
  glui32 n;
  check_endian();
  gidispatch_set_object_registry(my_vm_reg_object,my_vm_unreg_object);
  gidispatch_set_retained_registry(my_vm_reg_array,my_vm_unreg_array);
  glk_tick();
  begin:
  x_read(buf,2);
  func=(buf[0]<<8)|buf[1];
  narg=0;
  nextarg:
  x_read(&t,1);
  switch(t) {
    case 'C': // single byte
      x_read(&disp_args[narg].ch,1);
      argmode[narg].retain=0;
      argmode[narg].pass_out=0;
      argmode[narg].type='C';
      argmode[narg].need_free=0;
      narg++;
      break;
    case 'I': // integer
      x_read(buf,4);
      disp_args[narg].uint=(buf[0]<<24)|(buf[1]<<16)|(buf[2]<<8)|buf[3];
      argmode[narg].retain=0;
      argmode[narg].pass_out=0;
      argmode[narg].type='I';
      argmode[narg].need_free=0;
      narg++;
      break;
    case 'Q': // opaque object
      x_read(buf,4);
      disp_args[narg].opaqueref=registry[(buf[0]<<24)|(buf[1]<<16)|(buf[2]<<8)|buf[3]];
      argmode[narg].retain=0;
      argmode[narg].pass_out=0;
      argmode[narg].type='Q';
      argmode[narg].need_free=0;
      narg++;
      break;
    case 'S': // string of 8-bit characters
      disp_args[narg].charstr=read_string(1);
      argmode[narg].retain=0;
      argmode[narg].pass_out=0;
      argmode[narg].type='S';
      argmode[narg].need_free=1;
      narg++;
      break;
    case 'U': // string of 32-bit characters
      disp_args[narg].charstr=read_string(4);
      argmode[narg].retain=0;
      argmode[narg].pass_out=0;
      argmode[narg].type='S';
      argmode[narg].need_free=1;
      narg++;
      break;
    case 'Z': // null object
      disp_args[narg].opaqueref=0;
      argmode[narg].retain=0;
      argmode[narg].pass_out=0;
      argmode[narg].type='Q';
      argmode[narg].need_free=0;
      narg++;
      break;
    case '0': case '1': // pointer flag
      disp_args[narg].ptrflag=t-'0';
      argmode[narg].retain=0;
      argmode[narg].pass_out=0;
      argmode[narg].type=0;
      argmode[narg].need_free=0;
      narg++;
      break;
    case '#': // array
      x_read(buf,5);
      disp_args[narg+1].uint=(buf[0]<<24)|(buf[1]<<16)|(buf[2]<<8)|buf[3];
      if(buf[4]=='C') disp_args[narg].array=read_array_C(disp_args[narg+1].uint);
      if(buf[4]=='I') disp_args[narg].array=read_array_I(disp_args[narg+1].uint);
      if(buf[4]=='Q') disp_args[narg].array=read_array_Q(disp_args[narg+1].uint);
      argmode[narg].retain=0;
      argmode[narg].pass_out=0;
      argmode[narg].type='#';
      argmode[narg].need_free=1;
      argmode[narg+1].retain=0;
      argmode[narg+1].pass_out=0;
      argmode[narg+1].type=buf[4];
      argmode[narg+1].need_free=0;
      narg+=2;
      break;
    case '!': // retain array
      x_read(buf,4);
      argmode[narg-2].pass_out=0;
      argmode[narg-2].array_id=(buf[0]<<24)|(buf[1]<<16)|(buf[2]<<8)|buf[3];
      argmode[narg-2].retain=1;
      break;
    case '<': // pass out reference
      argmode[narg-1].pass_out=1;
      break;
    case 'a' ... 'z': // pass out reference to opaque object
      argmode[narg-1].pass_out=1;
      argmode[narg-1].objclass=t-'a';
      break;
    case '.': // end of argument list
      goto finish;
  }
  if(narg>64) narg=64;
  goto nextarg;
  finish:
  gidispatch_call(func,narg,disp_args);
  for(t=0;t<narg;t++) {
    if(argmode[t].type=='#' && argmode[t+1].pass_out) argmode[t].pass_out=1;
    if(argmode[t].pass_out) switch(argmode[t].type) {
      case 'C':
        buf[0]='C';
        buf[1]=disp_args[t].uch;
        x_write(buf,2);
        break;
      case 'I':
        buf[0]='I';
        n=disp_args[t].uint;
        buf[1]=n>>24; buf[2]=n>>16; buf[3]=n>>8; buf[4]=n;
        x_write(buf,5);
        break;
      case 'Q':
        if(disp_args[t].opaqueref) {
          buf[0]='Q';
          n=gidispatch_get_objrock(disp_args[t].opaqueref,argmode[t].objclass).num;
          buf[1]=n>>24; buf[2]=n>>16; buf[3]=n>>8; buf[4]=n;
          x_write(buf,5);
        } else {
          buf[0]='Z';
          x_write(buf,1);
        }
        break;
      case '#':
        x_write(&argmode[t].type,1);
        send_array(disp_args[t].array,disp_args[t+1].uint,argmode[t+1].type=='Q'?argmode[t+1].objclass+'a':argmode[t+1].type);
        argmode[t+1].pass_out=0;
        break;
    }
    if(argmode[t].need_free) free(disp_args[t].array);
  }
  *buf='.';
  x_write(buf,1);
  goto begin;
}
