/* vi: set sw=4 ts=4: */
/*
 * Mini tail implementation for busybox
 *
 * Copyright (C) 2001 by Matt Kraai <kraai@alumni.carnegiemellon.edu>
 *
 * Licensed under GPLv2 or later, see file LICENSE in this tarball for details.
 */

/* BB_AUDIT SUSv3 compliant (need fancy for -c) */
/* BB_AUDIT GNU compatible -c, -q, and -v options in 'fancy' configuration. */
/* http://www.opengroup.org/onlinepubs/007904975/utilities/tail.html */

/* Mar 16, 2003      Manuel Novoa III   (mjn3@codepoet.org)
 *
 * Pretty much rewritten to fix numerous bugs and reduce realloc() calls.
 * Bugs fixed (although I may have forgotten one or two... it was pretty bad)
 * 1) mixing printf/write without fflush()ing stdout
 * 2) no check that any open files are present
 * 3) optstring had -q taking an arg
 * 4) no error checking on write in some cases, and a warning even then
 * 5) q and s interaction bug
 * 6) no check for lseek error
 * 7) lseek attempted when count==0 even if arg was +0 (from top)
 */

/* Sep 01, 2007     Tito Ragusa  (farmatito@tiscali.it)
 *
 * Partial rewrite, size reduction and breakage :-) .
 * scripts/bloat-o-meter busybox_old busybox_unstripped
 * function                                             old     new   delta
 * xmalloc_fgetc                                          -      40     +40
 * .rodata                                           122102  122134     +32
 * header_fmt                                            13       -     -13
 * eat_num                                               37       -     -37
 * tail_read                                            126       -    -126
 * tail_main                                           1109     784    -325
 * ------------------------------------------------------------------------------
 * (add/remove: 1/3 grow/shrink: 1/1 up/down: 72/-501)          Total: -429 bytes
 */

#include "libbb.h"

#if ENABLE_FEATURE_FANCY_TAIL
static const struct suffix_mult tail_suffixes[] = {
	{ "b", 512 },
	{ "k", 1024 },
	{ "m", 1024*1024 },
	{ }
};
#endif

static char *xmalloc_fgetc(FILE *file)
{
	int c = fgetc(file);
	if ( !c || c == EOF)
		return NULL;
	return xasprintf("%c", (char) c);
}

static void tail_xprint_header(const char *fmt, const char *filename)
{
	if (fdprintf(STDOUT_FILENO, fmt, filename) < 0)
		bb_perror_nomsg_and_die();
}

#define FOLLOW      (opt & 0x1)
#define COUNT_BYTES (opt & 0x2)
#define COUNT_LINES (opt & 0x4)
#define QUIET       (opt & 0x8)
#define SLEEP       (opt & 0x10)
#define VERBOSE     (opt & 0x20)

/*
	The options:

-c	number  output the last N bytes

-f	If the input file is a regular file or if the file operand specifies a FIFO,
	do not terminate after the last line of the input file has been copied, but
	read and copy further bytes from the input file when they become available.
	 If no file operand is specified and standard input is a pipe, the -f option
	shall be ignored. If the input file is not a FIFO, pipe, or regular file, it
	is unspecified whether or not the -f option shall be ignored.

-n	number (or -number)   output the last N lines

	If neither -c nor -n is specified, -n 10 shall be assumed.

-s	with -f, sleep for approximately S seconds (default 1.0) between iterations.

-q	never output headers giving file names.

	With no FILE, or when FILE is -, read standard input.
*/

