/*
    ratings.c

    fics - An internet chess server.
    Copyright (C) 1993  Richard V. Nash

    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.
*/

/* Revision history:
   name		email		yy/mm/dd	Change
   Richard Nash                 93/10/22	Created
   vek leeds@math.gatech.edu    95/04/05	Glicko system, with sterr
   Markus Uhlin                 23/12/17	Fixed the includes
   Markus Uhlin                 23/12/17	Usage of 'time_t'
   Markus Uhlin                 24/04/05	Reformatted ALL functions and
						reenabled disabled code.
   Markus Uhlin                 24/04/05	Replaced unbounded string
						handling functions.
   Markus Uhlin                 24/05/20	Fixed clang warnings
   Markus Uhlin                 24/07/07	Return value checking of the
						fscanf() calls.
   Markus Uhlin                 24/11/27	Added sscanf() width spec and
						fixed ignored retvals.
   Markus Uhlin                 24/11/28	Added null checks
*/

#include "stdinclude.h"
#include "common.h"

#include <err.h>
#include <errno.h>

#include "command.h"
#include "comproc.h"
#include "config.h"
#include "gamedb.h"
#include "lists.h"
#include "playerdb.h"
#include "ratings.h"
#include "utils.h"

#if __linux__
#include <bsd/string.h>
#endif

// Constants for Glicko system
#define Gd	3.25
#define Gr0	1720
#define Gs0	350
#define Gq	0.00575646273249
#define Gp	0.000010072398601964
// End of Glicko system variables

#define LOWESTHIST	800
#define MAXHIST		30

PRIVATE double	Ratings_B_Average;
PRIVATE double	Ratings_B_StdDev;
PRIVATE double	Ratings_S_Average;
PRIVATE double	Ratings_S_StdDev;
PRIVATE double	Ratings_L_Average;
PRIVATE double	Ratings_L_StdDev;
PRIVATE double	Ratings_W_Average;
PRIVATE double	Ratings_W_StdDev;

PRIVATE double	Rb_M = 0.0,
		Rb_S = 0.0,
		Rb_total = 0.0;
PRIVATE int	Rb_count = 0;

PRIVATE double	Rs_M = 0.0,
		Rs_S = 0.0,
		Rs_total = 0.0;
PRIVATE int	Rs_count = 0;

PRIVATE double	Rl_M = 0.0,
		Rl_S = 0.0,
		Rl_total = 0.0;
PRIVATE int	Rl_count = 0;

PRIVATE double	Rw_M = 0.0,
		Rw_S = 0.0,
		Rw_total = 0.0;
PRIVATE int	Rw_count = 0;

PRIVATE rateStruct	bestS[MAX_BEST];
PRIVATE int		numS = 0;
PRIVATE rateStruct	bestB[MAX_BEST];
PRIVATE int		numB = 0;
PRIVATE rateStruct	bestW[MAX_BEST];
PRIVATE int		numW = 0;

PRIVATE int	sHist[MAXHIST] = { 0 };
PRIVATE int	bHist[MAXHIST] = { 0 };
PRIVATE int	wHist[MAXHIST] = { 0 };
PRIVATE int	lHist[MAXHIST] = { 0 };

PRIVATE char sdir[] = DEFAULT_STATS;

PUBLIC int
is_active(int Games)
{
	return (Games >= PROVISIONAL);
}

PUBLIC void
rating_add(int rating, int type)
{
	int	which;

	if ((which = (rating - LOWESTHIST) / 100) < 0)
		which = 0;

	if (which >= MAXHIST)
		which = MAXHIST - 1;

	if (type == TYPE_BLITZ) {
		bHist[which] += 1;

		Rb_count++;
		Rb_total += rating;

		if (Rb_count == 1) {
			Rb_M = rating;
		} else {
			Rb_S = Rb_S + (rating - Rb_M) * (rating - Rb_M);
			Rb_M = Rb_M + (rating - Rb_M) / (Rb_count);
		}

		Ratings_B_StdDev	= sqrt(Rb_S / Rb_count);
		Ratings_B_Average	= (Rb_total / (double)Rb_count);
	} else if (type == TYPE_WILD) { // TYPE_WILD
		wHist[which] += 1;

		Rw_count++;
		Rw_total += rating;

		if (Rw_count == 1) {
			Rw_M = rating;
		} else {
			Rw_S = Rw_S + (rating - Rw_M) * (rating - Rw_M);
			Rw_M = Rw_M + (rating - Rw_M) / (Rw_count);
		}

		Ratings_W_StdDev	= sqrt(Rw_S / Rw_count);
		Ratings_W_Average	= (Rw_total / (double)Rw_count);
	} else if (type == TYPE_LIGHT) { // TYPE_LIGHT
		lHist[which] += 1;

		Rl_count++;
		Rl_total += rating;

		if (Rl_count == 1) {
			Rl_M = rating;
		} else {
			Rl_S = Rl_S + (rating - Rl_M) * (rating - Rl_M);
			Rl_M = Rl_M + (rating - Rl_M) / (Rl_count);
		}

		Ratings_L_StdDev	= sqrt(Rl_S / Rl_count);
		Ratings_L_Average	= (Rl_total / (double)Rl_count);

		// Insert bughouse stuff
	} else { // TYPE_STAND
		sHist[which] += 1;

		Rs_count++;
		Rs_total += rating;

		if (Rs_count == 1) {
			Rs_M = rating;
		} else {
			Rs_S = Rs_S + (rating - Rs_M) * (rating - Rs_M);
			Rs_M = Rs_M + (rating - Rs_M) / (Rs_count);
		}

		Ratings_S_StdDev	= sqrt(Rs_S / Rs_count);
		Ratings_S_Average	= (Rs_total / (double)Rs_count);
	}
}

