/* $NetBSD: cgram.c,v 1.30 2023/05/10 12:30:27 rillig Exp $ */

/*-
 * Copyright (c) 2013, 2021 The NetBSD Foundation, Inc.
 * All rights reserved.
 *
 * This code is derived from software contributed to The NetBSD Foundation
 * by Roland Illig.
 *
 * 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.
 *
 * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. 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 FOUNDATION 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>
#if defined(__RCSID) && !defined(lint)
__RCSID("$NetBSD: cgram.c,v 1.30 2023/05/10 12:30:27 rillig Exp $");
#endif

#include <assert.h>
#include <ctype.h>
#include <curses.h>
#include <err.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#include "pathnames.h"


static bool
ch_islower(char ch)
{
	return ch >= 'a' && ch <= 'z';
}

static bool
ch_isupper(char ch)
{
	return ch >= 'A' && ch <= 'Z';
}

static bool
ch_isalpha(char ch)
{
	return ch_islower(ch) || ch_isupper(ch);
}

static char
ch_toupper(char ch)
{
	return ch_islower(ch) ? (char)(ch - 'a' + 'A') : ch;
}

static char
ch_tolower(char ch)
{
	return ch_isupper(ch) ? (char)(ch - 'A' + 'a') : ch;
}

static int
imax(int a, int b)
{
	return a > b ? a : b;
}

static int
imin(int a, int b)
{
	return a < b ? a : b;
}

////////////////////////////////////////////////////////////

struct string {
	char *s;
	size_t len;
	size_t cap;
};

struct stringarray {
	struct string *v;
	size_t num;
};

static void
string_init(struct string *s)
{
	s->s = NULL;
	s->len = 0;
	s->cap = 0;
}

static void
string_add(struct string *s, char ch)
{
	if (s->len >= s->cap) {
		s->cap = 2 * s->cap + 16;
		s->s = realloc(s->s, s->cap);
		if (s->s == NULL)
			errx(1, "Out of memory");
	}
	s->s[s->len++] = ch;
}

static void
string_finish(struct string *s)
{
	string_add(s, '\0');
	s->len--;
}

static void
stringarray_init(struct stringarray *a)
{
	a->v = NULL;
	a->num = 0;
}

static void
stringarray_done(struct stringarray *a)
{
	for (size_t i = 0; i < a->num; i++)
		free(a->v[i].s);
	free(a->v);
}

static void
stringarray_add(struct stringarray *a, struct string *s)
{
	size_t num = a->num++;
	if (reallocarr(&a->v, a->num, sizeof(a->v[0])) != 0)
		errx(1, "Out of memory");
	a->v[num] = *s;
}

static void
stringarray_dup(struct stringarray *dst, const struct stringarray *src)
{
	assert(dst->num == 0);
	for (size_t i = 0; i < src->num; i++) {
		struct string str;
		string_init(&str);
		for (const char *p = src->v[i].s; *p != '\0'; p++)
			string_add(&str, *p);
		string_finish(&str);
		stringarray_add(dst, &str);
	}
}

////////////////////////////////////////////////////////////

static struct stringarray lines;
static struct stringarray sollines;
static bool hinting;
static int extent_x;
static int extent_y;
static int offset_x;
static int offset_y;
static int cursor_x;
static int cursor_y;

static int
cur_max_x(void)
{
	return (int)lines.v[cursor_y].len;
}

static int
cur_max_y(void)
{
	return extent_y - 1;
}

static char
char_left_of_cursor(void)
{
	if (cursor_x > 0)
		return lines.v[cursor_y].s[cursor_x - 1];
	assert(cursor_y > 0);
	return '\n'; /* eol of previous line */
}

static char
char_at_cursor(void)
{
	if (cursor_x == cur_max_x())
		return '\n';
	return lines.v[cursor_y].s[cursor_x];
}

