/*
 * xve.c --- X Video Explorer
 */
#include <stdio.h>
#include <math.h>
#include <string.h>
#include <ctype.h>

#ifndef TRUE
typedef int bool;
#define TRUE	1
#define FALSE	0
#endif /* !TRUE */

#ifndef PATH_MAX
#define PATH_MAX	256
#endif

#define KHZ	1000
#define MHZ	1000000
#define K	1024

#define NEW	0
#define CLOCK	1
#define DEPTH	2
#define EDIT	3
#define DONE	4

#ifndef XCONFIG
#define XCONFIG	"/usr/X386/lib/X11/Xconfig"
#endif

/* Floating-point modulo operation.  SVr4 has this directly */
#define fmodf(x, y)	(x - (floor(x/y) * y))

/**************************************************************************
 *
 * Globals 
 *
 **************************************************************************/

/* location of the Xconfig file */
static char *xconfig;

/* informational stuff */
static char card[PATH_MAX];	/* Card type (informational only) */
static char monitor[PATH_MAX];	/* Monitor type (informational only) */

/* the components of our mini-spreadsheet */
struct vparam
{
    char	*tag;
    float	value;
    int		level;
    char	*legend;
}
parameters[] =
{
    /* basic monitor parameters */
    {"HSF",	31.5,	NEW,	"Maximum horizontal scan frequency (in KHz)"},
    {"VSF",	60,	NEW,	"Maximum vertical scan frequency (in Hz)"},
    {"VB",	65,	NEW,	"Video bandwidth (in MHz)"},

    /* basic card parameters */
    {"VRAM",	1024,	NEW,	"The card's on-board VRAM (in K)"},

    /* dot clock */
    {"DCF",	0,	CLOCK,	"The card's dot clock frequency (in MHz)"},

    /* optional monitor parameters (the defaults work for these) */
    {"HSP",	3.8,	NEW,	"Horizontal sync pulse width (microseconds)"},
    {"HGT",	30,	NEW,	"Guard pixels for horizontal sync pulse"},
    {"VSP",	150,	NEW,	"Vertical sync pulse width (in microseconds)"},
    {"VGT",	3,	NEW,	"Guard pixels for vertical sync pulse"},

    /* sizes and sync pulses */
    {"HFL",	0,	CLOCK,	"Horizontal frame length (pixels)"},
    {"VFL",	0,	CLOCK,	"Vertical frame length (pixels)"},
    {"HR",	0,	CLOCK,	"Horizontal resolution (pixels)"},
    {"VR",	0,	CLOCK,	"Vertical resolution (pixels)"},
    {"RR",	0,	CLOCK,	"Refresh rate (screens per second)"},
    {"HS1",	0,	CLOCK,	"Leading edge of horizontal sync (ticks)"},
    {"HS2",	0,	CLOCK,	"Trailing edge of horizontal sync (ticks)"},
    {"VS1",	0,	CLOCK,	"Leading edge of verical sync (ticks)"},
    {"VS2",	0,	CLOCK,	"Trailing edge of verical sync (ticks)"},

    /* pixel depth */
    {"BPP",	8,	DEPTH,	"Bits per pixel"},
	
};

#define hsf	parameters[0]
#define vsf	parameters[1]
#define vb	parameters[2]

#define vram	parameters[3]

#define dcf	parameters[4]

#define hsp	parameters[5]
#define hgt	parameters[6]
#define vsp	parameters[7]
#define vgt	parameters[8]

#define hfl	parameters[9]
#define vfl	parameters[10]
#define hr	parameters[11]
#define vr	parameters[12]
#define rr	parameters[13]
#define hs1	parameters[14]
#define hs2	parameters[15]
#define vs1	parameters[16]
#define vs2	parameters[17]

#define bpp	parameters[18]

#define for_params(sp) \
	    for (sp = parameters; \
		 sp < parameters + sizeof(parameters)/sizeof(struct vparam); \
		 sp++)

/* available clock timings */
static int nclocks;
static float clocks[16];

/**************************************************************************
 *
 * Query functions
 *
 **************************************************************************/

static char buf[BUFSIZ];

static void getstring(char *sparam, const char *prompt)
/* get a string value (default on bare \n) */
{
    if (sparam[0] == '\0')
	(void) printf("%s [unknown]: ", prompt, sparam);
    else
	(void) printf("%s [%s]: ", prompt, sparam);
    (void) fgets(buf, BUFSIZ, stdin);

    if (buf[0] != '\n')
    {
	(void) strcpy(sparam, buf);
	sparam[strlen(sparam) - 1] = '\0';
    }
}

