#include <pugixml.hpp>
#include <iostream>
#include <string>
#include <fstream>
#include <vector>
#include <algorithm>
#include <filesystem>

bool waylandEnums = false;
bool clientCode   = false;
bool noInterfaces = false;

struct SRequestArgument {
    std::string wlType;
    std::string interface;
    std::string enumName;
    std::string name;
    bool        newType   = false;
    bool        allowNull = false;
};

struct SWaylandFunction {
    std::vector<SRequestArgument> args;
    std::string                   name;
    std::string                   since;
    std::string                   newIdType  = ""; // client only
    bool                          destructor = false;
};

struct SInterface {
    std::vector<SWaylandFunction> requests;
    std::vector<SWaylandFunction> events;
    std::string                   name;
    int                           version = 1;
};

struct SEnum {
    std::string                              name;
    std::string                              nameOriginal;
    std::vector<std::pair<std::string, int>> values;
};

struct {
    std::vector<SInterface> ifaces;
    std::vector<SEnum>      enums;
} XMLDATA;

const char* resourceName() {
    return clientCode ? "wl_proxy" : "wl_resource";
}

std::string sanitize(const std::string& in) {
    if (in == "namespace")
        return "namespace_";
    if (in == "class")
        return "class_";
    if (in == "delete")
        return "delete_";
    if (in == "new")
        return "new_";
    return in;
}

std::string argsToShort(std::vector<SRequestArgument>& args, const std::string& since) {
    std::string shortt = since;
    for (auto& a : args) {
        if (a.wlType == "int")
            shortt += "i";
        else if (a.wlType == "new_id") {
            if (a.interface.empty())
                shortt += "su";
            shortt += "n";
        } else if (a.wlType == "uint")
            shortt += "u";
        else if (a.wlType == "fixed")
            shortt += "f";
        else if (a.wlType == "string")
            shortt += std::string(a.allowNull ? "?s" : "s");
        else if (a.wlType == "object")
            shortt += std::string(a.allowNull ? "?" : "") + "o";
        else if (a.wlType == "array")
            shortt += "a";
        else if (a.wlType == "fd")
            shortt += "h";
        else
            throw std::runtime_error("Unknown arg in argsToShort");
    }
    return shortt;
}

std::string camelize(std::string snake) {
    std::string result = "";
    for (size_t i = 0; i < snake.length(); ++i) {
        if (snake[i] == '_' && i != 0 && i + 1 < snake.length() && snake[i + 1] != '_') {
            result += ::toupper(snake[i + 1]);
            i++;
            continue;
        }

        result += snake[i];
    }

    return result;
}

std::string WPTypeToCType(const SRequestArgument& arg, bool event /* events pass iface ptrs, requests ids */, bool ignoreTypes = false /* for dangerous */) {
    if (arg.wlType == "uint" || arg.wlType == "new_id") {
        if (arg.enumName.empty() && arg.interface.empty())
            return "uint32_t";

        // enum
        if (!arg.enumName.empty()) {
            for (auto& e : XMLDATA.enums) {
                if (e.nameOriginal == arg.enumName)
                    return e.name;
            }
            return "uint32_t";
        }

        if (!event && clientCode && arg.wlType == "new_id")
            return "wl_proxy*";

        // iface
        if (!arg.interface.empty() && event) {
            for (auto& i : XMLDATA.ifaces) {
                if (i.name == arg.interface)
                    return camelize((clientCode ? "CC_" : "C_") + arg.interface + "*");
            }
            return std::string{resourceName()} + "*";
        }

        return "uint32_t";
    }
    if (arg.wlType == "object") {
        if (!arg.interface.empty() && event && !ignoreTypes) {
            for (auto& i : XMLDATA.ifaces) {
                if (i.name == arg.interface)
                    return camelize((clientCode ? "CC_" : "C_") + arg.interface + "*");
            }
        }
        return std::string{resourceName()} + "*";
    }
    if (arg.wlType == "int" || arg.wlType == "fd")
        return "int32_t";
    if (arg.wlType == "fixed")
        return "wl_fixed_t";
    if (arg.wlType == "array")
        return "wl_array*";
    if (arg.wlType == "string")
        return "const char*";
    throw std::runtime_error("unknown wp type");
    return "";
}

std::string HEADER;
std::string SOURCE;

struct {
    std::string name;
    std::string nameOriginal;
    std::string fileName;
} PROTO_DATA;