static void
getquote(FILE *f)
{
	struct string line;
	string_init(&line);

	int ch;
	while ((ch = fgetc(f)) != EOF) {
		if (ch == '\n') {
			string_finish(&line);
			stringarray_add(&lines, &line);
			string_init(&line);
		} else if (ch == '\t') {
			string_add(&line, ' ');
			while (line.len % 8 != 0)
				string_add(&line, ' ');
		} else if (ch == '\b') {
			if (line.len > 0)
				line.len--;
		} else {
			string_add(&line, (char)ch);
		}
	}

	stringarray_dup(&sollines, &lines);

	extent_y = (int)lines.num;
	for (int i = 0; i < extent_y; i++)
		extent_x = imax(extent_x, (int)lines.v[i].len);
}

static void
readfile(const char *name)
{
	FILE *f = fopen(name, "r");
	if (f == NULL)
		err(1, "%s", name);

	getquote(f);

	if (fclose(f) != 0)
		err(1, "%s", name);
}


static void
readquote(void)
{
	FILE *f = popen(_PATH_FORTUNE, "r");
	if (f == NULL)
		err(1, "%s", _PATH_FORTUNE);

	getquote(f);

	if (pclose(f) != 0)
		exit(1); /* error message must come from child process */
}

static void
encode(void)
{
	int key[26];

	for (int i = 0; i < 26; i++)
		key[i] = i;

	for (int i = 26; i > 1; i--) {
		int c = (int)(random() % i);
		int t = key[i - 1];
		key[i - 1] = key[c];
		key[c] = t;
	}

	for (int y = 0; y < extent_y; y++) {
		for (char *p = lines.v[y].s; *p != '\0'; p++) {
			if (ch_islower(*p))
				*p = (char)('a' + key[*p - 'a']);
			if (ch_isupper(*p))
				*p = (char)('A' + key[*p - 'A']);
		}
	}
}

static void
substitute(char a, char b)
{
	char la = ch_tolower(a);
	char ua = ch_toupper(a);
	char lb = ch_tolower(b);
	char ub = ch_toupper(b);

	for (int y = 0; y < (int)lines.num; y++) {
		for (char *p = lines.v[y].s; *p != '\0'; p++) {
			if (*p == la)
				*p = lb;
			else if (*p == ua)
				*p = ub;
			else if (*p == lb)
				*p = la;
			else if (*p == ub)
				*p = ua;
		}
	}
}

static bool
is_solved(void)
{
	for (size_t i = 0; i < lines.num; i++)
		if (strcmp(lines.v[i].s, sollines.v[i].s) != 0)
			return false;
	return true;
}

////////////////////////////////////////////////////////////

static void
redraw(void)
{
	erase();

	int max_y = imin(LINES - 1, extent_y - offset_y);
	for (int y = 0; y < max_y; y++) {
		move(y, 0);

		int len = (int)lines.v[offset_y + y].len;
		int max_x = imin(COLS - 1, len - offset_x);
		const char *line = lines.v[offset_y + y].s;
		const char *solline = sollines.v[offset_y + y].s;

		for (int x = 0; x < max_x; x++) {
			char ch = line[offset_x + x];
			bool bold = hinting &&
			    (ch == solline[offset_x + x] || !ch_isalpha(ch));

			if (bold)
				attron(A_BOLD);
			addch(ch);
			if (bold)
				attroff(A_BOLD);
		}
		clrtoeol();
	}

	move(LINES - 1, 0);
	addstr("~ to quit, * to cheat, ^pnfb to move");

	if (is_solved()) {
		if (extent_y + 1 - offset_y < LINES - 2)
			move(extent_y + 1 - offset_y, 0);
		else
			addch(' ');
		attron(A_BOLD | A_STANDOUT);
		addstr("*solved*");
		attroff(A_BOLD | A_STANDOUT);
	}

	move(cursor_y - offset_y, cursor_x - offset_x);

	refresh();
}

////////////////////////////////////////////////////////////

static void
saturate_cursor(void)
{
	cursor_y = imax(cursor_y, 0);
	cursor_y = imin(cursor_y, cur_max_y());

	assert(cursor_x >= 0);
	cursor_x = imin(cursor_x, cur_max_x());
}

static void
scroll_into_view(void)
{
	if (cursor_x < offset_x)
		offset_x = cursor_x;
	if (cursor_x > offset_x + COLS - 1)
		offset_x = cursor_x - (COLS - 1);

	if (cursor_y < offset_y)
		offset_y = cursor_y;
	if (cursor_y > offset_y + LINES - 2)
		offset_y = cursor_y - (LINES - 2);
}

