/*
 * Copyright © 2017-2019 Canonical Ltd.
 *
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 3,
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Authored by: Christopher James Halse Rogers <christopher.halse.rogers@canonical.com>
 */

#include "in_process_server.h"
#include "thread_proxy.h"
#include "version_specifier.h"
#include "wlcs/display_server.h"
#include "helpers.h"
#include "wlcs/pointer.h"
#include "wlcs/touch.h"
#include "xdg_shell_v6.h"
#include "xdg_shell_stable.h"
#include "generated/wayland-client.h"
#include "generated/xdg-shell-unstable-v6-client.h"
#include "generated/xdg-shell-client.h"

#include "linux/input.h"
#include <boost/throw_exception.hpp>
#include <stdexcept>
#include <memory>
#include <vector>
#include <algorithm>
#include <optional>
#include <map>
#include <unordered_map>
#include <chrono>
#include <poll.h>

using namespace std::literals::chrono_literals;

class ShimNotImplemented : public std::logic_error
{
public:
    ShimNotImplemented() : std::logic_error("Function not implemented in display server shim")
    {
    }

    ShimNotImplemented(const std::string& name)
        : std::logic_error("Function '" + name + "()' not implemented in display server shim")
    {
    }
};

namespace
{
auto interface_description_if_valid(wl_interface const* interface) -> std::string
{
    if (interface)
    {
        using namespace std::literals::string_literals;
        return interface->name + " v"s + std::to_string(interface->version);
    }
    return "<UNKNOWN INTERFACE>";
}
}
wlcs::ProtocolError::ProtocolError(wl_interface const* interface, uint32_t code)
    : std::system_error(EPROTO, std::system_category(), "Wayland protocol error"),
      interface_{interface},
      code_{code},
      message{
          std::string{"Wayland protocol error: "} +
          std::to_string(code) +
          " on interface " + interface_description_if_valid(interface)}
    {
    }

char const* wlcs::ProtocolError::what() const noexcept
{
    return message.c_str();
}

uint32_t wlcs::ProtocolError::error_code() const
{
    return code_;
}

wl_interface const* wlcs::ProtocolError::interface() const
{
    return interface_;
}

wlcs::ExtensionExpectedlyNotSupported::ExtensionExpectedlyNotSupported(char const* extension, VersionSpecifier const& version)
    : std::runtime_error{
        std::string{"Extension: "} +
        extension + " version " + version.describe() +
        " not supported by compositor under test."}
{
    auto const skip_reason =
        std::string{"Missing extension: "} + extension + version.describe();
    ::testing::Test::RecordProperty("wlcs-skip-test", skip_reason);
}

wlcs::Timeout::Timeout(char const* message)
    : std::runtime_error(message)
{
}

class wlcs::Pointer::Impl
{
public:
    template<typename Proxy>
    Impl(
        WlcsPointer* raw_device,
        std::shared_ptr<Proxy> const& proxy,
        std::shared_ptr<void const> const& keep_dso_loaded)
        : keep_dso_loaded{keep_dso_loaded},
          pointer{raw_device}
    {
        setup_thunks(proxy);
    }

    ~Impl()
    {
        destroy_thunk();
    }

    void move_to(int x, int y)
    {
        move_absolute_thunk(wl_fixed_from_int(x), wl_fixed_from_int(y));
    }

    void move_by(int dx, int dy)
    {
        move_relative_thunk(wl_fixed_from_int(dx), wl_fixed_from_int(dy));
    }

    void button_down(int button)
    {
        button_down_thunk(button);
    }

    void button_up(int button)
    {
        button_up_thunk(button);
    }

private:
    template<typename Proxy>
    void setup_thunks(std::shared_ptr<Proxy> const& proxy)
    {
        move_absolute_thunk = proxy->register_op(
            [this](wl_fixed_t x, wl_fixed_t y)
            {
                pointer->move_absolute(pointer, x, y);
            });
        move_relative_thunk = proxy->register_op(
            [this](wl_fixed_t dx, wl_fixed_t dy)
            {
                pointer->move_relative(pointer, dx, dy);
            });
        button_down_thunk = proxy->register_op(
            [this](int button)
            {
                pointer->button_down(pointer, button);
            });
        button_up_thunk = proxy->register_op(
            [this](int button)
            {
                pointer->button_up(pointer, button);
            });
        destroy_thunk = proxy->register_op(
            [this]()
            {
                pointer->destroy(pointer);
            });
    }

    std::shared_ptr<void const> const keep_dso_loaded;
    WlcsPointer* const pointer;

    std::function<void(wl_fixed_t, wl_fixed_t)> move_absolute_thunk;
    std::function<void(wl_fixed_t, wl_fixed_t)> move_relative_thunk;
    std::function<void(int)> button_down_thunk;
    std::function<void(int)> button_up_thunk;
    std::function<void()> destroy_thunk;
};

wlcs::Pointer::~Pointer() = default;
wlcs::Pointer::Pointer(Pointer&&) = default;

template<typename Proxy>
wlcs::Pointer::Pointer(
    WlcsPointer* raw_device,
    std::shared_ptr<Proxy> const& proxy,
    std::shared_ptr<void const> const& keep_dso_loaded)
    : impl{std::make_unique<Impl>(raw_device, proxy, keep_dso_loaded)}
{
}

void wlcs::Pointer::move_to(int x, int y)
{
    impl->move_to(x, y);
}

void wlcs::Pointer::move_by(int dx, int dy)
{
    impl->move_by(dx, dy);
}

void wlcs::Pointer::button_down(int button)
{
    impl->button_down(button);
}

void wlcs::Pointer::button_up(int button)
{
    impl->button_up(button);
}

void wlcs::Pointer::click(int button)
{
    button_down(button);
    button_up(button);
}

void wlcs::Pointer::left_button_down()
{
    button_down(BTN_LEFT);
}

void wlcs::Pointer::left_button_up()
{
    button_up(BTN_LEFT);
}

void wlcs::Pointer::left_click()
{
    click(BTN_LEFT);
}

class wlcs::Touch::Impl
{
public:
    template<typename Proxy>
    Impl(
        WlcsTouch* raw_device,
        std::shared_ptr<Proxy> const& proxy,
        std::shared_ptr<void const> const& keep_dso_loaded)
        : keep_dso_loaded{keep_dso_loaded},
          touch{raw_device, proxy->register_op([](WlcsTouch* raw_device) { raw_device->destroy(raw_device); })}
    {
        if (touch->version != WLCS_TOUCH_VERSION)
        {
            BOOST_THROW_EXCEPTION((
                std::runtime_error{
                    std::string{"Unexpected WlcsTouch version. Expected: "} +
                    std::to_string(WLCS_TOUCH_VERSION) +
                    " received: " +
                    std::to_string(touch->version)}));
        }
        set_up_thunks(proxy);
    }

    void down_at(int x, int y)
    {
        touch->touch_down(touch.get(), x, y);
    }

    void move_to(int x, int y)
    {
        touch->touch_move(touch.get(), x, y);
    }

    void up()
    {
        touch->touch_up(touch.get());
    }

private:
    template<typename Proxy>
    void set_up_thunks(std::shared_ptr<Proxy> const& proxy)
    {
        touch_down_thunk = proxy->register_op(
            [this](int x, int y)
            {
                touch->touch_down(touch.get(), x, y);
            });
        touch_move_thunk = proxy->register_op(
            [this](int x, int y)
            {
                touch->touch_move(touch.get(), x, y);
            });
        touch_up_thunk = proxy->register_op(
            [this]()
            {
                touch->touch_up(touch.get());
            });
    }

    std::shared_ptr<void const> const keep_dso_loaded;
    std::unique_ptr<WlcsTouch, std::function<void(WlcsTouch*)>> const touch;