PUBLIC void
rating_remove(int rating, int type)
{
	int	which;

	if ((which = (rating - LOWESTHIST) / 100) < 0)
		which = 0;

	if (which >= MAXHIST)
		which = MAXHIST - 1;

	if (type == TYPE_BLITZ) {
		bHist[which] = bHist[which] - 1;

		if (bHist[which] < 0)
			bHist[which] = 0;
		if (Rb_count == 0)
			return;

		Rb_count--;
		Rb_total -= rating;

		if (Rb_count == 0) {
			Rb_M = 0;
			Rb_S = 0;
		} else {
			Rb_M = Rb_M - (rating - Rb_M) / (Rb_count);
			Rb_S = Rb_S - (rating - Rb_M) * (rating - Rb_M);

			if (Rb_S < 0)
				Rb_S = 0;
		}

		if (Rb_count) {
			Ratings_B_StdDev	= sqrt(Rb_S / Rb_count);
			Ratings_B_Average	= (Rb_total / (double)Rb_count);
		} else {
			Ratings_B_StdDev	= 0;
			Ratings_B_Average	= 0;
		}
	} else if (type == TYPE_WILD) { // TYPE_WILD
		wHist[which] = wHist[which] - 1;

		if (wHist[which] < 0)
			wHist[which] = 0;
		if (Rw_count == 0)
			return;

		Rw_count--;
		Rw_total -= rating;

		if (Rw_count == 0) {
			Rw_M = 0;
			Rw_S = 0;
		} else {
			Rw_M = Rw_M - (rating - Rw_M) / (Rw_count);
			Rw_S = Rw_S - (rating - Rw_M) * (rating - Rw_M);

			if (Rw_S < 0)
				Rw_S = 0;
		}

		if (Rw_count) {
			Ratings_W_StdDev	= sqrt(Rw_S / Rw_count);
			Ratings_W_Average	= (Rw_total / (double)Rw_count);
		} else {
			Ratings_W_StdDev	= 0;
			Ratings_W_Average	= 0;
		}
	} else if (type == TYPE_LIGHT) { // TYPE_LIGHT
		lHist[which] = lHist[which] - 1;

		if (lHist[which] < 0)
			lHist[which] = 0;
		if (Rl_count == 0)
			return;

		Rl_count--;
		Rl_total -= rating;

		if (Rl_count == 0) {
			Rl_M = 0;
			Rl_S = 0;
		} else {
			Rl_M = Rl_M - (rating - Rl_M) / (Rl_count);
			Rl_S = Rl_S - (rating - Rl_M) * (rating - Rl_M);

			if (Rl_S < 0)
				Rl_S = 0;
		}

		if (Rl_count) {
			Ratings_L_StdDev	= sqrt(Rl_S / Rl_count);
			Ratings_L_Average	= (Rl_total / (double)Rl_count);
		} else {
			Ratings_L_StdDev	= 0;
			Ratings_L_Average	= 0;
		}

		// insert bughouse stuff here
	} else { // TYPE_STAND
		sHist[which] = sHist[which] - 1;

		if (sHist[which] < 0)
			sHist[which] = 0;
		if (Rs_count == 0)
			return;

		Rs_count--;
		Rs_total -= rating;

		if (Rs_count == 0) {
			Rs_M = 0;
			Rs_S = 0;
		} else {
			Rs_M = Rs_M - (rating - Rs_M) / (Rs_count);
			Rs_S = Rs_S - (rating - Rs_M) * (rating - Rs_M);

			if (Rs_S < 0)
				Rs_S = 0;
		}

		if (Rs_count) {
			Ratings_S_StdDev	= sqrt(Rs_S / Rs_count);
			Ratings_S_Average	= (Rs_total / (double)Rs_count);
		} else {
			Ratings_S_StdDev	= 0;
			Ratings_S_Average	= 0;
		}
	}
}

PRIVATE void
load_ratings(void)
{
	FILE	*fp;
	char	 fname[MAX_FILENAME_SIZE] = { '\0' };

	snprintf(fname, sizeof fname, "%s/newratingsV%d_data", stats_dir,
	    STATS_VERSION);

	if ((fp = fopen(fname, "r")) == NULL) {
		warn("%s: can't read ratings data", __func__);
		return;
	} else if (fscanf(fp, "%lf %lf %lf %d", &Rb_M, &Rb_S, &Rb_total,
	    &Rb_count) != 4 ||
	    fscanf(fp, "%lf %lf %lf %d", &Rs_M, &Rs_S, &Rs_total,
	    &Rs_count) != 4 ||
	    fscanf(fp, "%lf %lf %lf %d", &Rw_M, &Rw_S, &Rw_total,
	    &Rw_count) != 4 ||
	    fscanf(fp, "%lf %lf %lf %d", &Rl_M, &Rl_S, &Rl_total,
	    &Rl_count) != 4) {
		warn("%s: fscanf", __func__);
		fclose(fp);
		return;
	}

	for (int i = 0; i < MAXHIST; i++) {
		int ret, errno_save;

		sHist[i] = bHist[i] = wHist[i] = lHist[i] = 0;

		errno = 0;
		ret = fscanf(fp, "%d %d %d %d", &sHist[i], &bHist[i], &wHist[i],
		    &lHist[i]);
		errno_save = errno;
		if (ret != 4) {
			if (feof(fp) || ferror(fp))
				break;
			errno = errno_save;
			warn("%s: too few items assigned (iteration: %d)",
			    __func__, i);
		}
	}

	fclose(fp);

	if (Rs_count) {
		Ratings_S_StdDev	= sqrt(Rs_S / Rs_count);
		Ratings_S_Average	= (Rs_total / (double)Rs_count);
	} else {
		Ratings_S_StdDev	= 0;
		Ratings_S_Average	= 0;
	}
	if (Rb_count) {
		Ratings_B_StdDev	= sqrt(Rb_S / Rb_count);
		Ratings_B_Average	= (Rb_total / (double)Rb_count);
	} else {
		Ratings_B_StdDev	= 0;
		Ratings_B_Average	= 0;
	}
	if (Rw_count) {
		Ratings_W_StdDev	= sqrt(Rw_S / Rw_count);
		Ratings_W_Average	= (Rw_total / (double)Rw_count);
	} else {
		Ratings_W_StdDev	= 0;
		Ratings_W_Average	= 0;
	}
	if (Rl_count) {
		Ratings_L_StdDev	= sqrt(Rl_S / Rl_count);
		Ratings_L_Average	= (Rl_total / (double)Rl_count);
	} else {
		Ratings_L_StdDev	= 0;
		Ratings_L_Average	= 0;
	}
}

PUBLIC void
save_ratings(void)
{
	FILE	*fp;
	char	 fname[MAX_FILENAME_SIZE] = { '\0' };

	snprintf(fname, sizeof fname, "%s/newratingsV%d_data", stats_dir,
	    STATS_VERSION);

	if ((fp = fopen(fname, "w")) == NULL) {
		warn("%s: can't write ratings data", __func__);
		return;
	}

	fprintf(fp, "%10f %10f %10f %d\n", Rb_M, Rb_S, Rb_total, Rb_count);
	fprintf(fp, "%10f %10f %10f %d\n", Rs_M, Rs_S, Rs_total, Rs_count);
	fprintf(fp, "%10f %10f %10f %d\n", Rw_M, Rw_S, Rw_total, Rw_count);
	fprintf(fp, "%10f %10f %10f %d\n", Rl_M, Rl_S, Rl_total, Rl_count);

	for (int i = 0; i < MAXHIST; i++) {
		fprintf(fp, "%d %d %d %d\n", sHist[i], bHist[i], wHist[i],
		    lHist[i]);
	}

	fclose(fp);
}

PRIVATE void
BestRemove(int p)
{
	int	i;

	for (i = 0; i < numB; i++) {
		if (!strcmp(bestB[i].name, parray[p].name))
			break;
	}
	if (i < numB) {
		for (; i < numB - 1; i++) {
			strlcpy(bestB[i].name, bestB[i + 1].name,
			    sizeof bestB[i].name);
			bestB[i].rating = bestB[i + 1].rating;
		}
		numB--;
	}

	for (i = 0; i < numS; i++) {
		if (!strcmp(bestS[i].name, parray[p].name))
			break;
	}
	if (i < numS) {
		for (; i < numS - 1; i++) {
			strlcpy(bestS[i].name, bestS[i + 1].name,
			    sizeof bestS[i].name);
			bestS[i].rating = bestS[i + 1].rating;
		}
		numS--;
	}

	for (i = 0; i < numW; i++) {
		if (!strcmp(bestW[i].name, parray[p].name))
			break;
	}
	if (i < numW) {
		for (; i < numW - 1; i++) {
			strlcpy(bestW[i].name, bestW[i + 1].name,
			    sizeof bestW[i].name);
			bestW[i].rating = bestW[i + 1].rating;
		}
		numW--;
	}
}

