/*	$NetBSD: main.c,v 1.73 2022/05/29 21:02:37 rillig Exp $	*/

/*
 * Copyright (c) 1994
 *	The Regents of the University of California.  All rights reserved.
 *
 * This code is derived from software contributed to Berkeley by
 * Ralph Campbell.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the University nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

#include <sys/cdefs.h>
__COPYRIGHT("@(#) Copyright (c) 1994\
 The Regents of the University of California.  All rights reserved.");
/*	@(#)main.c	8.4 (Berkeley) 5/4/95	*/
__RCSID("$NetBSD: main.c,v 1.73 2022/05/29 21:02:37 rillig Exp $");

#include <sys/stat.h>
#include <curses.h>
#include <err.h>
#include <limits.h>
#include <signal.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>

#include "gomoku.h"

enum input_source {
	USER,			/* get input from standard input */
	PROGRAM,		/* get input from program */
	INPUTF			/* get input from a file */
};

enum testing_mode {
	NORMAL_PLAY,
	USER_VS_USER,
	PROGRAM_VS_PROGRAM
};

bool	interactive = true;	/* true if interactive */
int	debug;			/* > 0 if debugging */
static enum testing_mode test = NORMAL_PLAY;
static char *prog;		/* name of program */
static char user[LOGIN_NAME_MAX]; /* name of player */
static FILE *debugfp;		/* file for debug output */
static FILE *inputfp;		/* file for debug input */

const char	pdir[4]		= "-\\|/";

struct	spotstr	board[BAREA];		/* info for board */
struct	combostr frames[FAREA];		/* storage for all frames */
struct	combostr *sortframes[2];	/* sorted list of non-empty frames */
u_char	overlap[FAREA * FAREA];		/* non-zero if frame [a][b] overlap;
					 * see init_overlap */
spot_index intersect[FAREA * FAREA];	/* frame [a][b] intersection */
struct game game;
const char *plyr[2] = { "???", "???" };	/* who's who */

static spot_index readinput(FILE *);
static void misclog(const char *, ...) __printflike(1, 2);
static void quit(void) __dead;
#if !defined(DEBUG)
static void quitsig(int) __dead;
#endif

static void
warn_if_exists(const char *fname)
{
	struct stat st;

	if (lstat(fname, &st) == 0) {
		int x, y;
		getyx(stdscr, y, x);
		addstr("  (already exists)");
		move(y, x);
	} else
		clrtoeol();
}

static void
save_game(void)
{
	char fname[PATH_MAX];
	FILE *fp;

	ask("Save file name? ");
	(void)get_line(fname, sizeof(fname), warn_if_exists);
	if ((fp = fopen(fname, "w")) == NULL) {
		misclog("cannot create save file");
		return;
	}
	for (unsigned int m = 0; m < game.nmoves; m++)
		fprintf(fp, "%s\n", stoc(game.moves[m]));
	fclose(fp);
}

static void
parse_args(int argc, char **argv)
{
	int ch;

	prog = strrchr(argv[0], '/');
	prog = prog != NULL ? prog + 1 : argv[0];

	while ((ch = getopt(argc, argv, "bcdD:u")) != -1) {
		switch (ch) {
		case 'b':	/* background */
			interactive = false;
			break;
		case 'c':
			test = PROGRAM_VS_PROGRAM;
			break;
		case 'd':
			debug++;
			break;
		case 'D':	/* log debug output to file */
			if ((debugfp = fopen(optarg, "w")) == NULL)
				err(1, "%s", optarg);
			break;
		case 'u':
			test = USER_VS_USER;
			break;
		default:
		usage:
			fprintf(stderr, "usage: %s [-bcdu] [-Dfile] [file]\n",
			    getprogname());
			exit(EXIT_FAILURE);
		}
	}
	argc -= optind;
	argv += optind;
	if (argc > 1)
		goto usage;
	if (argc == 1 && (inputfp = fopen(*argv, "r")) == NULL)
		err(1, "%s", *argv);
}

static void
set_input_sources(enum input_source *input, player_color color)
{
	switch (test) {
	case NORMAL_PLAY:
		input[color] = USER;
		input[color != BLACK ? BLACK : WHITE] = PROGRAM;
		break;
	case USER_VS_USER:
		input[BLACK] = USER;
		input[WHITE] = USER;
		break;
	case PROGRAM_VS_PROGRAM:
		input[BLACK] = PROGRAM;
		input[WHITE] = PROGRAM;
		break;
	}
}