    std::function<void(int, int)> touch_down_thunk;
    std::function<void(int, int)> touch_move_thunk;
    std::function<void()> touch_up_thunk;
};

wlcs::Touch::~Touch() = default;
wlcs::Touch::Touch(Touch&&) = default;

template<typename Proxy>
wlcs::Touch::Touch(
    WlcsTouch* raw_device,
    std::shared_ptr<Proxy> const& proxy,
    std::shared_ptr<void const> const& keep_dso_loaded)
    : impl{std::make_unique<Impl>(raw_device, proxy, keep_dso_loaded)}
{
}

void wlcs::Touch::down_at(int x, int y)
{
    impl->down_at(x, y);
}

void wlcs::Touch::move_to(int x, int y)
{
    impl->move_to(x, y);
}

void wlcs::Touch::up()
{
    impl->up();
}

namespace
{
std::shared_ptr<std::unordered_map<std::string, uint32_t> const> extract_supported_extensions(WlcsDisplayServer* server)
{
    if (server->version < 2)
    {
        return {};
    }

    auto const descriptor = server->get_descriptor(server);
    auto extensions = std::make_shared<std::unordered_map<std::string, uint32_t>>();

    for (auto i = 0u; i < descriptor->num_extensions; ++i)
    {
        (*extensions)[descriptor->supported_extensions[i].name] = descriptor->supported_extensions[i].version;
    }
    return extensions;
}

class NullProxy
{
public:
    template<typename Callable>
    auto register_op(Callable handler)
    {
        return handler;
    }
};
}

class wlcs::Server::Impl
{
public:
    Impl(std::shared_ptr<WlcsServerIntegration const> const& hooks, int argc, char const** argv)
        : server{hooks->create_server(argc, argv), hooks->destroy_server},
          thread_context{make_context_if_needed(*server)},
          hooks{hooks},
          supported_extensions_{extract_supported_extensions(server.get())}
    {
        if (hooks->version < 1)
        {
            BOOST_THROW_EXCEPTION((std::runtime_error{"Server integration too old"}));
        }
        if (server->version < 1)
        {
            BOOST_THROW_EXCEPTION((std::runtime_error{"Server integration too old"}));
        }
        if (!server->stop)
        {
            BOOST_THROW_EXCEPTION((std::logic_error{"Missing required WlcsDisplayServer.stop definition"}));
        }
        if (thread_context)
        {
            initialise_thunks(thread_context->proxy);
        }
        else
        {
            initialise_thunks(std::make_shared<NullProxy>());
        }
    }

    void start()
    {
        if (thread_context)
        {
            thread_context->server_thread = std::thread{
                [this]()
                {
                    server->start_on_this_thread(server.get(), thread_context->event_loop.get());
                }};
        }
        else
        {
            server->start(server.get());
        }
    }

    void stop()
    {
        stop_thunk();
    }

    int create_client_socket()
    {
        auto fd = create_client_socket_thunk();
        if (fd < 0)
        {
            BOOST_THROW_EXCEPTION((std::system_error{
                errno,
                std::system_category(),
                "Failed to get client socket from server"}));
        }
        return fd;
    }

    Pointer create_pointer()
    {
        if (thread_context)
        {
            return Pointer{create_pointer_thunk(), thread_context->proxy, hooks};
        }
        else
        {
            return Pointer{create_pointer_thunk(), std::make_shared<NullProxy>(), hooks};
        }
    }

    Touch create_touch()
    {
        if (thread_context)
        {
            return Touch{create_touch_thunk(), thread_context->proxy, hooks};
        }
        else
        {
            return Touch{create_touch_thunk(), std::make_shared<NullProxy>(), hooks};
        }
    }

    void move_surface_to(Surface& surface, int x, int y)
    {
        // Ensure the server knows about the IDs we're about to send...
        surface.owner().roundtrip();

        position_window_absolute_thunk(surface.owner(), surface, x, y);
    }

    WlcsDisplayServer* wlcs_server() const
    {
        return server.get();
    }

    std::shared_ptr<const std::unordered_map<std::string, uint32_t>> supported_extensions() const
    {
        return supported_extensions_;
    }

private:
    struct ThreadContext
    {
        explicit ThreadContext(std::unique_ptr<struct wl_event_loop, decltype(&wl_event_loop_destroy)> loop)
            : event_loop{std::move(loop)},
              proxy{std::make_shared<ThreadProxy>(event_loop.get())}
        {
        }
        ThreadContext(ThreadContext&&) = default;

        ~ThreadContext()
        {
            if (server_thread.joinable())
            {
                server_thread.join();
            }
        }

        std::unique_ptr<struct wl_event_loop, void(*)(struct wl_event_loop*)> event_loop;
        std::thread server_thread;
        std::shared_ptr<ThreadProxy> proxy;
    };

    static std::optional<ThreadContext> make_context_if_needed(WlcsDisplayServer const& server)
    {
        if (server.version >= 3)
        {
            if (!server.start)
            {
                if (!server.start_on_this_thread)
                {
                    BOOST_THROW_EXCEPTION((
                        std::runtime_error{"Server integration missing both start() and start_on_this_thread()"}));
                }
                auto loop = std::unique_ptr<struct wl_event_loop, decltype(&wl_event_loop_destroy)>{
                    wl_event_loop_create(),
                    &wl_event_loop_destroy
                };
                if (!loop)
                {
                    BOOST_THROW_EXCEPTION((
                        std::runtime_error{"Failed to create eventloop for WLCS events"}));
                }

                return {ThreadContext{std::move(loop)}};
            }
        }
        return {};
    };

    std::unique_ptr<WlcsDisplayServer, void(*)(WlcsDisplayServer*)> const server;
    std::optional<ThreadContext> thread_context;
    std::shared_ptr<WlcsServerIntegration const> const hooks;
    std::shared_ptr<std::unordered_map<std::string, uint32_t> const> const supported_extensions_;

    template<typename Proxy>
    void initialise_thunks(std::shared_ptr<Proxy> proxy)
    {
        stop_thunk = proxy->register_op(
            [this]()
            {
                server->stop(server.get());
            });
        create_client_socket_thunk = proxy->register_op(
            [this]()
            {
                return server->create_client_socket(server.get());
            });
        create_pointer_thunk = proxy->register_op(
            [this]()
            {
                return server->create_pointer(server.get());
            });
        create_touch_thunk = proxy->register_op(
            [this]()
            {
                return server->create_touch(server.get());
            });
        position_window_absolute_thunk = proxy->register_op(
            [this](
                struct wl_display* client,
                struct wl_surface* surface,
                int x,
                int y)
            {
                server->position_window_absolute(
                    server.get(),
                    client,
                    surface,
                    x,
                    y);
            });
    }
    std::function<void()> stop_thunk;
    std::function<int()> create_client_socket_thunk;
    std::function<WlcsPointer*()> create_pointer_thunk;
    std::function<WlcsTouch*()> create_touch_thunk;
    std::function<void(struct wl_display*, struct wl_surface*, int, int)> position_window_absolute_thunk;
};

wlcs::Server::Server(
    std::shared_ptr<WlcsServerIntegration const> const& hooks,
    int argc,
    char const** argv)
    : impl{std::make_unique<wlcs::Server::Impl>(hooks, argc, argv)}
{
}

wlcs::Server::~Server() = default;

void wlcs::Server::start()
{
    impl->start();
}

void wlcs::Server::stop()
{
    impl->stop();
}

std::shared_ptr<const std::unordered_map<std::string, uint32_t>> wlcs::Server::supported_extensions()
{
    return impl->supported_extensions();
}

