/*	SCCS Id: @(#)topten.c	3.1	92/11/20	*/
/* Copyright (c) Stichting Mathematisch Centrum, Amsterdam, 1985. */
/* NetHack may be freely redistributed.  See license for details. */

#include "hack.h"

#ifdef VMS
 /* We don't want to rewrite the whole file, because that entails	 */
 /* creating a new version which requires that the old one be deletable. */
# define UPDATE_RECORD_IN_PLACE
#endif

/*
 * Updating in place can leave junk at the end of the file in some
 * circumstances (if it shrinks and the O.S. doesn't have a straightforward
 * way to truncate it).  The trailing junk is harmless and the code
 * which reads the scores will ignore it.
 */
#ifdef UPDATE_RECORD_IN_PLACE
# ifndef SEEK_SET
#  define SEEK_SET 0
# endif
static long final_fpos;
#endif

#ifdef NO_SCAN_BRACK
static void FDECL(nsb_mung_line,(char*));
static void FDECL(nsb_unmung_line,(char*));
#endif

#define newttentry() (struct toptenentry *) alloc(sizeof(struct toptenentry))
#define dealloc_ttentry(ttent) free((genericptr_t) (ttent))
#define	NAMSZ	10
#define	DTHSZ	60
#define	PERSMAX	 3		/* entries per name/uid per char. allowed */
#define	POINTSMIN	1	/* must be > 0 */
#define	ENTRYMAX	100	/* must be >= 10 */

#ifndef MICRO
#define	PERS_IS_UID		/* delete for PERSMAX per name; now per uid */
#endif
struct toptenentry {
	struct toptenentry *tt_next;
#ifdef UPDATE_RECORD_IN_PLACE
	long fpos;
#endif
	long points;
	int deathdnum, deathlev;
	int maxlvl,hp,maxhp;
	int uid;
	char plchar;
	char sex;
	char name[NAMSZ+1];
	char death[DTHSZ+1];
	char date[7];		/* yymmdd */
} *tt_head;

static void NDECL(outheader);
static int FDECL(outentry, (int,struct toptenentry *,int));
static void FDECL(readentry, (FILE *,struct toptenentry *));
static void FDECL(writeentry, (FILE *,struct toptenentry *));
static int FDECL(classmon, (CHAR_P,BOOLEAN_P));

/* must fit with end.c */
const char NEARDATA *killed_by_prefix[] = {
	"killed by ", "choked on ", "poisoned by ", "", "drowned in ",
	"", "crushed to death by ", "petrified by ", "",
	"", "",
	"", "", "" };

static void
readentry(rfile,tt)
FILE *rfile;
struct toptenentry *tt;
{
#ifdef UPDATE_RECORD_IN_PLACE
	/* note: fscanf() below must read the record's terminating newline */
	final_fpos = tt->fpos = ftell(rfile);
#endif
#ifdef NO_SCAN_BRACK
	if(fscanf(rfile,"%6s %d %d %d %d %d %d %ld%*c%c%c %s %s%*c",
#  define TTFIELDS 13
#else
	if(fscanf(rfile, "%6s %d %d %d %d %d %d %ld %c%c %[^,],%[^\n]%*c",
#  define TTFIELDS 12
#endif
			tt->date, &tt->uid,
			&tt->deathdnum, &tt->deathlev,
			&tt->maxlvl, &tt->hp, &tt->maxhp, &tt->points,
			&tt->plchar, &tt->sex,
#ifdef LATTICE	/* return value is broken also, sigh */
			tt->name, tt->death) < 1)
#else
			tt->name, tt->death) != TTFIELDS)
#endif
#undef TTFIELDS
		tt->points = 0;
	else {
#ifdef NO_SCAN_BRACK
		if(tt->points > 0) {
			nsb_unmung_line(tt->name);
			nsb_unmung_line(tt->death);
		}
#endif
	}
}

