/**************************************************************************/
/*  discord_rpc.cpp                                                       */
/**************************************************************************/
/*                     AeThex Engine - Discord Rich Presence              */
/**************************************************************************/

#include "discord_rpc.h"
#include "core/io/json.h"
#include "core/os/os.h"
#include "core/os/time.h"

#ifdef WINDOWS_ENABLED
#include <windows.h>
#else
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <fcntl.h>
#endif

DiscordRPC *DiscordRPC::singleton = nullptr;

// Discord IPC opcodes
enum Opcode {
	OP_HANDSHAKE = 0,
	OP_FRAME = 1,
	OP_CLOSE = 2,
	OP_PING = 3,
	OP_PONG = 4,
};

DiscordRPC *DiscordRPC::get_singleton() {
	return singleton;
}

void DiscordRPC::_bind_methods() {
	ClassDB::bind_method(D_METHOD("initialize", "application_id"), &DiscordRPC::initialize);
	ClassDB::bind_method(D_METHOD("shutdown"), &DiscordRPC::shutdown);
	ClassDB::bind_method(D_METHOD("is_connected"), &DiscordRPC::is_connected);
	ClassDB::bind_method(D_METHOD("get_connection_state"), &DiscordRPC::get_state);
	
	ClassDB::bind_method(D_METHOD("set_details", "details"), &DiscordRPC::set_details);
	ClassDB::bind_method(D_METHOD("get_details"), &DiscordRPC::get_details);
	
	ClassDB::bind_method(D_METHOD("set_state", "state"), &DiscordRPC::set_state);
	ClassDB::bind_method(D_METHOD("get_state_text"), &DiscordRPC::get_state_text);
	
	ClassDB::bind_method(D_METHOD("set_large_image", "key", "text"), &DiscordRPC::set_large_image, DEFVAL(""));
	ClassDB::bind_method(D_METHOD("set_small_image", "key", "text"), &DiscordRPC::set_small_image, DEFVAL(""));
	
	ClassDB::bind_method(D_METHOD("set_timestamps", "start", "end"), &DiscordRPC::set_timestamps, DEFVAL(0), DEFVAL(0));
	ClassDB::bind_method(D_METHOD("set_party", "id", "size", "max"), &DiscordRPC::set_party);
	ClassDB::bind_method(D_METHOD("set_secrets", "match", "join", "spectate"), &DiscordRPC::set_secrets, DEFVAL(""), DEFVAL(""), DEFVAL(""));
	
	ClassDB::bind_method(D_METHOD("update_presence"), &DiscordRPC::update_presence);
	ClassDB::bind_method(D_METHOD("clear_presence"), &DiscordRPC::clear_presence);
	
	ClassDB::bind_method(D_METHOD("set_editor_presence", "project_name", "scene_name"), &DiscordRPC::set_editor_presence, DEFVAL(""));
	ClassDB::bind_method(D_METHOD("set_game_presence", "game_name", "activity"), &DiscordRPC::set_game_presence, DEFVAL(""));
	
	BIND_ENUM_CONSTANT(DISCONNECTED);
	BIND_ENUM_CONSTANT(CONNECTING);
	BIND_ENUM_CONSTANT(CONNECTED);
	BIND_ENUM_CONSTANT(DISCONNECTING);
}

DiscordRPC::DiscordRPC() {
	singleton = this;
}

DiscordRPC::~DiscordRPC() {
	shutdown();
	singleton = nullptr;
}

bool DiscordRPC::_connect_pipe() {
#ifdef WINDOWS_ENABLED
	// Try pipes 0-9
	for (int i = 0; i < 10; i++) {
		String pipe_name = vformat("\\\\.\\pipe\\discord-ipc-%d", i);
		
		HANDLE handle = CreateFileW(
			(LPCWSTR)pipe_name.utf16().get_data(),
			GENERIC_READ | GENERIC_WRITE,
			0,
			NULL,
			OPEN_EXISTING,
			0,
			NULL
		);
		
		if (handle != INVALID_HANDLE_VALUE) {
			pipe_fd = (int)(intptr_t)handle;
			return true;
		}
	}
	return false;
#else
	// Unix socket approach
	const char *env_vars[] = { "XDG_RUNTIME_DIR", "TMPDIR", "TMP", "TEMP" };
	String base_path;
	
	for (const char *var : env_vars) {
		if (getenv(var)) {
			base_path = getenv(var);
			break;
		}
	}
	
	if (base_path.is_empty()) {
		base_path = "/tmp";
	}
	
	// Try sockets 0-9
	for (int i = 0; i < 10; i++) {
		String socket_path = base_path + "/discord-ipc-" + String::num(i);
		
		int sock = socket(AF_UNIX, SOCK_STREAM, 0);
		if (sock == -1) continue;
		
		struct sockaddr_un addr;
		memset(&addr, 0, sizeof(addr));
		addr.sun_family = AF_UNIX;
		strncpy(addr.sun_path, socket_path.utf8().get_data(), sizeof(addr.sun_path) - 1);
		
		if (connect(sock, (struct sockaddr *)&addr, sizeof(addr)) == 0) {
			pipe_fd = sock;
			return true;
		}
		
		close(sock);
	}
	return false;
#endif
}