static void getfloat(struct vparam *p)
/* accept a new value for a given parameter */
{
    float	floatval;

    if (p->value == 0)
	(void) printf("%s: %s [unknown]: ", p->tag, p->legend);
    else
	(void) printf("%s: %s [%4.2f]: ", p->tag, p->legend, p->value);
    (void) fgets(buf, BUFSIZ, stdin);
    if (sscanf(buf, "%f\n", &floatval) == 1)
	p->value = floatval;
}

static void getint(struct vparam *p)
/* accept a new value for a given parameter */
{
    float	floatval;

    if (p->value == 0)
	(void) printf("%s: %s [unknown]: ", p->tag, p->legend);
    else
	(void) printf("%s: %s [%d]: ", p->tag, p->legend, (int)p->value);
    (void) fgets(buf, BUFSIZ, stdin);
    if (sscanf(buf, "%f\n", &floatval) == 1)
	p->value = floatval;
}

static char getchoice(const char *prompt, const char *opts, const char deflt)
{
   (void) printf("%s? [%c] ", prompt, deflt);
   (void) fgets(buf, BUFSIZ, stdin);

   if (strchr(opts, buf[0]))
       return(buf[0]);
   else
       return(deflt);
}

static bool yornp(const char *prompt, const bool deflt)
{
   (void) printf("%s? [%c] ", prompt, deflt ? 'y' : 'c');
   (void) fgets(buf, BUFSIZ, stdin);

   if (buf[0] == 'y' || buf[0] == 'Y')
       return(TRUE);
   if (buf[0] == 'n' || buf[0] == 'N')
       return(FALSE);

   return(deflt);
}

static int floatcmp(const float *f1, const float *f2)
/* reverse-compare floats, so we get decreasing order */
{
    return((int) (*f2 - *f1));
}

static void getclocks(char *bfr)
/* fill the clocks array */
{
    char	clockbuf[BUFSIZ], *cp;

    if (bfr == (char *)NULL)
	(void) fgets(clockbuf, BUFSIZ, stdin);
    else
	(void) strcpy(clockbuf, bfr);

    nclocks = 0;
    cp = strtok(clockbuf, " \t,");
    do {
	clocks[nclocks++] = atof(cp);
    } while
	(cp = strtok((char *)NULL, " \t,"));

    qsort(clocks, nclocks, sizeof(float), floatcmp);

    /* lose trailing zero entries */
    while (clocks[nclocks-1] == 0)
	nclocks--;
}

static void redisplay(struct vparam *lo, struct vparam *hi)
/* display a given range of parameters */
{
    struct vparam	*sp;

    if (lo == &dcf)
    {
	int	i;

	(void) printf("CARD:    %s \t# Your card type\n", card);
	(void) printf("MONITOR: %s \t# Your monitor type\n", monitor);

	(void) printf("CLOCKS:");
	for (i = 0; i < nclocks; i++)
	    if (clocks[i])
		(void) printf(" %4.2f", clocks[i]);
	(void) printf("\n");
    }

    for (sp = lo; sp <= hi; sp++)
	if (sp->value)
	    (void) printf("%s: %4.2f \t# %s\n", sp->tag,sp->value,sp->legend);

    (void) putchar('\n');
}

/**************************************************************************
 *
 * Parameter file handling
 *
 **************************************************************************/

static char path[PATH_MAX];
static FILE *pfp;

#define PREFIX(key)	(strncmp(key, buf, strlen(key)) == 0)

static bool read_config()
/* read configuration from file */
{
    char	buf[BUFSIZ];

    (void) sprintf(path, "%s/.xve", getenv("HOME"));

    if ((pfp = fopen(path, "r")) == (FILE *)NULL)
	return(FALSE);

    while (fgets(buf, BUFSIZ, pfp) != (char *)NULL)
    {
	struct vparam	*sp;
	char	*cp;

	if (cp = strchr(buf, '#'))
	{
	    while (isspace(*--cp))
		continue;
	    cp[1] = '\0';
	}

	if (PREFIX("CARD:"))
	    (void) sscanf(buf, "%*s %[^\n]", card);
	else if (PREFIX("MONITOR:"))
	    (void) sscanf(buf, "%*s %[^\n]", monitor);
	else if (PREFIX("CLOCKS:"))
	    getclocks(buf + 7);
	else
	{
	    for_params(sp)
		if (PREFIX(sp->tag))
		    (void) sscanf(buf, "%*s %f", &sp->value);
	}
    }

    (void) fclose(pfp);

    return(TRUE);
}