int wlcs::Server::create_client_socket()
{
    return impl->create_client_socket();
}

void wlcs::Server::move_surface_to(Surface& surface, int x, int y)
{
    impl->move_surface_to(surface, x, y);
}

wlcs::Pointer wlcs::Server::create_pointer()
{
    return impl->create_pointer();
}

wlcs::Touch wlcs::Server::create_touch()
{
    return impl->create_touch();
}

wlcs::InProcessServer::InProcessServer()
    : server{helpers::get_test_hooks(), helpers::get_argc(), helpers::get_argv()}
{
}

void wlcs::InProcessServer::SetUp()
{
    server.start();
}

void wlcs::InProcessServer::TearDown()
{
    server.stop();
}

wlcs::Server& wlcs::InProcessServer::the_server()
{
    return server;
}

void throw_wayland_error(wl_display* display)
{
    auto err = wl_display_get_error(display);
    if (err != EPROTO)
    {
        BOOST_THROW_EXCEPTION((std::system_error{
            err,
            std::system_category(),
            "Error while dispatching Wayland events"
        }));
    }
    else
    {
        uint32_t object_id;
        uint32_t protocol_error;
        wl_interface const* interface;
        protocol_error = wl_display_get_protocol_error(display, &interface, &object_id);
        BOOST_THROW_EXCEPTION((wlcs::ProtocolError{interface, protocol_error}));
    }
}

class wlcs::Client::Impl
{
public:
    Impl(Server& server)
        : supported_extensions{server.supported_extensions()}
    {
        try
        {
            display = wl_display_connect_to_fd(server.create_client_socket());
        }
        catch (ShimNotImplemented const&)
        {
            // TODO: Warn about connecting to who-knows-what
            display = wl_display_connect(NULL);
        }

        if (!display)
        {
            BOOST_THROW_EXCEPTION((std::runtime_error{"Failed to connect to Wayland socket"}));
        }

        registry = wl_display_get_registry(display);
        wl_registry_add_listener(registry, &registry_listener, this);

        server_roundtrip();
    }

    ~Impl()
    {
        if (shm) wl_shm_destroy(shm);
        if (shell) wl_shell_destroy(shell);
        if (compositor) wl_compositor_destroy(compositor);
        if (subcompositor) wl_subcompositor_destroy(subcompositor);
        if (registry) wl_registry_destroy(registry);
        if (seat) wl_seat_destroy(seat);
        if (keyboard) wl_keyboard_destroy(keyboard);
        if (pointer) wl_pointer_destroy(pointer);
        if (touch) wl_touch_destroy(touch);
        if (xdg_shell_v6) zxdg_shell_v6_destroy(xdg_shell_v6);
        if (xdg_shell_stable) xdg_wm_base_destroy(xdg_shell_stable);
        for (auto const& output: outputs)
            wl_output_destroy(output->current.output);
        for (auto const& callback: destruction_callbacks)
            callback();
        destruction_callbacks.clear();
        wl_display_disconnect(display);
    }

    struct wl_display* wl_display() const
    {
        return display;
    }

    struct wl_compositor* wl_compositor() const
    {
        return compositor;
    }

    struct wl_subcompositor* wl_subcompositor() const
    {
        return subcompositor;
    }

    struct wl_shm* wl_shm() const
    {
        return shm;
    }

    struct wl_seat* wl_seat() const
    {
        return seat;
    }

    void run_on_destruction(std::function<void()> callback)
    {
        destruction_callbacks.push_back(callback);
    }

    ShmBuffer const& create_buffer(Client& client, int width, int height)
    {
        auto buffer = std::make_shared<ShmBuffer>(client, width, height);
        run_on_destruction([buffer]() mutable
            {
                buffer.reset();
            });
        return *buffer;
    }

    Surface create_wl_shell_surface(Client& client, int width, int height)
    {
        Surface surface{client};

        wl_shell_surface * shell_surface = wl_shell_get_shell_surface(the_shell(), surface);
        surface.run_on_destruction([shell_surface]()
            {
                wl_shell_surface_destroy(shell_surface);
            });
        wl_shell_surface_set_toplevel(shell_surface);

        wl_surface_commit(surface);

        surface.attach_visible_buffer(width, height);

        return surface;
    }

    Surface create_xdg_shell_v6_surface(Client& client, int width, int height)
    {
        Surface surface{client};

        auto xdg_surface = std::make_shared<XdgSurfaceV6>(client, surface);
        auto xdg_toplevel = std::make_shared<XdgToplevelV6>(*xdg_surface);

        surface.run_on_destruction([xdg_surface, xdg_toplevel]() mutable
            {
                xdg_toplevel.reset();
                xdg_surface.reset();
            });

        wl_surface_commit(surface);

        surface.attach_visible_buffer(width, height);

        return surface;
    }

    Surface create_xdg_shell_stable_surface(Client& client, int width, int height)
    {
        Surface surface{client};

        auto xdg_surface = std::make_shared<XdgSurfaceStable>(client, surface);
        auto xdg_toplevel = std::make_shared<XdgToplevelStable>(*xdg_surface);

        // Protocol *requires* us to ack the initial configure before attaching a buffer
        bool initial_configure_received = false;
        EXPECT_CALL(*xdg_surface, configure(testing::_))
            .WillOnce(
                [&initial_configure_received, xdg_surface = static_cast<struct xdg_surface*>(*xdg_surface)](uint32_t serial)
                {
                    xdg_surface_ack_configure(xdg_surface, serial);
                    initial_configure_received = true;
                });

        surface.run_on_destruction([xdg_surface, xdg_toplevel]() mutable
            {
                xdg_toplevel.reset();
                xdg_surface.reset();
            });

        wl_surface_commit(surface);

        client.dispatch_until([&initial_configure_received]() { return initial_configure_received; });
        /* Make absolutely sure the functor above, which captures a stack variable by reference
         * is not going to be invoked outside this function
         */
        testing::Mock::VerifyAndClearExpectations(xdg_surface.get());

        /* And we might as well ack any subsequent configures.
         * Since we don't need to capture anything but the xdg_surface, which has to be
         * live if we're getting a callback on it, this is safe to escape this frame
         */
        ON_CALL(*xdg_surface, configure(testing::_))
            .WillByDefault(
                [xdg_surface = static_cast<struct xdg_surface*>(*xdg_surface)](uint32_t serial)
                {
                    xdg_surface_ack_configure(xdg_surface, serial);
                });

        surface.attach_visible_buffer(width, height);

        return surface;
    }

    Surface create_visible_surface(Client& client, int width, int height)
    {
        if (shell)
        {
            return create_wl_shell_surface(client, width, height);
        }
        else if (xdg_shell_stable)
        {
            return create_xdg_shell_stable_surface(client, width, height);
        }
        else if (xdg_shell_v6)
        {
            return create_xdg_shell_v6_surface(client, width, height);
        }
        else
        {
            throw std::runtime_error("compositor does not support any known shell protocols");
        }
    }

    wl_shell* the_shell() const
    {
        if (shell)
        {
            return shell;
        }
        else
        {
            if (!supported_extensions || !supported_extensions->count("wl_shell"))
            {
                BOOST_THROW_EXCEPTION((ExtensionExpectedlyNotSupported{"wl_shell", AnyVersion}));
            }
            else
            {
                throw std::runtime_error("Failed to bind to wl_shell");
            }
        }
    }

    zxdg_shell_v6* the_xdg_shell_v6() const
    {
        if (xdg_shell_v6)
        {
            return xdg_shell_v6;
        }
        else
        {
            if (!supported_extensions || !supported_extensions->count("zxdg_shell_v6"))
            {
                BOOST_THROW_EXCEPTION((ExtensionExpectedlyNotSupported{"zxdg_shell_v6", AnyVersion}));
            }
            else
            {
                throw std::runtime_error("Failed to bind to zxdg_shell_v6");
            }
        }
    }

