/**
 * @file File.cpp
 * @author apio (cloudapio.eu)
 * @brief A C++-friendly API for file access.
 *
 * @copyright Copyright (c) 2023, the Luna authors.
 *
 */

#include <errno.h>
#include <luna/StringBuilder.h>
#include <os/File.h>
#include <sys/syscall.h>
#include <unistd.h>

static SharedPtr<os::File> g_stdin = {};
static SharedPtr<os::File> g_stdout = {};
static SharedPtr<os::File> g_stderr = {};

static const char* stdio_mode_from_openmode(int mode)
{
    mode &= ~(O_CREAT | O_EXCL);

    switch (mode)
    {
    case os::File::ReadOnly: return "r";
    case os::File::ReadWrite: return "w+";
    case os::File::WriteOnly: return "w";
    case os::File::ReadAppend: return "a+";
    case os::File::Append: return "a";
    default: fail("Mode incompatible with fdopen()");
    }
}

namespace os
{
    File::File(Badge<File>)
    {
    }

    File::~File()
    {
        if (m_file) fclose(m_file);
    }

    void File::initialize_standard_streams()
    {
        g_stdin = adopt_shared_if_nonnull(new (std::nothrow) File({}))
                      .expect_release_value("Cannot open standard input stream");
        g_stdin->m_file = stdin;

        g_stdout = adopt_shared_if_nonnull(new (std::nothrow) File({}))
                       .expect_release_value("Cannot open standard output stream");
        g_stdout->m_file = stdout;

        g_stderr = adopt_shared_if_nonnull(new (std::nothrow) File({}))
                       .expect_release_value("Cannot open standard error stream");
        g_stderr->m_file = stderr;
    }

    SharedPtr<File> File::standard_input()
    {
        if (!g_stdin) initialize_standard_streams();

        return g_stdin;
    }

    SharedPtr<File> File::standard_output()
    {
        if (!g_stdout) initialize_standard_streams();

        return g_stdout;
    }

    SharedPtr<File> File::standard_error()
    {
        if (!g_stderr) initialize_standard_streams();

        return g_stderr;
    }

    Result<SharedPtr<File>> File::construct(const Path& path, int flags, mode_t mode)
    {
        auto file = TRY(adopt_shared_if_nonnull(new (std::nothrow) File({})));

        long rc = syscall(SYS_openat, path.dirfd(), path.name().chars(), flags, mode);
        int fd = TRY(Result<int>::from_syscall(rc));

        file->m_file = fdopen(fd, stdio_mode_from_openmode(flags));
        if (!file->m_file) return err(errno);

        return file;
    }

    Result<SharedPtr<File>> File::open(const Path& path, OpenMode flags)
    {
        return construct(path, (int)flags, 0);
    }

    Result<SharedPtr<File>> File::open_or_create(const Path& path, OpenMode flags, mode_t mode)
    {
        return construct(path, (int)flags | O_CREAT, mode);
    }

    Result<SharedPtr<File>> File::create(const Path& path, OpenMode flags, mode_t mode)
    {
        return construct(path, (int)flags | (O_CREAT | O_EXCL), mode);
    }

    Result<SharedPtr<File>> File::open_input_file(StringView path)
    {
        if (path == "-"_sv) return standard_input();

        return construct(path, O_RDONLY, 0);
    }

    Result<usize> File::raw_read(u8* buf, usize length)
    {
        size_t nread = fread(buf, 1, length, m_file);
        if (nread == 0 && ferror(m_file)) return err(errno);
        return nread;
    }

    Result<usize> File::raw_write(const u8* buf, usize length)
    {
        size_t nwrite = fwrite(buf, 1, length, m_file);
        if (nwrite == 0 && ferror(m_file)) return err(errno);
        return nwrite;
    }

    Result<void> File::write(StringView str)
    {
        TRY(raw_write((const u8*)str.chars(), str.length()));

        return {};
    }

    Result<void> File::write(const Buffer& buf)
    {
        TRY(raw_write(buf.data(), buf.size()));

        return {};
    }

    Result<String> File::read_line()
    {
        Vector<char> data;

        int current;
        while (true)
        {
            current = TRY(getchar());

            if (current == EOF) break;

            TRY(data.try_append((char)current));

            if (current == '\n') break;
        }

        if (!data.size()) return String {};

        TRY(data.try_append('\0'));

        return String { data.release_data() };
    }

    Result<String> File::read_all_as_string()
    {
        StringBuilder sb;

        while (true)
        {
            auto line = TRY(read_line());
            if (line.is_empty()) break;
            TRY(sb.add(line));
        }

        return sb.string();
    }

    Result<Buffer> File::read_all()
    {
        Buffer data;

        u8 buf[2048];

        while (true)
        {
            usize nread = TRY(raw_read(buf, sizeof(buf)));
            TRY(data.append_data(buf, nread));
            if (nread < sizeof(buf)) break;
        }

        return data;
    }

    Result<void> File::read(Buffer& buf, usize size)
    {
        u8* slice = TRY(buf.slice(0, size));

        usize nread = TRY(raw_read(slice, size));

        TRY(buf.try_resize(nread));

        return {};
    }

    Result<int> File::getchar()
    {
        int rc = fgetc(m_file);
        if (rc == EOF && ferror(m_file)) return err(errno);
        return rc;
    }

    void File::set_close_on_exec()
    {
        fcntl(fd(), F_SETFD, FD_CLOEXEC);
    }

    void File::rewind()
    {
        ::rewind(m_file);
    }

    void File::flush()
    {
        fflush(m_file);
    }

    void File::set_buffer(BufferingMode mode)
    {
        setvbuf(m_file, NULL, mode, 0);
    }

    // FIXME: Do not allocate memory for printing.
    Result<void> print_impl(SharedPtr<File> f, StringView fmt, va_list ap)
    {
        auto str = TRY(String::vformat(fmt, ap));
        auto rc = f->write(str.view());
        f->flush();
        return rc;
    }

    Result<void> print(StringView fmt, ...)
    {
        va_list ap;
        va_start(ap, fmt);

        auto rc = print_impl(File::standard_output(), fmt, ap);

        va_end(ap);

        return rc;
    }

    Result<void> println(StringView fmt, ...)
    {
        va_list ap;
        va_start(ap, fmt);

        auto rc = print_impl(File::standard_output(), fmt, ap);

        va_end(ap);

        TRY(rc);

        return File::standard_output()->write("\n"_sv);
    }

    Result<void> eprint(StringView fmt, ...)
    {
        va_list ap;
        va_start(ap, fmt);

        auto rc = print_impl(File::standard_error(), fmt, ap);

        va_end(ap);

        return rc;
    }

    Result<void> eprintln(StringView fmt, ...)
    {
        va_list ap;
        va_start(ap, fmt);

        auto rc = print_impl(File::standard_error(), fmt, ap);

        va_end(ap);

        TRY(rc);

        return File::standard_error()->write("\n"_sv);
    }
}