void parseXML(pugi::xml_document& doc) {

    for (auto& ge : doc.child("protocol").children("enum")) {
        SEnum enum_;
        enum_.nameOriginal = ge.attribute("name").as_string();
        enum_.name         = waylandEnums ? "enum " + PROTO_DATA.name + "_" + enum_.nameOriginal : camelize(PROTO_DATA.name + "_" + enum_.nameOriginal);
        for (auto& entry : ge.children("entry")) {
            auto VALUENAME = enum_.nameOriginal + "_" + entry.attribute("name").as_string();
            std::transform(VALUENAME.begin(), VALUENAME.end(), VALUENAME.begin(), ::toupper);
            enum_.values.emplace_back(std::make_pair<>(VALUENAME, entry.attribute("value").as_int()));
        }
        XMLDATA.enums.push_back(enum_);
    }

    for (auto& iface : doc.child("protocol").children("interface")) {
        SInterface ifc;
        ifc.name    = iface.attribute("name").as_string();
        ifc.version = iface.attribute("version").as_int();

        for (auto& en : iface.children("enum")) {
            SEnum enum_;
            enum_.nameOriginal = en.attribute("name").as_string();
            enum_.name         = waylandEnums ? "enum " + ifc.name + "_" + enum_.nameOriginal : camelize(ifc.name + "_" + enum_.nameOriginal);
            for (auto& entry : en.children("entry")) {
                auto VALUENAME = ifc.name + "_" + enum_.nameOriginal + "_" + entry.attribute("name").as_string();
                std::transform(VALUENAME.begin(), VALUENAME.end(), VALUENAME.begin(), ::toupper);
                enum_.values.emplace_back(std::make_pair<>(VALUENAME, entry.attribute("value").as_int()));
            }
            XMLDATA.enums.push_back(enum_);
        }

        for (auto& rq : iface.children("request")) {
            SWaylandFunction srq;
            srq.name       = rq.attribute("name").as_string();
            srq.since      = rq.attribute("since").as_string();
            srq.destructor = rq.attribute("type").as_string() == std::string{"destructor"};

            for (auto& arg : rq.children("arg")) {
                SRequestArgument sargm;
                if (arg.attribute("type").as_string() == std::string{"new_id"} && clientCode)
                    srq.newIdType = arg.attribute("interface").as_string();

                sargm.newType   = arg.attribute("type").as_string() == std::string{"new_id"} && clientCode;
                sargm.name      = sanitize(arg.attribute("name").as_string());
                sargm.wlType    = arg.attribute("type").as_string();
                sargm.interface = arg.attribute("interface").as_string();
                sargm.enumName  = arg.attribute("enum").as_string();
                sargm.allowNull = arg.attribute("allow-null").as_string() == std::string{"true"};

                srq.args.push_back(sargm);
            }

            ifc.requests.push_back(srq);
        }

        for (auto& ev : iface.children("event")) {
            SWaylandFunction sev;
            sev.name       = ev.attribute("name").as_string();
            sev.since      = ev.attribute("since").as_string();
            sev.destructor = ev.attribute("type").as_string() == std::string{"destructor"};

            for (auto& arg : ev.children("arg")) {
                SRequestArgument sargm;
                sargm.name      = sanitize(arg.attribute("name").as_string());
                sargm.interface = arg.attribute("interface").as_string();
                sargm.wlType    = arg.attribute("type").as_string();
                sargm.enumName  = arg.attribute("enum").as_string();
                sargm.allowNull = arg.attribute("allow-null").as_string() == std::string{"true"};

                sev.args.push_back(sargm);
            }

            ifc.events.push_back(sev);
        }

        XMLDATA.ifaces.push_back(ifc);
    }
}