PRIVATE void
BestAdd(int p)
{
	int where, j;

	if (parray[p].b_stats.rating > 0 &&
	    parray[p].b_stats.num > 19) {
		for (where = 0; where < numB; where++) {
			if (parray[p].b_stats.rating > bestB[where].rating)
				break;
		}

		if (where < MAX_BEST) {
			for (j = numB; j > where; j--) {
				if (j == MAX_BEST)
					continue;
				strlcpy(bestB[j].name, bestB[j - 1].name,
				    sizeof(bestB[j].name));
				bestB[j].rating = bestB[j - 1].rating;
			}

			strlcpy(bestB[where].name, parray[p].name,
			    sizeof(bestB[where].name));
			bestB[where].rating = parray[p].b_stats.rating;

			if (numB < MAX_BEST)
				numB++;
		}
	}

	if (parray[p].s_stats.rating > 0 &&
	    parray[p].s_stats.num > 19) {
		for (where = 0; where < numS; where++) {
			if (parray[p].s_stats.rating > bestS[where].rating)
				break;
		}

		if (where < MAX_BEST) {
			for (j = numS; j > where; j--) {
				if (j == MAX_BEST)
					continue;
				strlcpy(bestS[j].name, bestS[j - 1].name,
				    sizeof(bestS[j].name));
				bestS[j].rating = bestS[j - 1].rating;
			}

			strlcpy(bestS[where].name, parray[p].name,
			    sizeof(bestS[where].name));
			bestS[where].rating = parray[p].s_stats.rating;

			if (numS < MAX_BEST)
				numS++;
		}
	}

	if (parray[p].w_stats.rating > 0 &&
	    parray[p].w_stats.num > 19) {
		for (where = 0; where < numW; where++) {
			if (parray[p].w_stats.rating > bestW[where].rating)
				break;
		}

		if (where < MAX_BEST) {
			for (j = numW; j > where; j--) {
				if (j == MAX_BEST)
					continue;
				strlcpy(bestW[j].name, bestW[j - 1].name,
				    sizeof(bestW[j].name));
				bestW[j].rating = bestW[j - 1].rating;
			}

			strlcpy(bestW[where].name, parray[p].name,
			    sizeof(bestW[where].name));
			bestW[where].rating = parray[p].w_stats.rating;

			if (numW < MAX_BEST)
				numW++;
		}
	}
}

PUBLIC void
BestUpdate(int p)
{
	BestRemove(p);
	BestAdd(p);
}

PRIVATE void
zero_stats(void)
{
	for (int i = 0; i < MAXHIST; i++) {
		sHist[i] = 0;
		bHist[i] = 0;
		wHist[i] = 0;
		lHist[i] = 0;
	}

	Rb_M = 0.0, Rb_S = 0.0, Rb_total = 0.0;
	Rb_count = 0;

	Rs_M = 0.0, Rs_S = 0.0, Rs_total = 0.0;
	Rs_count = 0;

	Rw_M = 0.0, Rw_S = 0.0, Rw_total = 0.0;
	Rw_count = 0;

	Rl_M = 0.0, Rl_S = 0.0, Rl_total = 0.0;
	Rl_count = 0;

	numS = 0;
	numB = 0;
	numW = 0;
}

#if 0
PUBLIC int
com_best(int p, param_list param)
{
	pprintf(p, "Standard                Blitz                   Wild\n");

	for (int i = 0; i < MAX_BEST; i++) {
		if (i >= numS && i >= numB)
			break;

		if (i < numS) {
			pprintf(p, "%4d %-17s  ",
			    bestS[i].rating,
			    bestS[i].name);
		} else {
			pprintf(p, "                        ");
		}

		if (i < numB) {
			pprintf(p, "%4d %-17s  ",
			    bestB[i].rating,
			    bestB[i].name);
		} else {
			pprintf(p, "                        ");
		}

		if (i < numW) {
			pprintf(p, "%4d %-17s\n",
			    bestW[i].rating,
			    bestW[i].name);
		} else {
			pprintf(p, "\n");
		}
	}

	return COM_OK;
}
#endif

PUBLIC void
rating_init(void)
{
	zero_stats();
	load_ratings();
}

/*
 * This recalculates the rating info from the player data. (Which can
 * take a long time!)
 */
PUBLIC void
rating_recalc(void)
{
	DIR		*dirp;
	char		 dname[MAX_FILENAME_SIZE];
	int		 c;
	int		 p1;
#if USE_DIRENT
	struct dirent	*dp;
#else
	struct direct	*dp;
#endif
	time_t		 t = time(NULL);

	fprintf(stderr, "FICS: Recalculating ratings at %s\n", strltime(&t));
	zero_stats();

	for (c = 'a'; c <= 'z'; c++) {
		snprintf(dname, sizeof dname, "%s/%c", player_dir, c);

		if ((dirp = opendir(dname)) == NULL)
			continue;

		for (dp = readdir(dirp); dp != NULL; dp = readdir(dirp)) {
			if (dp->d_name[0] != '.') {
				p1 = player_new();

				if (player_read(p1, dp->d_name)) {
					player_remove(p1);

					fprintf(stderr, "FICS: Problem reading "
					    "player %s.\n", dp->d_name);

					continue;
				}

				if (parray[p1].b_stats.rating > 0) {
					rating_add(parray[p1].b_stats.rating,
					    TYPE_BLITZ);
				}
				if (parray[p1].s_stats.rating > 0) {
					rating_add(parray[p1].s_stats.rating,
					    TYPE_STAND);
				}
				if (parray[p1].l_stats.rating > 0) {
					rating_add(parray[p1].l_stats.rating,
					    TYPE_LIGHT);
				}

				// insert bughouse stuff here

				if (parray[p1].w_stats.rating > 0) {
					rating_add(parray[p1].w_stats.rating,
					    TYPE_WILD);
				}

				player_remove(p1);
			}
		} /* for */

		closedir(dirp);
	} /* for */

	if (Rs_count) {
		Ratings_S_StdDev	= sqrt(Rs_S / Rs_count);
		Ratings_S_Average	= (Rs_total / (double)Rs_count);
	} else {
		Ratings_S_StdDev	= 0;
		Ratings_S_Average	= 0;
	}
	if (Rb_count) {
		Ratings_B_StdDev	= sqrt(Rb_S / Rb_count);
		Ratings_B_Average	= (Rb_total / (double)Rb_count);
	} else {
		Ratings_B_StdDev	= 0;
		Ratings_B_Average	= 0;
	}
	if (Rl_count) {
		Ratings_L_StdDev	= sqrt(Rl_S / Rl_count);
		Ratings_L_Average	= (Rl_total / (double)Rl_count);
	} else {
		Ratings_L_StdDev	= 0;
		Ratings_L_Average	= 0;
	}
	if (Rw_count) {
		Ratings_W_StdDev	= sqrt(Rw_S / Rw_count);
		Ratings_W_Average	= (Rw_total / (double)Rw_count);
	} else {
		Ratings_W_StdDev	= 0;
		Ratings_W_Average	= 0;
	}

	save_ratings();

	t = time(NULL);
	fprintf(stderr, "FICS: Finished at %s\n", strltime(&t));
}

PRIVATE int
Round(double x)
{
	return (x < 0 ? (int)(x - 0.5) : (int)(x + 0.5));
}

PRIVATE double
Gf(double ss)
{
	return (1.0 / sqrt(1.0 + Gp * ss * ss));
}

/*
 * Confusing but economical: calculate error and attenuation function
 * together.
 */
PRIVATE double
GE(int r, int rr, double ss, double *fss)
{
	*fss = Gf(ss);
	return (1.0 / (1.0 + pow(10.0, (rr - r) * (*fss) / 400.0)));
}

PRIVATE double
current_sterr(double s, int t)
{
	if (t < 0)
		t = 0; // this shouldn't happen
	return sqrt(s * s + Gd * Gd * log(1.0 + t / 60.0));
}

/*
 * Calculates new rating and standard error. By vek. The person who
 * invented the ratings system is Mark E. Glickman, Ph.D. His e-mail
 * address is glickman@hustat.harvard.edu as of April '95. Postscript
 * copy of the note I coded this from should be available for ftp from
 * ics.onenet.net, if not elsewhere.
 */
