#define _POSIX_C_SOURCE 200809L
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <stdint.h>
#include <limits.h>
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

static long long count = 10;
static int anycount, follow, file = STDIN_FILENO;
static enum { BEG, END } origin = END;
static enum { CHARS, LINES } units = LINES;
static enum { FWD, REV } direction = FWD;

static void
fatal(const char *name)
{
    perror(name);
    exit(1);
}

static void
usage(void)
{
    fprintf(stderr, "usage: tail [-n N | -c N] [-f | -r] [file]\n");
    exit(1);
}

static ssize_t
tread(char *buf, size_t size)
{
    ssize_t n;
    do { n = read(file, buf, size); } while (n < 0 && errno == EINTR);
    if (n < 0) fatal("tail: read");
    return n;
}

static void
twrite(const char *buf, size_t size)
{
    while (size) {
        ssize_t n = write(STDOUT_FILENO, buf, size);
        if (n < 0 && errno == EINTR) continue;
        if (n <= 0) fatal("tail: stdout");
        buf += n; size -= (size_t)n;
    }
}

static off_t
tseek(off_t offset, int whence)
{
    off_t position = lseek(file, offset, whence);
    if (position == (off_t)-1) fatal("tail: seek");
    return position;
}

static void
copy(void)
{
    char buf[8192];
    ssize_t n;
    while ((n = tread(buf, sizeof(buf))) > 0) twrite(buf, (size_t)n);
}

static void
skip(void)
{
    char buf[8192];
    while (count > 0) {
        size_t i = 0, size = sizeof(buf);
        ssize_t n;
        if (units == CHARS && count < (long long)size) size = (size_t)count;
        n = tread(buf, size);
        if (!n) return;
        if (units == CHARS) count -= n;
        else {
            while (i < (size_t)n && count > 0)
                if (buf[i++] == '\n') --count;
            if (count == 0) twrite(buf + i, (size_t)n - i);
        }
    }
    copy();
}

/* Keep only the requested suffix when the input cannot be sought. */
static void
keep(void)
{
    char *buf = NULL;
    size_t len = 0, capacity = 0;
    for (;;) {
        ssize_t n;
        size_t start, lines = 0;
        if (capacity - len < 8192) {
            char *newbuf;
            if (len > SIZE_MAX - 8193) { errno = ENOMEM; fatal("tail"); }
            capacity = len + 8193;
            newbuf = realloc(buf, capacity);
            if (newbuf == NULL) fatal("tail: realloc");
            buf = newbuf;
        }
        n = tread(buf + len, capacity - len - 1);
        if (!n) break;
        len += (size_t)n;
        if (units == CHARS) start = len > (size_t)count ? len - (size_t)count : 0;
        else {
            start = len && buf[len - 1] == '\n' ? len - 1 : len;
            while (start > 0) {
                if (buf[start - 1] == '\n' && ++lines >= (size_t)count) break;
                --start;
            }
        }
        memmove(buf, buf + start, len - start);
        len -= start;
    }
    if (direction == REV) {
        size_t end = len, start;
        if (len && buf[len - 1] != '\n') buf[end++] = '\n';
        while (end) {
            start = end - 1;
            while (start && buf[start - 1] != '\n') --start;
            twrite(buf + start, end - start);
            end = start;
        }
    } else twrite(buf, len);
    free(buf);
}

