/*
 * Copyright (c) 2003, Jilles Tjoelker
 * All rights reserved.
 *
 * 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 COPYRIGHT HOLDERS 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
 * COPYRIGHT OWNER 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.
 */
/*
 * playnethack2 1.1
 * A better attempt at making up some timing for nethack playback.
 * Designed for the files from the /dev/null NetHack tournament.
 * Also, some keys are available during playback:
 * s     - pause playback until key pressed
 * q     - quit now
 * space - abort current delay and continue immediately (best repeated)
 * n     - skip until next screen (Dlvl: written)
 * .     - toggle no-delay playback (*very* fast)
 * /     - no-delay until specified string seen (screen may mess up a little)
 * Note that this program is not very efficient, it mostly writes only one
 * byte per system call. It also reads the entire file into memory. Nonetheless
 * when the delays are disabled in both the perl and the C program, this one
 * is much faster.
 */
/*
 * Compilation:
 * The program has been tested on FreeBSD, Linux and Solaris.
 * FreeBSD/Linux:
 * Simply compile with something like 'make playnethack2' or
 * 'gcc -W -Wall -o playnethack2 playnethack2.c'.
 * Solaris:
 * You need some implementation of err() and similar functions.
 * I compile with 'gcc -Wall playnethack2.c -o playnethack2
 * -I/usr/local/include -L/usr/local/lib -lbsderr' where
 * /usr/local/lib/libbsderr.a is an err implementation taken from
 * NetBSD pkgsrc ftp (pkgtools/libnbcompat was not tried).
 */

#include	<sys/types.h>
#include	<sys/stat.h>
#include	<sys/time.h>

#include	<ctype.h>
#include	<err.h>
#include	<fcntl.h>
#include	<signal.h>
#include	<stdio.h>
#include	<stdlib.h>
#include	<string.h>
#include	<termios.h>
#include	<unistd.h>

/* Extra, zeroed space behind file so we don't touch unallocated memory. */
#define SLACK 256

struct termios origT, progT;

void
cleanup_term(int sig)
{
    (void)sig;
    write(STDOUT_FILENO, "\017\033[H\033[J", 7);
    tcsetattr(STDOUT_FILENO, TCSANOW, &origT);
    exit(0);
}

int inputstr(char *buf, int buflen)
{
    int n = 0;
    char c;

    for (;;)
    {
	if (read(STDIN_FILENO, &c, 1) != 1)
	    return 0;
	if (c == 8 || c == 127)
	{
	    if (n > 0)
	    {
		n--;
		write(STDOUT_FILENO, "\010 \010", 3);
	    }
	    else
		return 0;
	    buf[n] = 0;
	}
	else if (c == 21 || c == 24)
	{
	    while (n > 0)
	    {
		n--;
		write(STDOUT_FILENO, "\010 \010", 3);
	    }
	    buf[n] = 0;
	}
	else if (c == 10 || c == 13)
	{
	    /* Erase on display */
	    while (n > 0)
	    {
		n--;
		write(STDOUT_FILENO, "\010 \010", 3);
	    }
	    /* Tricky, keep old string if Enter pressed as first char */
	    return 1;
	}
	else if (isprint(c) && n < buflen)
	{
	    buf[n] = c;
	    n++;
	    buf[n] = '\0';
	    write(STDOUT_FILENO, &c, 1);
	}
    }
    return 0;
}