static void
writeentry(rfile,tt)
FILE *rfile;
struct toptenentry *tt;
{
#ifdef NO_SCAN_BRACK
	nsb_mung_line(tt->name);
	nsb_mung_line(tt->death);
	(void) fprintf(rfile,"%6s %d %d %d %d %d %d %ld %c%c %s %s\n",
#else
	(void) fprintf(rfile,"%6s %d %d %d %d %d %d %ld %c%c %s,%s\n",
#endif
		tt->date, tt->uid,
		tt->deathdnum, tt->deathlev,
		tt->maxlvl, tt->hp, tt->maxhp, tt->points,
		tt->plchar, tt->sex,
		onlyspace(tt->name) ? "_" : tt->name, tt->death);
#ifdef NO_SCAN_BRACK
	nsb_unmung_line(tt->name);
	nsb_unmung_line(tt->death);
#endif
}

void
topten(how)
int how;
{
	int uid = getuid();
	int rank, rank0 = -1, rank1 = 0;
	int occ_cnt = PERSMAX;
	register struct toptenentry *t0, *tprev;
	struct toptenentry *t1;
	FILE *rfile;
	register int flg = 0;
#ifdef LOGFILE
	FILE *lfile;
#endif /* LOGFILE */

#if defined(MICRO)
#define HUP
#else
#define	HUP	if(!done_hup)
#endif
	/* create a new 'topten' entry */
	t0 = newttentry();
	/* deepest_lev_reached() is in terms of depth(), and reporting the
	 * deepest level reached in the dungeon death occurred in doesn't
	 * seem right, so we have to report the death level in depth() terms
	 * as well (which also seems reasonable since that's all the player
	 * sees on the screen anyway)
	 */
	t0->deathdnum = u.uz.dnum;
	t0->deathlev = depth(&u.uz);
	t0->maxlvl = deepest_lev_reached(TRUE);
	t0->hp = u.uhp;
	t0->maxhp = u.uhpmax;
	t0->points = u.urexp;
	t0->plchar = pl_character[0];
	t0->sex = (flags.female ? 'F' : 'M');
	t0->uid = uid;
	(void) strncpy(t0->name, plname, NAMSZ);
	t0->name[NAMSZ] = '\0';
	t0->death[0] = '\0';
	switch (killer_format) {
		default: impossible("bad killer format?");
		case KILLED_BY_AN:
			Strcat(t0->death, killed_by_prefix[how]);
			(void) strncat(t0->death, an(killer), DTHSZ);
			break;
		case KILLED_BY:
			Strcat(t0->death, killed_by_prefix[how]);
			(void) strncat(t0->death, killer, DTHSZ);
			break;
		case NO_KILLER_PREFIX:
			(void) strncat(t0->death, killer, DTHSZ);
			break;
	}
	Strcpy(t0->date, get_date());
	t0->tt_next = 0;
#ifdef UPDATE_RECORD_IN_PLACE
	t0->fpos = -1L;
#endif

#ifdef LOGFILE		/* used for debugging (who dies of what, where) */
	if (lock_file(LOGFILE, 10)) {
	    if(!(lfile = fopen_datafile(LOGFILE,"a"))) {
		HUP raw_print("Cannot open log file!");
	    } else {
		writeentry(lfile, t0);
		(void) fclose(lfile);
	    }
	    unlock_file(LOGFILE);
	}
#endif /* LOGFILE */

#if defined(WIZARD) || defined(EXPLORE_MODE)
	if (wizard || discover) {
	    raw_print("");
	    raw_printf(
	      "Since you were in %s mode, the score list will not be checked.",
		    wizard ? "wizard" : "discover");
	    return;
	}
#endif

	if (!lock_file(RECORD, 60)) return;

#ifdef UPDATE_RECORD_IN_PLACE
	rfile = fopen_datafile(RECORD, "r+");
#else
	rfile = fopen_datafile(RECORD, "r");
#endif

	if (!rfile) {
		HUP raw_print("Cannot open record file!");
		unlock_file(RECORD);
		return;
	}

	HUP raw_print("");

	/* assure minimum number of points */
	if(t0->points < POINTSMIN) t0->points = 0;

	t1 = tt_head = newttentry();
	tprev = 0;
	/* rank0: -1 undefined, 0 not_on_list, n n_th on list */
	for(rank = 1; ; ) {
	    readentry(rfile, t1);
	    if (t1->points < POINTSMIN) t1->points = 0;
	    if(rank0 < 0 && t1->points < t0->points) {
		rank0 = rank++;
		if(tprev == 0)
			tt_head = t0;
		else
			tprev->tt_next = t0;
		t0->tt_next = t1;
#ifdef UPDATE_RECORD_IN_PLACE
		t0->fpos = t1->fpos;	/* insert here */
#endif
		occ_cnt--;
		flg++;		/* ask for a rewrite */
	    } else tprev = t1;

	    if(t1->points == 0) break;
	    if(
#ifdef PERS_IS_UID
		t1->uid == t0->uid &&
#else
		strncmp(t1->name, t0->name, NAMSZ) == 0 &&
#endif
		t1->plchar == t0->plchar && --occ_cnt <= 0) {
		    if(rank0 < 0) {
			rank0 = 0;
			rank1 = rank;
			HUP {
			    raw_printf(
			  "You didn't beat your previous score of %ld points.",
				    t1->points);
			    raw_print("");
			}
		    }
		    if(occ_cnt < 0) {
			flg++;
			continue;
		    }
		}
	    if(rank <= ENTRYMAX) {
		t1 = t1->tt_next = newttentry();
		rank++;
	    }
	    if(rank > ENTRYMAX) {
		t1->points = 0;
		break;
	    }
	}
	if(flg) {	/* rewrite record file */
#ifdef UPDATE_RECORD_IN_PLACE
		(void) fseek(rfile, (t0->fpos >= 0 ?
				     t0->fpos : final_fpos), SEEK_SET);
#else
		(void) fclose(rfile);
		if(!(rfile = fopen_datafile(RECORD,"w"))){
			HUP raw_print("Cannot write record file");
			unlock_file(RECORD);
			return;
		}
#endif	/* UPDATE_RECORD_IN_PLACE */
		if(!done_stopprint) if(rank0 > 0){
		    if(rank0 <= 10)
			raw_print("You made the top ten list!");
		    else {
			raw_printf(
			  "You reached the %d%s place on the top %d list.",
				rank0, ordin(rank0), ENTRYMAX);
		    }
		    raw_print("");
		}
	}
	if(rank0 == 0) rank0 = rank1;
	if(rank0 <= 0) rank0 = rank;
	if(!done_stopprint) outheader();
	t1 = tt_head;
	for(rank = 1; t1->points != 0; rank++, t1 = t1->tt_next) {
	    if(flg
#ifdef UPDATE_RECORD_IN_PLACE
		    && rank >= rank0
#endif
		) writeentry(rfile, t1);
	    if(done_stopprint) continue;
	    if(rank > flags.end_top &&
	      (rank < rank0-flags.end_around || rank > rank0+flags.end_around)
	      && (!flags.end_own ||
#ifdef PERS_IS_UID
				  t1->uid != t0->uid
#else
				  strncmp(t1->name, t0->name, NAMSZ)
#endif
		)) continue;
	    if(rank == rank0-flags.end_around &&
	       rank0 > flags.end_top+flags.end_around+1 &&
	       !flags.end_own)
	  	raw_print("");
	    if(rank != rank0)
		(void) outentry(rank, t1, 0);
	    else if(!rank1)
		(void) outentry(rank, t1, 1);
	    else {
		int t0lth = outentry(0, t0, -1);
		int t1lth = outentry(rank, t1, t0lth);
		if(t1lth > t0lth) t0lth = t1lth;
		(void) outentry(0, t0, t0lth);
	    }
	}
	if(rank0 >= rank) if(!done_stopprint)
		(void) outentry(0, t0, 1);
#ifdef UPDATE_RECORD_IN_PLACE
	if (flg) {
# ifdef TRUNCATE_FILE
		/* if a reasonable way to truncate a file exists, use it */
		truncate_file(rfile);
# else
		/* use sentinel record rather than relying on truncation */
		t0->points = 0L;	/* terminates file when read back in */
		t0->uid = t0->deathdnum = t0->deathlev = 0;
		t0->maxlvl = t0->hp = t0->maxhp = 0;
		t0->plchar = t0->sex = '-';
		Strcpy(t0->name, "@");
		Strcpy(t0->death, "<eod>\n");
		writeentry(rfile, t0);
		(void) fflush(rfile);
# endif	/* TRUNCATE_FILE */
	}
#endif	/* UPDATE_RECORD_IN_PLACE */
	(void) fclose(rfile);
	unlock_file(RECORD);
}

static void
outheader() {
	char linebuf[BUFSZ];
	register char *bp;

	Strcpy(linebuf, " No  Points     Name");
	bp = eos(linebuf);
	while(bp < linebuf + COLNO - 9) *bp++ = ' ';
	Strcpy(bp, "Hp [max]");
	raw_print(linebuf);
}

/* so>0: standout line; so=0: ordinary line; so<0: no output, return lth */
static int
outentry(rank, t1, so)
register struct toptenentry *t1;
register int rank, so;
{
	register boolean second_line = TRUE;
	char linebuf[BUFSZ], linebuf2[BUFSZ], linebuf3[BUFSZ], pbuf[BUFSZ];

	linebuf[0] = linebuf2[0] = linebuf3[0] = 0;
	if(rank) Sprintf(eos(linebuf), "%3d", rank);
	else Strcat(linebuf, "   ");

	Sprintf(eos(linebuf), " %10ld  %.10s", t1->points, t1->name);
	Sprintf(eos(linebuf), "-%c ", t1->plchar);
	if(!strncmp("escaped", t1->death, 7)) {
	  second_line = FALSE;
	  if(!strcmp(" (with the Amulet)", t1->death+7))
	    Strcat(linebuf, "escaped the dungeon with the Amulet");
	  else
	    Sprintf(eos(linebuf), "escaped the dungeon [max level %d]",
	      t1->maxlvl);
	} else if(!strncmp("ascended", t1->death, 8)) {
	   Strcat(linebuf, "ascended to demigod");
	   if (t1->sex == 'F') Strcat(linebuf, "dess");
	   Strcat(linebuf, "-hood");
	   second_line = FALSE;
	} else {
	  if(!strncmp(t1->death,"quit",4)) {
		Strcat(linebuf, "quit");
		second_line = FALSE;
	  } else if(!strncmp(t1->death,"starv",5)) {
		Strcat(linebuf, "starved to death");
		second_line = FALSE;
	  } else if(!strncmp(t1->death,"choked",6)) {
		Sprintf(eos(linebuf), "choked on h%s food",
			(t1->sex == 'F') ? "er" : "is");
	  } else if(!strncmp(t1->death,"poisoned",8)) {
		Strcat(linebuf, "was poisoned");
	  } else if(!strncmp(t1->death,"crushed",7)) {
		Strcat(linebuf, "was crushed to death");
	  } else if(!strncmp(t1->death, "petrified by ",13)) {
		Strcat(linebuf, "turned to stone");
	  } else Strcat(linebuf, "died");

	  if (t1->deathdnum == astral_level.dnum)
		Strcpy(linebuf3, " in the endgame");
	  else
		Sprintf(linebuf3, " in %s on level %d",
		    dungeons[t1->deathdnum].dname, t1->deathlev);
	  if(t1->deathlev != t1->maxlvl)
		Sprintf(eos(linebuf3), " [max %d]", t1->maxlvl);
	  /* kludge for "quit while already on Charon's boat" */
	  if(!strncmp(t1->death, "quit ", 5))
		Strcat(linebuf3, t1->death + 4);
	}
	Strcat(linebuf3, ".");

	if(t1->maxhp) {
	  register char *bp;
	  char hpbuf[10];
	  int hppos;
	  int lngr = strlen(linebuf) + strlen(linebuf3);
	  if (t1->hp <= 0) hpbuf[0] = '-', hpbuf[1] = '\0';
	  else Sprintf(hpbuf, "%d", t1->hp);
	  hppos = COLNO - 7 - (int)strlen(hpbuf);
	  if (lngr >= hppos) {
	      if(so > 0) {
		  bp = eos(linebuf);
		  while(bp < linebuf + (COLNO-1)) *bp++ = ' ';
		  *bp = 0;
		  raw_print_bold(linebuf);
	      } else if(so == 0)
		  raw_print(linebuf);
	      Strcpy(linebuf, "               ");
	  }
	  Strcat(linebuf, linebuf3);
	  bp = eos(linebuf);

	  if(bp <= linebuf + hppos) {
	    /* pad any necessary blanks to the hit point entry */
	    while(bp < linebuf + hppos) *bp++ = ' ';
	    Strcpy(bp, hpbuf);
	    if(t1->maxhp < 10)
		 Sprintf(eos(bp), "   [%d]", t1->maxhp);
	    else if(t1->maxhp < 100)
		 Sprintf(eos(bp), "  [%d]", t1->maxhp);
	    else Sprintf(eos(bp), " [%d]", t1->maxhp);
	  }
	}

/*	Line 2 now contains the killer name */

	/* Quit, starved, ascended, and escaped contain no second line */
	if (second_line) {
		Strcpy(linebuf2, t1->death);
		*linebuf2 = highc(*linebuf2);
		Strcat(linebuf2, ".");
	}

	if(so == 0) {
	    raw_print(linebuf);
	    if (second_line)
		raw_printf("                %s", linebuf2);
	} else if(so > 0) {
	  register char *bp = eos(linebuf);
	  if(so >= COLNO) so = COLNO-1;
	  while(bp < linebuf + so) *bp++ = ' ';
	  *bp = 0;
	  raw_print_bold(linebuf);
	  if(second_line) {
	      Sprintf(pbuf, "                %s", linebuf2);
	      raw_print_bold(pbuf);
	  }
	}
	return((int)strlen(linebuf)+(int)strlen(linebuf2));
}

/*
 * Called with args from main if argc >= 0. In this case, list scores as
 * requested. Otherwise, find scores for the current player (and list them
 * if argc == -1).
 */
void
prscore(argc,argv)
int argc;
char **argv;
{
	const char **players;
	int playerct;
	int rank;
	register struct toptenentry *t1, *t2;
	FILE *rfile;
	register int flg = 0, i;
	char pbuf[BUFSZ];
#ifdef nonsense
	long total_score = 0L;
	char totchars[10];
	int totcharct = 0;
#endif
	int outflg = (argc >= -1);
#ifdef PERS_IS_UID
	int uid = -1;
#else
	const char *player0;
#endif
	rfile = fopen_datafile(RECORD, "r");
	if (!rfile) {
		raw_print("Cannot open record file!");
		return;
	}

	/* If the score list isn't after a game, we never went through */
	/* init_dungeons() */
	if (wiz1_level.dlevel == 0) init_dungeons();

	if(argc > 1 && !strncmp(argv[1], "-s", 2)){
		if(!argv[1][2]){
			argc--;
			argv++;
		} else if(!argv[1][3] && index(pl_classes, argv[1][2])) {
			argv[1]++;
			argv[1][0] = '-';
		} else	argv[1] += 2;
	}
	if(argc <= 1){
#ifdef PERS_IS_UID
		uid = getuid();
		playerct = 0;
#  if defined(LINT) || defined(GCC_WARN)
		players = 0;
#  endif
#else
		player0 = plname;
		if(!*player0)
			player0 = "hackplayer";
		playerct = 1;
		players = &player0;
#endif
	} else {
		playerct = --argc;
		players = (const char **)++argv;
	}
	if(outflg) raw_print("");

	t1 = tt_head = newttentry();
	for(rank = 1; ; rank++) {
	    readentry(rfile, t1);
	    if(t1->points == 0) break;
#ifdef PERS_IS_UID
	    if(!playerct && t1->uid == uid)
		flg++;
	    else
#endif
	    for(i = 0; i < playerct; i++){
		if(strcmp(players[i], "all") == 0 ||
		   strncmp(t1->name, players[i], NAMSZ) == 0 ||
		  (players[i][0] == '-' &&
		   players[i][1] == t1->plchar &&
		   players[i][2] == 0) ||
		  (digit(players[i][0]) && rank <= atoi(players[i])))
			flg++;
	    }
	    t1 = t1->tt_next = newttentry();
	}
	(void) fclose(rfile);
	if(!flg) {
	    if(outflg) {
		Strcpy(pbuf, "Cannot find any entries for ");
		if(playerct < 1) Strcat(pbuf, "you.");
		else {
		  if(playerct > 1) Strcat(pbuf, "any of ");
		  for(i=0; i<playerct; i++) {
		      Strcat(pbuf, players[i]);
		      if(i<playerct-1) Strcat(pbuf, ":");
		  }
		  raw_print(pbuf);
		  raw_printf("Call is: %s -s [-role] [maxrank] [playernames]",
			     hname);
		}
	    }
	    return;
	}

	if(outflg) outheader();
	t1 = tt_head;
	for(rank = 1; t1->points != 0; rank++, t1 = t2) {
		t2 = t1->tt_next;
#ifdef PERS_IS_UID
		if(!playerct && t1->uid == uid)
			goto outwithit;
		else
#endif
		for(i = 0; i < playerct; i++){
			if(strcmp(players[i], "all") == 0 ||
			   strncmp(t1->name, players[i], NAMSZ) == 0 ||
			  (players[i][0] == '-' &&
			   players[i][1] == t1->plchar &&
			   players[i][2] == 0) ||
			  (digit(players[i][0]) && rank <= atoi(players[i]))){
#ifdef PERS_IS_UID
			outwithit:
#endif
				if(outflg)
				    (void) outentry(rank, t1, 0);
#ifdef nonsense
				total_score += t1->points;
				if(totcharct < sizeof(totchars)-1)
				    totchars[totcharct++] = t1->plchar;
#endif
				break;
			}
		}
		dealloc_ttentry(t1);
	}
#ifdef nonsense
	totchars[totcharct] = 0;

	/* We would like to determine whether you're experienced.  However,
	   the information collected here only tells about the scores/roles
	   that got into the topten (top 100?).  We should maintain a
	   .hacklog or something in his home directory. */
	flags.beginner = (total_score < 6000);
	for(i=0; i<6; i++)
	    if(!index(totchars, pl_classes[i])) {
		flags.beginner = 1;
		if(!pl_character[0]) pl_character[0] = pl_classes[i];
		break;
	}
#endif /* nonsense /**/
}

static int
classmon(plch, fem)
char plch;
boolean fem;
{
	switch (plch) {
		case 'A': return PM_ARCHEOLOGIST;
		case 'B': return PM_BARBARIAN;
		case 'C': return (fem ? PM_CAVEWOMAN : PM_CAVEMAN);
		case 'E': return PM_ELF;
		case 'H': return PM_HEALER;
		case 'F':	/* accept old Fighter class */
		case 'K': return PM_KNIGHT;
		case 'P': return (fem ? PM_PRIESTESS : PM_PRIEST);
		case 'R': return PM_ROGUE;
		case 'N':	/* accept old Ninja class */
		case 'S': return PM_SAMURAI;
#ifdef TOURIST
		case 'T': return PM_TOURIST;
#else
		case 'T': return PM_HUMAN;
#endif
		case 'V': return PM_VALKYRIE;
		case 'W': return PM_WIZARD;
		default: impossible("What weird class is this? (%c)", plch);
			return PM_HUMAN_ZOMBIE;
	}
}

/*
 * Get a random player name and class from the high score list,
 * and attach them to an object (for statues or morgue corpses).
 */
struct obj *
tt_oname(otmp)
struct obj *otmp;
{
	int rank;
	register int i;
	register struct toptenentry *tt;
	FILE *rfile;