PUBLIC void
rating_sterr_delta(int p1, int p2, int type, int gtime, int result,
    int *deltarating, double *newsterr)
{
	double		 E, fs2, denominator, GK, w; // Parts of fancy formulas
	double		 delta, sigma; // Results to return
	double		 s1, s2;
	int		 t1, r1, t2, r2; // Initial sterrs and ratings
	statistics	*p1_stats;
	statistics	*p2_stats;

	if (type == TYPE_BLITZ) {
		p1_stats = &parray[p1].b_stats;
		p2_stats = &parray[p2].b_stats;
	} else if (type == TYPE_WILD) {
		p1_stats = &parray[p1].w_stats;
		p2_stats = &parray[p2].w_stats;
	} else if (type == TYPE_LIGHT) {
		p1_stats = &parray[p1].l_stats;
		p2_stats = &parray[p2].l_stats;

		// insert bughouse stuff here
	} else {
		p1_stats = &parray[p1].s_stats;
		p2_stats = &parray[p2].s_stats;
	}

	/*
	 * Calculate effective pre-game sterr's. ltime == 0 implies
	 * never had sterr.
	 */
	if (p1_stats->ltime == 0)
		s1 = Gs0;
	else {
		t1 = gtime - p1_stats->ltime;
		s1 = current_sterr(p1_stats->sterr, t1);

		if (s1 > Gs0)
			s1 = Gs0;
	}

	if (p2_stats->ltime == 0)
		s2 = Gs0;
	else {
		t2 = gtime - p2_stats->ltime;
		s2 = current_sterr(p2_stats->sterr, t2);

		if (s2 > Gs0)
			s2 = Gs0;
	}

	if (p1_stats->rating == 0 && p1_stats->num == 0)
		r1 = Gr0;
	else
		r1 = p1_stats->rating;

	if (p2_stats->rating == 0 && p2_stats->num == 0)
		r2 = Gr0;
	else
		r2 = p2_stats->rating;

	if (result == RESULT_WIN) {
		w = 1.0;
	} else if (result == RESULT_DRAW) {
		w = 0.5;
	} else {
		w = 0.0;
	}

	E = GE(r1, r2, s2, &fs2);
	denominator = 1.0 / (s1 * s1) + Gq * Gq * fs2 * fs2 * E * (1.0 - E);
	GK = Gq * fs2 / denominator;
	delta = GK * (w - E);

	if (p1_stats->rating == 0 && p1_stats->num == 0)
		*deltarating = Round(Gr0 + delta);
	else
		*deltarating = Round(delta);	// Returned values: deltarating,
						// newsterr.

	sigma = 1.0 / sqrt(denominator);
	*newsterr = (double) sigma;
}

PUBLIC int
rating_delta(int p1, int p2, int type, int result, int gtime)
{
	int	delta;
	double	sigma;

	rating_sterr_delta(p1, p2, type, gtime, result, &delta, &sigma);
	return delta;
}

PUBLIC int
rating_update(int g)
{
	double		 wSigma, bSigma;
	int		 gtime;
	int		 inprogress = (g == parray[garray[g].black].game);
	int		 wDelta, bDelta;
	int		 wRes, bRes;
	statistics	*b_stats;
	statistics	*w_stats;

	/*
	 * If this is adjudication of stored game - be quiet about
	 * ratings change.
	 */
	if (garray[g].type == TYPE_BLITZ) {
		w_stats = &parray[garray[g].white].b_stats;
		b_stats = &parray[garray[g].black].b_stats;
	} else if (garray[g].type == TYPE_STAND) {
		w_stats = &parray[garray[g].white].s_stats;
		b_stats = &parray[garray[g].black].s_stats;
	} else if (garray[g].type == TYPE_WILD) {
		w_stats = &parray[garray[g].white].w_stats;
		b_stats = &parray[garray[g].black].w_stats;
	} else if (garray[g].type == TYPE_LIGHT) {
		w_stats = &parray[garray[g].white].l_stats;
		b_stats = &parray[garray[g].black].l_stats;
	} else {
		fprintf(stderr, "FICS: Can't update untimed ratings!\n");
		return -1;
	}

	switch (garray[g].result) {
	case END_CHECKMATE:
	case END_RESIGN:
	case END_FLAG:
	case END_ADJWIN:
		if (garray[g].winner == WHITE) {
			wRes = RESULT_WIN;
			bRes = RESULT_LOSS;
		} else {
			bRes = RESULT_WIN;
			wRes = RESULT_LOSS;
		}
		break;
	case END_AGREEDDRAW:
	case END_REPETITION:
	case END_50MOVERULE:
	case END_STALEMATE:
	case END_NOMATERIAL:
	case END_BOTHFLAG:
	case END_ADJDRAW:
	case END_FLAGNOMATERIAL:
		wRes = bRes = RESULT_DRAW;
		break;
	default:
		fprintf(stderr, "FICS: Update undecided game %d?\n",
		    garray[g].result);
		return -1;
	}

	gtime = untenths(garray[g].timeOfStart);

	rating_sterr_delta(garray[g].white, garray[g].black, garray[g].type,
	    gtime, wRes, &wDelta, &wSigma);
	rating_sterr_delta(garray[g].black, garray[g].white, garray[g].type,
	    gtime, bRes, &bDelta, &bSigma);

	w_stats->ltime = gtime;
	b_stats->ltime = gtime;

	if (wRes == RESULT_WIN) {
		w_stats->win++;
	} else if (wRes == RESULT_LOSS) {
		w_stats->los++;
	} else {
		w_stats->dra++;
	}

	w_stats->num++;

	if (bRes == RESULT_WIN) {
		b_stats->win++;
	} else if (bRes == RESULT_LOSS) {
		b_stats->los++;
	} else {
		b_stats->dra++;
	}

	b_stats->num++;

	rating_remove(w_stats->rating, garray[g].type);
	rating_remove(b_stats->rating, garray[g].type);

	if (inprogress) {
		pprintf(garray[g].white, "%s rating adjustment: %d ",
		    ((garray[g].type == TYPE_BLITZ) ? "Blitz" :
		    ((garray[g].type == TYPE_WILD) ? "Wild" :
		    ((garray[g].type == TYPE_LIGHT) ? "Lightning" :
		    "Standard"))),
		    w_stats->rating);
		pprintf(garray[g].black, "%s rating adjustment: %d ",
		    ((garray[g].type == TYPE_BLITZ) ? "Blitz" :
		    ((garray[g].type == TYPE_WILD) ? "Wild" :
		    ((garray[g].type == TYPE_LIGHT) ? "Lightning" :
		    "Standard"))),
		    b_stats->rating);
	}

	if (wDelta < -1000) {
		pprintf(garray[g].white, "not changed due to bug "
		    "(way too small)! sorry!\n");
		fprintf(stderr, "FICS: Got too small ratings bug for %s "
		    "(w) vs. %s\n",
		    parray[garray[g].white].login,
		    parray[garray[g].black].login);
	} else if (wDelta > 3000) {
		pprintf(garray[g].white, "not changed due to bug "
		    "(way too big)! sorry!\n");
		fprintf(stderr, "FICS: Got too big ratings bug for %s "
		    "(w) vs. %s\n",
		    parray[garray[g].white].login,
		    parray[garray[g].black].login);
	} else {
		w_stats->rating += wDelta;
		w_stats->sterr = wSigma;
	}

	if (bDelta < -1000) {
		pprintf(garray[g].black, "not changed due to bug "
		    "(way too small)! sorry!\n");
		fprintf(stderr, "FICS: Got too small ratings bug for %s "
		    "(b) vs. %s\n",
		    parray[garray[g].black].login,
		    parray[garray[g].white].login);
	} else if (bDelta > 3000) {
		pprintf(garray[g].black, "not changed due to bug "
		    "(way too big)! sorry!\n");
		fprintf(stderr, "FICS: Got too big ratings bug for %s "
		    "(b) vs. %s\n",
		    parray[garray[g].black].login,
		    parray[garray[g].white].login);
	} else {
		b_stats->rating += bDelta;
		b_stats->sterr = bSigma;
	}

	rating_add(w_stats->rating, garray[g].type);
	rating_add(b_stats->rating, garray[g].type);

	if (w_stats->rating > w_stats->best &&
	    is_active(w_stats->num)) {
		w_stats->best = w_stats->rating;
		w_stats->whenbest = time(NULL);
	}
	if (b_stats->rating > b_stats->best &&
	    is_active(b_stats->num)) {
		b_stats->best = b_stats->rating;
		b_stats->whenbest = time(NULL);
	}

	// Ratings are now saved to disk after each game
	player_save(garray[g].white);
	player_save(garray[g].black);

	// foxbat 3.11.95
	if (garray[g].type == TYPE_BLITZ) {
		Rb_count++;
		Rb_total += (w_stats->rating + b_stats->rating) / 2.0;
	} else if (garray[g].type == TYPE_STAND) {
		Rs_count++;
		Rs_total += (w_stats->rating + b_stats->rating) / 2.0;
	} else if (garray[g].type == TYPE_LIGHT) {
		Rl_count++;
		Rl_total += (w_stats->rating + b_stats->rating) / 2.0;
	} else if (garray[g].type == TYPE_WILD) {
		Rw_count++;
		Rw_total += (w_stats->rating + b_stats->rating) / 2.0;
	}

	// end add
	if (inprogress) {
		pprintf(garray[g].white, "--> %d\n", w_stats->rating);
		pprintf(garray[g].black, "--> %d\n", b_stats->rating);
	}

	save_ratings();

	UpdateRank(garray[g].type, parray[garray[g].white].name, w_stats,
	    parray[garray[g].white].name);
	UpdateRank(garray[g].type, parray[garray[g].black].name, b_stats,
	    parray[garray[g].black].name);
	return 0;
}