void parseHeader() {

    // add some boilerplate
    HEADER +=
        std::format(R"#(#pragma once

#include <functional>
#include <cstdint>
#include <string>
{}

#define F std::function

{}

)#",
                    (clientCode ? "#include <wayland-client.h>" : "#include <wayland-server.h>"), (clientCode ? "struct wl_proxy;" : "struct wl_client;\nstruct wl_resource;"));

    // parse all enums
    if (!waylandEnums) {
        for (auto& en : XMLDATA.enums) {
            HEADER += std::format("enum {} : uint32_t {{\n", en.name);
            for (auto& [k, v] : en.values) {
                HEADER += std::format("    {} = {},\n", k, v);
            }
            HEADER += "};\n\n";
        }
    }

    // fw declare all classes
    for (auto& iface : XMLDATA.ifaces) {
        const auto IFACE_CLASS_NAME_CAMEL = camelize((clientCode ? "CC_" : "C_") + iface.name);
        HEADER += std::format("\nclass {};", IFACE_CLASS_NAME_CAMEL);

        for (auto& rq : iface.requests) {
            for (auto& arg : rq.args) {
                if (!arg.interface.empty()) {
                    HEADER += std::format("\nclass {};", camelize((clientCode ? "CC_" : "C_") + arg.interface));
                }
            }
        }

        for (auto& rq : iface.events) {
            for (auto& arg : rq.args) {
                if (!arg.interface.empty()) {
                    HEADER += std::format("\nclass {};", camelize((clientCode ? "CC_" : "C_") + arg.interface));
                }
            }
        }
    }

    HEADER += "\n\n#ifndef HYPRWAYLAND_SCANNER_NO_INTERFACES\n";

    for (auto& iface : XMLDATA.ifaces) {
        const auto IFACE_WL_NAME       = iface.name + "_interface";
        const auto IFACE_WL_NAME_CAMEL = camelize(iface.name + "_interface");

        HEADER += std::format("extern const wl_interface {};\n", IFACE_WL_NAME, IFACE_WL_NAME_CAMEL, IFACE_WL_NAME);
    }

    HEADER += "\n#endif\n";

    for (auto& iface : XMLDATA.ifaces) {
        const auto IFACE_NAME_CAMEL       = camelize(iface.name);
        const auto IFACE_CLASS_NAME_CAMEL = camelize((clientCode ? "CC_" : "C_") + iface.name);

        if (!clientCode) {
            HEADER += std::format(R"#(
struct {}DestroyWrapper {{
    wl_listener listener;
    {}* parent = nullptr;
}};
            )#",
                                  IFACE_CLASS_NAME_CAMEL, IFACE_CLASS_NAME_CAMEL);
        }

        // begin the class
        HEADER +=
            std::format(R"#(

class {} {{
  public:
    {}({});
    ~{}();

)#",
                        IFACE_CLASS_NAME_CAMEL, IFACE_CLASS_NAME_CAMEL, (clientCode ? "wl_proxy*" : "wl_client* client, uint32_t version, uint32_t id"), IFACE_CLASS_NAME_CAMEL);

        if (!clientCode) {
            HEADER += std::format(R"#(
    // set a listener for when this resource is _being_ destroyed
    void setOnDestroy(F<void({}*)> &&handler) {{
        onDestroy = std::move(handler);
    }}

    // set the data for this resource
    void setData(void* data) {{
        pData = data;
    }}

    // get the data for this resource
    void* data() {{
        return pData;
    }}

    // get the raw wl_resource ptr
    wl_resource* resource() {{
        return pResource;
    }}

    // get the client
    wl_client* client() {{
        return wl_resource_get_client(pResource);
    }}

    // send an error
    void error(uint32_t error, const std::string& message) {{
        wl_resource_post_error(pResource, error, "%s", message.c_str());
    }}

    // send out of memory
    void noMemory() {{
        wl_resource_post_no_memory(pResource);
    }}

    // get the resource version
    int version() {{
        return wl_resource_get_version(pResource);
    }}
            )#",
                                  IFACE_CLASS_NAME_CAMEL);
        } else {
            HEADER += R"#(
    // set the data for this resource
    void setData(void* data) {{
        pData = data;
    }}

    // get the data for this resource
    void* data() {{
        return pData;
    }}

    // get the raw wl_resource (wl_proxy) ptr
    wl_proxy* resource() {{
        return pResource;
    }}

    // get the raw wl_proxy ptr
    wl_proxy* proxy() {{
        return pResource;
    }}

    // get the resource version
    int version() {{
        return wl_proxy_get_version(pResource);
    }}
            )#";
        }

        // add all setters for requests
        HEADER += "\n    // --------------- Requests --------------- //\n\n";

        for (auto& rq : (clientCode ? iface.events : iface.requests)) {

            std::string args = ", ";
            for (auto& arg : rq.args) {
                if (arg.newType)
                    continue;
                args += WPTypeToCType(arg, false) + ", ";
            }

            args.pop_back();
            args.pop_back();

            HEADER += std::format("    void {}(F<void({}*{})> &&handler);\n", camelize("set_" + rq.name), IFACE_CLASS_NAME_CAMEL, args);
        }

        // start events

        HEADER += "\n    // --------------- Events --------------- //\n\n";

        for (auto& ev : (!clientCode ? iface.events : iface.requests)) {
            std::string args = "";
            for (auto& arg : ev.args) {
                if (arg.newType)
                    continue;
                args += WPTypeToCType(arg, true) + ", ";
            }

            if (!args.empty()) {
                args.pop_back();
                args.pop_back();
            }

            HEADER += std::format("    {} {}({});\n", ev.newIdType.empty() ? "void" : "wl_proxy*", camelize("send_" + ev.name), args);
        }

        // dangerous ones
        if (!clientCode) {
            for (auto& ev : (!clientCode ? iface.events : iface.requests)) {
                std::string args = "";
                for (auto& arg : ev.args) {
                    if (arg.newType)
                        continue;
                    args += WPTypeToCType(arg, true, true) + ", ";
                }

                if (!args.empty()) {
                    args.pop_back();
                    args.pop_back();
                }

                HEADER += std::format("    void {}({});\n", camelize("send_" + ev.name + "_raw"), args);
            }
        }

        // end events

        // start private section
        HEADER += "\n  private:\n";

        // start requests storage
        HEADER += "    struct {\n";

        for (auto& rq : (clientCode ? iface.events : iface.requests)) {

            std::string args = ", ";
            for (auto& arg : rq.args) {
                if (arg.newType)
                    continue;
                args += WPTypeToCType(arg, false) + ", ";
            }

            if (!args.empty()) {
                args.pop_back();
                args.pop_back();
            }

            HEADER += std::format("        F<void({}*{})> {};\n", IFACE_CLASS_NAME_CAMEL, args, camelize(rq.name));
        }

        // end requests storage
        HEADER += "    } requests;\n";

        // constant resource stuff
        if (!clientCode) {
            HEADER += std::format(R"#(
    void onDestroyCalled();

    F<void({}*)> onDestroy;

    wl_resource* pResource = nullptr;

    {}DestroyWrapper resourceDestroyListener;

    void* pData = nullptr;)#",
                                  IFACE_CLASS_NAME_CAMEL, IFACE_CLASS_NAME_CAMEL);
        } else {
            HEADER += R"#(
    wl_proxy* pResource = nullptr;

    bool destroyed = false;

    void* pData = nullptr;)#";
        }

        HEADER += "\n};\n\n";
    }

    HEADER += "\n\n#undef F\n";
}

