/*
 *  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.,
 *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

/*
 * Purpose: handling of the dosemu-supplied utilities, AKA builtins.
 *
 * Author: Stas Sergeev
 * Some code is taken from coopthreads.c by Hans Lermen.
 */

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <ctype.h>
#include <sys/param.h>
#include "dos2linux.h"
#include "doshelpers.h"
#include "cpu.h"
#include "emu.h"
#include "int.h"
#include "utilities.h"
#include "lowmem.h"
#include "coopth.h"
#include "smalloc.h"
#include "redirect.h"
#include "plugin_config.h"
#include "msetenv.h"
#include "system.h"
#include "builtins.h"

/* hope 2K is enough */
#define LOWMEM_POOL_SIZE 0x800
#define MAX_NESTING 32

static int pool_used = 0;
//#define current_builtin (pool_used - 1)
#define current_builtin pool_used

struct {
    char name[9];
    char *cmd;
    struct lowstring *cmdl;
    struct param4a *pa4;
    uint16_t es;
    uint16_t retcode;
    int allocated;
    run_dos_cb run_dos;
    get_psp_cb get_psp;
} builtin_mem[MAX_NESTING];
#define BMEM(x) (builtin_mem[current_builtin].x)

char *com_getenv(const char *keyword)
{
	struct PSP  *psp = COM_PSP_ADDR;
	char *env = SEG2UNIX(psp->envir_frame);
	char key[128];
	int len;

	len = strlen(keyword);
	memcpy(key, keyword, len+1);
	strupperDOS(key);
	while (*env) {
		if (!strncmp(key, env, len) && (env[len] == '=')) {
			return env + len + 1;
		}
		env += strlen(env) + 1;
	}
	return 0;
}

static int load_and_run_DOS_program(const char *command, const char *cmdline)
{
	BMEM(pa4) = (struct param4a *)lowmem_alloc(sizeof(struct param4a));
	if (!BMEM(pa4)) return -1;

	BMEM(allocated) = 1;

	BMEM(cmd) = com_strdup(command);
	if (!BMEM(cmd)) {
		com_errno = 8;
		return -1;
	}
	BMEM(cmdl) = (struct lowstring *)lowmem_alloc(256);
	if (!BMEM(cmdl)) {
		com_strfree(BMEM(cmd));
		com_errno = 8;
		return -1;
	}
	if (!cmdline) cmdline = "";
	BMEM(cmdl)->len = strlen(cmdline) + 1;
	snprintf(BMEM(cmdl)->s, 255, " %s\r", cmdline);

	BMEM(es) = SREG(es);
	/* prepare param block */
	BMEM(pa4)->envframe = 0; // ctcb->envir_frame;
	BMEM(pa4)->cmdline = MK_FARt(DOSEMU_LMHEAP_SEG, DOSEMU_LMHEAP_OFFS_OF(BMEM(cmdl)));
	BMEM(pa4)->fcb1 = MK_FARt(com_psp_seg(), offsetof(struct PSP, FCB1));
	BMEM(pa4)->fcb2 = MK_FARt(com_psp_seg(), offsetof(struct PSP, FCB2));
	SREG(es) = DOSEMU_LMHEAP_SEG;
	LWORD(ebx) = DOSEMU_LMHEAP_OFFS_OF(BMEM(pa4));
	/* path of program to load */
	SREG(ds) = DOSEMU_LMHEAP_SEG;
	LWORD(edx) = DOSEMU_LMHEAP_OFFS_OF(BMEM(cmd));

	fake_call_to(BIOSSEG, GET_RETCODE_HELPER);
	LWORD(eax) = 0x4b00;
	real_run_int(0x21);

	return 0;
}

static int do_system(const char *command)
{
	const char *program = com_getenv("COMSPEC");
	char cmdline[128];

	if (!program) program = "C:\\COMMAND.COM";
	snprintf(cmdline, sizeof(cmdline), "/E:2048 /C %s", command);
	coopth_leave();
	return load_and_run_DOS_program(program, cmdline);
}