    xdg_wm_base* the_xdg_shell_stable() const
    {
        return xdg_shell_stable;
    }

    wl_pointer* the_pointer() const { return pointer; }

    wl_surface* keyboard_focused_window() const
    {
        return keyboard_focused_surface;
    }

    wl_surface* window_under_cursor() const
    {
        if (current_pointer_location)
        {
            return current_pointer_location->surface;
        }
        return nullptr;
    }

    wl_surface* touched_window() const
    {
        wl_surface* surface = nullptr;
        for (auto const& touch : current_touches)
        {
            if (surface && touch.second.surface != surface)
                BOOST_THROW_EXCEPTION(std::runtime_error("Multiple surfaces have active touches"));
            else
                surface = touch.second.surface;
        }
        return surface;
    }

    std::pair<wl_fixed_t, wl_fixed_t> pointer_position() const
    {
        return current_pointer_location.value().coordinates;
    };

    std::pair<wl_fixed_t, wl_fixed_t> touch_position() const
    {
        if (current_touches.empty())
            BOOST_THROW_EXCEPTION(std::runtime_error("No touches"));
        else if (current_touches.size() == 1)
            return current_touches.begin()->second.coordinates;
        else
            BOOST_THROW_EXCEPTION(std::runtime_error("More than one touches"));
    };

    std::optional<uint32_t> latest_serial() const
    {
        return latest_serial_;
    }

    bool pointer_events_pending() const
    {
        return !pending_buttons.empty() || pending_pointer_location;
    }

    bool touch_events_pending() const
    {
        return !pending_touches.empty() || !pending_up_touches.empty();
    }

    void add_pointer_enter_notification(PointerEnterNotifier const& on_enter)
    {
        enter_notifiers.push_back(on_enter);
    }
    void add_pointer_leave_notification(PointerLeaveNotifier const& on_leave)
    {
        leave_notifiers.push_back(on_leave);
    }
    void add_pointer_motion_notification(PointerMotionNotifier const& on_motion)
    {
        motion_notifiers.push_back(on_motion);
    }
    void add_pointer_button_notification(PointerButtonNotifier const& on_button)
    {
        button_notifiers.push_back(on_button);
    }

    void* bind_if_supported(wl_interface const& to_bind, VersionSpecifier const& version) const
    {
        std::optional<bool> expected_to_be_supported{};

        if (supported_extensions)
        {
            expected_to_be_supported =
                supported_extensions->count(to_bind.name) &&
                version.select_version(supported_extensions->at(to_bind.name), to_bind.version);
        }

        /* TODO: Mark tests using globals which exist, but are listed as unsupported as
         * may-fail.
         * This should help incrementally implementing a protocol, while getting test
         * feedback.
         */
        auto const global = globals.find(to_bind.name);
        if (global != globals.end())
        {
            auto const version_to_bind = version.select_version(global->second.version, to_bind.version);
            if (version_to_bind)
            {
                auto global_proxy = wl_registry_bind(registry, global->second.id, &to_bind, version_to_bind.value());
                if (!global_proxy)
                {
                    throw_wayland_error(display);
                }
                return global_proxy;
            }
            else if (!expected_to_be_supported.value_or(true))
            {
                // We didn't expect to find the needed version of this extension anyway…
                BOOST_THROW_EXCEPTION((ExtensionExpectedlyNotSupported{to_bind.name, version}));
            }

            BOOST_THROW_EXCEPTION((std::runtime_error{
                "Failed to bind to " + std::string{to_bind.name} + " version " + version.describe()}));
        }
        else if (!expected_to_be_supported.value_or(true))
        {
            // We didn't expect to find this extension anyway…
            BOOST_THROW_EXCEPTION((ExtensionExpectedlyNotSupported{to_bind.name, version}));
        }
        BOOST_THROW_EXCEPTION((std::runtime_error{
            "Failed to bind to " + std::string{to_bind.name} + " version " + version.describe()}));
    }

    void dispatch_until(
        std::function<bool()> const& predicate, std::chrono::seconds timeout)
    {
        using namespace std::chrono;

        auto const end_time = steady_clock::now() + timeout;
        while (!predicate())
        {
            while (wl_display_prepare_read(display) != 0)
            {
                if (wl_display_dispatch_pending(display) < 0)
                    throw_wayland_error(display);
            }
            wl_display_flush(display);

            auto const time_left = end_time - steady_clock::now();
            if (time_left.count() < 0)
            {
                BOOST_THROW_EXCEPTION((Timeout{"Timeout waiting for condition"}));
            }

            /*
             * TODO: We really want std::chrono::duration::ceil() here, but that's C++17
             */
            /*
             * We want to wait *at least* as long as time_left. duration_cast<milliseconds>
             * will perform integer division, so any fractional milliseconds will get dropped.
             *
             * Adding 1ms will ensure we wait until *after* we're meant to timeout.
             */
            auto const maximum_wait_ms = duration_cast<milliseconds>(time_left) + 1ms;
            pollfd fd{
                wl_display_get_fd(display),
                POLLIN | POLLERR,
                0
            };

            auto const poll_result = poll(&fd, 1, maximum_wait_ms.count());
            if (poll_result < 0)
            {
                wl_display_cancel_read(display);
                BOOST_THROW_EXCEPTION((std::system_error{
                    errno,
                    std::system_category(),
                    "Failed to wait for Wayland event"}));
            }

            if (poll_result == 0)
            {
                wl_display_cancel_read(display);
                BOOST_THROW_EXCEPTION((Timeout{"Timeout waiting for condition"}));
            }

            if (wl_display_read_events(display) < 0)
            {
                throw_wayland_error(display);
            }

            if (wl_display_dispatch_pending(display) < 0)
            {
                throw_wayland_error(display);
            }
        }
    }

    void server_roundtrip()
    {
        if (wl_display_roundtrip(display) < 0)
        {
            throw_wayland_error(display);
        }
    }

    void client_flush()
    {
        if (wl_display_flush(display) == -1)
        {
            /* flush will return a (non-fatal) EAGAIN if the send buffer is
             * full.
             *
             * We don't particularly want to care about the EAGAIN case
             * at the moment, so just ignore it.
             */
            if (errno != EAGAIN)
            {
                throw_wayland_error(display);
            }
        }
    }

    struct Output
    {
        OutputState current;
        OutputState pending;
        std::vector<std::function<void()>> done_notifiers;

        Output(struct wl_output* output)
            : current{output},
              pending{output}
        {
        }

        static void geometry_thunk(
            void* ctx,
            struct wl_output */*wl_output*/,
            int32_t x,
            int32_t y,
            int32_t /*physical_width*/,
            int32_t /*physical_height*/,
            int32_t /*subpixel*/,
            const char */*make*/,
            const char */*model*/,
            int32_t /*transform*/)
        {
            auto me = static_cast<Output*>(ctx);
            me->pending.geometry_position = std::make_pair(x, y);
        }

        static void mode_thunk(
            void* ctx,
            struct wl_output */*wl_output*/,
            uint32_t /*flags*/,
            int32_t width,
            int32_t height,
            int32_t /*refresh*/)
        {
            auto me = static_cast<Output*>(ctx);
            me->pending.mode_size = std::make_pair(width, height);
        }

        static void done_thunk(void* ctx, struct wl_output */*wl_output*/)
        {
            auto me = static_cast<Output*>(ctx);

            if (me->pending.geometry_position)
                me->current.geometry_position = me->pending.geometry_position;
            if (me->pending.mode_size)
                me->current.mode_size = me->pending.mode_size;
            if (me->pending.scale)
                me->current.scale = me->pending.scale;

            me->pending = OutputState{me->current.output};

            for (auto const& notifier : me->done_notifiers)
                notifier();
        }

