/*
 * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#include <LibCore/ArgsParser.h>
#include <LibCore/EventLoop.h>
#include <LibCore/FileWatcher.h>
#include <LibCore/Stream.h>
#include <LibCore/System.h>

#define DEFAULT_LINE_COUNT 10

static ErrorOr<void> tail_from_pos(Core::Stream::File& file, off_t startline)
{
    TRY(file.seek(startline + 1, Core::Stream::SeekMode::SetPosition));
    auto buffer = TRY(file.read_all());
    out("{}", StringView { buffer });
    return {};
}

static ErrorOr<off_t> find_seek_pos(Core::Stream::File& file, int wanted_lines)
{
    // Rather than reading the whole file, start at the end and work backwards,
    // stopping when we've found the number of lines we want.
    off_t pos = TRY(file.seek(0, Core::Stream::SeekMode::FromEndPosition));

    off_t end = pos;
    int lines = 0;

    for (; pos >= 0; pos--) {
        TRY(file.seek(pos, Core::Stream::SeekMode::SetPosition));

        if (file.is_eof())
            break;
        Array<u8, 1> buffer;
        auto ch = TRY(file.read(buffer));
        if (*ch.data() == '\n' && (end - pos) > 1) {
            lines++;
            if (lines == wanted_lines)
                break;
        }
    }

    return pos;
}

ErrorOr<int> serenity_main(Main::Arguments arguments)
{
    TRY(Core::System::pledge("stdio rpath"));

    bool follow = false;
    size_t wanted_line_count = DEFAULT_LINE_COUNT;
    StringView file;

    Core::ArgsParser args_parser;
    args_parser.set_general_help("Print the end ('tail') of a file.");
    args_parser.add_option(follow, "Output data as it is written to the file", "follow", 'f');
    args_parser.add_option(wanted_line_count, "Fetch the specified number of lines", "lines", 'n', "number");
    args_parser.add_positional_argument(file, "File path", "file", Core::ArgsParser::Required::No);
    args_parser.parse(arguments);

    auto f = TRY(Core::Stream::File::open_file_or_standard_stream(file, Core::Stream::OpenMode::Read));
    if (!follow)
        TRY(Core::System::pledge("stdio"));

    auto file_is_seekable = !f->tell().is_error();
    if (!file_is_seekable) {
        do {
            // FIXME: If f is the standard input, f->read_all() does not block
            // anymore after sending EOF (^D), despite f->is_open() returning true.
            auto buffer = TRY(f->read_all(PAGE_SIZE));
            auto line_count = StringView(buffer).count("\n"sv);
            auto bytes = buffer.bytes();
            size_t line_index = 0;
            StringBuilder line;

            if (!line_count && wanted_line_count) {
                out("{}", StringView { bytes });
                continue;
            }

            for (size_t i = 0; i < bytes.size(); i++) {
                auto ch = bytes.at(i);
                line.append(ch);
                if (ch == '\n') {
                    if (wanted_line_count > line_count || line_index >= line_count - wanted_line_count)
                        out("{}", line.build());
                    line_index++;
                    line.clear();
                }
            }

            // Since we can't have FileWatchers on the standard input either,
            // we just loop forever if the -f option was passed.
        } while (follow);
        return 0;
    }

    auto pos = TRY(find_seek_pos(*f, wanted_line_count));
    TRY(tail_from_pos(*f, pos));

    if (follow) {
        TRY(f->seek(0, Core::Stream::SeekMode::FromEndPosition));

        Core::EventLoop event_loop;
        auto watcher = TRY(Core::FileWatcher::create());
        watcher->on_change = [&](Core::FileWatcherEvent const& event) {
            if (event.type == Core::FileWatcherEvent::Type::ContentModified) {
                auto buffer_or_error = f->read_all();
                if (buffer_or_error.is_error()) {
                    auto error = buffer_or_error.error();
                    warnln(error.string_literal());
                    event_loop.quit(error.code());
                    return;
                }
                auto bytes = buffer_or_error.value().bytes();
                out("{}", StringView { bytes });

                auto potential_error = f->seek(0, Core::Stream::SeekMode::FromEndPosition);
                if (potential_error.is_error()) {
                    auto error = potential_error.error();
                    warnln(error.string_literal());
                    event_loop.quit(error.code());
                    return;
                }
            }
        };
        TRY(watcher->add_watch(file, Core::FileWatcherEvent::Type::ContentModified));
        TRY(Core::System::pledge("stdio"));
        return event_loop.exec();
    }
    return 0;
}
