/*
 * 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.
*/
#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>
#include <ApplicationServices/ApplicationServices.h>

namespace animia::internal::quartz {

/* all of these LaunchServices things use *internal functions* that are subject
 * to change. Granted, it's not very likely that these will change very much
 * because I'm fairly sure Apple uses them lots in their own internal code.
*/
#if __LP64__
typedef long NSInteger;
#else
typedef int NSInteger;
#endif
typedef int CGSConnection;

typedef CGSConnection (*CGSDefaultConnectionForThreadSpec)(void);
typedef CGError (*CGSCopyWindowPropertySpec)(const CGSConnection, NSInteger, CFStringRef, CFStringRef*);

static CGSDefaultConnectionForThreadSpec CGSDefaultConnectionForThread = nullptr;
static CGSCopyWindowPropertySpec CGSCopyWindowProperty = nullptr;

static const CFStringRef kCoreGraphicsBundleID = CFSTR("com.apple.CoreGraphics");

/* Objective-C */
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 GetCoreGraphicsPrivateSymbols() {
	CFBundleRef core_graphics_bundle = CFBundleGetBundleWithIdentifier(kCoreGraphicsBundleID);
	if (!core_graphics_bundle)
		return false;

	CGSDefaultConnectionForThread = (CGSDefaultConnectionForThreadSpec)CFBundleGetFunctionPointerForName(core_graphics_bundle, CFSTR("CGSDefaultConnectionForThread"));
	if (!CGSDefaultConnectionForThread)
		return false;

	CGSCopyWindowProperty = (CGSCopyWindowPropertySpec)CFBundleGetFunctionPointerForName(core_graphics_bundle, CFSTR("CGSCopyWindowProperty"));
	if (!CGSCopyWindowProperty)
		return false;

	return true;
}

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;
}

static bool GetWindowTitleAccessibility(unsigned int wid, pid_t pid, std::string& result) {
	CGRect bounds = {0};
	{
		const CGWindowID wids[1] = {wid};
		CFArrayRef arr = CFArrayCreate(kCFAllocatorDefault, (CFTypeRef*)wids, 1, NULL);

		CFArrayRef dicts = CGWindowListCreateDescriptionFromArray(arr);

		CFRelease(arr);

		if (!dicts || CFArrayGetCount(dicts) < 1)
			return false;

		CFDictionaryRef dict = (CFDictionaryRef)CFArrayGetValueAtIndex(dicts, 0);
		if (!dict) {
			CFRelease(dicts);
			return false;
		}

		CFDictionaryRef bounds_dict = nullptr;
		if (!CFDictionaryGetValueIfPresent(dict, kCGWindowBounds, reinterpret_cast<CFTypeRef*>(&bounds_dict)) || !bounds_dict) {
			CFRelease(dicts);
			return false;
		}

		if (!CGRectMakeWithDictionaryRepresentation(bounds_dict, &bounds)) {
			CFRelease(dicts);
			return false;
		}

		CFRelease(dicts);
	}

	/* now we can actually do stuff */
	AXUIElementRef axapp = AXUIElementCreateApplication(pid);
	CFArrayRef windows;
	if ((AXUIElementCopyAttributeValue(axapp, kAXWindowsAttribute, (CFTypeRef*)&windows) != kAXErrorSuccess) || !windows)
		return false;

	const CFIndex count = CFArrayGetCount(windows);
	for (CFIndex i = 0; i < count; i++) {
		const AXUIElementRef window = (AXUIElementRef)CFArrayGetValueAtIndex(windows, i);

		AXValueRef val;
		if (AXUIElementCopyAttributeValue(window, kAXPositionAttribute, (CFTypeRef*)&val) == kAXErrorSuccess) {
			CGPoint point;
			if (!AXValueGetValue(val, kAXValueTypeCGPoint, (void*)&point) || (point.x != bounds.origin.x || point.y != bounds.origin.y))
				continue;
		} else continue;

		if (AXUIElementCopyAttributeValue(window, kAXSizeAttribute, (CFTypeRef*)&val) == kAXErrorSuccess) {
			CGSize size;
			if (!AXValueGetValue(val, kAXValueTypeCGSize, (void*)&size) || (size.width != bounds.size.width || size.height != bounds.size.height))
				continue;
		} else continue;

		CFStringRef title;
		if (AXUIElementCopyAttributeValue(window, kAXTitleAttribute, (CFTypeRef*)&title) == kAXErrorSuccess) {
			CFRelease(windows);
			return osx::util::StringFromCFString(title, result);
		}
	}

	CFRelease(windows);

	return false;
}

static bool GetWindowTitle(unsigned int wid, pid_t pid, std::string& result) {
	/* private internal OS X functions */
	if ((CGSDefaultConnectionForThread && CGSCopyWindowProperty) || GetCoreGraphicsPrivateSymbols()) {
		CFStringRef title = nullptr;

		CGSCopyWindowProperty(CGSDefaultConnectionForThread(), wid, CFSTR("kCGSWindowTitle"), &title);
		if (title && CFStringGetLength(title) && osx::util::StringFromCFString(title, result))
			return true;
	}

	/* don't attempt to use accessibility if we aren't trusted */
	return AXIsProcessTrusted() ? GetWindowTitleAccessibility(wid, pid, result) : false;
}

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.
	*/
	const id app = cls_send(objc_getClass("NSRunningApplication"), sel_getUid("runningApplicationWithProcessIdentifier:"), pid);
	if (!app)
		return false;

	CFStringRef bundle_id = reinterpret_cast<CFStringRef>(obj_send(app, sel_getUid("bundleIdentifier")));
	if (!bundle_id)
		return false;

	result = osx::util::StringFromCFString(bundle_id, result);
	return true;
}

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

	const CFArrayRef windows = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, 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, proc.pid, win.text);
		}

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

	CFRelease(windows);

	return true;
}

} // namespace animia::win::detail