PUBLIC int
com_assess(int p, param_list param)
{
	double	newsterr1;
	double	newsterr2;
	int	p1 = p, p2;
	int	p1_connected = 1, p2_connected = 1;
	int	win1, draw1, loss1;
	int	win2, draw2, loss2;
	time_t	nowtime;

	nowtime = time(NULL);

	if (param[0].type == TYPE_NULL) {
		if (parray[p].game < 0) {
			pprintf(p, "You are not playing a game.\n");
			return COM_OK;
		} else if (garray[parray[p].game].status == GAME_EXAMINE) {
			if (!strcmp(garray[parray[p].game].black_name,
			    parray[p].name)) {
				pcommand(p, "assess %s\n",
				    garray[parray[p].game].white_name);
			} else {
				pcommand(p, "assess %s %s\n",
				    garray[parray[p].game].white_name,
				    garray[parray[p].game].black_name);
			}

			return COM_OK;
		} else {
			p2 = parray[p].opponent;
		}
	} else {
		if (!FindPlayer(p, param[0].val.word, &p2, &p2_connected)) {
			pprintf(p, "No user named \"%s\" was found.\n",
			    param[0].val.word);
			return COM_OK;
		}

		if (param[1].type != TYPE_NULL) {
			p1 = p2;
			p1_connected = p2_connected;

			if (!FindPlayer(p, param[1].val.word, &p2,
			    &p2_connected)) {
				pprintf(p, "No user named \"%s\" was found.\n",
				    param[1].val.word);

				if (!p1_connected)
					player_remove(p1);
				return COM_OK;
			}
		}
	}

	if (p1 == p2) {
		pprintf(p, "You can't assess the same players.\n");

		if (!p1_connected)
			player_remove(p1);
		if (!p2_connected)
			player_remove(p2);
		return COM_OK;
	}

	rating_sterr_delta(p1, p2, TYPE_BLITZ, nowtime, RESULT_WIN, &win1,
	    &newsterr1);
	rating_sterr_delta(p1, p2, TYPE_BLITZ, nowtime, RESULT_DRAW, &draw1,
	    &newsterr1);
	rating_sterr_delta(p1, p2, TYPE_BLITZ, nowtime, RESULT_LOSS, &loss1,
	    &newsterr1);
	rating_sterr_delta(p2, p1, TYPE_BLITZ, nowtime, RESULT_WIN, &win2,
	    &newsterr2);
	rating_sterr_delta(p2, p1, TYPE_BLITZ, nowtime, RESULT_DRAW, &draw2,
	    &newsterr2);
	rating_sterr_delta(p2, p1, TYPE_BLITZ, nowtime, RESULT_LOSS, &loss2,
	    &newsterr2);

	pprintf(p, "\nBlitz\n   %10s (%4s, RD: %5.1f)  %10s (%4s, RD: %5.1f)\n",
	    parray[p1].name,
	    ratstrii(parray[p1].b_stats.rating, parray[p1].registered),
	    parray[p1].b_stats.sterr,
	    parray[p2].name,
	    ratstrii(parray[p2].b_stats.rating, parray[p2].registered),
	    parray[p2].b_stats.sterr);

	pprintf(p, "Win :         %4d                         %4d\n",
	    win1,
	    loss2);
	pprintf(p, "Draw:         %4d                         %4d\n",
	    draw1,
	    draw2);
	pprintf(p, "Loss:         %4d                         %4d\n",
	    loss1,
	    win2);
	pprintf(p, "New RD:        %5.1f                        %5.1f\n",
	    newsterr1,
	    newsterr2);

	rating_sterr_delta(p1, p2, TYPE_STAND, nowtime, RESULT_WIN, &win1,
	    &newsterr1);
	rating_sterr_delta(p1, p2, TYPE_STAND, nowtime, RESULT_DRAW, &draw1,
	    &newsterr1);
	rating_sterr_delta(p1, p2, TYPE_STAND, nowtime, RESULT_LOSS, &loss1,
	    &newsterr1);
	rating_sterr_delta(p2, p1, TYPE_STAND, nowtime, RESULT_WIN, &win2,
	    &newsterr2);
	rating_sterr_delta(p2, p1, TYPE_STAND, nowtime, RESULT_DRAW, &draw2,
	    &newsterr2);
	rating_sterr_delta(p2, p1, TYPE_STAND, nowtime, RESULT_LOSS, &loss2,
	    &newsterr2);

	pprintf(p, "\nStandard\n   %10s (%4s, RD: %5.1f)  %10s "
	    "(%4s, RD: %5.1f)\n",
	    parray[p1].name,
	    ratstrii(parray[p1].s_stats.rating, parray[p1].registered),
	    parray[p1].s_stats.sterr,
	    parray[p2].name,
	    ratstrii(parray[p2].s_stats.rating, parray[p2].registered),
	    parray[p2].s_stats.sterr);

	pprintf(p, "Win :         %4d                         %4d\n",
	    win1,
	    loss2);
	pprintf(p, "Draw:         %4d                         %4d\n",
	    draw1,
	    draw2);
	pprintf(p, "Loss:         %4d                         %4d\n",
	    loss1,
	    win2);
	pprintf(p, "New RD:        %5.1f                        %5.1f\n",
	    newsterr1,
	    newsterr2);

	rating_sterr_delta(p1, p2, TYPE_LIGHT, nowtime, RESULT_WIN, &win1,
	    &newsterr1);
	rating_sterr_delta(p1, p2, TYPE_LIGHT, nowtime, RESULT_DRAW, &draw1,
	    &newsterr1);
	rating_sterr_delta(p1, p2, TYPE_LIGHT, nowtime, RESULT_LOSS, &loss1,
	    &newsterr1);
	rating_sterr_delta(p2, p1, TYPE_LIGHT, nowtime, RESULT_WIN, &win2,
	    &newsterr2);
	rating_sterr_delta(p2, p1, TYPE_LIGHT, nowtime, RESULT_DRAW, &draw2,
	    &newsterr2);
	rating_sterr_delta(p2, p1, TYPE_LIGHT, nowtime, RESULT_LOSS, &loss2,
	    &newsterr2);

	pprintf(p, "\nLightning\n   %10s (%4s, RD: %5.1f)  %10s "
	    "(%4s, RD: %5.1f)\n",
	    parray[p1].name,
	    ratstrii(parray[p1].l_stats.rating, parray[p1].registered),
	    parray[p1].l_stats.sterr,
	    parray[p2].name,
	    ratstrii(parray[p2].l_stats.rating, parray[p2].registered),
	    parray[p2].l_stats.sterr);

	pprintf(p, "Win :         %4d                         %4d\n",
	    win1,
	    loss2);
	pprintf(p, "Draw:         %4d                         %4d\n",
	    draw1,
	    draw2);
	pprintf(p, "Loss:         %4d                         %4d\n",
	    loss1,
	    win2);
	pprintf(p, "New RD:        %5.1f                        %5.1f\n",
	    newsterr1,
	    newsterr2);

	rating_sterr_delta(p1, p2, TYPE_WILD, nowtime, RESULT_WIN, &win1,
	    &newsterr1);
	rating_sterr_delta(p1, p2, TYPE_WILD, nowtime, RESULT_DRAW, &draw1,
	    &newsterr1);
	rating_sterr_delta(p1, p2, TYPE_WILD, nowtime, RESULT_LOSS, &loss1,
	    &newsterr1);

	rating_sterr_delta(p2, p1, TYPE_WILD, nowtime, RESULT_WIN, &win2,
	    &newsterr2);
	rating_sterr_delta(p2, p1, TYPE_WILD, nowtime, RESULT_DRAW, &draw2,
	    &newsterr2);
	rating_sterr_delta(p2, p1, TYPE_WILD, nowtime, RESULT_LOSS, &loss2,
	    &newsterr2);

	pprintf(p, "\nWild\n   %10s (%4s, RD: %5.1f)  %10s (%4s, RD: %5.1f)\n",
	    parray[p1].name,
	    ratstrii(parray[p1].w_stats.rating, parray[p1].registered),
	    parray[p1].w_stats.sterr,
	    parray[p2].name,
	    ratstrii(parray[p2].w_stats.rating, parray[p2].registered),
	    parray[p2].w_stats.sterr);

	pprintf(p, "Win :         %4d                         %4d\n",
	    win1,
	    loss2);
	pprintf(p, "Draw:         %4d                         %4d\n",
	    draw1,
	    draw2);
	pprintf(p, "Loss:         %4d                         %4d\n",
	    loss1,
	    win2);
	pprintf(p, "New RD:        %5.1f                        %5.1f\n",
	    newsterr1,
	    newsterr2);

	if (!p1_connected)
		player_remove(p1);
	if (!p2_connected)
		player_remove(p2);
	return COM_OK;
}

