/*
 * Copyright (c) 2021 Juhani 'nortti' Krekelä.
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *
 * tail.c
 * Output the trailing part of files.
 */

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

#include <ctype.h>
#include <err.h>
#include <errno.h>
#include <getopt.h>
#include <inttypes.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdnoreturn.h>
#include <string.h>
#include <unistd.h>

#ifdef HEAD
#define TAIL false
#else
#define TAIL true
#endif

static bool byte_mode = false;
static bool beginning_relative = !TAIL;
static off_t file_offset = 10;
static size_t buffer_size = 10;
static bool verbose = false;
static bool quiet = false;
static bool follow = false;
static bool path_mode = false;

static void xputchar(int c)
{
	if ( putchar(c) < 0 )
		err(1, "stdout");
}

static void xputstr(const char* str)
{
	if ( fputs(str, stdout) < 0 )
		err(1, "stdout");
}

static int from_beginning(FILE* fp, const char* filename)
{
	int failure = 0;

	off_t offset = 0;
	while ( true )
	{
		offset++;

		if ( !TAIL && file_offset < offset )
			break;

		char* line = NULL;
		size_t size = 0;
		int c = 0;
		ssize_t length = 0;
		if ( byte_mode )
			c = fgetc(fp);
		else
			length = getline(&line, &size, fp);
		if ( c < 0 || length < 0 )
		{
			if ( !feof(fp) )
			{
				warn("%s", filename);
				failure = 1;
			}
			if ( !byte_mode )
				free(line);
			break;
		}

		if ( (TAIL && file_offset <= offset) ||
		     (!TAIL && offset <= file_offset) )
		{
			if ( byte_mode )
				xputchar(c);
			else
				xputstr(line);
		}

		if ( !byte_mode )
			free(line);
	}

	return failure;
}

static int from_end(FILE* fp, const char* filename)
{
	int failure = 0;

	void* buffer = calloc(buffer_size, byte_mode ? 1 : sizeof(char*));
	if ( !buffer )
		err(1, "calloc");
	char** lines = !byte_mode ? buffer : NULL;
	unsigned char* bytes = byte_mode ? buffer : NULL;
	size_t buffer_used = 0;

	size_t offset = 0;
	while ( true )
	{
		offset++;

		char* line = NULL;
		size_t size = 0;
		int c = 0;
		ssize_t length = 0;
		if ( byte_mode )
			c = fgetc(fp);
		else
			length = getline(&line, &size, fp);
		if ( c < 0 || length < 0 )
		{
			if ( !feof(fp) )
			{
				warn("%s", filename);
				failure = 1;
			}
			free(line);
			break;
		}

		if ( TAIL )
		{
			if ( buffer_size == 0 )
			{
				free(line);
				break;
			}

			size_t index = offset % buffer_size;
			if ( buffer_used < buffer_size )
				buffer_used++;
			else if ( !byte_mode )
				free(lines[index]);

			if ( byte_mode )
				bytes[index] = c;
			else
				lines[index] = line;
		}
		else
		{
			if ( buffer_size == 0 )
			{
				if ( byte_mode )
					xputchar(c);
				else
					xputstr(line);
				free(line);
				continue;
			}

			size_t index = offset % buffer_size;
			if ( buffer_used < buffer_size )
				buffer_used++;
			else
			{
				if ( byte_mode )
					xputchar(bytes[index]);
				else
				{
					xputstr(lines[index]);
					free(lines[index]);
				}
			}

			if ( byte_mode )
				bytes[index] = c;
			else
				lines[index] = line;
		}
	}

	if ( TAIL )
	{
		for ( size_t i = 0; i < buffer_used; i++ )
		{
			size_t index = (offset - buffer_used + i) % buffer_size;
			if ( byte_mode )
				xputchar(bytes[index]);
			else
				xputstr(lines[index]);
		}
	}

	if ( !byte_mode )
	{
		for ( size_t i = 0; i < buffer_used; i++ )
		{
			size_t index = (offset - buffer_used + i) % buffer_size;
			free(lines[index]);
		}
	}
	free(buffer);

	return failure;
}

static int process_file(FILE* fp, const char* filename)
{
	int failure = beginning_relative ? from_beginning(fp, filename) :
	                                   from_end(fp, filename);

	if ( fflush(stdout) != 0 )
		err(1, "fflush");

	return failure;
}

