#include "sys/x11/settings.h"
#include "core/byte_stream.h"

#include <array>
#include <cassert>
#include <climits>
#include <cstdint>
#include <cstring>
#include <map>
#include <memory>
#include <optional>
#include <string_view>

#include <xcb/xcb.h>

#include "fmt/core.h"

namespace x11 {

bool SettingsItem::VerifyType()
{
	switch (type) {
		case SettingsItem::TypeInt:
		case SettingsItem::TypeStr:
		case SettingsItem::TypeRgba: return true;
		default: return false;
	}
}

/* -------------------------------------------------------------------------- */
/* xsettings parser */

static constexpr std::size_t GetPadding(std::size_t length, std::size_t increment)
{
	/* ripped from xsettingsd */
	return (increment - (length % increment)) % increment;
}

class Parser {
public:
	Parser(std::uint8_t *bytes, std::size_t size);

	bool ParseHeader(void);
	std::optional<SettingsItem> ParseNextItem(void);

	std::uint32_t GetTotalItems(void);

private:
	enum ByteOrder {
		LSBFirst = 0,
		MSBFirst = 1,
	};

	ByteStream stream;

	/* parsed in the constructor */
	std::uint32_t serial_ = 0;
	std::uint32_t total_items_ = 0;
};

std::uint32_t Parser::GetTotalItems(void)
{
	return total_items_;
}

Parser::Parser(std::uint8_t *bytes, std::size_t size) : stream(bytes, size)
{
}

bool Parser::ParseHeader(void)
{
	std::uint8_t byte_order;
	if (!stream.ReadBinary<std::uint8_t>(byte_order))
		return false;

	switch (byte_order) {
		case MSBFirst: stream.SetEndianness(ByteStream::ByteOrder::Big); break;
		case LSBFirst: stream.SetEndianness(ByteStream::ByteOrder::Little); break;
		default: return false; /* errr */
	}

	stream.Advance(3);

	if (!stream.ReadInt<std::uint32_t>(serial_))
		return false;

	if (!stream.ReadInt<std::uint32_t>(total_items_))
		return false;

	return true;
}

std::optional<SettingsItem> Parser::ParseNextItem(void)
{
	SettingsItem item;

	/* read one byte */
	if (!stream.ReadInt<std::uint8_t>(item.type))
		return std::nullopt;

	if (!item.VerifyType())
		return std::nullopt;

	/* skip padding */
	if (!stream.Advance(1))
		return std::nullopt;

	/* parse the name */
	std::uint16_t name_size;
	if (!stream.ReadInt<std::uint16_t>(name_size))
		return std::nullopt;

	if (!stream.ReadString(item.name, name_size))
		return std::nullopt;

	/* padding */
	if (!stream.Advance(GetPadding(name_size, 4)))
		return std::nullopt;

	if (!stream.ReadInt<std::uint32_t>(item.serial))
		return std::nullopt;

	switch (item.type) {
		case SettingsItem::TypeInt: {
			if (!stream.ReadInt<std::int32_t>(item.data.integer))
				return std::nullopt;

			break;
		}
		case SettingsItem::TypeStr: {
			std::uint32_t size;
			if (!stream.ReadInt<std::uint32_t>(size))
				return std::nullopt;

			if (!stream.ReadString(item.data.string, size))
				return std::nullopt;

			/* don't fail if advancing fails on this padding,
			 * because this causes parsing to fail for strings
			 * at the end of the data */
			stream.Advance(GetPadding(size, 4));

			break;
		}
		case SettingsItem::TypeRgba: {
			/* it's actually RBGA, but whatever. */
			if (!stream.ReadInt<std::uint16_t>(item.data.rgba.red) ||
			    !stream.ReadInt<std::uint16_t>(item.data.rgba.blue) ||
			    !stream.ReadInt<std::uint16_t>(item.data.rgba.green) ||
			    !stream.ReadInt<std::uint16_t>(item.data.rgba.alpha))
				return std::nullopt;

			break;
		}
		default:
			/* can't do anything now, can we? */
			return std::nullopt;
	}

	return item;
}

/* ------------------------------------------------------------------------- */
/* real X11 code */

template<typename T>
struct MallocDestructor {
	void operator()(T *t) const { std::free(t); };
};

struct XcbConnectionDestructor {
	void operator()(xcb_connection_t *conn) const { ::xcb_disconnect(conn); };
};

template<typename T>
using MallocPtr = std::unique_ptr<T, MallocDestructor<T>>;

using XcbConnectionPtr = std::unique_ptr<xcb_connection_t, XcbConnectionDestructor>;

/* RAII is nice */
struct XcbGrabber {
	XcbGrabber(::xcb_connection_t *conn)
	{
		::xcb_grab_server(conn);
		conn_ = conn;
	}
	~XcbGrabber() { ::xcb_ungrab_server(conn_); }

private:
	::xcb_connection_t *conn_;
};

static ::xcb_window_t GetSelectionOwner(::xcb_connection_t *conn, ::xcb_atom_t selection)
{
	::xcb_window_t owner = XCB_NONE;
	MallocPtr<::xcb_get_selection_owner_reply_t> reply(
	    ::xcb_get_selection_owner_reply(conn, ::xcb_get_selection_owner(conn, selection), nullptr));

	if (reply)
		owner = reply->owner;

	return owner;
}

static bool GetRawSettingsData(std::vector<uint8_t> &bytes)
{
	int screen;

	XcbConnectionPtr conn(::xcb_connect(nullptr, &screen));
	if (::xcb_connection_has_error(conn.get()))
		return false;

	/* get our needed atoms, available as atoms[Atom] */
	enum Atom {
		XSETTINGS_SCREEN,   /* _XSETTINGS_S[N] */
		XSETTINGS_SETTINGS, /* _XSETTINGS_SETTINGS */
	};

	std::map<Atom, ::xcb_atom_t> atoms;
	{
		std::map<Atom, std::string> names = {
		    {XSETTINGS_SCREEN, fmt::format("_XSETTINGS_S{}", screen)},
		    {XSETTINGS_SETTINGS, "_XSETTINGS_SETTINGS"},
		};

		std::map<Atom, ::xcb_intern_atom_cookie_t> atom_cookies;
		for (const auto &name : names)
			atom_cookies[name.first] = ::xcb_intern_atom(conn.get(), false, name.second.size(), name.second.data());

		for (const auto &cookie : atom_cookies) {
			MallocPtr<::xcb_intern_atom_reply_t> reply(::xcb_intern_atom_reply(conn.get(), cookie.second, nullptr));
			if (!reply || reply->atom == XCB_NONE)
				return false;

			atoms[cookie.first] = reply->atom;
		}
	}

	MallocPtr<xcb_get_property_reply_t> reply;
	{
		/* grab the X server as *required* by xsettings docs */
		const XcbGrabber grabber(conn.get());

		::xcb_window_t win = GetSelectionOwner(conn.get(), atoms[XSETTINGS_SCREEN]);
		if (win == XCB_NONE)
			return false;

		reply.reset(::xcb_get_property_reply(
		    conn.get(), ::xcb_get_property(conn.get(), 0, win, atoms[XSETTINGS_SETTINGS], XCB_ATOM_ANY, 0L, UINT_MAX),
		    nullptr));
	};
	if (!reply)
		return false;

	uint8_t *data = reinterpret_cast<uint8_t *>(xcb_get_property_value(reply.get()));
	int size = xcb_get_property_value_length(reply.get());
	if (size < 0)
		return false;

	bytes.assign(data, data + size);

	return true;
}

/* ------------------------------------------------------------------------- */
/* now for the actual all-important public API stringing all this together */

bool GetSettings(std::vector<SettingsItem> &settings)
{
	std::vector<std::uint8_t> xsettings_raw;
	if (!GetRawSettingsData(xsettings_raw))
		return false;

	Parser parser(xsettings_raw.data(), xsettings_raw.size());
	if (!parser.ParseHeader())
		return false;

	std::uint32_t total = parser.GetTotalItems();

	while (total--) {
		std::optional<SettingsItem> opt_item = parser.ParseNextItem();
		if (!opt_item)
			break;

		settings.push_back(opt_item.value());
	}

	return true;
}

bool FindSetting(const std::string &name, SettingsItem &setting)
{
	std::vector<std::uint8_t> xsettings_raw;
	if (!GetRawSettingsData(xsettings_raw))
		return false;

	Parser parser(xsettings_raw.data(), xsettings_raw.size());
	if (!parser.ParseHeader())
		return false;

	std::uint32_t total = parser.GetTotalItems();

	while (total--) {
		std::optional<SettingsItem> opt_item = parser.ParseNextItem();
		if (!opt_item)
			return false;

		SettingsItem &item = opt_item.value();
		if (item.name == name) {
			setting = item;
			return true;
		}
	}

	return false;
}

} // namespace x11