void write_config(const int level)
/* write out parameter values to the save file */
{
    struct vparam	*sp;
    int			i;

    /* read_config() must have run already for this to work */
    if ((pfp = fopen(path, "w")) == (FILE *)NULL)
	return;

    (void) fprintf(pfp, "# X386 Video Space Explorer parameters\n");
    (void) fprintf(pfp, "CARD:    %s \t# Your card type\n", card);
    (void) fprintf(pfp, "MONITOR: %s \t# Your monitor type\n", monitor);

    (void) fprintf(pfp, "CLOCKS:");
    for (i = 0; i < nclocks; i++)
	if (clocks[i])
	    (void) fprintf(pfp, " %4.2f", clocks[i]);
    (void) fprintf(pfp, "\n");

    for_params(sp)
	if (sp->value && sp->level <= level)
	    (void) fprintf(pfp,"%s: %4.2f \t# %s\n", sp->tag, sp->value, sp->legend);

    (void) fclose(pfp);
}

/**************************************************************************
 *
 * Parameter pickers
 *
 * These functions automate the three stages of picking a trial configuration.
 * They have to run in order; each one takes as input parameters set by the
 * previous one.
 *
 **************************************************************************/

static void new_config()
/* accept basic parameters for a completely new card/monitor combination */
{
    FILE	*fp;

    getstring(card, "Your card type");
    getstring(monitor, "Your monitor type");

    (void) printf("We need to know some things to get started\n\n");

    /* see if we can mine the clocks vector out of the X config file */
    nclocks = 0;
    if ((fp = fopen(xconfig, "r")) != (FILE *)NULL)
    {
	char	*cp;

	while (fgets(buf, BUFSIZ, fp) != (char *)NULL)
	    if (buf[0] != '#' && (cp = strstr(buf, "Clocks")))
	    {
		cp += 7;
		getclocks(cp);
		break;
	    }
	(void) fclose(fp);

	if (nclocks)
	{
	    (void) printf("I found these timings in %s: %s\n", xconfig, cp);
	    if (!yornp("Use them", TRUE))
		nclocks = 0;
	}
    }

    if (nclocks == 0)
    {
	(void) printf("Please enter the available clock timings reported by the X386 banner line:\n");
	(void) printf("CLOCKS: ");
	getclocks((char *)NULL);
    }

    /* get basic params */
    getint(&vram);

    getfloat(&hsf);
    getfloat(&vsf);

    /* get optional params */
    (void) printf("\nIf you don't know these values, use the defaults.\n");

    getfloat(&vb);
    if (vb.value == 0)
    {
	(void) printf("We can estimate the monitor's video bandwidth from its highest-rated\n");
	(void) printf("resolution.  Pick one of the following:\n");
	(void) printf("    a = 640x480   b = 800x600  c = 1024x768  d = 1280x1024\n");
	switch (getchoice("Which one?", "abcd", 'c'))
	{
	case 'a':
	    vb.value = 25;
	    break;

	case 'b':
	    vb.value = 36;
	    break;

	case 'c':
	    vb.value = 65;
	    break;

	case 'd':
	    vb.value = 110;
	    break;
	}
    }

    getfloat(&hsp);
    getint(&hgt);
    getfloat(&vsp);
    getint(&vgt);
}

static void recalc()
/* recalculate derived parameters */
{
    float	deadcenter, syncwidth, excess;

    /* compute our first hack at a horizontal frame length */
    hfl.value = (dcf.value * MHZ) / (hsf.value * KHZ);

    /* horizontal resolution */
    hr.value = hfl.value * 0.8;

    /* now round the frame length to nearest multiple of 8 */
    excess = fmodf(hfl.value, 8);
    hfl.value = hfl.value - excess + 8 * (excess > 4);

    /* vertical resolution */
    vr.value = hr.value * 0.75;

    /* vertical frame length */
    vfl.value = vr.value * 1.05;

    /* trial-position the sync pulses */

    /* center the sync pulse in the inactive part */
    deadcenter = (hfl.value + hr.value) / 2;
    syncwidth = (hsp.value * dcf.value);
    hs1.value = (int) (deadcenter - syncwidth / 2);
    hs2.value = (int) (deadcenter + syncwidth / 2);

    /* we don't center the vertical sync pulse */
    syncwidth = (vsp.value * dcf.value) / hfl.value;
    vs1.value = (int) (vr.value + vgt.value);
    vs2.value = (int) (vs1.value + syncwidth);

    /* figure the refresh rate */
    rr.value = (dcf.value * MHZ) / (hfl.value * vfl.value);
}