/* Scan backward by blocks, as in the supplied implementation. */
static void
reverse(void)
{
    off_t pos = tseek(0, SEEK_END);
    char *buf = NULL;
    size_t len = 0, capacity = 0, split = 0;
    off_t start = 0;
    int first = 1;
    while (pos > 0 && count > 0) {
        size_t n = pos > 8192 ? 8192 : (size_t)pos, used = 0;
        char *newbuf;
        pos -= (off_t)n;
        if (n > SIZE_MAX - len - 1) { errno = ENOMEM; fatal("tail"); }
        if (len + n + 1 > capacity) {
            capacity = len + n + 1;
            newbuf = realloc(buf, capacity);
            if (newbuf == NULL) fatal("tail: realloc");
            buf = newbuf;
        }
        if (len) memmove(buf + n, buf, len);
        len += n;
        tseek(pos, SEEK_SET);
        while (used < n) {
            ssize_t got = tread(buf + used, n - used);
            if (!got) { fprintf(stderr, "tail: file changed while reading\n"); free(buf); exit(1); }
            used += (size_t)got;
        }
        if (first && buf[len - 1] != '\n') buf[len++] = '\n';
        first = 0;
        for (split = len - 1; split > 0 && count > 0; --split) {
            if (buf[split - 1] == '\n') {
                --count;
                if (direction == REV) twrite(buf + split, len - split);
                len = split;
                if (!count) { start = pos + (off_t)split; break; }
            }
        }
    }
    if (direction == FWD) {
        tseek(start, SEEK_SET);
        copy();
    } else if (count > 0) twrite(buf, len);
    free(buf);
}

static int
getnumber(char *s)
{
    char *end;
    int beginning = *s == '+';
    if (*s == '-' || *s == '+') ++s;
    if (!isdigit((unsigned char)*s)) return 0;
    if (anycount++) usage();
    errno = 0;
    count = strtoll(s, &end, 10);
    if (errno || count < 0 || count > INT_MAX || (*end && !strchr("bclfr", *end))) usage();
    if (beginning) origin = BEG;
    return 1;
}

static void
suffix(char *s)
{
    while (*s && strchr("0123456789+-", *s)) ++s;
    if (*s == 'b') {
        if (count > INT_MAX / 1024) usage();
        count *= 1024; units = CHARS; ++s;
    } else if (*s == 'c') { units = CHARS; ++s; }
    else if (*s == 'l') ++s;
    if (*s == 'r') { direction = REV; ++s; }
    else if (*s == 'f') { follow = 1; ++s; }
    if (*s) usage();
}

int
main(int argc, char **argv)
{
    int seekable;
    off_t previous;
    for (; argc > 1 && (argv[1][0] == '-' || argv[1][0] == '+'); --argc, ++argv) {
        char option = argv[1][1];
        if (strcmp(argv[1], "-") == 0) break;
        if (strcmp(argv[1], "--") == 0) { --argc; ++argv; break; }
        if (getnumber(argv[1])) { suffix(argv[1]); continue; }
        if (option == 'n' || option == 'c') {
            if (option == 'c') units = CHARS;
            if (getnumber(argv[1] + 2)) { suffix(argv[1] + 2); continue; }
            if (argv[1][2]) usage();
            if (argc <= 2 || !getnumber(argv[2])) usage();
            suffix(argv[2]); --argc; ++argv;
        } else if (strcmp(argv[1], "-r") == 0) direction = REV;
        else if (strcmp(argv[1], "-f") == 0) follow = 1;
        else usage();
    }
    if (argc > 2 || (direction == REV && (units == CHARS || follow || origin == BEG))) usage();
    if (!anycount) count = direction == REV ? INT_MAX : 10;
    if (origin == BEG && count > 0) --count;
    if (argc == 2 && strcmp(argv[1], "-") != 0) {
        file = open(argv[1], O_RDONLY);
        if (file < 0) fatal(argv[1]);
    }
    seekable = lseek(file, 0, SEEK_CUR) != (off_t)-1;
    if (count > 0 || origin == BEG) {
        if (origin == BEG) skip();
        else if (!seekable) keep();
        else if (units == CHARS) {
            off_t end = tseek(0, SEEK_END);
            tseek(end > count ? end - (off_t)count : 0, SEEK_SET);
            copy();
        } else reverse();
    } else if (seekable) tseek(0, SEEK_END);
    if (follow && seekable) {
        previous = tseek(0, SEEK_CUR);
        for (;;) {
            struct stat st;
            struct timespec delay = {1, 0};
            while (nanosleep(&delay, &delay) < 0 && errno == EINTR) ;
            if (fstat(file, &st) < 0) fatal("tail: fstat");
            if (st.st_size < previous) tseek(0, SEEK_SET);
            copy();
            previous = tseek(0, SEEK_CUR);
        }
    }
    if (file != STDIN_FILENO && close(file) < 0) fatal("tail: close");
    return 0;
}