void parseSource() {
    std::string DUMMY_TYPE_TABLE_NAME = PROTO_DATA.name + "_dummyTypes";

    SOURCE += std::format(R"#(#define private public
#define HYPRWAYLAND_SCANNER_NO_INTERFACES
#include "{}.hpp"
#undef private
#define F std::function
)#",
                          PROTO_DATA.fileName);

    // reference interfaces

    // dummy
    SOURCE += std::format(R"#(
static const wl_interface* {}[] = {{ nullptr }};
)#",
                          DUMMY_TYPE_TABLE_NAME);

    SOURCE += R"#(
// Reference all other interfaces.
// The reason why this is in snake is to
// be able to cooperate with existing
// wayland_scanner interfaces (they are interop)
)#";

    std::vector<std::string> declaredIfaces;

    for (auto& iface : XMLDATA.ifaces) {
        const auto IFACE_WL_NAME       = iface.name + "_interface";
        const auto IFACE_WL_NAME_CAMEL = camelize(iface.name + "_interface");
        declaredIfaces.push_back(IFACE_WL_NAME);

        SOURCE += std::format("extern const wl_interface {};\n", IFACE_WL_NAME, IFACE_WL_NAME_CAMEL, IFACE_WL_NAME);
    }

    for (auto& iface : XMLDATA.ifaces) {
        // do all referenced too
        for (auto& rq : iface.requests) {
            for (auto& arg : rq.args) {
                if (arg.interface.empty())
                    continue;

                const auto IFACE_WL_NAME2       = arg.interface + "_interface";
                const auto IFACE_WL_NAME_CAMEL2 = camelize(arg.interface + "_interface");

                if (std::find(declaredIfaces.begin(), declaredIfaces.end(), IFACE_WL_NAME2) == declaredIfaces.end()) {
                    SOURCE += std::format("extern const wl_interface {};\n", IFACE_WL_NAME2, IFACE_WL_NAME_CAMEL2, IFACE_WL_NAME2);
                    declaredIfaces.push_back(IFACE_WL_NAME2);
                }
            }
        }

        for (auto& ev : iface.events) {
            for (auto& arg : ev.args) {
                if (arg.interface.empty())
                    continue;

                const auto IFACE_WL_NAME2       = arg.interface + "_interface";
                const auto IFACE_WL_NAME_CAMEL2 = camelize(arg.interface + "_interface");

                if (std::find(declaredIfaces.begin(), declaredIfaces.end(), IFACE_WL_NAME2) == declaredIfaces.end()) {
                    SOURCE += std::format("extern const wl_interface {};\n", IFACE_WL_NAME2, IFACE_WL_NAME_CAMEL2, IFACE_WL_NAME2);
                    declaredIfaces.push_back(IFACE_WL_NAME2);
                }
            }
        }
    }

    // declare ifaces

    for (auto& iface : XMLDATA.ifaces) {

        const auto IFACE_WL_NAME          = iface.name + "_interface";
        const auto IFACE_NAME             = iface.name;
        const auto IFACE_NAME_CAMEL       = camelize(iface.name);
        const auto IFACE_CLASS_NAME_CAMEL = camelize((clientCode ? "CC_" : "C_") + iface.name);

        // create handlers
        for (auto& rq : (clientCode ? iface.events : iface.requests)) {
            const auto  REQUEST_NAME = camelize(std::string{"_"} + "C_" + IFACE_NAME + "_" + rq.name);

            std::string argsC = ", ";
            for (auto& arg : rq.args) {
                if (arg.newType)
                    continue;
                argsC += WPTypeToCType(arg, false) + " " + arg.name + ", ";
            }

            argsC.pop_back();
            argsC.pop_back();

            std::string argsN = ", ";
            for (auto& arg : rq.args) {
                argsN += arg.name + ", ";
            }

            if (!argsN.empty()) {
                argsN.pop_back();
                argsN.pop_back();
            }

            if (!clientCode) {
                SOURCE += std::format(R"#(
static void {}(wl_client* client, wl_resource* resource{}) {{
    const auto PO = ({}*)wl_resource_get_user_data(resource);
    if (PO && PO->requests.{})
        PO->requests.{}(PO{});
}}
)#",
                                      REQUEST_NAME, argsC, IFACE_CLASS_NAME_CAMEL, camelize(rq.name), camelize(rq.name), argsN);
            } else {
                SOURCE += std::format(R"#(
static void {}(void* data, void* resource{}) {{
    const auto PO = ({}*)data;
    if (PO && PO->requests.{})
        PO->requests.{}(PO{});
}}
)#",
                                      REQUEST_NAME, argsC, IFACE_CLASS_NAME_CAMEL, camelize(rq.name), camelize(rq.name), argsN);
            }
        }

        // destroy handler
        if (!clientCode) {
            SOURCE += std::format(R"#(
static void _{}__DestroyListener(wl_listener* l, void* d) {{
    {}DestroyWrapper *wrap = wl_container_of(l, wrap, listener);
    {}* pResource = wrap->parent;
    pResource->onDestroyCalled();
}}
)#",
                                  IFACE_CLASS_NAME_CAMEL, IFACE_CLASS_NAME_CAMEL, IFACE_CLASS_NAME_CAMEL);
        }

        // create vtable

        const auto IFACE_VTABLE_NAME = "_" + IFACE_CLASS_NAME_CAMEL + "VTable";

        SOURCE += std::format(R"#(
static const void* {}[] = {{
)#",
                              IFACE_VTABLE_NAME);

        for (auto& rq : (clientCode ? iface.events : iface.requests)) {
            const auto REQUEST_NAME = camelize(std::string{"_"} + "C_" + IFACE_NAME + "_" + rq.name);
            SOURCE += std::format("    (void*){},\n", REQUEST_NAME);
        }

        if ((clientCode ? iface.events : iface.requests).empty()) {
            SOURCE += "    nullptr,\n";
        }

        SOURCE += "};\n";

        // create events

        int evid = 0;
        for (auto& ev : (!clientCode ? iface.events : iface.requests)) {
            const auto  EVENT_NAME = camelize("send_" + ev.name);

            std::string argsC = "";
            for (auto& arg : ev.args) {
                if (arg.newType)
                    continue;
                argsC += WPTypeToCType(arg, true) + " " + arg.name + ", ";
            }

            if (!argsC.empty()) {
                argsC.pop_back();
                argsC.pop_back();
            }

            std::string argsN = ", ";
            for (auto& arg : ev.args) {
                if (arg.newType)
                    argsN += "nullptr, ";
                else if (!WPTypeToCType(arg, true).starts_with("C"))
                    argsN += arg.name + ", ";
                else
                    argsN += (arg.name + " ? " + arg.name + "->pResource : nullptr, ");
            }

            argsN.pop_back();
            argsN.pop_back();

            if (!clientCode) {
                SOURCE += std::format(R"#(
void {}::{}({}) {{
    if (!pResource)
        return;
    wl_resource_post_event(pResource, {}{});
}}
)#",
                                      IFACE_CLASS_NAME_CAMEL, EVENT_NAME, argsC, evid, argsN);
            } else {
                std::string retType    = ev.newIdType.empty() ? "void" : "wl_proxy";
                std::string ptrRetType = ev.newIdType.empty() ? "void" : "wl_proxy*";
                std::string flags      = ev.destructor ? "1" : "0";
                SOURCE += std::format(R"#(
{} {}::{}({}) {{
    if (!pResource)
        return{};{}

    auto proxy = wl_proxy_marshal_flags(pResource, {}, {}, wl_proxy_get_version(pResource), {}{});{}
}}
)#",
                                      ptrRetType, IFACE_CLASS_NAME_CAMEL, EVENT_NAME, argsC, (ev.newIdType.empty() ? "" : " nullptr"),
                                      (ev.destructor ? "\n    destroyed = true;" : ""), evid, (ev.newIdType.empty() ? "nullptr" : "&" + ev.newIdType + "_interface"), flags, argsN,
                                      (ev.newIdType.empty() ? "\n    proxy;" : "\n\n    return proxy;"));
            }

            evid++;
        }

        // dangerous
        if (!clientCode) {
            evid = 0;
            for (auto& ev : iface.events) {
                const auto  EVENT_NAME = camelize("send_" + ev.name + "_raw");

                std::string argsC = "";
                for (auto& arg : ev.args) {
                    if (arg.newType)
                        continue;
                    argsC += WPTypeToCType(arg, true, true) + " " + arg.name + ", ";
                }

                if (!argsC.empty()) {
                    argsC.pop_back();
                    argsC.pop_back();
                }

                std::string argsN = ", ";
                for (auto& arg : ev.args) {
                    if (arg.newType)
                        continue;
                    argsN += arg.name + ", ";
                }

                argsN.pop_back();
                argsN.pop_back();

                SOURCE += std::format(R"#(
void {}::{}({}) {{
    if (!pResource)
        return;
    wl_resource_post_event(pResource, {}{});
}}
)#",
                                      IFACE_CLASS_NAME_CAMEL, EVENT_NAME, argsC, evid, argsN);

                evid++;
            }
        }

        // wayland interfaces and stuff

        // type tables
        for (auto& rq : iface.requests) {
            if (rq.args.empty())
                continue;

            const auto TYPE_TABLE_NAME = camelize(std::string{"_"} + "C_" + IFACE_NAME + "_" + rq.name + "_types");
            SOURCE += std::format("static const wl_interface* {}[] = {{\n", TYPE_TABLE_NAME);

            for (auto& arg : rq.args) {
                if (arg.wlType == "new_id" && arg.interface.empty()) {
                    // untyped new_id expands to (string, uint, new_id)
                    SOURCE += "    nullptr,\n    nullptr,\n    nullptr,\n";
                    continue;
                }

                if (arg.interface.empty()) {
                    SOURCE += "    nullptr,\n";
                    continue;
                }

                SOURCE += std::format("    &{}_interface,\n", arg.interface);
            }

            SOURCE += "};\n";
        }
        for (auto& ev : iface.events) {
            if (ev.args.empty())
                continue;

            const auto TYPE_TABLE_NAME = camelize(std::string{"_"} + "C_" + IFACE_NAME + "_" + ev.name + "_types");
            SOURCE += std::format("static const wl_interface* {}[] = {{\n", TYPE_TABLE_NAME);

            for (auto& arg : ev.args) {
                if (arg.wlType == "new_id" && arg.interface.empty()) {
                    // untyped new_id expands to (string, uint, new_id)
                    SOURCE += "    nullptr,\n    nullptr,\n    nullptr,\n";
                    continue;
                }

                if (arg.interface.empty()) {
                    SOURCE += "    nullptr,\n";
                    continue;
                }

                SOURCE += std::format("    &{}_interface,\n", arg.interface);
            }

            SOURCE += "};\n";
        }

        const auto MESSAGE_NAME_REQUESTS = camelize(std::string{"_"} + "C_" + IFACE_NAME + "_requests");
        const auto MESSAGE_NAME_EVENTS   = camelize(std::string{"_"} + "C_" + IFACE_NAME + "_events");

        // message
        if (!noInterfaces) {
            if (iface.requests.size() > 0) {
                SOURCE += std::format(R"#(
static const wl_message {}[] = {{
)#",
                                      MESSAGE_NAME_REQUESTS);
                for (auto& rq : iface.requests) {
                    // create type table
                    const auto TYPE_TABLE_NAME = camelize(std::string{"_"} + "C_" + IFACE_NAME + "_" + rq.name + "_types");

                    SOURCE += std::format("    {{ .name = \"{}\", .signature = \"{}\", .types = {}}},\n", rq.name, argsToShort(rq.args, rq.since),
                                          rq.args.empty() ? std::format("{} + 0", DUMMY_TYPE_TABLE_NAME) : TYPE_TABLE_NAME + " + 0");
                }

                SOURCE += "};\n";
            }

            if (iface.events.size() > 0) {
                SOURCE += std::format(R"#(
static const wl_message {}[] = {{
)#",
                                      MESSAGE_NAME_EVENTS);
                for (auto& ev : iface.events) {
                    // create type table
                    const auto TYPE_TABLE_NAME = camelize(std::string{"_"} + "C_" + IFACE_NAME + "_" + ev.name + "_types");

                    SOURCE += std::format("    {{ .name = \"{}\", .signature = \"{}\", .types = {}}},\n", ev.name, argsToShort(ev.args, ev.since),
                                          ev.args.empty() ? std::format("{} + 0", DUMMY_TYPE_TABLE_NAME) : TYPE_TABLE_NAME + " + 0");
                }

                SOURCE += "};\n";
            }

            // iface
            SOURCE += std::format(R"#(
const wl_interface {} = {{
    .name = "{}", .version = {},
    .method_count = {}, .methods = {},
    .event_count = {}, .events = {},
}};
)#",
                                  IFACE_WL_NAME, iface.name, iface.version, iface.requests.size(), (iface.requests.size() > 0 ? MESSAGE_NAME_REQUESTS : "nullptr"),
                                  iface.events.size(), (iface.events.size() > 0 ? MESSAGE_NAME_EVENTS : "nullptr"));
        }

        // protocol body
        if (!clientCode) {
            SOURCE += std::format(R"#(
{}::{}(wl_client* client, uint32_t version, uint32_t id) :
    pResource(wl_resource_create(client, &{}, version, id)) {{

    if (!pResource)
        return;

    wl_resource_set_user_data(pResource, this);
    wl_list_init(&resourceDestroyListener.listener.link);
    resourceDestroyListener.listener.notify = _{}__DestroyListener;
    resourceDestroyListener.parent = this;
    wl_resource_add_destroy_listener(pResource, &resourceDestroyListener.listener);

    wl_resource_set_implementation(pResource, {}, this, nullptr);
}}

{}::~{}() {{
    wl_list_remove(&resourceDestroyListener.listener.link);
    wl_list_init(&resourceDestroyListener.listener.link);

    // if we still own the wayland resource,
    // it means we need to destroy it.
    if (pResource && wl_resource_get_user_data(pResource) == this) {{
        wl_resource_set_user_data(pResource, nullptr);
        wl_resource_destroy(pResource);
    }}
}}

void {}::onDestroyCalled() {{
    wl_resource_set_user_data(pResource, nullptr);
    wl_list_remove(&resourceDestroyListener.listener.link);
    wl_list_init(&resourceDestroyListener.listener.link);

    // set the resource to nullptr,
    // as it will be freed. If the consumer does not destroy this resource
    // in onDestroy here, we'd be doing a UAF in the ~dtor
    pResource = nullptr;

    if (onDestroy)
        onDestroy(this);
}}
)#",
                                  IFACE_CLASS_NAME_CAMEL, IFACE_CLASS_NAME_CAMEL, IFACE_NAME + "_interface", IFACE_CLASS_NAME_CAMEL, IFACE_VTABLE_NAME, IFACE_CLASS_NAME_CAMEL,
                                  IFACE_CLASS_NAME_CAMEL, IFACE_CLASS_NAME_CAMEL);
        } else {
            std::string DTOR_FUNC = "";

            for (auto& rq : iface.requests) {
                if (!rq.destructor)
                    continue;

                DTOR_FUNC = camelize("send_" + rq.name) + "()";
                break;
            }

            if (DTOR_FUNC.empty())
                DTOR_FUNC = "wl_proxy_destroy(pResource)";

            SOURCE += std::format(R"#(
{}::{}(wl_proxy* resource) : pResource(resource) {{

    if (!pResource)
        return;

    wl_proxy_add_listener(pResource, (void (**)(void))&{}, this);
}}

{}::~{}() {{
    if (!destroyed)
        {};
}}
)#",
                                  IFACE_CLASS_NAME_CAMEL, IFACE_CLASS_NAME_CAMEL, IFACE_VTABLE_NAME, IFACE_CLASS_NAME_CAMEL, IFACE_CLASS_NAME_CAMEL, DTOR_FUNC);
        }

        for (auto& rq : (clientCode ? iface.events : iface.requests)) {
            std::string args = ", ";
            for (auto& arg : rq.args) {
                args += WPTypeToCType(arg, false) + ", ";
            }

            args.pop_back();
            args.pop_back();

            SOURCE += std::format(R"#(
void {}::{}(F<void({}*{})> &&handler) {{
    requests.{} = std::move(handler);
}}
)#",
                                  IFACE_CLASS_NAME_CAMEL, camelize("set_" + rq.name), IFACE_CLASS_NAME_CAMEL, args, camelize(rq.name));
        }
    }

    SOURCE += "\n#undef F\n";
}