static int
ask_user_color(void)
{
	int color;

	mvprintw(BSZ + 3, 0, "Black moves first. ");
	ask("(B)lack or (W)hite? ");
	for (;;) {
		int ch = get_key(NULL);
		if (ch == 'b' || ch == 'B') {
			color = BLACK;
			break;
		}
		if (ch == 'w' || ch == 'W') {
			color = WHITE;
			break;
		}
		if (ch == 'q' || ch == 'Q')
			quit();

		beep();
		ask("Please choose (B)lack or (W)hite: ");
	}
	move(BSZ + 3, 0);
	clrtoeol();
	return color;
}

static int
read_color(void)
{
	char buf[128];

	get_line(buf, sizeof(buf), NULL);
	if (strcmp(buf, "black") == 0)
		return BLACK;
	if (strcmp(buf, "white") == 0)
		return WHITE;
	panic("Huh?  Expected `black' or `white', got `%s'\n", buf);
	/* NOTREACHED */
}

static spot_index
read_move(void)
{
again:
	if (interactive) {
		ask("Select move, (S)ave or (Q)uit.");
		spot_index s = get_coord();
		if (s == SAVE) {
			save_game();
			goto again;
		}
		if (s != RESIGN && board[s].s_occ != EMPTY) {
			beep();
			goto again;
		}
		return s;
	} else {
		char buf[128];
		if (!get_line(buf, sizeof(buf), NULL))
			return RESIGN;
		if (buf[0] == '\0')
			goto again;
		return ctos(buf);
	}
}

static void
declare_winner(int outcome, const enum input_source *input, player_color color)
{

	move(BSZ + 3, 0);
	switch (outcome) {
	case WIN:
		if (input[color] == PROGRAM)
			addstr("Ha ha, I won");
		else if (input[0] == USER && input[1] == USER)
			addstr("Well, you won (and lost)");
		else
			addstr("Rats! you won");
		break;
	case TIE:
		addstr("Wow! It's a tie");
		break;
	case ILLEGAL:
		addstr("Illegal move");
		break;
	}
	clrtoeol();
	bdisp();
}

struct outcome {
	int result;
	player_color winner;
};

static struct outcome
main_game_loop(enum input_source *input)
{
	spot_index curmove = 0;
	player_color color = BLACK;

again:
	switch (input[color]) {
	case INPUTF:
		curmove = readinput(inputfp);
		if (curmove != END_OF_INPUT)
			break;
		set_input_sources(input, color);
		plyr[BLACK] = input[BLACK] == USER ? user : prog;
		plyr[WHITE] = input[WHITE] == USER ? user : prog;
		bdwho();
		refresh();
		goto again;

	case USER:
		curmove = read_move();
		break;

	case PROGRAM:
		if (interactive)
			ask("Thinking...");
		curmove = pickmove(color);
		break;
	}

	if (interactive && curmove != ILLEGAL) {
		misclog("%3u%*s%-6s",
		    game.nmoves + 1, color == BLACK ? 2 : 9, "",
		    stoc(curmove));
	}

	int outcome;
	if ((outcome = makemove(color, curmove)) != MOVEOK)
		return (struct outcome){ outcome, color };

	if (interactive)
		bdisp();
	color = color != BLACK ? BLACK : WHITE;
	goto again;
}

int
main(int argc, char **argv)
{
	char *user_name;
	int color;
	enum input_source input[2];

	/* Revoke setgid privileges */
	setgid(getgid());

	setprogname(argv[0]);

	user_name = getlogin();
	strlcpy(user, user_name != NULL ? user_name : "you", sizeof(user));

	color = BLACK;

	parse_args(argc, argv);

	if (debug == 0)
		srandom((unsigned int)time(0));
	if (interactive)
		cursinit();		/* initialize curses */
again:
	init_board();			/* initialize board contents */

	if (interactive) {
		bdisp_init();		/* initialize display of board */
#ifdef DEBUG
		signal(SIGINT, whatsup);
#else
		signal(SIGINT, quitsig);
#endif

		if (inputfp == NULL && test == NORMAL_PLAY)
			color = ask_user_color();
	} else {
		setbuf(stdout, NULL);
		color = read_color();
	}

	if (inputfp != NULL) {
		input[BLACK] = INPUTF;
		input[WHITE] = INPUTF;
	} else {
		set_input_sources(input, color);
	}
	if (interactive) {
		plyr[BLACK] = input[BLACK] == USER ? user : prog;
		plyr[WHITE] = input[WHITE] == USER ? user : prog;
		bdwho();
		refresh();
	}

	struct outcome outcome = main_game_loop(input);

	if (interactive) {
		declare_winner(outcome.result, input, outcome.winner);
		if (outcome.result != RESIGN) {
		replay:
			ask("Play again? ");
			int ch = get_key("YyNnQqSs");
			if (ch == 'Y' || ch == 'y')
				goto again;
			if (ch == 'S' || ch == 's') {
				save_game();
				goto replay;
			}
		}
	}
	quit();
}