        static void scale_thunk(void* ctx, struct wl_output */*wl_output*/, int32_t factor)
        {
            auto me = static_cast<Output*>(ctx);
            me->pending.scale = factor;
        }

        static constexpr wl_output_listener listener = {
            &Impl::Output::geometry_thunk,
            &Impl::Output::mode_thunk,
            &Impl::Output::done_thunk,
            &Impl::Output::scale_thunk,
        };
    };

    std::vector<std::unique_ptr<Output>> outputs;

private:
    static void keyboard_keymap(void*, wl_keyboard*, uint32_t, int32_t fd, uint32_t)
    {
        close(fd);
    }

    static void keyboard_enter(
        void* ctx,
        wl_keyboard*,
        uint32_t serial,
        wl_surface *surface,
        wl_array* /*keys*/)
    {
        auto me = static_cast<Impl*>(ctx);
        me->keyboard_focused_surface = surface;
        me->latest_serial_ = serial;
    }

    static void keyboard_leave(
        void *ctx,
        wl_keyboard*,
        uint32_t serial,
        wl_surface* surface)
    {
        auto me = static_cast<Impl*>(ctx);
        if (me->keyboard_focused_surface == surface)
        {
            me->keyboard_focused_surface = nullptr;
        }
        me->latest_serial_ = serial;
    }

    static void keyboard_key(
        void* ctx,
        wl_keyboard*,
        uint32_t serial,
        uint32_t /*time*/,
        uint32_t /*key*/,
        uint32_t /*state*/)
    {
        auto me = static_cast<Impl*>(ctx);
        me->latest_serial_ = serial;
    }

    static void keyboard_modifiers(void*, wl_keyboard*, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t)
    {
    }

    static void keyboard_repeat_info(void*, wl_keyboard*, int32_t, int32_t)
    {
    }

    static constexpr wl_keyboard_listener keyboard_listener = {
        keyboard_keymap,
        keyboard_enter,
        keyboard_leave,
        keyboard_key,
        keyboard_modifiers,
        keyboard_repeat_info,
    };

    static void pointer_enter(
        void* ctx,
        wl_pointer* /*pointer*/,
        uint32_t serial,
        wl_surface* surface,
        wl_fixed_t x,
        wl_fixed_t y)
    {
        auto me = static_cast<Impl*>(ctx);
        me->latest_serial_ = serial;

        if (me->current_pointer_location && !me->pending_pointer_leave)
            FAIL()
                << "Pointer tried to enter surface " << surface
                << " without first leaving surface " << me->current_pointer_location.value().surface;

        me->pending_pointer_location = SurfaceLocation{
            surface,
            std::make_pair(x, y)
        };
    }

    static void pointer_leave(
        void* ctx,
        wl_pointer* /*pointer*/,
        uint32_t serial,
        wl_surface* surface)
    {
        auto me = static_cast<Impl*>(ctx);
        me->latest_serial_ = serial;

        if (!me->current_pointer_location)
            FAIL() << "Got wl_pointer.leave when the pointer was not on a surface";

        // the surface should never be null along the wire, but may come out as null if it's been destroyed
        if (surface != nullptr && surface != me->current_pointer_location.value().surface)
            FAIL()
                << "Got wl_pointer.leave with surface " << surface
                << " instead of " << me->current_pointer_location.value().surface;

        me->pending_pointer_location = std::nullopt;
        me->pending_pointer_leave = true;
    }

    static void pointer_motion(
        void* ctx,
        wl_pointer* /*pointer*/,
        uint32_t /*time*/,
        wl_fixed_t x,
        wl_fixed_t y)
    {
        auto me = static_cast<Impl*>(ctx);

        if (!me->current_pointer_location && !me->pending_pointer_location)
            FAIL() << "Got wl_pointer.motion when the pointer was not on a surface";

        if (!me->pending_pointer_location)
            me->pending_pointer_location = me->current_pointer_location;
        me->pending_pointer_location.value().coordinates = std::make_pair(x, y);
    }

    static void pointer_button(
        void *ctx,
        wl_pointer* /*wl_pointer*/,
        uint32_t serial,
        uint32_t /*time*/,
        uint32_t button,
        uint32_t state)
    {
        auto me = static_cast<Impl*>(ctx);
        me->latest_serial_ = serial;

        me->pending_buttons[button] = std::make_pair(serial, state == WL_POINTER_BUTTON_STATE_PRESSED);
    }

    static void pointer_frame(void* ctx, wl_pointer* /*pointer*/)
    {
        auto me = static_cast<Impl*>(ctx);

        if (me->pending_pointer_leave)
        {
            if (!me->current_pointer_location)
                FAIL() << "Pointer tried to leave when it was not on a surface";

            wl_surface* old_surface = me->current_pointer_location.value().surface;
            me->current_pointer_location = std::nullopt;
            me->pending_pointer_leave = false;

            me->notify_of_pointer_leave(old_surface);
        }

        if (me->pending_pointer_location)
        {
            auto const old_pointer_location = me->current_pointer_location;
            me->current_pointer_location = me->pending_pointer_location;
            me->pending_pointer_location = std::nullopt;

            if (!old_pointer_location)
            {
                me->notify_of_pointer_enter(
                    me->current_pointer_location.value().surface,
                    me->current_pointer_location.value().coordinates);
            }
            else
            {
                me->notify_of_pointer_motion(me->current_pointer_location.value().coordinates);
            }
        }

        if (!me->pending_buttons.empty())
        {
            me->notify_of_pointer_buttons(me->pending_buttons);
            me->pending_buttons.clear();
        }
    }

    void notify_of_pointer_enter(wl_surface* surface, std::pair<int, int> position)
    {
        std::vector<decltype(enter_notifiers)::const_iterator> to_remove;
        for (auto notifier = enter_notifiers.begin(); notifier != enter_notifiers.end(); ++notifier)
        {
            if (!(*notifier)(surface, position.first, position.second))
            {
                to_remove.push_back(notifier);
            }
        }
        for (auto removed : to_remove)
        {
            enter_notifiers.erase(removed);
        }
    }

    void notify_of_pointer_leave(wl_surface* surface)
    {
        std::vector<decltype(leave_notifiers)::const_iterator> to_remove;
        for (auto notifier = leave_notifiers.begin(); notifier != leave_notifiers.end(); ++notifier)
        {
            if (!(*notifier)(surface))
            {
                to_remove.push_back(notifier);
            }
        }
        for (auto removed : to_remove)
        {
            leave_notifiers.erase(removed);
        }
    }

    void notify_of_pointer_motion(std::pair<int, int> position)
    {

        std::vector<decltype(motion_notifiers)::const_iterator> to_remove;
        for (auto notifier = motion_notifiers.begin(); notifier != motion_notifiers.end(); ++notifier)
        {
            if (!(*notifier)(position.first, position.second))
            {
                to_remove.push_back(notifier);
            }
        }
        for (auto removed : to_remove)
        {
            motion_notifiers.erase(removed);
        }
    }

    void notify_of_pointer_buttons(std::map<uint32_t, std::pair<uint32_t, bool>> const& buttons)
    {
        for (auto const& button : buttons)
        {
            std::vector<decltype(button_notifiers)::const_iterator> to_remove;
            for (auto notifier = button_notifiers.begin(); notifier != button_notifiers.end(); ++notifier)
            {
                if (!(*notifier)(button.second.first, button.first, button.second.second))
                {
                    to_remove.push_back(notifier);
                }
            }
            for (auto removed : to_remove)
            {
                button_notifiers.erase(removed);
            }
        }
    }