int com_system(const char *command)
{
	return BMEM(run_dos)(command);
}

int com_error(const char *format, ...)
{
	int ret;
	va_list args;
	va_start(args, format);
	ret = com_vprintf(format, args);
	va_end(args);
	va_start(args, format);
	verror(format, args);
	va_end(args);
	return ret;
}

static int com_argparse(char *s, int i_argc, char **argvx, int maxarg)
{
   int mode = 0;
   int argcx = 0;
   char delim = 0;
   char *p;
   int len;

   len = strlen(s);
   p = strchr(s, '\r');
   if (p && ((p - s) < len))
     *p = 0;

   if (i_argc == 0) {
      int l;
      /* transform:
       *    dir/p to dir /p
       *    cd\ to cd \
       *    cd.. to cd ..
       */
      l = strcspn(s, "\\/. ");
      p = s + l;
      if (*p && *p != ' ' && (*p != '.' || (p[1] && strchr("\\/.", p[1]))) &&
            (!(l > 0 && p[0] == '\\' && p[-1] == ':'))) {
         memmove(p+1, p, len - (p - s) + 1/*NUL*/);
         *p = ' ';
         len++; /* update length */
      }
   }

   maxarg --;
   for ( ; *s; s++) {
      if (!mode) {
         if (*s > ' ') {
            mode = 1;
            switch (*s) {
              case '"':
              case '\'':
                delim = *s;
                mode = 2;
                argvx[argcx++] = s+1;
                break;
              default:
                argvx[argcx++] = s;
                break;
            }
            if (argcx >= maxarg)
               break;
         }
      } else if (mode == 1) {
         if (*s <= ' ') {
            mode = 0;
            *s = 0;
         }
      } else {
         if (*s == delim) {
           *s = 0;
           mode = 1;
         }
      }
   }
   argvx[argcx] = 0;
   return(argcx);
}

int com_dosallocmem(u_short para)
{
    int ret;
    pre_msdos();
    HI(ax) = 0x48;
    LWORD(ebx) = para;
    call_msdos();
    if (REG(eflags) & CF)
        ret = 0;
    else
        ret = LWORD(eax);
    post_msdos();
    return ret;
}

int com_dosfreemem(u_short para)
{
    int ret = 0;
    pre_msdos();
    HI(ax) = 0x49;
    SREG(es) = para;
    call_msdos();
    if (REG(eflags) & CF)
        ret = -1;
    post_msdos();
    return ret;
}

int com_dosgetdrive(void)
{
        int ret;
        pre_msdos();
        HI(ax) = 0x19;
        call_msdos();    /* call MSDOS */
        ret = LO(ax);  /* 0=A, 1=B, ... */
        post_msdos();
        return ret;
}

int com_dossetdrive(int drive)
{
        int ret;
        pre_msdos();
        HI(ax) = 0x0e;
        LO(dx) = drive; /* 0=A, 1=B, ... */
        call_msdos();    /* call MSDOS */
        ret = LO(ax);  /* number of available logical drives */
        post_msdos();
        return ret;
}

int com_dossetcurrentdir(const char *path)
{
        int ret = 0;
        /*struct com_starter_seg  *ctcb = owntcb->params;*/
        char *s = com_strdup(path);

        com_errno = 8;
        if (!s) return -1;
        pre_msdos();
        HI(ax) = 0x3b;
        SREG(ds) = DOSEMU_LMHEAP_SEG;
        LWORD(edx) = DOSEMU_LMHEAP_OFFS_OF(s);
        call_msdos();    /* call MSDOS */
        com_strfree(s);
        if (LWORD(eflags) & CF)
                ret = -1;
        post_msdos();
        return ret;
}