static spot_index
readinput(FILE *fp)
{
	int c;
	char buf[128];
	size_t pos;

	pos = 0;
	while ((c = getc(fp)) != EOF && c != '\n' && pos < sizeof(buf) - 1)
		buf[pos++] = c;
	buf[pos] = '\0';
	return c == EOF ? END_OF_INPUT : ctos(buf);
}

#ifdef DEBUG

static bool
skip_any(const char **pp, const char *s)
{
	while (strchr(s, **pp) != NULL)
		(*pp)++;
	return true;
}

static bool
parse_char_index(const char **pp, const char *s, unsigned int *out)
{
	const char *found = strchr(s, **pp);
	if (found != NULL)
		*out = (unsigned int)(found - s), (*pp)++;
	return found != NULL;
}

static bool
parse_direction(const char **pp, direction *out)
{
	unsigned int u;
	if (!parse_char_index(pp, "-\\|/", &u))
		return false;
	*out = (direction)u;
	return true;
}

static bool
parse_row(const char **pp, unsigned int *out)
{
	if (!('0' <= **pp && **pp <= '9'))
		return false;
	unsigned int u = *(*pp)++ - '0';
	if ('0' <= **pp && **pp <= '9')
		u = 10 * u + *(*pp)++ - '0';
	*out = u;
	return 1 <= u && u <= BSZ;
}

static bool
parse_spot(const char **pp, spot_index *out)
{
	unsigned row, col;
	if (!parse_char_index(pp, "abcdefghjklmnopqrst", &col) &&
	    !parse_char_index(pp, "ABCDEFGHJKLMNOPQRST", &col))
		return false;
	if (!parse_row(pp, &row))
		return false;
	*out = PT(col + 1, row);
	return true;
}

/*
 * Handle strange situations and ^C.
 */