    static constexpr wl_pointer_listener pointer_listener = {
        &Impl::pointer_enter,
        &Impl::pointer_leave,
        &Impl::pointer_motion,
        &Impl::pointer_button,
        [](auto...){},  // axis
        &Impl::pointer_frame,    // frame
        [](auto...){},  // axis_source
        [](auto...){},  // axis_stop
        [](auto...){},  // axis_discrete
    };

    static void touch_down(
        void* ctx,
        wl_touch* /*wl_touch*/,
        uint32_t serial,
        uint32_t /*time*/,
        wl_surface* surface,
        int32_t id,
        wl_fixed_t x,
        wl_fixed_t y)
    {
        auto me = static_cast<Impl*>(ctx);
        me->latest_serial_ = serial;

        auto touch = me->current_touches.find(id);
        if (touch != me->current_touches.end())
            FAIL() << "Got wl_touch.down with ID " << id << " which is already down";

        me->pending_touches[id] = SurfaceLocation {
            surface,
            std::make_pair(x,y)
        };
    }

    static void touch_up(
        void* ctx,
        wl_touch* /*wl_touch*/,
        uint32_t serial,
        uint32_t /*time*/,
        int32_t id)
    {
        auto me = static_cast<Impl*>(ctx);
        me->latest_serial_ = serial;

        auto touch = me->current_touches.find(id);
        if (touch == me->current_touches.end())
            FAIL() << "Got wl_touch.up with unknown ID " << id;

        me->pending_up_touches.insert(id);
    }

	static void touch_motion(
        void* ctx,
        wl_touch* /*wl_touch*/,
        uint32_t /*time*/,
        int32_t id,
        wl_fixed_t x,
        wl_fixed_t y)
    {
        auto me = static_cast<Impl*>(ctx);

        auto touch = me->current_touches.find(id);
        if (touch == me->current_touches.end())
            FAIL() << "Got wl_touch.up with unknown ID " << id;

        me->pending_touches[id] = SurfaceLocation {
            touch->second.surface,
            std::make_pair(x,y)
        };
    }

    static void touch_frame(void* ctx, wl_touch* /*touch*/)
    {
        auto me = static_cast<Impl*>(ctx);

        for (auto const& id : me->pending_up_touches)
        {
            me->current_touches.erase(id);
        }

        for (auto const& touch : me->pending_touches)
        {
            me->current_touches[touch.first] = touch.second;
        }

        me->pending_up_touches.clear();
        me->pending_touches.clear();
    }

    static constexpr wl_touch_listener touch_listener = {
        &Impl::touch_down,
        &Impl::touch_up,
        &Impl::touch_motion,
        &Impl::touch_frame,
        nullptr,    // cancel
        nullptr,    // shape
        nullptr,    // orientation
    };

    static void seat_capabilities(
        void* ctx,
        struct wl_seat* seat,
        uint32_t capabilities)
    {
        auto me = static_cast<Impl*>(ctx);

        if (capabilities & WL_SEAT_CAPABILITY_KEYBOARD)
        {
            me->keyboard = wl_seat_get_keyboard(seat);
            wl_keyboard_add_listener(me->keyboard, &keyboard_listener, me);
        }

        if (capabilities & WL_SEAT_CAPABILITY_POINTER)
        {
            me->pointer = wl_seat_get_pointer(seat);
            wl_pointer_add_listener(me->pointer, &pointer_listener, me);
        }

        if (capabilities & WL_SEAT_CAPABILITY_TOUCH)
        {
            me->touch = wl_seat_get_touch(seat);
            wl_touch_add_listener(me->touch, &touch_listener, me);
        }
    }

    static void seat_name(
        void*,
        struct wl_seat*,
        char const*)
    {
    }

    static constexpr wl_seat_listener seat_listener = {
        &Impl::seat_capabilities,
        &Impl::seat_name
    };

    static void global_handler(
        void* ctx,
        wl_registry* registry,
        uint32_t id,
        char const* interface,
        uint32_t version)
    {
        using namespace std::literals::string_literals;

        static auto const safe_bind = []
            (wl_registry* registry, uint32_t name, const wl_interface* iface, uint32_t version)
        {
            return wl_registry_bind(registry, name, iface, std::min(version, static_cast<uint32_t>(iface->version)));
        };

        auto me = static_cast<Impl*>(ctx);
        me->global_type_names[id] = interface;
        me->globals[interface] = Global{id, version};

        if ("wl_shm"s == interface)
        {
            me->shm = static_cast<struct wl_shm*>(safe_bind(registry, id, &wl_shm_interface, version));
        }
        else if ("wl_compositor"s == interface)
        {
            me->compositor = static_cast<struct wl_compositor*>(
                safe_bind(registry, id, &wl_compositor_interface, version));
        }
        else if ("wl_subcompositor"s == interface)
        {
            me->subcompositor = static_cast<struct wl_subcompositor*>(
                safe_bind(registry, id, &wl_subcompositor_interface, version));
        }
        else if ("wl_shell"s == interface)
        {
            me->shell = static_cast<wl_shell*>(safe_bind(registry, id, &wl_shell_interface, version));
        }
        else if ("wl_seat"s == interface)
        {
            me->seat = static_cast<struct wl_seat*>(safe_bind(registry, id, &wl_seat_interface, version));

            wl_seat_add_listener(me->seat, &seat_listener, me);
            // Ensure we receive the initial seat events.
            me->server_roundtrip();
        }
        else if ("wl_output"s == interface)
        {
            auto wl_output = static_cast<struct wl_output*>(safe_bind(registry, id, &wl_output_interface, version));
            auto output = std::make_unique<Output>(wl_output);
            wl_output_add_listener(wl_output, &Output::listener, output.get());
            me->outputs.push_back(std::move(output));

            // Ensure we receive the initial output events.
            me->server_roundtrip();
        }
        else if ("zxdg_shell_v6"s == interface)
        {
            me->xdg_shell_v6 = static_cast<zxdg_shell_v6*>(safe_bind(registry, id, &zxdg_shell_v6_interface, version));
        }
        else if ("xdg_wm_base"s == interface)
        {
            me->xdg_shell_stable = static_cast<xdg_wm_base*>(safe_bind(registry, id, &xdg_wm_base_interface, version));
        }
    }

    static void global_removed(void* ctx, wl_registry*, uint32_t id)
    {
        auto me = static_cast<Impl*>(ctx);
        auto const name = me->global_type_names.find(id);
        if (name != me->global_type_names.end())
        {
            me->globals.erase(name->second);
            me->global_type_names.erase(name);
        }
    }

    constexpr static wl_registry_listener registry_listener = {
        &global_handler,
        &global_removed
    };

    std::shared_ptr<std::unordered_map<std::string, uint32_t> const> const supported_extensions;

    struct wl_display* display;
    struct wl_registry* registry = nullptr;
    struct wl_compositor* compositor = nullptr;
    struct wl_subcompositor* subcompositor = nullptr;
    struct wl_shm* shm = nullptr;
    struct wl_shell* shell = nullptr;
    struct wl_seat* seat = nullptr;
    struct wl_keyboard* keyboard = nullptr;
    struct wl_pointer* pointer = nullptr;
    struct wl_touch* touch = nullptr;
    struct zxdg_shell_v6* xdg_shell_v6 = nullptr;
    std::vector<std::function<void()>> destruction_callbacks;
    struct xdg_wm_base* xdg_shell_stable = nullptr;

    struct Global
    {
        uint32_t id;
        uint32_t version;
    };
    std::map<std::string, Global> globals;
    std::map<uint32_t, std::string> global_type_names;