static bool
can_go_left(void)
{
	return cursor_y > 0 ||
	    (cursor_y == 0 && cursor_x > 0);
}

static bool
can_go_right(void)
{
	return cursor_y < cur_max_y() ||
	    (cursor_y == cur_max_y() && cursor_x < cur_max_x());
}

static void
go_to_prev_line(void)
{
	cursor_y--;
	cursor_x = cur_max_x();
}

static void
go_to_next_line(void)
{
	cursor_x = 0;
	cursor_y++;
}

static void
go_left(void)
{
	if (cursor_x > 0)
		cursor_x--;
	else if (cursor_y > 0)
		go_to_prev_line();
}

static void
go_right(void)
{
	if (cursor_x < cur_max_x())
		cursor_x++;
	else if (cursor_y < cur_max_y())
		go_to_next_line();
}

static void
go_to_prev_word(void)
{
	while (can_go_left() && !ch_isalpha(char_left_of_cursor()))
		go_left();

	while (can_go_left() && ch_isalpha(char_left_of_cursor()))
		go_left();
}

static void
go_to_next_word(void)
{
	while (can_go_right() && ch_isalpha(char_at_cursor()))
		go_right();

	while (can_go_right() && !ch_isalpha(char_at_cursor()))
		go_right();
}

static bool
can_substitute_here(int ch)
{
	return isascii(ch) &&
	    ch_isalpha((char)ch) &&
	    cursor_x < cur_max_x() &&
	    ch_isalpha(char_at_cursor());
}

static void
handle_char_input(int ch)
{
	if (ch == char_at_cursor())
		go_right();
	else if (can_substitute_here(ch)) {
		substitute(char_at_cursor(), (char)ch);
		go_right();
	} else
		beep();
}

static bool
handle_key(void)
{
	int ch = getch();

#define CTRL(letter) (letter - 64)
	switch (ch) {
	case CTRL('A'):
	case KEY_BEG:
	case KEY_HOME:
		cursor_x = 0;
		break;
	case CTRL('B'):
	case KEY_LEFT:
		go_left();
		break;
	case CTRL('E'):
	case KEY_END:
		cursor_x = cur_max_x();
		break;
	case CTRL('F'):
	case KEY_RIGHT:
		go_right();
		break;
	case '\t':
		go_to_next_word();
		break;
	case KEY_BTAB:
		go_to_prev_word();
		break;
	case '\n':
		go_to_next_line();
		break;
	case CTRL('L'):
		clear();
		break;
	case CTRL('N'):
	case KEY_DOWN:
		cursor_y++;
		break;
	case CTRL('P'):
	case KEY_UP:
		cursor_y--;
		break;
	case KEY_PPAGE:
		cursor_y -= LINES - 2;
		break;
	case KEY_NPAGE:
		cursor_y += LINES - 2;
		break;
	case '*':
		hinting = !hinting;
		break;
	case '~':
		return false;
	case KEY_RESIZE:
		break;
	default:
		handle_char_input(ch);
		break;
	}
	return true;
}

static void
init(const char *filename)
{
	stringarray_init(&lines);
	stringarray_init(&sollines);
	srandom((unsigned int)time(NULL));
	if (filename != NULL) {
	    readfile(filename);
	} else {
	    readquote();
	}
	encode();

	initscr();
	cbreak();
	noecho();
	keypad(stdscr, true);
}

static void
loop(void)
{
	for (;;) {
		redraw();
		if (!handle_key())
			break;
		saturate_cursor();
		scroll_into_view();
	}
}

static void
done(void)
{
	move(LINES - 1, 0);
	clrtoeol();
	refresh();

	endwin();

	stringarray_done(&sollines);
	stringarray_done(&lines);
}


static void __dead
usage(void)
{

	fprintf(stderr, "usage: %s [file]\n", getprogname());
	exit(1);
}

int
main(int argc, char *argv[])
{

	setprogname(argv[0]);
	if (argc != 1 && argc != 2)
		usage();

	init(argc > 1 ? argv[1] : NULL);
	loop();
	done();
	return 0;
}