PUBLIC int
com_best(int p, param_list param)
{
	return Best(p, param, 1);
}

PUBLIC int
com_hbest(int p, param_list param)
{
	return Best(p, param, 0);
}

PUBLIC int
com_statistics(int p, param_list param)
{
	pprintf(p, "                Standard       Blitz   Lightning        "
	    "Wild\n");
	pprintf(p, "average:         %7.2f     %7.2f     %7.2f     %7.2f\n",
	    Ratings_S_Average,
	    Ratings_B_Average,
	    Ratings_L_Average,
	    Ratings_W_Average);
	pprintf(p, "std dev:         %7.2f     %7.2f     %7.2f     %7.2f\n",
	    Ratings_S_StdDev,
	    Ratings_B_StdDev,
	    Ratings_L_StdDev,
	    Ratings_W_StdDev);
	pprintf(p, "number :      %7d     %7d     %7d     %7d\n",
	    Rs_count, Rb_count,  Rl_count, Rw_count);
	return COM_OK;
}

PUBLIC int
com_fixrank(int p, param_list param)
{
	int	p1, connected;

	if (!FindPlayer(p, param[0].val.word, &p1, &connected))
		return COM_OK;

	UpdateRank(TYPE_BLITZ, parray[p1].name, &parray[p1].b_stats,
	    parray[p1].name);
	UpdateRank(TYPE_STAND, parray[p1].name, &parray[p1].s_stats,
	    parray[p1].name);
	UpdateRank(TYPE_WILD, parray[p1].name, &parray[p1].w_stats,
	    parray[p1].name);

	if (!connected)
		player_remove(p1);
	return COM_OK;
}

PUBLIC int
com_rank(int p, param_list param)
{
	return DisplayRank(p, param, 1);
}

PUBLIC int
com_hrank(int p, param_list param)
{
	return DisplayRank(p, param, 0);
}

PUBLIC int
DisplayRank(int p, param_list param, int showComputers)
{
	int	show = (SHOW_BLITZ|SHOW_STANDARD|SHOW_WILD);
	int	start, end, target, connected;

	if (param[0].type == TYPE_NULL) {
		DisplayTargetRank(p, parray[p].name, show, showComputers);
		return COM_OK;
	} else if (isdigit(param[0].val.word[0])) {
		int ret;

		end = -1;
		ret = sscanf(param[0].val.word, "%d-%d", &start, &end);

		if (ret != 2) {
			warnx("%s: sscanf() == %d", __func__, ret);
			return COM_FAILED;
		}

		if (end > 0 && (param[1].type != TYPE_NULL))
			show = ShowFromString(param[1].val.word);

		DisplayRankedPlayers(p, start, end, show, showComputers);
		return COM_OK;
	} else {
		target = player_search(p, param[0].val.word);

		if (target == 0) {
			pprintf(p, "Target %s not found.\n", param[0].val.word);
			return COM_OK;
		}

		connected = (target > 0);

		if (!connected)
			target = -target - 1;
		else
			target--;

		if (param[1].type != TYPE_NULL)
			show = ShowFromString(param[1].val.word);

		DisplayTargetRank(p, parray[target].name, show, showComputers);

		if (!connected)
			player_remove(target);
		return COM_OK;
	}
}

/*
 * CompareStats() returns:
 *   - 1 if s1 comes first,
 *   - -1 if s2 comes first,
 *   - and 0 if neither takes precedence.
 */
PRIVATE int
CompareStats(char *name1, statistics *s1,
	     char *name2, statistics *s2)
{
	int	i, l1;

	if (s1 == NULL) {
		if (s2 == NULL)
			return 0;
		else
			return -1;
	} else if (s2 == NULL)
		return 1;

	if (s1->rating > s2->rating)
		return 1;
	if (s1->rating < s2->rating)
		return -1;

	l1 = strlen(name1);

	for (i = 0; i < l1; i++) {
		if (name2[i] == '\0')
			return -1;
		if (tolower(name1[i]) < tolower(name2[i]))
			return 1;
		if (tolower(name1[i]) > tolower(name2[i]))
			return -1;
	}

	if (name2[i] != '\0')
		return 1;

	fprintf(stderr, "Duplicate entries found: %s.\n", name1);
	return 0;
}