	if (!otmp) return((struct obj *) 0);

	rfile = fopen_datafile(RECORD, "r");
	if (!rfile) {
		panic("Cannot open record file!");
	}

	tt = newttentry();
	rank = rnd(10);
pickentry:
	for(i = rank; i; i--) {
	    readentry(rfile, tt);
	    if(tt->points == 0) break;
	}

	if(tt->points == 0) {
		if(rank > 1) {
			rank = 1;
			rewind(rfile);
			goto pickentry;
		}
		dealloc_ttentry(tt);
		otmp = (struct obj *) 0;
	} else {
		otmp->corpsenm = classmon(tt->plchar, (tt->sex == 'F'));
		otmp->owt = weight(otmp);
		/* Note: oname() is safe since otmp is first in chains */
		otmp = oname(otmp, tt->name, 0);
		fobj = otmp;
		level.objects[otmp->ox][otmp->oy] = otmp;
		dealloc_ttentry(tt);
	}

	(void) fclose(rfile);
	return otmp;
}

#ifdef NO_SCAN_BRACK
/* Lattice scanf isn't up to reading the scorefile.  What */
/* follows deals with that; I admit it's ugly. (KL) */
/* Now generally available (KL) */
static void
nsb_mung_line(p)
	char *p;
	{
	while(p=index(p,' '))*p='|';
}

static void
nsb_unmung_line(p)
	char *p;
	{
	while(p=index(p,'|'))*p=' ';
}
#endif

/*topten.c*/
