#include "window-utils.h"

#include <util/platform.h>

#define WINDOW_NAME   ((NSString *) kCGWindowName)
#define WINDOW_NUMBER ((NSString *) kCGWindowNumber)
#define OWNER_NAME    ((NSString *) kCGWindowOwnerName)
#define OWNER_PID     ((NSString *) kCGWindowOwnerPID)

static NSComparator win_info_cmp = ^(NSDictionary *o1, NSDictionary *o2) {
    NSComparisonResult res = [o1[OWNER_NAME] compare:o2[OWNER_NAME]];
    if (res != NSOrderedSame)
        return res;

    res = [o1[OWNER_PID] compare:o2[OWNER_PID]];
    if (res != NSOrderedSame)
        return res;

    res = [o1[WINDOW_NAME] compare:o2[WINDOW_NAME]];
    if (res != NSOrderedSame)
        return res;

    return [o1[WINDOW_NUMBER] compare:o2[WINDOW_NUMBER]];
};

NSArray *enumerate_windows(void)
{
    NSArray *arr = (NSArray *) CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID);

    [arr autorelease];

    return [arr sortedArrayUsingComparator:win_info_cmp];
}

#define WAIT_TIME_MS 500
#define WAIT_TIME_US WAIT_TIME_MS * 1000
#define WAIT_TIME_NS WAIT_TIME_US * 1000

bool find_window(cocoa_window_t cw, obs_data_t *settings, bool force)
{
    if (!force && cw->next_search_time > os_gettime_ns())
        return false;

    cw->next_search_time = os_gettime_ns() + WAIT_TIME_NS;

    pthread_mutex_lock(&cw->name_lock);

    if (!cw->window_name.length && !cw->owner_name.length)
        goto invalid_name;

    for (NSDictionary *dict in enumerate_windows()) {
        if (![cw->owner_name isEqualToString:dict[OWNER_NAME]])
            continue;

        if (![cw->window_name isEqualToString:dict[WINDOW_NAME]])
            continue;

        pthread_mutex_unlock(&cw->name_lock);

        NSNumber *window_id = (NSNumber *) dict[WINDOW_NUMBER];
        cw->window_id = window_id.intValue;
        NSNumber *owner_pid = (NSNumber *) dict[OWNER_PID];
        cw->owner_pid = owner_pid.intValue;

        obs_data_set_int(settings, "window", cw->window_id);
        obs_data_set_int(settings, "owner_pid", cw->owner_pid);
        return true;
    }

invalid_name:
    pthread_mutex_unlock(&cw->name_lock);
    return false;
}

void init_window(cocoa_window_t cw, obs_data_t *settings)
{
    pthread_mutex_init(&cw->name_lock, NULL);

    cw->owner_name = @(obs_data_get_string(settings, "owner_name"));
    cw->window_name = @(obs_data_get_string(settings, "window_name"));
    [cw->owner_name retain];
    [cw->window_name retain];

    // Find initial window.
    pthread_mutex_lock(&cw->name_lock);

    if (!cw->window_name.length && !cw->owner_name.length)
        goto invalid_name;

    NSNumber *owner_pid = @(obs_data_get_int(settings, "owner_pid"));
    NSNumber *window_id = @(obs_data_get_int(settings, "window"));
    for (NSDictionary *dict in enumerate_windows()) {
        bool owner_names_match = [cw->owner_name isEqualToString:dict[OWNER_NAME]];
        bool ids_match = [owner_pid isEqualToNumber:dict[OWNER_PID]] && [window_id isEqualToNumber:dict[WINDOW_NUMBER]];
        bool window_names_match = [cw->window_name isEqualToString:dict[WINDOW_NAME]];

        if (owner_names_match && (ids_match || window_names_match)) {
            pthread_mutex_unlock(&cw->name_lock);

            cw->window_id = [dict[WINDOW_NUMBER] intValue];
            cw->owner_pid = [dict[OWNER_PID] intValue];

            obs_data_set_int(settings, "window", cw->window_id);
            obs_data_set_int(settings, "owner_pid", cw->owner_pid);
            return;
        }
    }

invalid_name:
    pthread_mutex_unlock(&cw->name_lock);
    return;
}

void destroy_window(cocoa_window_t cw)
{
    pthread_mutex_destroy(&cw->name_lock);
    [cw->owner_name release];
    [cw->window_name release];
}

void update_window(cocoa_window_t cw, obs_data_t *settings)
{
    pthread_mutex_lock(&cw->name_lock);
    [cw->owner_name release];
    [cw->window_name release];
    cw->owner_name = @(obs_data_get_string(settings, "owner_name"));
    cw->window_name = @(obs_data_get_string(settings, "window_name"));
    [cw->owner_name retain];
    [cw->window_name retain];
    pthread_mutex_unlock(&cw->name_lock);

    cw->owner_pid = (int) obs_data_get_int(settings, "owner_pid");
    cw->window_id = (unsigned int) obs_data_get_int(settings, "window");
}

static inline const char *make_name(NSString *owner, NSString *name)
{
    if (!owner.length)
        return "";

    NSString *str = [NSString stringWithFormat:@"[%@] %@", owner, name];
    return str.UTF8String;
}