int
main(int argc, char *argv[])
{
    char *filename;
    int in;
    struct stat S;
    char *buf, *p, *q, *end;
    char c;
    struct timeval TV;
    struct timeval TV_long, TV_short, TV_zero;
    int escseq = 1;
    int letter = 1;
    int do_delay = 1;
    int delay_on_next_screen = 0;
    int do_search = 0;
    int issymbol = 0;
    int isnormalcolor = 1;
    int line = 1;
    int prevline = -1;
    char searchstr[256] = "";
    fd_set FRFDS, RFDS;

    TV_long.tv_sec = 0;
    TV_long.tv_usec = 500000;
    TV_short.tv_sec = 0;
    TV_short.tv_usec = 100000;
    TV_zero.tv_sec = 0;
    TV_zero.tv_usec = 0;

    if (argc != 2)
    {
	fprintf(stderr, "Usage: %s filename\n", argv[0]);
	return 1;
    }

    if (!isatty(STDOUT_FILENO))
	errx(1, "stdout must be a terminal");

    filename = argv[1];

    in = open(filename, O_RDONLY);
    if (in == -1)
	err(1, "open %s", filename);
    if (fstat(in, &S) == -1)
	err(1, "fstat %s", filename);
    if (S.st_size == 0)
	errx(1, "%s: zero-size file or bad file type", filename);
    buf = malloc(S.st_size + SLACK);
    if (buf == NULL)
	errx(1, "malloc failed");
    if (read(in, buf, S.st_size) != S.st_size)
	err(1, "read");

    end = buf + S.st_size;
    memset(end, 0, SLACK);
    p = buf;

    if (tcgetattr(STDOUT_FILENO, &origT) == -1)
	err(1, "tcgetattr");
    signal(SIGINT, cleanup_term);
    signal(SIGTERM, cleanup_term);
    progT = origT;
    progT.c_lflag &= ~(ECHO | ICANON);
    progT.c_cc[VMIN] = 1; /* Solaris box I tested on has 4 by default. */
    progT.c_cc[VTIME] = 0;
    if (tcsetattr(STDOUT_FILENO, TCSANOW, &progT) == -1)
	err(1, "tcsetattr");

    FD_ZERO(&FRFDS);
    FD_SET(STDIN_FILENO, &FRFDS);

    while (p < end)
    {
	TV = TV_zero;
	if (delay_on_next_screen)
	{
	    if (*p == '\014' /* ^L */ || !memcmp(p, "Dlvl:", 5))
	    {
		delay_on_next_screen = 0;
		do_delay = 1;
	    }
	}
	if (do_search)
	    if (!memcmp(p, searchstr, strlen(searchstr)))
	    {
		do_search = 0;
		do_delay = 1;
	    }
	if (*p == 27)
	{
	    q = p;
	    p++;
	    if (*p++ == '[')
	    {
		while (*p >= ' ' && *p < '@')
		    p++;
		if (*p == 'm')
		{
		    isnormalcolor = (q[2] == '0' && q + 3 == p) || q + 2 == p;
		    /* \e[0m or \e[m */
		}
		if (*p == 'H')
		{
		    line = atoi(q + 2);
		    if (line == 0)
			line = 1;
		    /* \e[y;xH, \e[yH or \e[H */
		    if (prevline == 1 && letter)
		    {
			TV = TV_long;
			letter = 0;
		    }
		    prevline = line;
		}
		if (*p != '\0')
		    p++;
#if 0
		if (letter)
		{
		    TV.tv_usec = 200000;
		    RFDS = FRFDS;
		    if (do_delay && isnormalcolor && line != 24)
			select(STDIN_FILENO + 1, &RFDS, NULL, NULL, &TV);
		    TV.tv_usec = 0;
		    letter = 0;
		}
#endif
		write(STDOUT_FILENO, q, p - q);
		escseq = 1;
	    }
	    else
		write(STDOUT_FILENO, q, p - q);
	}
	else if (*p == '\016') /* ^N */
	{
	    write(STDOUT_FILENO, p, 1);
	    p++;
	    issymbol = 1;
	}
	else if (*p == '\017') /* ^O */
	{
	    write(STDOUT_FILENO, p, 1);
	    p++;
	    issymbol = 0;
	}
	else if (*p == '@')
	{
	    write(STDOUT_FILENO, p, 1);
	    p++;
	    TV = TV_short;
	}
	else if (!issymbol && (isdigit(*p) || isalpha(*p)))
	{
#if 0
	    if (escseq)
	    {
		TV.tv_usec = 5000;
		RFDS = FRFDS;
		if (do_delay && isnormalcolor && line != 24)
		    select(STDIN_FILENO + 1, &RFDS, NULL, NULL, &TV);
		TV.tv_usec = 0;
		escseq = 0;
	    }
	    else
		TV.tv_usec = 0;
#endif
	    write(STDOUT_FILENO, p, 1);
	    p++;
	    letter = 1;
	}
	else if (!memcmp(p, "--More--", 8))
	{
	    write(STDOUT_FILENO, p, 8);
	    p += 8;
	    TV = TV_long;
	}
	else if (p[0] == '(' && isdigit(p[1]) && !memcmp(p + 2, " of ", 4) &&
		isdigit(p[6]) && p[7] == ')')
	{
	    write(STDOUT_FILENO, p, 8);
	    p += 8;
	    TV = TV_long;
	}
	else if (!memcmp(p, "(end)", 5))
	{
	    write(STDOUT_FILENO, p, 5);
	    p += 5;
	    TV = TV_long;
	}
	else
	{
	    write(STDOUT_FILENO, p, 1);
	    p++;
	}
	if (TV.tv_sec != 0 || TV.tv_usec != 0)
	{
	    RFDS = FRFDS;
	    if (!do_delay)
		TV.tv_sec = TV.tv_usec = 0;
	    if (select(STDIN_FILENO + 1, &RFDS, NULL, NULL, &TV) == 1)
	    {
		read(STDIN_FILENO, &c, 1);
		switch (c)
		{
		    case 'q': case 'Q':
			cleanup_term(0);
			exit(0);
			break;
		    case 's': case 'S':
			read(STDIN_FILENO, &c, 1);
			do_delay = 1;
			do_search = delay_on_next_screen = 0;
			break;
		    case ' ':
			break;
		    case '.':
			do_delay = !do_delay;
			do_search = delay_on_next_screen = 0;
			break;
		    case 'n': case 'N':
			do_delay = 0;
			do_search = 0;
			delay_on_next_screen = 1;
			break;
		    case '/':
			write(STDOUT_FILENO, "/", 1);
			if (inputstr(searchstr, sizeof searchstr - 1) &&
				*searchstr != '\0')
			{
			    do_delay = 0;
			    do_search = 1;
			    delay_on_next_screen = 0;
			}
			write(STDOUT_FILENO, "\010 \010", 3);
			break;
		    default:
			write(STDOUT_FILENO, "\a", 1);
		}
	    }
	}
    }

    read(STDIN_FILENO, &c, 1);

    cleanup_term(0);

    return 0;
}

/* vim:ts=8:cin:sw=4
 */
