/* Public Domain - By Ross Ridge */

/*
 *  This programme demonstrates just how small of a TSR you can write
 *  entirely in C.  When compiled with Borland C++ 3.1 the resident
 *  portion of this programme is only 64 bytes long, as reported by
 *  MS-DOS's MEM command.  The function of this programme is trivial,
 *  but possibly useful.  It acts as replacement Caps Lock keyboard
 *  light by displaying a character in the upper-right hand corner of
 *  the screen according to the current state of the Caps Lock key.
 *
 *  The TSR achieves such a small memory footprint because, unlike a
 *  normal TSR, it doesn't "Terminate and Stay Resident."  IT installs
 *  an interrupt service routine (ISR) at the end of memory (or the end
 *  of a UMB), and then exits normally.
 *
 *  This programme uses a number of Borland C++ specific features,
 *  like segment pointers (the _seg keyword), pragmas to set segment
 *  names and CS relative near pointers.  The use of the last feature
 *  in particular will probably make this hard to port other compilers.
 *
 */

#include <dos.h>
#include <mem.h>
#include <stdlib.h>
#include <alloc.h>
#include <stdio.h>
#include <string.h>

#if 1

#define INT21() geninterrupt(0x21)
#define get_strategy() (_AX=0x5800, INT21(), _AX)
#define set_strategy(strat) (_BL = (strat), _BH = 0, _AX = 0x5801, INT21())
#define get_umb_link_state() (_AX=0x5802, INT21(), _AL)
#define set_umb_link_state(state) (_BX = (state), _AX = 0x5803, INT21())
#define set_current_psp(psp) (_BX = (psp), _AH = 0x50, INT21())
#define get_mode() (_AH = 0x0f, geninterrupt(0x10), _AL)

#else

/* Function versions of the above macro's are provided for those
   who don't like register variable (eg. _AX).  None of them are
   used in the ISR so your choice here has no effect on the size
   of the resident portion of the programme.  */

union REGS regs;

unsigned short
get_strategy(void) {
	regs.x.ax = 0x5800;
	intdos(&regs, &regs);
	return regs.x.ax;
}

void
set_strategy(unsigned char strat) {
	regs.x.ax = 0x5801;
	regs.h.bl = strat;
	regs.h.bh = 0;
	intdos(&regs, &regs);
}

unsigned char
get_umb_link_state(void) {
	regs.x.ax = 0x5802;
	intdos(&regs, &regs);
	return regs.h.al;
}

void
set_umb_link_state(unsigned short state) {
	regs.x.ax = 0x5803;
	regs.x.bx = state;
	intdos(&regs, &regs);
}

void
set_current_psp(unsigned short psp) {
	regs.h.ah = 0x50;
	regs.x.bx = psp;
	intdos(&regs, &regs);
}

unsigned char
get_mode(void) {
	regs.h.ah = 0x0F;
	int86(0x10, &regs, &regs);
	return regs.h.al;
}

#endif


#define MDA_SEG ((unsigned short _seg *)0xB000)
#define CGA_SEG ((unsigned short _seg *)0xB800)

/* Imports from ISR.C */

extern void interrupt (far isr)(void);
extern void interrupt (*far old_isr)(void);
extern char far end_isr;
extern unsigned short _seg *far screen_seg;

int
main(void) {
	char _seg *new_psp;
	unsigned short old_strat;
	unsigned short isr_size;
	unsigned short isr_paras;
	int old_state;
	int ret;
	char _seg *new_env;

	if (_osmajor < 3) {
		fprintf(stderr, "MS-DOS 3.0 or better required.\n");
		return 1;
	}

	/* Calculate the size of the ISR of the programme. */

	isr_size = FP_OFF(&end_isr) - FP_OFF(isr);
	isr_paras = (FP_OFF(&end_isr) + 15) / 16;
	if (isr_paras < 4) {
		isr_paras = 4; /* need to be at least 48 bytes */
	}

	if (allocmem(2, (unsigned *)&new_env) != -1) {
		abort();
	}
	_fmemcpy(new_env + 0, "\0\001\0C:\\ISR.EXE\0\0\0", 32);


	/* If possible load the ISR into a UMB, otherwise just
	   try to load it at the end of memory.  (Windows doesn't
	   like that though.) */

	old_strat = get_strategy();

	if (_osmajor >= 5 && _osmajor != 10) {
		old_state = get_umb_link_state();
		set_umb_link_state(1);
		set_strategy(0x82);
	} else {
		set_strategy(0x02);
	}

	/* Now allocate and zero out the memory for the ISR */

	if (allocmem(isr_paras, (unsigned *) &new_psp) != -1) {
		abort();
	}

	_fmemset(new_psp + 0, 0, isr_paras * 16);

	/* Give ownership the ISR's memory to the ISR itself.  */

	* (short far *) (new_psp + 0x2C) = (short) new_env;

	set_current_psp((unsigned) new_psp);
	ret = setblock((unsigned) new_psp, isr_paras);
	set_current_psp(_psp);

	/* Restore the old allocation strategy and UMB link state */

	set_strategy(old_strat);
	if (_osmajor >= 5 && _osmajor != 10) {
		set_umb_link_state(old_state);
	}

	if (ret != -1) {
		abort();
	}


#if 0
	_fmemset(MK_FP((unsigned) new_psp - 1, 0x0e), 0, 2);
#else
	/* A hack to put a name in to the MCB of our ISR's memory block.
	   This lets MEM display something more interesting than nothing
	   or garbage as the owner of this block. */
	_fmemcpy(MK_FP((unsigned) new_psp - 1, 0x08), "ISR\0\0\0\0", 8);
#endif

	/* Initialize the ISR's data */

	old_isr = getvect(0x1c);

	if ((get_mode() & 0x7f) == 7) {
		screen_seg = MDA_SEG;
	} else {
		screen_seg = CGA_SEG;
	}

	_fmemcpy(new_psp + FP_OFF(isr), isr, isr_size);

	/* Install the ISR in to timer tick interrupt and we're done. */

	setvect(0x1c, (void interrupt (*)(void)) (new_psp + FP_OFF(isr)));

	printf("Old strat: %d; _osmajor: %d\n", old_strat, _osmajor);
	printf("Link state: %d\n", get_umb_link_state());
	printf("isr = %Fp; new_psp = %Fp\n", (void far *)isr,
	       (void far *)new_psp);
	printf("isr_size = %u\n", isr_size);
	printf("isr_paras = %u\n", isr_paras);
	printf("mcb_name = '%Fs'\n", MK_FP((unsigned)new_psp - 1, 0x8));

	return 0;
}