static inline NSDictionary *find_window_dict(NSArray *arr, int window_id)
{
    for (NSDictionary *dict in arr) {
        NSNumber *wid = (NSNumber *) dict[WINDOW_NUMBER];
        if (wid.intValue == window_id)
            return dict;
    }

    return nil;
}

static inline bool window_changed_internal(obs_property_t *p, obs_data_t *settings)
{
    int window_id = (int) obs_data_get_int(settings, "window");
    NSString *window_owner = @(obs_data_get_string(settings, "owner_name"));
    NSString *window_name = @(obs_data_get_string(settings, "window_name"));

    NSDictionary *win_info = @ {
        OWNER_NAME: window_owner,
        WINDOW_NAME: window_name,
    };

    NSArray *arr = enumerate_windows();

    bool show_empty_names = obs_data_get_bool(settings, "show_empty_names");

    NSDictionary *cur = find_window_dict(arr, window_id);
    bool window_found = cur != nil;
    bool window_added = window_found;

    obs_property_list_clear(p);
    for (NSDictionary *dict in arr) {
        NSString *owner = (NSString *) dict[OWNER_NAME];
        NSString *name = (NSString *) dict[WINDOW_NAME];
        NSNumber *wid = (NSNumber *) dict[WINDOW_NUMBER];

        if (!window_added && win_info_cmp(win_info, dict) == NSOrderedAscending) {
            window_added = true;
            size_t idx = obs_property_list_add_int(p, make_name(window_owner, window_name), window_id);
            obs_property_list_item_disable(p, idx, true);
        }

        if (!show_empty_names && !name.length && window_id != wid.intValue)
            continue;

        obs_property_list_add_int(p, make_name(owner, name), wid.intValue);
    }

    if (!window_added) {
        size_t idx = obs_property_list_add_int(p, make_name(window_owner, window_name), window_id);
        obs_property_list_item_disable(p, idx, true);
    }

    if (!window_found)
        return true;

    NSString *owner = (NSString *) cur[OWNER_NAME];
    NSString *window = (NSString *) cur[WINDOW_NAME];

    obs_data_set_string(settings, "owner_name", owner.UTF8String);
    obs_data_set_string(settings, "window_name", window.UTF8String);

    return true;
}

static bool window_changed(obs_properties_t *props, obs_property_t *p, obs_data_t *settings)
{
    UNUSED_PARAMETER(props);

    @autoreleasepool {
        return window_changed_internal(p, settings);
    }
}

static bool toggle_empty_names(obs_properties_t *props, obs_property_t *p, obs_data_t *settings)
{
    UNUSED_PARAMETER(p);

    return window_changed(props, obs_properties_get(props, "window"), settings);
}

void window_defaults(obs_data_t *settings)
{
    obs_data_set_default_int(settings, "window", kCGNullWindowID);
    obs_data_set_default_bool(settings, "show_empty_names", false);
}

void add_window_properties(obs_properties_t *props)
{
    obs_property_t *window_list = obs_properties_add_list(props, "window", obs_module_text("WindowUtils.Window"),
                                                          OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
    obs_property_set_modified_callback(window_list, window_changed);

    obs_property_t *empty =
        obs_properties_add_bool(props, "show_empty_names", obs_module_text("WindowUtils.ShowEmptyNames"));
    obs_property_set_modified_callback(empty, toggle_empty_names);
}

void show_window_properties(obs_properties_t *props, bool show)
{
    obs_property_set_visible(obs_properties_get(props, "window"), show);
    obs_property_set_visible(obs_properties_get(props, "show_empty_names"), show);
}

CGDirectDisplayID get_display_migrate_settings(obs_data_t *settings)
{
    bool legacy_display_id = obs_data_has_user_value(settings, "display");
    bool new_display_id = obs_data_has_user_value(settings, "display_uuid");
    if (legacy_display_id && !new_display_id) {
        CGDirectDisplayID display_id = (CGDirectDisplayID) obs_data_get_int(settings, "display");
        CFUUIDRef display_uuid = CGDisplayCreateUUIDFromDisplayID(display_id);
        if (display_uuid) {
            CFStringRef uuid_string = CFUUIDCreateString(kCFAllocatorDefault, display_uuid);
            obs_data_set_string(settings, "display_uuid", CFStringGetCStringPtr(uuid_string, kCFStringEncodingUTF8));
            obs_data_erase(settings, "display");
            CFRelease(uuid_string);
            CFRelease(display_uuid);
        } else {
            return 0;
        }
    } else if (legacy_display_id && new_display_id) {
        obs_data_erase(settings, "display");
    }

    const char *display_uuid = obs_data_get_string(settings, "display_uuid");
    if (display_uuid && *display_uuid) {
        CFStringRef uuid_string = CFStringCreateWithCString(kCFAllocatorDefault, display_uuid, kCFStringEncodingUTF8);
        CFUUIDRef uuid_ref = CFUUIDCreateFromString(kCFAllocatorDefault, uuid_string);
        CGDirectDisplayID display = CGDisplayGetDisplayIDFromUUID(uuid_ref);
        CFRelease(uuid_string);
        CFRelease(uuid_ref);
        return display;
    } else {
        return 0;
    }
}