/********************************************
 * com_RedirectDevice - redirect a device to a remote resource
 * ON ENTRY:
 *  deviceStr has a string with the device name:
 *    either disk or printer (ex. 'D:' or 'LPT1')
 *  resourceStr has a string with the server and name of resource
 *    (ex. 'TIM\TOOLS')
 *  deviceType indicates the type of device being redirected
 *    3 = printer, 4 = disk
 *  deviceParameter is a value to be saved with this redirection
 *  which will be returned on com_GetRedirectionList
 * ON EXIT:
 *  returns CC_SUCCESS if the operation was successful,
 *  otherwise returns the DOS error code
 * NOTES:
 *  deviceParameter is used in DOSEMU to return the drive attribute
 *  It is not actually saved and returned as specified by the redirector
 *  specification.  This type of usage is common among commercial redirectors.
 ********************************************/
uint16_t com_RedirectDevice(char *deviceStr, char *slashedResourceStr,
                        uint8_t deviceType, uint16_t deviceParameter)
{
  char *dStr = com_strdup(deviceStr);
  char *sStr = com_strdup(slashedResourceStr);
  uint16_t ret = RedirectDevice(dStr, sStr, deviceType, deviceParameter);

  com_strfree(sStr);
  com_strfree(dStr);

  return ret;
}

struct REGPACK regs_to_regpack(struct vm86_regs *regs)
{
	struct REGPACK regpack;

	regpack.r_ax =  regs->eax;
	regpack.r_bx =  regs->ebx;
	regpack.r_cx =  regs->ecx;
	regpack.r_dx =  regs->edx;
	regpack.r_bp =  regs->ebp;
	regpack.r_si =  regs->esi;
	regpack.r_di =  regs->edi;
	regpack.r_ds =  regs->ds;
	regpack.r_es =  regs->es;
	regpack.r_flags =  regs->eflags;

	return regpack;
}

struct vm86_regs regpack_to_regs(struct REGPACK *regpack)
{
	struct vm86_regs regs = _regs;

	regs.eax = regpack->r_ax;
	regs.ebx = regpack->r_bx;
	regs.ecx = regpack->r_cx;
	regs.edx = regpack->r_dx;
	regs.ebp = regpack->r_bp;
	regs.esi = regpack->r_si;
	regs.edi = regpack->r_di;
	regs.ds = regpack->r_ds;
	regs.es = regpack->r_es;
	regs.eflags = regpack->r_flags;

	return regs;
}

void com_intr(int intno, struct REGPACK *regpack)
{
	struct vm86_regs saved_regs = _regs;
	_regs = regpack_to_regs(regpack);

	do_int_call_back(intno);

	*regpack = regs_to_regpack(&_regs);
	_regs = saved_regs;
}


static struct com_program_entry *com_program_list = 0;

static struct com_program_entry *find_com_program(const char *name)
{
	struct com_program_entry *com = com_program_list;

	while (com) {
		if (!strcmp(com->name, name)) return com;
		com = com->next;
	}
	return 0;
}

void register_com_program(const char *name, com_program_type *program)
{
	struct com_program_entry *com;

	if ((com = find_com_program(name)) == 0) {
		com = malloc(sizeof(struct com_program_entry));
		if (!com) return;
		com->next = com_program_list;
		com_program_list = com;
	}
	com->name = name;
	com->program = program;
}

static char *com_getarg0(void)
{
	char *env = SEG2UNIX(((struct PSP *)SEG2UNIX(COM_PSP_SEG))->envir_frame);
	return memchr(env, 1, 0x10000) + 2;
}

unsigned short com_psp_seg(void)
{
	return BMEM(get_psp)(0);
}

unsigned short com_parent_psp_seg(void)
{
	return BMEM(get_psp)(1);
}

static int unsigned short do_get_psp(int parent)
{
	if (parent) {
		struct PSP *psp = COM_PSP_ADDR;
		unsigned short parent = psp->parent_psp;

		if (!parent) {
			error("No parent PSP\n");
			return com_psp_seg();
		}
		return parent;
	}
	return COM_PSP_SEG;
}

