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

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

#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);

	std::vector<SettingsItem> ParseAllItems(void);
	std::optional<SettingsItem> ParseNextItem(void);

	std::uint32_t GetTotalItems(void);

private:
	/* byte order values */
	enum {
		LSBFirst = 0,
		MSBFirst = 1,
	};

	template<typename T>
	bool ReadData(T& ret) {
		if (offset_ + sizeof(T) >= size_)
			return false;

		ret = *reinterpret_cast<T*>(bytes_ + offset_);
		Advance(sizeof(T));
		return true;
	}

	/* will fail on signed integers; xsettings has none of those though */
	template<typename T>
	bool ReadInt(T& ret) {
		static_assert(std::is_integral<T>::value);

		if (!ReadData<T>(ret))
			return false;

		switch (byte_order_) {
			case LSBFirst:
				ret = Endian::byteswap_little_to_host(ret);
				break;
			case MSBFirst:
				ret = Endian::byteswap_big_to_host(ret);
				break;
			default:
				/* can't know for sure. punt */
				return false;
		}

		return true;
	}

	bool ReadString(std::string& str, std::size_t size);
	bool Advance(std::size_t amount);

	/* raw data */
	std::uint8_t *bytes_ = nullptr;
	std::size_t offset_ = 0;
	std::size_t size_ = 0;

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

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

bool Parser::ReadString(std::string& str, std::size_t size) {
	if (offset_ + size >= size_)
		return false;

	str.assign(reinterpret_cast<const char *>(bytes_ + offset_), size);
	Advance(size);
	return true;
}

bool Parser::Advance(std::size_t amount) {
	if (offset_ + amount >= size_)
		return false;

	offset_ += amount;
	return true;
}

Parser::Parser(std::uint8_t *bytes, std::size_t size) {
	bytes_ = bytes;
	size_ = size;

	if (!ReadData<std::uint8_t>(byte_order_))
		return;

	Advance(3);

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

	if (!ReadInt<std::uint32_t>(total_items_))
		return;
}

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

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

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

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

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

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

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

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

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

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

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

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

			break;
		}
		case SettingsItem::TypeRgba: {
			/* The order here is important!! */
			if (!ReadInt<std::uint16_t>(item.data.rgba.red)
				|| !ReadInt<std::uint16_t>(item.data.rgba.blue)
				|| !ReadInt<std::uint16_t>(item.data.rgba.green)
				|| !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;
}

std::vector<SettingsItem> Parser::ParseAllItems(void) {
	offset_ = 0;

	std::uint32_t i;
	std::vector<SettingsItem> items;

	for (i = 0; i < total_items_; i++) {
		std::optional<SettingsItem> item = ParseNextItem();
		if (!item)
			break;

		items.push_back(item.value());
	}

	return items;
}

/* ------------------------------------------------------------------------- */
/* 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());
	settings = parser.ParseAllItems();

	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());

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

	for (; total; 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