void DiscordRPC::_disconnect_pipe() {
	if (pipe_fd != -1) {
#ifdef WINDOWS_ENABLED
		CloseHandle((HANDLE)(intptr_t)pipe_fd);
#else
		close(pipe_fd);
#endif
		pipe_fd = -1;
	}
}

bool DiscordRPC::_write_frame(int opcode, const String &payload) {
	if (pipe_fd == -1) return false;
	
	CharString utf8 = payload.utf8();
	int len = utf8.length();
	
	// Frame header: opcode (4 bytes) + length (4 bytes)
	uint8_t header[8];
	memcpy(header, &opcode, 4);
	memcpy(header + 4, &len, 4);
	
#ifdef WINDOWS_ENABLED
	DWORD written;
	if (!WriteFile((HANDLE)(intptr_t)pipe_fd, header, 8, &written, NULL)) return false;
	if (!WriteFile((HANDLE)(intptr_t)pipe_fd, utf8.get_data(), len, &written, NULL)) return false;
#else
	if (write(pipe_fd, header, 8) != 8) return false;
	if (write(pipe_fd, utf8.get_data(), len) != len) return false;
#endif
	
	return true;
}

String DiscordRPC::_read_frame() {
	if (pipe_fd == -1) return "";
	
	uint8_t header[8];
	
#ifdef WINDOWS_ENABLED
	DWORD read_bytes;
	if (!ReadFile((HANDLE)(intptr_t)pipe_fd, header, 8, &read_bytes, NULL) || read_bytes != 8) {
		return "";
	}
#else
	if (read(pipe_fd, header, 8) != 8) return "";
#endif
	
	int opcode, len;
	memcpy(&opcode, header, 4);
	memcpy(&len, header + 4, 4);
	
	if (len > 0 && len < 65536) {
		Vector<uint8_t> buffer;
		buffer.resize(len + 1);
		
#ifdef WINDOWS_ENABLED
		if (!ReadFile((HANDLE)(intptr_t)pipe_fd, buffer.ptrw(), len, &read_bytes, NULL)) {
			return "";
		}
#else
		if (read(pipe_fd, buffer.ptrw(), len) != len) return "";
#endif
		buffer.write[len] = 0;
		return String::utf8((const char *)buffer.ptr());
	}
	
	return "";
}

String DiscordRPC::_build_handshake() {
	Dictionary handshake;
	handshake["v"] = 1;
	handshake["client_id"] = application_id;
	return JSON::stringify(handshake);
}

String DiscordRPC::_build_presence_payload() {
	Dictionary activity;
	
	if (!details.is_empty()) {
		activity["details"] = details;
	}
	if (!state_text.is_empty()) {
		activity["state"] = state_text;
	}
	
	// Timestamps
	if (start_timestamp > 0 || end_timestamp > 0) {
		Dictionary timestamps;
		if (start_timestamp > 0) timestamps["start"] = start_timestamp;
		if (end_timestamp > 0) timestamps["end"] = end_timestamp;
		activity["timestamps"] = timestamps;
	}
	
	// Assets (images)
	Dictionary assets;
	bool has_assets = false;
	
	if (!large_image_key.is_empty()) {
		assets["large_image"] = large_image_key;
		if (!large_image_text.is_empty()) {
			assets["large_text"] = large_image_text;
		}
		has_assets = true;
	}
	if (!small_image_key.is_empty()) {
		assets["small_image"] = small_image_key;
		if (!small_image_text.is_empty()) {
			assets["small_text"] = small_image_text;
		}
		has_assets = true;
	}
	
	if (has_assets) {
		activity["assets"] = assets;
	}
	
	// Party
	if (!party_id.is_empty() && party_size > 0) {
		Dictionary party;
		party["id"] = party_id;
		
		Array size;
		size.push_back(party_size);
		size.push_back(party_max);
		party["size"] = size;
		
		activity["party"] = party;
	}
	
	// Secrets
	Dictionary secrets;
	bool has_secrets = false;
	
	if (!match_secret.is_empty()) {
		secrets["match"] = match_secret;
		has_secrets = true;
	}
	if (!join_secret.is_empty()) {
		secrets["join"] = join_secret;
		has_secrets = true;
	}
	if (!spectate_secret.is_empty()) {
		secrets["spectate"] = spectate_secret;
		has_secrets = true;
	}
	
	if (has_secrets) {
		activity["secrets"] = secrets;
	}
	
	// Build full payload
	Dictionary args;
	args["pid"] = OS::get_singleton()->get_process_id();
	args["activity"] = activity;
	
	Dictionary payload;
	payload["cmd"] = "SET_ACTIVITY";
	payload["args"] = args;
	payload["nonce"] = String::num(Time::get_singleton()->get_ticks_msec());
	
	return JSON::stringify(payload);
}

void DiscordRPC::_thread_wrapper(void *userdata) {
	DiscordRPC *self = (DiscordRPC *)userdata;
	self->_thread_func();
}

