#include "IPC.h"
#include <errno.h>
#include <luna/Alignment.h>
#include <luna/String.h>
#include <os/File.h>
#include <string.h>
#include <sys/mman.h>
#include <time.h>
#include <unistd.h>

#define TRY_OR_IPC_ERROR(expr)                                                                                         \
    ({                                                                                                                 \
        auto _expr_rc = (expr);                                                                                        \
        if (!_expr_rc.has_value())                                                                                     \
        {                                                                                                              \
            delete window;                                                                                             \
            os::IPC::send_error(client.conn, _expr_rc.error());                                                        \
            return {};                                                                                                 \
        }                                                                                                              \
        _expr_rc.release_value();                                                                                      \
    })

static Result<u32*> create_shm_region(const char* path, int* outfd, ui::Rect rect)
{
    int fd = shm_open(path, O_RDWR | O_CREAT | O_EXCL, 0600);
    if (fd < 0)
    {
        os::eprintln("wind: could not create shared memory region: shm_open failed (%s) - %s", path, strerror(errno));
        return err(errno);
    }

    usize size = align_up<PAGE_SIZE>(rect.width * rect.height * 4); // 4 bytes per pixel

    if (ftruncate(fd, size) < 0)
    {
        os::eprintln("wind: could not create shared memory region: ftruncate failed (%d, %zu) - %s", fd, size,
                     strerror(errno));
        shm_unlink(path);
        close(fd);
        return 0;
    }

    void* p = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (p == MAP_FAILED)
    {
        os::eprintln("wind: could not create shared memory region: mmap failed (%zu, %d) - %s", size, fd,
                     strerror(errno));
        shm_unlink(path);
        close(fd);
        return 0;
    }

    if (outfd) *outfd = fd;
    else
        close(fd);
    return (u32*)p;
}

#define READ_MESSAGE(request)                                                                                          \
    do {                                                                                                               \
        auto rc = client.conn.recv_typed(request);                                                                     \
        if (rc.has_error())                                                                                            \
        {                                                                                                              \
            if (rc.error() == EAGAIN)                                                                                  \
            {                                                                                                          \
                client.rpc_in_progress = true;                                                                         \
                client.rpc_id = decltype(request)::ID;                                                                 \
                return {};                                                                                             \
            }                                                                                                          \
            else                                                                                                       \
                return rc.release_error();                                                                             \
        }                                                                                                              \
    } while (0)

#define CHECK_WINDOW_ID(request)                                                                                       \
    do {                                                                                                               \
        if ((usize)request.window >= client.windows.size() || !client.windows[request.window])                         \
        {                                                                                                              \
            os::eprintln("wind: Window id is invalid!");                                                               \
            return {};                                                                                                 \
        }                                                                                                              \
    } while (0)

static Result<void> handle_create_window_message(Client& client)
{
    ui::CreateWindowRequest request;
    READ_MESSAGE(request);

    request.rect = request.rect.normalized();
    request.rect.height += Window::titlebar_height(); // Make sure we provide the full contents rect that was asked for.

    auto name = TRY(String::from_cstring("Window"));

    auto* window = new (std::nothrow) Window(request.rect, move(name));
    if (!window)
    {
        os::IPC::send_error(client.conn, ENOMEM);
        return {};
    }

    auto shm_path = TRY_OR_IPC_ERROR(String::format("/wind-shm-%d-%lu"_sv, client.conn.fd(), time(NULL)));

    window->pixels = TRY_OR_IPC_ERROR(create_shm_region(shm_path.chars(), nullptr, window->contents));

    TRY_OR_IPC_ERROR(client.windows.try_append(window));
    int id = static_cast<int>(client.windows.size() - 1);

    window->client = &client;
    window->id = id;

    ui::CreateWindowResponse response;
    response.window = id;
    SET_IPC_STRING(response.shm_path, shm_path.chars());
    os::IPC::send_async(client.conn, response);
    return {};
}

static Result<void> handle_set_window_title_message(Client& client)
{
    ui::SetWindowTitleRequest request;
    READ_MESSAGE(request);

    auto name = COPY_IPC_STRING(request.title);

    os::println("wind: SetWindowTitle(\"%s\") for window %d", name.chars(), request.window);

    CHECK_WINDOW_ID(request);

    client.windows[request.window]->name = move(name);

    return {};
}

static Result<void> handle_invalidate_message(Client& client)
{
    ui::InvalidateRequest request;
    READ_MESSAGE(request);

    CHECK_WINDOW_ID(request);

    client.windows[request.window]->dirty = true;

    return {};
}

static Result<void> handle_close_window_message(Client& client)
{
    ui::CloseWindowRequest request;
    READ_MESSAGE(request);

    CHECK_WINDOW_ID(request);

    auto* window = client.windows[request.window];
    client.windows[request.window] = nullptr;
    g_windows.remove(window);
    delete window;

    return {};
}

namespace wind
{
    Result<void> handle_ipc_message(Client& client, u8 id)
    {
        client.rpc_in_progress = false;
        switch (id)
        {
        case ui::CREATE_WINDOW_ID: return handle_create_window_message(client);
        case ui::SET_WINDOW_TITLE_ID: return handle_set_window_title_message(client);
        case ui::INVALIDATE_ID: return handle_invalidate_message(client);
        case ui::CLOSE_WINDOW_ID: return handle_close_window_message(client);
        default: os::eprintln("wind: Invalid IPC message from client!"); return err(EINVAL);
        }
    }

    Result<void> handle_ipc(Client& client)
    {
        if (client.rpc_in_progress) return handle_ipc_message(client, client.rpc_id);

        u8 id;
        auto rc = client.conn.recv_typed(id);
        if (rc.has_error())
        {
            if (rc.error() == EAGAIN) { return {}; }
            else
                return rc.release_error();
        }

        return handle_ipc_message(client, id);
    }
}