/*
 * win/quartz.cc: support for macOS (the Quartz Compositor)
 *
 * This file does not require an Objective-C++ compiler,
 * but it *does* require an Objective-C runtime and linking
 * with AppKit in order to receive proper window titles.
*/
#include "animia/win/quartz.h"
#include "animia/util/osx.h"
#include "animia.h"

#include <objc/runtime.h>
#include <objc/message.h>

#include <CoreFoundation/CoreFoundation.h>
#include <CoreGraphics/CoreGraphics.h>

namespace animia::internal::quartz {

typedef id (*object_message_send)(id, SEL, ...);
typedef id (*class_message_send)(Class, SEL, ...);

static const object_message_send obj_send = reinterpret_cast<object_message_send>(objc_msgSend);
static const class_message_send cls_send = reinterpret_cast<class_message_send>(objc_msgSend);

static bool GetWindowTitle(unsigned int wid, std::string& result) {
	// NSApplication* app = [NSApplication sharedApplication];
	const id app = cls_send(objc_getClass("NSApplication"), sel_getUid("sharedApplication"));

	// NSWindow* window = [app windowWithWindowNumber: wid];
	const id window = obj_send(app, sel_getUid("windowWithWindowNumber:"), wid);
	if (!window)
		return false;

	// NSString* title = [window title];
	const CFStringRef title = reinterpret_cast<CFStringRef>(obj_send(window, sel_getUid("title")));
	if (!title)
		return false;

	// return [title UTF8String];
	return osx::util::StringFromCFString(title, result);
}

static bool GetProcessBundleIdentifier(pid_t pid, std::string& result) {
	/* The Bundle ID is essentially OS X's solution to Windows'
	 * "class name"; theoretically, it should be different for
	 * each program, although it requires an app bundle.
	*/

	// NSRunningApplication* app = [NSRunningApplication runningApplicationWithProcessIdentifier: pid];
	const id app = cls_send(objc_getClass("NSRunningApplication"), sel_getUid("runningApplicationWithProcessIdentifier:"), pid);
	if (!app)
		return false;

	// NSString* bundle_id = [app bundleIdentifier];
	const CFStringRef bundle_id = reinterpret_cast<CFStringRef>(obj_send(app, sel_getUid("bundleIdentifier")));
	if (!bundle_id)
		return false;

	// return [bundle_id UTF8String];
	return osx::util::StringFromCFString(bundle_id, result);
}

template<typename T>
static bool CFDictionaryGetValue(CFDictionaryRef thedict, CFStringRef key, T& out) {
	CFTypeRef data = nullptr;
	if (!CFDictionaryGetValueIfPresent(thedict, key, reinterpret_cast<const void**>(&data)) || !data)
		return false;

	if constexpr (std::is_arithmetic<T>::value)
		osx::util::GetCFNumber(reinterpret_cast<CFNumberRef>(data), out);
	else if constexpr (std::is_same<T, std::string>::value)
		osx::util::StringFromCFString(reinterpret_cast<CFStringRef>(data), out);
	else
		return false;

	return true;
}

bool EnumerateWindows(window_proc_t window_proc) {
	if (!window_proc)
		return false;

	const CFArrayRef windows = CGWindowListCopyWindowInfo(kCGWindowListOptionAll, kCGNullWindowID);
	if (!windows)
		return false;

	const CFIndex count = CFArrayGetCount(windows);
	for (CFIndex i = 0; i < count; i++) {
		CFDictionaryRef window = reinterpret_cast<CFDictionaryRef>(CFArrayGetValueAtIndex(windows, i));
		if (!window)
			continue;

		Process proc;
		{
			CFDictionaryGetValue(window, CFSTR("kCGWindowOwnerPID"), proc.pid);
			if (!CFDictionaryGetValue(window, CFSTR("kCGWindowOwnerName"), proc.name))
				osx::util::GetProcessName(proc.pid, proc.name);
		}

		Window win;
		{
			CFDictionaryGetValue(window, CFSTR("kCGWindowNumber"), win.id);

			if (!GetProcessBundleIdentifier(proc.pid, win.class_name)) {
				// Fallback to the Quartz window name, which is unlikely to be filled, but it
				// *could* be.
				CFDictionaryGetValue(window, CFSTR("kCGWindowName"), win.class_name);
			}

			GetWindowTitle(win.id, win.text);
		}

		if (!window_proc(proc, win)) {
			CFRelease(windows);
			return false;
		}
	}

	CFRelease(windows);

	return true;
}

} // namespace animia::win::detail