int tail_main(int argc, char **argv) ATTRIBUTE_NORETURN ;
int tail_main(int argc, char **argv)
{
	/* list of strings or chars */
	llist_t *slist = NULL;
	/* list of FILE *file pointers */
	llist_t *flist = NULL;
	/* list of file names */
	llist_t *nlist = NULL;
	/* number of lines or bytes (we assume 1 char == 1 byte) */
	unsigned n;
	unsigned count;
	/* Remove leading newline for first filename header */
	smallint first_file = 1;
	/* exit status */
	smallint status = EXIT_SUCCESS;
	unsigned opt;
	const char *fmt ALIGN1 = "\n==> %s <==\n";
	char *line;
	/* By default print the last 10 lines of each FILE to standard output */
	const char *str_n = "10";
	char *str_c = NULL;
	/* By default sleep 1 second */
	USE_FEATURE_FANCY_TAIL(const char *str_s = "1";)
	FILE* file;

	if (ENABLE_INCLUDE_SUSv2 || ENABLE_FEATURE_FANCY_TAIL) {
		/* Allow legacy syntax of an initial numeric option without -n. */
		if (argc >= 2 && (argv[1][0] == '+' || argv[1][0] == '-')
		&& isdigit(argv[1][1])
		) {
			/* replacing arg[0] with "-n" can segfault, so... */
			argv[1] = xasprintf("-n%s", argv[1]);
			/* If we ever decide to make tail NOFORK */
			//char *s = alloca(strlen(argv[1]) + 3);
			//sprintf(s, "-n%s", argv[1]);
			//argv[1] = s;
		}
	}

	opt_complementary = "c-n:n-c"USE_FEATURE_FANCY_TAIL(":v-q:q-v");

	opt = getopt32(argv, "fc:n:" USE_FEATURE_FANCY_TAIL("qs:v"),
				&str_c, &str_n USE_FEATURE_FANCY_TAIL(,&str_s));

	/* Move to the remaining args */
	argc -= optind;
	argv += optind;

	n = (COUNT_BYTES) ? xatou_sfx(str_c, tail_suffixes) : xatou_range(str_n, 1, INT_MAX);
	
	/* With more than one FILE, precede each with a header giving the file name */
	if (argc > 1 && !QUIET)
		opt |= 0x20;  /* set VERBOSE */

	do {
		/* With no FILE or When FILE is -, read standard input. */
		if (argc == 0 || LONE_DASH(*argv)) {
			opt |= ~0x1;  /* unset FOLLOW */
			llist_add_to_end(&flist, stdin);
			llist_add_to_end(&nlist, (char *)bb_msg_standard_input);
			continue;
		}
		file = fopen_or_warn(*argv, "r");
		if (!file) {
			status = EXIT_FAILURE;
			continue;
		}
		/* Create lists of FILE* pointers and file names to reuse for -f FOLLOW */
		llist_add_to_end(&flist, file);
		llist_add_to_end(&nlist, *argv);
	} while (*++argv);

	/* No files in the list */
	if (!flist)
		bb_error_msg_and_die("no files");

	/* Iterate on the file list */
	while (flist) {
		count = 0;
		/* if we are reading stdin print the header before reading as read can block */
		if ((FILE*)flist->data == stdin)
			tail_xprint_header(fmt + first_file, nlist->data);

		while ((line = (((COUNT_BYTES) ? xmalloc_fgetc : xmalloc_fgets)((FILE*)flist->data)))) {
			/* Show if there was an error while reading */
			if (ferror((FILE*)flist->data)) {
				bb_perror_msg("%s", nlist->data);
				status = EXIT_FAILURE;
			}
			/* Create a list of the lines or chars */
			llist_add_to_end(&slist, line);
			/* If we exceed our -n or -c value remove one element from the head of the list */
			if (++count > n)
				free(llist_pop(&slist));
		}
		/* Print the header if needed */
		if ((FILE*)flist->data != stdin 
			&& ((VERBOSE && argc > 0) || (VERBOSE && slist &&  argc < 0))
		)
			tail_xprint_header(fmt + first_file, nlist->data);
		/* Zero first file flag */
		first_file = 0;
		/* Print what we have read */
		while ((line = llist_pop(&slist))) {
			tail_xprint_header("%s", line);
			free(line);
		}
		/* Was the file truncated ? */
		if (!slist && lseek(fileno((FILE*)flist->data), 0, SEEK_END) 
			< lseek(fileno((FILE*)flist->data), 0, SEEK_CUR)
		)
			bb_error_msg("file truncated");

		if (!FOLLOW) {
			/* Close the current file */
			fclose_if_not_stdin(llist_pop(&flist));
			/* Move the head of the list to the next file name */
			llist_pop(&nlist);
		} else { 
			/* FOLLOW */
			/* Sleep for approximately S seconds (default 1) between iterations */
			USE_FEATURE_FANCY_TAIL(sleep(xatou(str_s));)
			/* Move FILE* pointer from the top to the end fo the list */
			llist_add_to_end(&flist, llist_pop(&flist));
			/* Move filename from the top to the end fo the list */
			llist_add_to_end(&nlist, llist_pop(&nlist));
			/* Decrement argc as this is our indicator of the first complete iteration of the list */
			argc--;
		}
	}
	if (ENABLE_FEATURE_CLEAN_UP && FOLLOW) {
		/* Free the list in case of -f FOLLOW. */
		/* Don't free list->data as they are not malloced */
		llist_free(flist, NULL);
		llist_free(nlist, NULL);
	}
	fflush_stdout_and_exit(status);
}