noreturn static void follow_file(FILE* fp, const char* filename)
{
	off_t previous_size = ftello(fp);
	if ( previous_size == -1 && errno != ESPIPE )
		warn("ftello: %s", filename);
	int reopen_errno = 0;
	while ( true )
	{
		// Currently we have nothing like kqueue or inotify, so keep polling.
		// This is used also by e.g. 2.9BSD tail(1) and GNU tail(1) as fallback.
		sleep(1);

		struct stat st;
		if ( fstat(fileno(fp), &st) < 0 )
			err(1, "fstat");

		if ( st.st_size < previous_size )
		{
			warnx("File truncated: %s", filename);
			if ( fseeko(fp, 0, SEEK_END) < 0 )
				warn("fseeko: %s", filename);
		}
		previous_size = st.st_size;

		bool reopen_file = false;
		struct stat path_st;
		if ( fp != stdin && path_mode && stat(filename, &path_st) == 0 &&
			 (st.st_dev != path_st.st_dev || st.st_ino != path_st.st_ino) )
		{
			reopen_file = true;
			previous_size = path_st.st_size;
		}

		clearerr(fp);
		while ( true )
		{
			int c = fgetc(fp);
			if ( c < 0 && feof(fp) )
				break;
			else if ( c < 0 )
				err(1, "%s", filename);
			xputchar(c);
		}

		if ( fflush(stdout) != 0 )
			err(1, "stdout");

		// Reopen file only after draining the old one, in case there was any
		// change of contents before the file was replaced.
		// Additionally, keep the old file open as long as we can't succesfully
		// open the new one.
		if ( reopen_file )
		{
			FILE* new_fp = fopen(filename, "r");
			if ( new_fp )
			{
				fclose(fp);
				fp = new_fp;
				reopen_errno = 0;
				warnx("reopened: %s", filename);
			}
			else if ( errno != reopen_errno )
			{
				warn("reopening: %s", filename);
				reopen_errno = errno;
			}
		}
	}
}

static void parse_number(const char* num_string)
{
	// Handle explicit marking of whether it's relative to beginning or end.
	const char* str = num_string;

	if ( *str == '+' )
	{
		beginning_relative = true;
		str++;
	}
	else if ( *str == '-' )
	{
		beginning_relative = false;
		str++;
	}

	if ( *str == '+' || *str == '-' )
		errx(1, "Invalid number of %s: %s",
		     byte_mode ? "bytes" : "lines", num_string);

	char* end;
	errno = 0;
	intmax_t parsed = strtoimax(str, &end, 10);
	if ( errno == ERANGE ||
		 (beginning_relative && (off_t) parsed != parsed) ||
		 (!beginning_relative && (size_t) parsed != (uintmax_t) parsed) )
		errx(1, "Number of %s too large",
			 byte_mode ? "bytes" : "lines");
	if ( !*str || *end || errno )
		errx(1, "Invalid number of %s: %s",
			 byte_mode ? "bytes" : "lines", num_string);

	if ( beginning_relative )
		file_offset = parsed;
	else
		buffer_size = parsed;
}

int main(int argc, char* argv[])
{
	const struct option longopts[] =
	{
		{"bytes", required_argument, NULL, 'c'},
		{"lines", required_argument, NULL, 'n'},
		{"follow", no_argument, NULL, 'f'},
		{"quiet", no_argument, NULL, 'q'},
		{"verbose", no_argument, NULL, 'v'},
		{0, 0, 0, 0}
	};
#ifndef HEAD
	const char* opts = "c:fFn:qv";
#else
	const char* opts = "c:n:qv";
#endif
	while ( true )
	{
		// Handle the historical but still widely used -num option format.
		if ( optind < argc && argv[optind][0] == '-' &&
		     isdigit((unsigned char) argv[optind][1]) )
		{
			parse_number(&argv[optind][1]);
			optind++;
			continue;
		}

		int opt = getopt_long(argc, argv, opts, longopts, NULL);
		if ( opt == -1 )
			break;
		switch ( opt )
		{
		case 'c':
		case 'n':
			byte_mode = opt == 'c';
			parse_number(optarg);
			break;
		case 'f': follow = true; path_mode = false; break;
		case 'F': follow = true; path_mode = true; break;
		case 'q': quiet = true; verbose = false; break;
		case 'v': verbose = true; quiet = false; break;
		default: return 1;
		}
	}

	if ( follow && 1 < argc - optind )
		errx(1, "-%c cannot be used with multiple files",
		     path_mode ? 'F' : 'f');

	int failure = 0;

	if ( argc - optind < 1 )
	{
		if ( verbose )
		{
			if ( printf("==> standard input <==\n") < 0 )
				err(1, "stdout");
		}

		failure |= process_file(stdin, "standard input");

		// -f is ignored if stdin is a fifo.
		struct stat st;
		if ( fstat(0, &st) < 0 )
			err(1, "fstat");
		if ( follow && !S_ISFIFO(st.st_mode) )
			follow_file(stdin, "standard input");
	}
	else
	{
		for ( int i = optind; i < argc; i++ )
		{
			bool is_stdin = !strcmp(argv[i], "-");
			const char* filename = is_stdin ? "standard input" : argv[i];
			if ( verbose || (!quiet && 1 < argc - optind) )
			{
				if ( printf("%s==> %s <==\n", i == optind ? "" : "\n",
				            filename) < 0 )
					err(1, "stdout");
			}

			FILE* fp = is_stdin ? stdin : fopen(argv[i], "r");
			if ( !fp )
			{
				warn("%s", argv[i]);
				failure = 1;
				continue;
			}

			failure |= process_file(fp, filename);

			struct stat st;
			if ( fstat(fileno(fp), &st) < 0 )
				err(1, "fstat");
			if ( follow && (!is_stdin || !S_ISFIFO(st.st_mode)) )
				follow_file(fp, filename);

			if ( !is_stdin )
				fclose(fp);
		}
	}

	return failure;
}