static void new_clock()
/* select a clock speed */
{
    for (;;) 
    {
	int	i;
	bool	found = FALSE;

	(void) printf("Available dot clocks are:");
	for (i = 0; i < nclocks; i++)
	    if (clocks[i])
		(void) printf(" %4.2f", clocks[i]);
	(void) printf("\n");

	for (i = 0; i < nclocks; i++)
	    if (clocks[i] <= vb.value)
		break;
	dcf.value = clocks[i];
	getfloat(&dcf);

	for (i = 0; i < nclocks; i++)
	    if (clocks[i] == dcf.value)
		found =TRUE;
	if (!found)
	    continue;

	if (vb.value < dcf.value * .7)
	    (void) printf("Your video bandwidth is probably too low for a %4.2f dot clock.\n", dcf.value);
	else
	    break;
    }

    recalc();
}

static void new_depth()
/* get a new pixel depth for the display */
{
    for (;;)
    {
	getint(&bpp);

	if (bpp.value!=1 && bpp.value!=2 && bpp.value!=4 && bpp.value!=4 && bpp.value!=8)
	    (void) printf("Only 1, 2, 4, or 8-bit displays are supported\n");
	else
	    break;
    }
}

/**************************************************************************
 *
 * Parameter tweaking and recalculation
 *
 **************************************************************************/

static struct vparam controls[] =
{
#define lsh	controls[0]
    {"LSH",	1,	EDIT,	"Left shift (in units of 8 pixels)"},
#define rsh	controls[1]
    {"RSH",	1,	EDIT,	"Right shift (in units of 8 pixels)"},
};

static int sanecheck(const int level)
/* check for configuration problems */
{
#define	P	(void) printf
    if (level >= CLOCK)
    {
	bool sync_ok = TRUE;
	int minhfl;

	if (hs1.value <= hr.value)
	{
	    P("Horizontal sync pulse overlaps the visible area by %.0f ticks.\n",
	      (hr.value - hs1.value) + 1);
	    sync_ok = FALSE;
	}
	else if (hs1.value - hgt.value <= hr.value)
	{
	    P("Horizontal guard time overlaps the visible area by %.0f ticks.\n",
	      (hr.value - (hs1.value - hgt.value)) + 1);
	    sync_ok = FALSE;
	}

	if (hfl.value <= hs2.value)
	{
	    P("Horizontal sync pulse overlaps the end of scan by %.0f ticks.\n",
	      (hs2.value - hs2.value) + 1);
	    sync_ok = FALSE;
	}
	else if (hfl.value <= hs2.value + hgt.value)
	{
	    P("Horizontal guard time overlaps the end of scan by %.0f ticks.\n",
	      (hs2.value + hgt.value - hfl.value) + 1);
	    sync_ok = FALSE;
	}

	if (vs1.value <= vr.value)
	{
	    P("Vertical sync pulse overlaps the visible area by %.0f ticks.\n",
	      (vr.value - vs1.value));
	    sync_ok = FALSE;
	}
	else if (vs1.value - vgt.value <= vr.value)
	{
	    P("Vertical guard time overlaps the visible area by %.0f ticks.\n",
	      (vr.value - (vs1.value - vgt.value)) + 1);
	    sync_ok = FALSE;
	}

	if (vfl.value <= vs2.value)
	{
	    P("Vertical sync pulse overlaps the end of scan by %.0f ticks.\n",
	      (vs2.value - vfl.value) + 1);
	    sync_ok = FALSE;
	}

	minhfl = (int) ((dcf.value * MHZ) / (hsf.value * KHZ));
	minhfl -= fmodf(minhfl, 8);
	if (hfl.value < minhfl)
	{
	    (void) printf("Horizontal frame length must be at least %.0f at this clock speed.\n", minhfl);
	    sync_ok =FALSE;
	}

	if (rr.value > vsf.value)
	{
	    (void) printf("You have pushed the refresh rate above the vertical sync frequency.\n");
	    sync_ok =FALSE;
	}
	else if (rr.value < 45)
	    P("%4.2fHz refresh will hurt your eyes.\n", rr.value);
	else if (rr.value < 60)
	    P("%4.2fHz refresh is pretty poor.\n", rr.value);
	else if (rr.value < 72)
	    P("%4.2fHz refresh is marginally OK.\n", rr.value);

	if (!sync_ok)
	    return(EDIT);
    }

    if (level >= DEPTH && vr.value * hr.value * bpp.value > vram.value * 8 * K)
    {
	P("You don't have enough VRAM for %dx%dx%d.\n",
		      (int)hr.value, (int)vr.value, (int)bpp.value);
	P("You'll have to configure for either fewer colors\n");
	P("or lower resolution (that is, a lower dot clock).\n\n");
	return(CLOCK);
    }
#undef P

    if (level == DONE)
	return(DONE);
    else
	return(level + 1);
}