/* ARGSUSED */
void
whatsup(int signum __unused)
{
	unsigned int n;
	player_color color;
	spot_index s, s1, s2;
	direction r1, r2;
	struct spotstr *sp;
	FILE *fp;
	const char *str;
	struct elist *ep;
	struct combostr *cbp;
	char input[128];
	char tmp[128];

	if (!interactive)
		quit();
top:
	ask("debug command: ");
	if (!get_line(input, sizeof(input), NULL))
		quit();
	switch (*input) {
	case '\0':
		goto top;
	case 'q':		/* conservative quit */
		quit();
		/* NOTREACHED */
	case 'd':		/* set debug level */
		debug = input[1] - '0';
		debuglog("Debug set to %d", debug);
		goto top;
	case 'c':
		ask("");
		return;
	case 'b':		/* back up a move */
		if (game.nmoves > 0) {
			game.nmoves--;
			board[game.moves[game.nmoves]].s_occ = EMPTY;
			bdisp();
		}
		goto top;
	case 's':		/* suggest a move */
		color = input[1] == 'b' ? BLACK : WHITE;
		debuglog("suggest %c %s", color == BLACK ? 'B' : 'W',
			stoc(pickmove(color)));
		goto top;
	case 'f':		/* go forward a move */
		board[game.moves[game.nmoves]].s_occ =
		    game.nmoves % 2 == 0 ? BLACK : WHITE;
		game.nmoves++;
		bdisp();
		goto top;
	case 'l':		/* print move history */
		if (input[1] == '\0') {
			for (unsigned int m = 0; m < game.nmoves; m++)
				debuglog("%s", stoc(game.moves[m]));
			goto top;
		}
		if ((fp = fopen(input + 1, "w")) == NULL)
			goto top;
		for (unsigned int m = 0; m < game.nmoves; m++) {
			fprintf(fp, "%s", stoc(game.moves[m]));
			if (++m < game.nmoves)
				fprintf(fp, " %s\n", stoc(game.moves[m]));
			else
				fputc('\n', fp);
		}
		bdump(fp);
		fclose(fp);
		goto top;
	case 'o':
		str = input + 1;
		if (skip_any(&str, " ") &&
		    parse_spot(&str, &s1) &&
		    parse_direction(&str, &r1) &&
		    skip_any(&str, ", ") &&
		    parse_spot(&str, &s2) &&
		    parse_direction(&str, &r2) &&
		    *str == '\0') {
			n = board[s1].s_frame[r1] * FAREA
			    + board[s2].s_frame[r2];
			debuglog("overlap %s%c,%s%c = %02x",
			    stoc(s1), pdir[r1], stoc(s2), pdir[r2],
			    overlap[n]);
		} else
			debuglog("usage: o <spot><dir> <spot><dir>");
		goto top;
	case 'p':
		sp = &board[s = ctos(input + 1)];
		debuglog("V %s %x/%d %d %x/%d %d %d %x", stoc(s),
			sp->s_combo[BLACK].s, sp->s_level[BLACK],
			sp->s_nforce[BLACK],
			sp->s_combo[WHITE].s, sp->s_level[WHITE],
			sp->s_nforce[WHITE], sp->s_wval, sp->s_flags);
		debuglog("FB %s %x %x %x %x", stoc(s),
			sp->s_fval[BLACK][0].s, sp->s_fval[BLACK][1].s,
			sp->s_fval[BLACK][2].s, sp->s_fval[BLACK][3].s);
		debuglog("FW %s %x %x %x %x", stoc(s),
			sp->s_fval[WHITE][0].s, sp->s_fval[WHITE][1].s,
			sp->s_fval[WHITE][2].s, sp->s_fval[WHITE][3].s);
		goto top;
	case 'e':	/* e [0-9] spot */
		str = input + 1;
		if (*str >= '0' && *str <= '9')
			n = *str++ - '0';
		else
			n = 0;
		sp = &board[ctos(str)];
		for (ep = sp->s_empty; ep != NULL; ep = ep->e_next) {
			cbp = ep->e_combo;
			if (n != 0) {
				if (cbp->c_nframes > n)
					continue;
				if (cbp->c_nframes != n)
					break;
			}
			printcombo(cbp, tmp, sizeof(tmp));
			debuglog("%s", tmp);
		}
		goto top;
	default:
		debuglog("Options are:");
		debuglog("q    - quit");
		debuglog("c    - continue");
		debuglog("d#   - set debug level to #");
		debuglog("p#   - print values at #");
		goto top;
	}
}
#endif /* DEBUG */

/*
 * Display debug info.
 */
void
debuglog(const char *fmt, ...)
{
	va_list ap;
	char buf[128];

	va_start(ap, fmt);
	vsnprintf(buf, sizeof(buf), fmt, ap);
	va_end(ap);

	if (debugfp != NULL)
		fprintf(debugfp, "%s\n", buf);
	if (interactive)
		dislog(buf);
	else
		fprintf(stderr, "%s\n", buf);
}

static void
misclog(const char *fmt, ...)
{
	va_list ap;
	char buf[128];

	va_start(ap, fmt);
	vsnprintf(buf, sizeof(buf), fmt, ap);
	va_end(ap);

	if (debugfp != NULL)
		fprintf(debugfp, "%s\n", buf);
	if (interactive)
		dislog(buf);
	else
		printf("%s\n", buf);
}

static void
quit(void)
{
	if (interactive) {
		bdisp();		/* show final board */
		cursfini();
	}
	exit(0);
}

#if !defined(DEBUG)
static void
quitsig(int dummy __unused)
{
	quit();
}
#endif

/*
 * Die gracefully.
 */
void
panic(const char *fmt, ...)
{
	va_list ap;

	if (interactive) {
		bdisp();
		cursfini();
	}

	fprintf(stderr, "%s: ", prog);
	va_start(ap, fmt);
	vfprintf(stderr, fmt, ap);
	va_end(ap);
	fprintf(stderr, "\n");

	fputs("I resign\n", stdout);
	exit(1);
}