int run_command_plugin(const char *name, const char *argv0, char *cmdbuf,
	run_dos_cb run_cb, get_psp_cb psp_cb)
{
	struct com_program_entry *com;
#define MAX_ARGS 63
	char *args[MAX_ARGS + 1];
	char builtin_name[9];
	char arg0[256];
	int argc = 0;
	int err;

	/* first parse commandline */
	if (argv0) {
		strncpy(arg0, argv0, sizeof(arg0) - 1);
		arg0[sizeof(arg0) - 1] = 0;
		strupperDOS(arg0);
		args[0] = arg0;
		argc++;
	}
	argc += com_argparse(cmdbuf, argc, &args[argc], MAX_ARGS - argc);
	strncpy(builtin_name, name, sizeof(builtin_name) - 1);
	builtin_name[sizeof(builtin_name) - 1] = 0;
	strupperDOS(builtin_name);

	com = find_com_program(builtin_name);
	if (!com) {
//		com_error("inte6: unknown builtin: %s\n", builtin_name);
		return 0;
	}

	BMEM(allocated) = 0;
	BMEM(retcode) = 0;
	strcpy(BMEM(name), builtin_name);
	BMEM(run_dos) = run_cb;
	BMEM(get_psp) = psp_cb;
	err = com->program(argc, args);
	if (err)
		return -1;

	/* if DOS prog spawned, we can't free resources here */
	return 1;
}

int commands_plugin_inte6(void)
{
	char builtin_name[9];
	char cmdbuf[256];
	char *arg0;
	struct PSP *psp;
	struct MCB *mcb;
	int i;
	int err;

	CARRY;		// prevents pligin_done() call
	if (HI(ax) != BUILTINS_PLUGIN_VERSION) {
	    com_error("builtins plugin version mismatch: found %i, required %i\n",
		HI(ax), BUILTINS_PLUGIN_VERSION);
	    return 0;
	}

	psp = SEG2UNIX(COM_PSP_SEG);
	mcb = SEG2UNIX(COM_PSP_SEG - 1);
	arg0 = com_getarg0();
	/* see if we have valid asciiz name in MCB */
	err = 0;
	for (i = 0; i < 8 && mcb->name[i]; i++) {
		if (!isprint(mcb->name[i])) {
			err = 1;
			break;
		}
	}
	if (!err) {
		/* DOS 4 and up */
		strncpy(builtin_name, mcb->name, sizeof(builtin_name) - 1);
		builtin_name[sizeof(builtin_name) - 1] = 0;
	} else {
		/* DOS 3.0->3.31 construct the program name from the environment */
		char *p = strrchr(arg0, '\\');
		strncpy(builtin_name, p+1, sizeof(builtin_name) - 1);
		builtin_name[sizeof(builtin_name) - 1] = 0;
		p = strchr(builtin_name, '.');
		if (p)
			*p = '\0';
	}
//	assert(psp->cmdline_len < sizeof(cmdbuf)); // len uint8_t, can't assert
	memcpy(cmdbuf, psp->cmdline, psp->cmdline_len);
	cmdbuf[psp->cmdline_len] = '\0';
	if (pool_used >= MAX_NESTING) {
	    com_error("Cannot invoke more than %i builtins\n", MAX_NESTING);
	    return 0;
	}
	pool_used++;
	NOCARRY;
	err = run_command_plugin(builtin_name, arg0, cmdbuf,
			do_system, do_get_psp);
	if (err <= 0) {
		pool_used--;
		CARRY;
	}
	return err;
}

int commands_plugin_inte6_done(void)
{
	if (!pool_used)
	    return 0;

	SREG(es) = BMEM(es);
	LWORD(ebx) = BMEM(retcode);
	if (BMEM(allocated)) {
	    com_strfree(BMEM(cmd));
	    lowmem_free((void *)BMEM(pa4));
	    lowmem_free((void *)BMEM(cmdl));
	}
	pool_used--;
	return 1;
}

int commands_plugin_inte6_set_retcode(void)
{
	if (!pool_used)
	    return 0;

	BMEM(retcode) = LWORD(ebx);
	return 1;
}

void commands_plugin_inte6_reset(void)
{
	pool_used = 0;
	system_com_reset();
}