    struct SurfaceLocation
    {
        wl_surface* surface;
        std::pair<wl_fixed_t, wl_fixed_t> coordinates;
    };
    wl_surface* keyboard_focused_surface = nullptr;
    std::optional<SurfaceLocation> current_pointer_location;
    std::optional<SurfaceLocation> pending_pointer_location;
    bool pending_pointer_leave{false};
    std::map<uint32_t, std::pair<uint32_t, bool>> pending_buttons; ///< Maps button id to the serial and if it's down
    std::map<int, SurfaceLocation> current_touches; ///< Touches that have gotten a frame event
    std::map<int, SurfaceLocation> pending_touches; ///< Touches that have gotten down or motion events without a frame
    std::set<int> pending_up_touches; ///< Touches that have gotten up events without a frame
    std::optional<uint32_t> latest_serial_;

    std::vector<PointerEnterNotifier> enter_notifiers;
    std::vector<PointerLeaveNotifier> leave_notifiers;
    std::vector<PointerMotionNotifier> motion_notifiers;
    std::vector<PointerButtonNotifier> button_notifiers;
};

constexpr wl_keyboard_listener wlcs::Client::Impl::keyboard_listener;
constexpr wl_pointer_listener wlcs::Client::Impl::pointer_listener;
constexpr wl_touch_listener wlcs::Client::Impl::touch_listener;
constexpr wl_seat_listener wlcs::Client::Impl::seat_listener;
constexpr wl_output_listener wlcs::Client::Impl::Output::listener;
constexpr wl_registry_listener wlcs::Client::Impl::registry_listener;

wlcs::Client::Client(Server& server)
    : impl{std::make_unique<Impl>(server)}
{
}

wlcs::Client::~Client() = default;

wlcs::Client::operator wl_display*() const
{
    return impl->wl_display();
}

wl_compositor* wlcs::Client::compositor() const
{
    return impl->wl_compositor();
}

wl_subcompositor* wlcs::Client::subcompositor() const
{
    return impl->wl_subcompositor();
}

wl_shm* wlcs::Client::shm() const
{
    return impl->wl_shm();
}

wl_seat* wlcs::Client::seat() const
{
    return impl->wl_seat();
}

void wlcs::Client::run_on_destruction(std::function<void()> callback)
{
    impl->run_on_destruction(callback);
}

wlcs::ShmBuffer const& wlcs::Client::create_buffer(int width, int height)
{
    return impl->create_buffer(*this, width, height);
}

wlcs::Surface wlcs::Client::create_wl_shell_surface(int width, int height)
{
    return impl->create_wl_shell_surface(*this, width, height);
}

wlcs::Surface wlcs::Client::create_xdg_shell_v6_surface(int width, int height)
{
    return impl->create_xdg_shell_v6_surface(*this, width, height);
}

wlcs::Surface wlcs::Client::create_xdg_shell_stable_surface(int width, int height)
{
    return impl->create_xdg_shell_stable_surface(*this, width, height);
}

wlcs::Surface wlcs::Client::create_visible_surface(int width, int height)
{
    return impl->create_visible_surface(*this, width, height);
}

size_t wlcs::Client::output_count() const
{
    return impl->outputs.size();
}

wlcs::OutputState wlcs::Client::output_state(size_t index) const
{
    if (index > output_count())
        throw std::out_of_range("Invalid output index");

    return impl->outputs[index]->current;
}

void wlcs::Client::add_output_done_notifier(size_t index, std::function<void()> const& notifier)
{
    if (index > output_count())
        throw std::runtime_error("Invalid output index");

    impl->outputs[index]->done_notifiers.push_back(notifier);
}

wl_shell* wlcs::Client::shell() const
{
    return impl->the_shell();
}

zxdg_shell_v6* wlcs::Client::xdg_shell_v6() const
{
    return impl->the_xdg_shell_v6();
}

xdg_wm_base* wlcs::Client::xdg_shell_stable() const
{
    return impl->the_xdg_shell_stable();
}

wl_surface* wlcs::Client::keyboard_focused_window() const
{
    return impl->keyboard_focused_window();
}

wl_surface* wlcs::Client::window_under_cursor() const
{
    return impl->window_under_cursor();
}

wl_surface* wlcs::Client::touched_window() const
{
    return impl->touched_window();
}

std::pair<wl_fixed_t, wl_fixed_t> wlcs::Client::pointer_position() const
{
    return impl->pointer_position();
}

std::pair<wl_fixed_t, wl_fixed_t> wlcs::Client::touch_position() const
{
    return impl->touch_position();
}

std::optional<uint32_t> wlcs::Client::latest_serial() const
{
    return impl->latest_serial();
}

void wlcs::Client::add_pointer_enter_notification(PointerEnterNotifier const& on_enter)
{
    impl->add_pointer_enter_notification(on_enter);
}

void wlcs::Client::add_pointer_leave_notification(PointerLeaveNotifier const& on_leave)
{
    impl->add_pointer_leave_notification(on_leave);
}

void wlcs::Client::add_pointer_motion_notification(PointerMotionNotifier const& on_motion)
{
    impl->add_pointer_motion_notification(on_motion);
}

void wlcs::Client::add_pointer_button_notification(PointerButtonNotifier const& on_button)
{
    impl->add_pointer_button_notification(on_button);
}

void wlcs::Client::dispatch_until(std::function<bool()> const& predicate, std::chrono::seconds timeout)
{
    impl->dispatch_until(predicate, timeout);
}

void wlcs::Client::roundtrip()
{
    impl->server_roundtrip();
}

void wlcs::Client::flush()
{
    impl->client_flush();
}

void* wlcs::Client::bind_if_supported(wl_interface const& interface, VersionSpecifier const& version) const
{
    return impl->bind_if_supported(interface, version);
}

wl_pointer* wlcs::Client::the_pointer() const
{
    return impl->the_pointer();
}

class wlcs::Surface::Impl
{
public:
    Impl(Client& client)
        : surface_{wl_compositor_create_surface(client.compositor())},
          owner_{client}
    {
        wl_surface_add_listener(surface_, &surface_listener, this);
    }

    ~Impl()
    {
        for (auto i = 0u; i < pending_callbacks.size(); )
        {
            if (pending_callbacks[i].first == this)
            {
                auto pending_callback = pending_callbacks[i].second;
                delete static_cast<std::function<void(uint32_t)>*>(wl_callback_get_user_data(pending_callback));

                wl_callback_destroy(pending_callback);
                pending_callbacks.erase(pending_callbacks.begin() + i);
            }
            else
            {
                ++i;
            }
        }

        for (auto const& callback: destruction_callbacks)
            callback();
        destruction_callbacks.clear();

        wl_surface_destroy(surface_);
    }

    ::wl_surface* surface() const
    {
        return surface_;
    }

    void attach_buffer(int width, int height)
    {
        auto& buffer = owner_.create_buffer(width, height);
        wl_surface_attach(surface_, buffer, 0, 0);
    }

    void add_frame_callback(std::function<void(uint32_t)> const& on_frame)
    {
        std::unique_ptr<std::function<void(uint32_t)>> holder{
            new std::function<void(uint32_t)>(on_frame)};

        auto callback = wl_surface_frame(surface_);

        pending_callbacks.push_back(std::make_pair(this, callback));

        wl_callback_add_listener(callback, &frame_listener, holder.release());
    }

    void attach_visible_buffer(int width, int height)
    {
        attach_buffer(width, height);
        auto surface_rendered = std::make_shared<bool>(false);
        add_frame_callback([surface_rendered](auto) { *surface_rendered = true; });
        wl_surface_commit(surface_);
        owner_.dispatch_until([surface_rendered]() { return *surface_rendered; });
    }