int main(int argc, char** argv, char** envp) {
    std::string outpath   = "";
    std::string protopath = "";

    int         pathsTaken = 0;

    for (int i = 1; i < argc; ++i) {
        std::string curarg = argv[i];

        if (curarg == "-v" || curarg == "--version") {
            std::cout << SCANNER_VERSION << "\n";
            return 0;
        }

        if (curarg == "-c" || curarg == "--client") {
            clientCode = true;
            continue;
        }

        if (curarg == "--no-interfaces") {
            noInterfaces = true;
            continue;
        }

        if (curarg == "--wayland-enums") {
            waylandEnums = true;
            continue;
        }

        if (pathsTaken == 0) {
            protopath = curarg;
            pathsTaken++;
            continue;
        } else if (pathsTaken == 1) {
            outpath = curarg;
            pathsTaken++;
            continue;
        }

        std::cout << "Too many args or unknown arg " << curarg << "\n";
        return 1;
    }

    if (outpath.empty() || protopath.empty()) {
        std::cerr << "Not enough args\n";
        return 1;
    }

    // build!

    pugi::xml_document doc;
    if (!doc.load_file(protopath.c_str())) {
        std::cerr << "Couldn't load proto\n";
        return 1;
    }

    PROTO_DATA.nameOriginal = doc.child("protocol").attribute("name").as_string();
    PROTO_DATA.name         = camelize(PROTO_DATA.nameOriginal);
    PROTO_DATA.fileName     = protopath.substr(protopath.find_last_of('/') + 1, protopath.length() - (protopath.find_last_of('/') + 1) - 4);

    const auto COPYRIGHT =
        std::format("// Generated with hyprwayland-scanner {}. Made with vaxry's keyboard and ❤️.\n// {}\n\n/*\n This protocol's authors' copyright notice is:\n\n{}\n*/\n\n",
                    SCANNER_VERSION, PROTO_DATA.nameOriginal, std::string{doc.child("protocol").child("copyright").child_value()});

    parseXML(doc);
    parseHeader();
    parseSource();

    const auto HPATH              = outpath + "/" + PROTO_DATA.fileName + ".hpp";
    const auto CPATH              = outpath + "/" + PROTO_DATA.fileName + ".cpp";
    bool       needsToWriteHeader = true, needsToWriteSource = true;

    if (std::filesystem::exists(HPATH)) {
        // check if we need to overwrite

        std::ifstream headerIn(HPATH);
        std::string   content((std::istreambuf_iterator<char>(headerIn)), (std::istreambuf_iterator<char>()));

        if (content == COPYRIGHT + HEADER)
            needsToWriteHeader = false;

        headerIn.close();
    }

    if (std::filesystem::exists(CPATH)) {
        // check if we need to overwrite

        std::ifstream sourceIn(CPATH);
        std::string   content((std::istreambuf_iterator<char>(sourceIn)), (std::istreambuf_iterator<char>()));

        if (content == COPYRIGHT + SOURCE)
            needsToWriteSource = false;

        sourceIn.close();
    }

    if (needsToWriteHeader) {
        std::ofstream header(HPATH, std::ios::trunc);
        header << COPYRIGHT << HEADER;
        header.close();
    }

    if (needsToWriteSource) {
        std::ofstream source(CPATH, std::ios::trunc);
        source << COPYRIGHT << SOURCE;
        source.close();
    }

    return 0;
}