void DiscordRPC::_thread_func() {
	// Connect to Discord
	if (!_connect_pipe()) {
		state = DISCONNECTED;
		return;
	}
	
	// Send handshake
	if (!_write_frame(OP_HANDSHAKE, _build_handshake())) {
		_disconnect_pipe();
		state = DISCONNECTED;
		return;
	}
	
	// Read handshake response
	String response = _read_frame();
	if (response.is_empty()) {
		_disconnect_pipe();
		state = DISCONNECTED;
		return;
	}
	
	// Check for READY event
	Variant parsed;
	JSON::parse(response, parsed);
	Dictionary resp = parsed;
	
	if (resp.get("evt", "") != "READY") {
		_disconnect_pipe();
		state = DISCONNECTED;
		return;
	}
	
	state = CONNECTED;
	
	// Main loop - keep connection alive
	while (running && pipe_fd != -1) {
		// Read any incoming messages (errors, etc.)
		// Could extend to handle join requests, spectate requests, etc.
		OS::get_singleton()->delay_usec(100000); // 100ms
	}
	
	_disconnect_pipe();
	state = DISCONNECTED;
}

void DiscordRPC::initialize(const String &p_app_id) {
	if (state != DISCONNECTED) {
		shutdown();
	}
	
	application_id = p_app_id;
	state = CONNECTING;
	running = true;
	
	thread = Thread::create(_thread_wrapper, this);
}

void DiscordRPC::shutdown() {
	if (state == DISCONNECTED) return;
	
	state = DISCONNECTING;
	running = false;
	
	if (thread) {
		Thread::wait_to_finish(thread);
		memdelete(thread);
		thread = nullptr;
	}
	
	_disconnect_pipe();
	state = DISCONNECTED;
}

bool DiscordRPC::is_connected() const {
	return state == CONNECTED;
}

DiscordRPC::ConnectionState DiscordRPC::get_state() const {
	return state;
}

void DiscordRPC::set_details(const String &p_details) {
	details = p_details;
}

String DiscordRPC::get_details() const {
	return details;
}

void DiscordRPC::set_state(const String &p_state) {
	state_text = p_state;
}

String DiscordRPC::get_state_text() const {
	return state_text;
}

void DiscordRPC::set_large_image(const String &p_key, const String &p_text) {
	large_image_key = p_key;
	large_image_text = p_text;
}

void DiscordRPC::set_small_image(const String &p_key, const String &p_text) {
	small_image_key = p_key;
	small_image_text = p_text;
}

void DiscordRPC::set_timestamps(int64_t p_start, int64_t p_end) {
	start_timestamp = p_start;
	end_timestamp = p_end;
}

void DiscordRPC::set_party(const String &p_id, int p_size, int p_max) {
	party_id = p_id;
	party_size = p_size;
	party_max = p_max;
}

void DiscordRPC::set_secrets(const String &p_match, const String &p_join, const String &p_spectate) {
	match_secret = p_match;
	join_secret = p_join;
	spectate_secret = p_spectate;
}

void DiscordRPC::update_presence() {
	if (state != CONNECTED) return;
	
	MutexLock lock(mutex);
	_write_frame(OP_FRAME, _build_presence_payload());
}

void DiscordRPC::clear_presence() {
	if (state != CONNECTED) return;
	
	// Clear all presence data
	details = "";
	state_text = "";
	large_image_key = "";
	large_image_text = "";
	small_image_key = "";
	small_image_text = "";
	start_timestamp = 0;
	end_timestamp = 0;
	party_id = "";
	party_size = 0;
	party_max = 0;
	match_secret = "";
	join_secret = "";
	spectate_secret = "";
	
	// Send empty activity
	Dictionary args;
	args["pid"] = OS::get_singleton()->get_process_id();
	
	Dictionary payload;
	payload["cmd"] = "SET_ACTIVITY";
	payload["args"] = args;
	payload["nonce"] = String::num(Time::get_singleton()->get_ticks_msec());
	
	MutexLock lock(mutex);
	_write_frame(OP_FRAME, JSON::stringify(payload));
}

void DiscordRPC::set_editor_presence(const String &p_project_name, const String &p_scene_name) {
	details = "Working on: " + p_project_name;
	if (!p_scene_name.is_empty()) {
		state_text = "Editing: " + p_scene_name;
	} else {
		state_text = "In Editor";
	}
	
	large_image_key = "aethex_engine";
	large_image_text = "AeThex Engine";
	small_image_key = "editor";
	small_image_text = "Editor Mode";
	
	if (start_timestamp == 0) {
		start_timestamp = Time::get_singleton()->get_unix_time_from_system();
	}
	
	update_presence();
}

void DiscordRPC::set_game_presence(const String &p_game_name, const String &p_activity) {
	details = p_game_name;
	if (!p_activity.is_empty()) {
		state_text = p_activity;
	}
	
	large_image_key = "aethex_engine";
	large_image_text = "Made with AeThex Engine";
	
	if (start_timestamp == 0) {
		start_timestamp = Time::get_singleton()->get_unix_time_from_system();
	}
	
	update_presence();
}