    void run_on_destruction(std::function<void()> callback)
    {
        destruction_callbacks.push_back(callback);
    }

    Client& owner() const
    {
        return owner_;
    }

    auto current_outputs() -> std::set<wl_output*> const&
    {
        return outputs;
    }

private:

    static std::vector<std::pair<Impl const*, wl_callback*>> pending_callbacks;
    std::set<wl_output*> outputs;

    static void frame_callback(void* ctx, wl_callback* callback, uint32_t frame_time)
    {
        auto us = std::find_if(
            pending_callbacks.begin(),
            pending_callbacks.end(),
            [callback](auto const& elem)
            {
                return elem.second == callback;
            });
        pending_callbacks.erase(us);

        auto frame_callback = static_cast<std::function<void(uint32_t)>*>(ctx);

        (*frame_callback)(frame_time);

        wl_callback_destroy(callback);
        delete frame_callback;
    }

    static constexpr wl_callback_listener frame_listener = {
        &frame_callback
    };

    static void on_enter(void* data, ::wl_surface* /*wl_surface*/, wl_output* output)
    {
        auto const self = static_cast<Impl*>(data);

        auto const inserted = self->outputs.insert(output);

        if (!inserted.second)
        {
            BOOST_THROW_EXCEPTION(std::runtime_error(
                "Got wl_surface.enter(wl_output@" +
                std::to_string(wl_proxy_get_id(reinterpret_cast<wl_proxy*>(output))) +
                ") for an output the surface is already on"));
        }
    }

    static void on_leave(void* data, ::wl_surface* /*wl_surface*/, wl_output* output)
    {
        auto const self = static_cast<Impl*>(data);

        auto const erased = self->outputs.erase(output);

        if (!erased)
        {
            BOOST_THROW_EXCEPTION(std::runtime_error(
                "Got wl_surface.leave(wl_output@" +
                std::to_string(wl_proxy_get_id(reinterpret_cast<wl_proxy*>(output))) +
                ") for an output the surface is not on"));
        }
    }

    static constexpr wl_surface_listener surface_listener{
        &on_enter,
        &on_leave,
    };

    struct wl_surface* const surface_;
    Client& owner_;
    std::vector<std::function<void()>> destruction_callbacks;
};

std::vector<std::pair<wlcs::Surface::Impl const*, wl_callback*>> wlcs::Surface::Impl::pending_callbacks;

constexpr wl_callback_listener wlcs::Surface::Impl::frame_listener;
constexpr wl_surface_listener wlcs::Surface::Impl::surface_listener;

wlcs::Surface::Surface(Client& client)
    : impl{std::make_unique<Impl>(client)}
{
}

wlcs::Surface::~Surface() = default;

wlcs::Surface::Surface(Surface&&) = default;

wlcs::Surface::operator ::wl_surface*() const
{
    return impl->surface();
}

void wlcs::Surface::attach_buffer(int width, int height)
{
    impl->attach_buffer(width, height);
}

void wlcs::Surface::add_frame_callback(std::function<void(int)> const& on_frame)
{
    impl->add_frame_callback(on_frame);
}

void wlcs::Surface::attach_visible_buffer(int width, int height)
{
    impl->attach_visible_buffer(width, height);
}

void wlcs::Surface::run_on_destruction(std::function<void()> callback)
{
    impl->run_on_destruction(callback);
}

wlcs::Client& wlcs::Surface::owner() const
{
    return impl->owner();
}

auto wlcs::Surface:: current_outputs() -> std::set<wl_output*> const&
{
    return impl->current_outputs();
}

class wlcs::Subsurface::Impl
{
public:
    Impl(Client& client, Surface& surface, Surface& parent)
        : subsurface_{wl_subcompositor_get_subsurface(client.subcompositor(), surface, parent)},
          parent_{parent}
    {
    }

    ~Impl()
    {
        wl_subsurface_destroy(subsurface_);
    }

    struct wl_subsurface* const subsurface_;
    Surface& parent_;
};

wlcs::Subsurface wlcs::Subsurface::create_visible(Surface& parent, int x, int y, int width, int height)
{
    wlcs::Subsurface subsurface{parent};
    wl_subsurface_set_position(subsurface, x, y);

    subsurface.attach_buffer(width, height);

    bool surface_rendered{false};
    subsurface.add_frame_callback([&surface_rendered](auto) { surface_rendered = true; });
    wlcs::Surface* surface_ptr = &subsurface;
    while (surface_ptr)
    {
        wl_surface_commit(*surface_ptr);
        auto subsurface_ptr = dynamic_cast<wlcs::Subsurface*>(surface_ptr);
        if (subsurface_ptr)
            surface_ptr = &subsurface_ptr->parent();
        else
            surface_ptr = nullptr;
    }
    parent.owner().dispatch_until([&surface_rendered]() { return surface_rendered; });

    return subsurface;
}

wlcs::Subsurface::Subsurface(wlcs::Surface& parent)
    : Surface{parent.owner()},
      impl{std::make_unique<wlcs::Subsurface::Impl>(parent.owner(), *this, parent)}
{
}

wlcs::Subsurface::Subsurface(Subsurface &&) = default;

wlcs::Subsurface::~Subsurface() = default;

wlcs::Subsurface::operator wl_subsurface*() const
{
    return impl->subsurface_;
}

wlcs::Surface& wlcs::Subsurface::parent() const
{
    return impl->parent_;
}

class wlcs::ShmBuffer::Impl
{
public:
    Impl(Client& client, int width, int height)
    {
        auto stride = width * 4;
        auto size = stride * height;
        auto fd = wlcs::helpers::create_anonymous_file(size);

        auto pool = wl_shm_create_pool(client.shm(), fd, size);
        buffer_ = wl_shm_pool_create_buffer(
            pool,
            0,
            width,
            height,
            stride,
            WL_SHM_FORMAT_ARGB8888);
        wl_shm_pool_destroy(pool);
        close(fd);

        wl_buffer_add_listener(buffer_, &listener, this);
    }

    ~Impl()
    {
        wl_buffer_destroy(buffer_);
    }

    wl_buffer* buffer() const
    {
        return buffer_;
    }

    void add_release_listener(std::function<bool()> const& on_release)
    {
        release_notifiers.push_back(on_release);
    }

private:
    static void on_release(void* ctx, wl_buffer* /*buffer*/)
    {
        auto me = static_cast<Impl*>(ctx);

        std::vector<decltype(me->release_notifiers.begin())> expired_notifiers;

        for (auto notifier = me->release_notifiers.begin(); notifier != me->release_notifiers.end(); ++notifier)
        {
            if (!(*notifier)())
            {
                expired_notifiers.push_back(notifier);
            }
        }
        for (auto const& expired : expired_notifiers)
            me->release_notifiers.erase(expired);
    }

    static constexpr wl_buffer_listener listener {
        &on_release
    };

    wl_buffer* buffer_;
    std::vector<std::function<bool()>> release_notifiers;
};

constexpr wl_buffer_listener wlcs::ShmBuffer::Impl::listener;

wlcs::ShmBuffer::ShmBuffer(Client &client, int width, int height)
    : impl{std::make_unique<Impl>(client, width, height)}
{
}

wlcs::ShmBuffer::ShmBuffer(ShmBuffer&&) = default;
wlcs::ShmBuffer::~ShmBuffer() = default;

wlcs::ShmBuffer::operator wl_buffer*() const
{
    return impl->buffer();
}

void wlcs::ShmBuffer::add_release_listener(std::function<bool()> const &on_release)
{
    impl->add_release_listener(on_release);
}

wlcs::OutputState::OutputState(wl_output* output) : output{output}
{
}