static int tweak_config()
{
    int	newlevel, c;
    float diff;


    c = '\0';
    for (;;)
    {
	if (c)
	    c = getchoice("Command", "abx", 'x');
	else
	{
	    (void) printf("\nRecomputing...\n");
	    c = '?';
	}

	switch (c)
	{
	case 'a':				/* change HSP */
	    diff = (hsp.value * dcf.value);
	    getfloat(&hsp);
	    diff -= (hsp.value * dcf.value);
	    hs1.value += (diff / 2);
	    hs2.value -= (diff / 2);
	    break;

	case 'b':				/* change VSP width */
	    diff = (vsp.value * dcf.value) / vfl.value;
	    getfloat(&vsp);
	    diff -= (vsp.value * dcf.value) / vfl.value;
	    vs1.value += (diff / 2);
	    vs2.value -= (diff / 2);
	    break;


	case 'c':				/* shift left */
	    getint(&lsh);
	    hs1.value += lsh.value;
	    hs2.value += lsh.value;
	    break;

	case 'd':				/* shift right */
	    getint(&rsh);
	    hs1.value += rsh.value;
	    hs2.value += rsh.value;
	    break;

	case 'x':
	    return(DONE);

	default:
	    (void) printf("Available commands are:\n");
	    /* FALL THROUGH */
	case '?':
	    (void) printf("a -- change horizontal sync pulse width (HSP).\n");
	    (void) printf("b -- change vertical sync pulse width (VSP).\n");
	    (void) printf("c -- shift image to the left.\n");
	    (void) printf("d -- shift image to the right.\n");
	    (void) printf("? -- display commands.\n");
	    (void) printf("x -- return to main menu.\n");
	    break;
	}

	redisplay(&hfl, &vs2);

	(void) printf("Current timing mumbers: %d %d %d %d    %d %d %d %d\n",
	      (int)hr.value, (int)hs1.value, (int)hs2.value, (int)hfl.value,
	      (int)vr.value, (int)vs1.value, (int)vs2.value, (int)vfl.value);

	if ((newlevel = sanecheck(EDIT)) < EDIT)
	    return(newlevel);
	(void) putchar('\n');
    }
}

/**************************************************************************
 *
 * Main sequence
 *
 **************************************************************************/

main(int argc, char **argv)
{
    int	enable;
    char command;

    (void) printf("Welcome to the X Video Explorer.\n\n");

    if (argc > 1)
	xconfig = argv[1];
    else
	xconfig = XCONFIG;

    if (read_config())
    {
	(void) printf("Loading configuration from %s...\n", path);

	redisplay(&dcf, &vs2);

	if (dcf.value == 0)
	    enable = CLOCK;
	else
	    enable = EDIT;
	command = ' ';
    }
    else
    {
	/* force new configuration */
	(void) printf("No parameter file.\n\n");
	command = '0';
    }

    for (;;)
    {
	int	newlevel;

	if (command == ' ')
	{
	    (void) printf("Commands available:\n");
	    (void) printf("  0 - New video card or monitor\n");
	    if (enable >= CLOCK)
		(void) printf("  1 - New dot clock value\n");
	    if (enable >= DEPTH)
		(void) printf("  2 - New pixel depth\n");
	    if (enable >= EDIT)
		(void) printf("  3 - Tweak a configuration\n");
	    if (enable >= DONE)
		(void) printf("  4 - Save configuration and exit\n");
	    command = getchoice("Command", "01234", '0' + enable);
	    (void) printf("\n");
	}

	switch(command)
	{
	case '0':
	    new_config();
	    if (yornp("Save card & monitor parameters", TRUE))
		write_config(NEW);
	    /* FALL THROUGH */
	case '1':
	    new_clock();
	    /* FALL THROUGH */
	case '2':
	    new_depth();
	    /* FALL THROUGH */
	case '3':
	    if ((enable = tweak_config()) < DONE)
		break;
	    /* FALL THROUGH */
	case '4':
	    write_config(DONE);
	    exit(0);
	    break;

	default:
	    (void) printf("Unknown command, please try again.\n");
	    continue;
	}

	command = ' ';
    }
}

/* xve.c ends here */