PRIVATE int
GetRankFileName(char *out, const size_t size, int type)
{
	switch (type) {
	case TYPE_BLITZ:
		snprintf(out, size, "%s/rank.blitz", sdir);
		return type;
	case TYPE_STAND:
		snprintf(out, size, "%s/rank.std", sdir);
		return type;
	case TYPE_WILD:
		snprintf(out, size, "%s/rank.wild", sdir);
		return type;
	default:
		return -1;
	}
}

PUBLIC void
UpdateRank(int type, char *addName, statistics *sNew, char *delName)
{
	FILE		*fp;
	FILE		*fptemp;
	char		 RankFile[MAX_FILENAME_SIZE];
	char		 TmpRankFile[MAX_FILENAME_SIZE];
	char		 command[MAX_STRING_LENGTH];
	char		 line[MAX_RANK_LINE] = { '\0' };
	char		 login[MAX_LOGIN_NAME] = { '\0' };
	int		 comp;
	statistics	 sCur;

	if (GetRankFileName(RankFile, sizeof RankFile, type) < 0)
		return;

	if ((fp = fopen(RankFile, "r")) == NULL) {
		warn("%s: can't open rank file to update", __func__);
		return;
	}

	snprintf(TmpRankFile, sizeof TmpRankFile, "%s/tmpRank", sdir);

	if ((fptemp = fopen(TmpRankFile, "w")) == NULL) {
		warn("%s: unable to open rank file for updating", __func__);
		fclose(fp);
		return;
	}

	while (fgets(line, sizeof line, fp) != NULL) {
		_Static_assert(ARRAY_SIZE(login) > 19, "'login' too small");

		if (sscanf(line, "%19s %d %d %d", login,
		    &sCur.rating, &sCur.num, &comp) != 4) {
			warnx("%s: %s: sscanf() error  --  too few items",
			    __func__, RankFile);
			continue;
		}

		if (delName != NULL &&
		    !strcasecmp(delName, login)) {	// Kill name.
			delName = NULL;
			continue;
		}

		if (addName != NULL &&
		    CompareStats(addName, sNew, login, &sCur) > 0) {
			int computer = in_list(-1, L_COMPUTER, addName);

			fprintf(fptemp, "%s %d %d %d\n", addName, sNew->rating,
			    sNew->num, computer);
			addName = NULL;
		}

		fprintf(fptemp, "%s %d %d %d\n", login, sCur.rating, sCur.num,
		    comp);
	}

	fclose(fptemp);
	fclose(fp);

	// XXX
#define NASH_CODE 0
#if NASH_CODE
	snprintf(command, sizeof command, "mv %s %s", TmpRankFile, RankFile);
	system(command);
#else
	if (rename(TmpRankFile, RankFile) == -1)
		warn("%s: rename()", __func__);
	UNUSED_VAR(command);
#endif
}

PRIVATE void
DisplayRankHead(int p, int show)
{
	char Line[MAX_STRING_LENGTH] = { '\0' };

	if (CheckFlag(show, SHOW_BLITZ))
		strlcat(Line, "         Blitz          ", sizeof(Line));
	if (CheckFlag(show, SHOW_STANDARD))
		strlcat(Line, "       Standard          ", sizeof(Line));
	if (CheckFlag(show, SHOW_WILD))
		strlcat(Line, "          Wild", sizeof(Line));

	pprintf(p, "%s\n\n", Line);
}

PRIVATE int
CountRankLine(int countComp, char *loginName, int num, int is_computer)
{
	if (loginName == NULL || loginName[0] == '\0')
		return 0;
	return (countComp || !is_computer) && (is_active(num));
}

PRIVATE int
GetRank(FILE *fp, char *target, int countComp)
{
	char	line[MAX_RANK_LINE] = { '\0' };
	char	login[MAX_LOGIN_NAME] = { '\0' };
	int	count = 0;
	int	nGames, is_computer;
	int	playerFound = 0;

	while (fgets(line, sizeof line, fp) != NULL &&
	    !playerFound) {
		_Static_assert(ARRAY_SIZE(login) > 19, "'login' too small");

		if (sscanf(line, "%19s %*d %d %d", login, &nGames, &is_computer)
		    != 1) {
			warnx("%s: sscanf() error", __func__);
			continue;
		}

		if ((playerFound = !strcasecmp(login, target)) ||
		    CountRankLine(countComp, login, nGames, is_computer))
			count++;
	}

	return (playerFound ? count : -1);
}

PRIVATE void
PositionFilePtr(FILE *fp, int count, int *last, int *nTied, int showComp)
{
	char	line[MAX_RANK_LINE] = { '\0' };
	char	login[MAX_LOGIN_NAME] = { '\0' };
	int	rating, nGames, is_computer;

	if (fp == NULL)
		return;

	rating = nGames = is_computer = 0;
	rewind(fp);

	for (int i = 1; i < count; i++) {
		do {
			_Static_assert(ARRAY_SIZE(login) > 19,
			    "'login' too small");

			if (fgets(line, sizeof line, fp) == NULL ||
			    feof(fp) ||
			    ferror(fp))
				break;
			else if (sscanf(line, "%19s %d %d %d", login, &rating,
			    &nGames, &is_computer) != 4) {
				warnx("%s: sscanf() error", __func__);
				break;
			}
		} while (!CountRankLine(showComp, login, nGames, is_computer));

		if (rating != *last) {
			*nTied = 1;
			*last = rating;
		} else
			(*nTied)++;
	}
}

PRIVATE int
ShowRankEntry(int p, FILE *fp, int count, int comp, char *target,
    int *lastRating, int *nTied)
{
	char	login[MAX_LOGIN_NAME] = { '\0' };
	char	newLine[MAX_RANK_LINE] = { '\0' };
	int	rating, findable, nGames, is_comp;

	// XXX
	rating		= 0;
	findable	= (count > 0 && !feof(fp));
	nGames		= 0;
	is_comp		= 0;

	if (findable) {
		do {
			if (fgets(newLine, sizeof newLine, fp) == NULL ||
			    feof(fp) ||
			    ferror(fp)) {
				findable = 0;
			} else if (newLine[0] != '\0') {
				_Static_assert(ARRAY_SIZE(login) > 19,
				    "Assertion has failed");

				if (sscanf(newLine, "%19s %d %d %d", login,
				    &rating, &nGames, &is_comp) != 4) {
					warnx("%s: sscanf() error", __func__);
					findable = 0;
					break;
				}
			} else {
				login[0] = '\0';
			}
		} while (!CountRankLine(comp, login, nGames, is_comp) &&
		    findable &&
		    strcasecmp(login, target));
	}

	if (findable) {
		if (!strcasecmp(login, target) && !CountRankLine(comp, login,
		    nGames, is_comp)) {
			pprintf_highlight(p, "----  %-12.12s %4s", login,
			    ratstr(rating));
			pprintf(p, "  ");
			return 0;
		} else if (*lastRating == rating && *nTied < 1) {
			pprintf(p, "      ");

			if (!strcasecmp(login, target)) {
				pprintf_highlight(p, "%-12.12s %4s", login,
				    ratstr(rating));
			} else {
				pprintf(p, "%-12.12s %4s", login,
				    ratstr(rating));
			}

			pprintf(p, "  ");
			return 1;
		} else {
			if (*nTied >= 1) {
				if (*lastRating == rating)
					count -= *nTied;
				*nTied = -1;
			}

			if (!strcasecmp(login, target)) {
				pprintf_highlight(p, "%4d. %-12.12s %4s", count,
				    login, ratstr(rating));
			} else {
				pprintf(p, "%4d. %-12.12s %4s", count, login,
				    ratstr(rating));
			}

			pprintf(p, "  ");
			*lastRating = rating;
			return 1;
		}
	} else {
		pprintf(p, "%25s", "");
		return 1;
	}
}

PRIVATE int
CountAbove(int num, int blitz, int std, int wild, int which)
{
	int	max = blitz;

	if (max < std)
		max = std;
	if (max < wild)
		max = wild;
	return (max <= (num + 1) / 2 ? max - 1 : (num + 1) / 2);
}

PRIVATE int
ShowRankLines(int p, FILE *fb, FILE *fs, FILE *fw, int bCount, int sCount,
    int wCount, int n, int showComp, int show, char *target)
{
	int	lastBlitz = 9999, nTiedBlitz = 0;
	int	lastStd = 9999, nTiedStd = 0;
	int	lastWild = 9999, nTiedWild = 0;

	if (n <= 0)
		return 0;

	if (fb != NULL && CheckFlag(show, SHOW_BLITZ)) {
		PositionFilePtr(fb, bCount, &lastBlitz, &nTiedBlitz, showComp);

		if (feof(fb))
			ClearFlag(show, SHOW_BLITZ);
	}

	if (fs != NULL && CheckFlag(show, SHOW_STANDARD)) {
		PositionFilePtr(fs, sCount, &lastStd, &nTiedStd, showComp);

		if (feof(fs))
			ClearFlag(show, SHOW_STANDARD);
	}

	if (fw != NULL && CheckFlag(show, SHOW_WILD)) {
		PositionFilePtr(fw, wCount, &lastWild, &nTiedWild, showComp);

		if (feof(fw))
			ClearFlag(show, SHOW_WILD);
	}

	if (!CheckFlag(show, (SHOW_BLITZ | SHOW_STANDARD | SHOW_WILD)))
		return 0;

	DisplayRankHead(p, show);

	for (int i = 0; i < n && show; i++) {
		if (fb != NULL && CheckFlag(show, SHOW_BLITZ)) {
			bCount += ShowRankEntry(p, fb, bCount, showComp, target,
			    &lastBlitz, &nTiedBlitz);
		}
		if (fs != NULL && CheckFlag(show, SHOW_STANDARD)) {
			sCount += ShowRankEntry(p, fs, sCount, showComp, target,
			    &lastStd, &nTiedStd);
		}
		if (fw != NULL && CheckFlag(show, SHOW_WILD)) {
			wCount += ShowRankEntry(p, fw, wCount, showComp, target,
			    &lastWild, &nTiedWild);
		}
		pprintf(p, "\n");
	}

	return 1;
}

PUBLIC int
DisplayTargetRank(int p, char *target, int show, int showComp)
{
	FILE	*fb = NULL;
	FILE	*fs = NULL;
	FILE	*fw = NULL;
	char	 Path[MAX_FILENAME_SIZE] = { '\0' };
	int	 numAbove;
	int	 numToShow = 20;
	int	 blitzRank = -1, blitzCount;
	int	 stdRank = -1, stdCount;
	int	 wildRank = -1, wildCount;

	if (CheckFlag(show, SHOW_BLITZ)) {
		GetRankFileName(Path, sizeof Path, TYPE_BLITZ);

		if ((fb = fopen(Path, "r")) != NULL)
			blitzRank = GetRank(fb, target, showComp);
		if (blitzRank < 0)
			ClearFlag(show, SHOW_BLITZ);
	}

	if (CheckFlag(show, SHOW_STANDARD)) {
		GetRankFileName(Path, sizeof Path, TYPE_STAND);

		if ((fs = fopen(Path, "r")) != NULL)
			stdRank = GetRank(fs, target, showComp);
		if (stdRank < 0)
			ClearFlag(show, SHOW_STANDARD);
	}

	if (CheckFlag(show, SHOW_WILD)) {
		GetRankFileName(Path, sizeof Path, TYPE_WILD);

		if (CheckFlag(show, SHOW_WILD))
			fw = fopen(Path, "r");
		if (fw != NULL)
			wildRank = GetRank(fw, target, showComp);
		if (wildRank < 0)
			ClearFlag(show, SHOW_WILD);
	}

	if (!CheckFlag(show, (SHOW_BLITZ | SHOW_STANDARD | SHOW_WILD))) {
		pprintf(p, "No ratings to show.\n");

		if (fb != NULL)
			fclose(fb);
		if (fs != NULL)
			fclose(fs);
		if (fw != NULL)
			fclose(fw);
		return 0;
	}

	numAbove = CountAbove(numToShow, blitzRank, stdRank, wildRank, show);

	blitzCount	= (blitzRank - numAbove);
	stdCount	= (stdRank - numAbove);
	wildCount	= (wildRank - numAbove);

	ShowRankLines(p, fb, fs, fw, blitzCount, stdCount, wildCount, numToShow,
	    showComp, show, target);

	if (fb != NULL)
		fclose(fb);
	if (fs != NULL)
		fclose(fs);
	if (fw != NULL)
		fclose(fw);
	return 1;
}

PUBLIC int
DisplayRankedPlayers(int p, int start, int end, int show, int showComp)
{
	FILE	*fb = NULL;
	FILE	*fs = NULL;
	FILE	*fw = NULL;
	char	 Path[MAX_FILENAME_SIZE] = { '\0' };
	int	 num = (end - start + 1);

	if (start <= 0)
		start = 1;
	if (num <= 0)
		return 0;
	if (num > 100)
		num = 100;

	if (CheckFlag(show, SHOW_BLITZ)) {
		GetRankFileName(Path, sizeof Path, TYPE_BLITZ);

		if ((fb = fopen(Path, "r")) == NULL)
			ClearFlag(show, SHOW_BLITZ);
	}
	if (CheckFlag(show, SHOW_STANDARD)) {
		GetRankFileName(Path, sizeof Path, TYPE_STAND);

		if ((fs = fopen(Path, "r")) == NULL)
			ClearFlag(show, SHOW_STANDARD);
	}
	if (CheckFlag(show, SHOW_WILD)) {
		GetRankFileName(Path, sizeof Path, TYPE_WILD);

		if ((fw = fopen(Path, "r")) == NULL)
			ClearFlag(show, SHOW_WILD);
	}

	ShowRankLines(p, fb, fs, fw, start, start, start, num, showComp, show,
	    "");

	if (fb)
		fclose(fb);
	if (fs)
		fclose(fs);
	if (fw)
		fclose(fw);
	return 1;
}

PUBLIC int
ShowFromString(char *s)
{
	int	len = strlen(s ? s : "");
	int	show = 0;

	if (s == NULL || s[0] == '\0')
		return (SHOW_BLITZ | SHOW_STANDARD | SHOW_WILD);

	for (int i = 0; i < len; i++) {
		switch (s[i]) {
		case 'b':
			SetFlag(show, SHOW_BLITZ);
			break;
		case 's':
			SetFlag(show, SHOW_STANDARD);
			break;
		case 'w':
			SetFlag(show, SHOW_WILD);
			break;
		}
	}

	return show;
}

PUBLIC int
Best(int p, param_list param, int ShowComp)
{
	int show = (SHOW_BLITZ | SHOW_STANDARD | SHOW_WILD);

	if (param[0].type != TYPE_NULL)
		show = ShowFromString(param[0].val.word);

	DisplayRankedPlayers(p, 1, 20, show, ShowComp);
	return COM_OK;
}