#include "mac-sck-common.h"

const char *sck_audio_capture_getname(void *unused __unused)
{
    return obs_module_text("SCK.Audio.Name");
}

static void destroy_audio_screen_stream(struct screen_capture *sc)
{
    if (sc->disp && !sc->capture_failed) {
        [sc->disp stopCaptureWithCompletionHandler:^(NSError *_Nullable error) {
            if (error && error.code != SCStreamErrorAttemptToStopStreamState) {
                MACCAP_ERR("destroy_audio_screen_stream: Failed to stop stream with error %s\n",
                           [[error localizedFailureReason] cStringUsingEncoding:NSUTF8StringEncoding]);
            }
            os_event_signal(sc->disp_finished);
        }];
        os_event_wait(sc->disp_finished);
    }

    if (sc->stream_properties) {
        [sc->stream_properties release];
        sc->stream_properties = NULL;
    }

    if (sc->disp) {
        [sc->disp release];
        sc->disp = NULL;
    }

    os_event_destroy(sc->disp_finished);
    os_event_destroy(sc->stream_start_completed);
}

static void sck_audio_capture_destroy(void *data)
{
    struct screen_capture *sc = data;

    if (!sc)
        return;

    destroy_audio_screen_stream(sc);

    if (sc->shareable_content) {
        os_sem_wait(sc->shareable_content_available);
        [sc->shareable_content release];
        os_sem_destroy(sc->shareable_content_available);
        sc->shareable_content_available = NULL;
    }

    if (sc->capture_delegate) {
        [sc->capture_delegate release];
    }
    [sc->application_id release];

    pthread_mutex_destroy(&sc->mutex);
    bfree(sc);
}

static bool init_audio_screen_stream(struct screen_capture *sc)
{
    SCContentFilter *content_filter;
    if (sc->capture_failed) {
        sc->capture_failed = false;
        obs_source_update_properties(sc->source);
    }

    sc->stream_properties = [[SCStreamConfiguration alloc] init];
    os_sem_wait(sc->shareable_content_available);

    SCDisplayRef (^get_target_display)(void) = ^SCDisplayRef {
        for (SCDisplay *display in sc->shareable_content.displays) {
            if (display.displayID == sc->display) {
                return display;
            }
        }
        return nil;
    };

    switch (sc->audio_capture_type) {
        case ScreenCaptureAudioDesktopStream: {
            SCDisplay *target_display = get_target_display();

            NSArray *empty = [[NSArray alloc] init];
            content_filter = [[SCContentFilter alloc] initWithDisplay:target_display excludingWindows:empty];
            [empty release];
        } break;
        case ScreenCaptureAudioApplicationStream: {
            SCDisplay *target_display = get_target_display();
            SCRunningApplication *target_application = nil;
            for (SCRunningApplication *application in sc->shareable_content.applications) {
                if ([application.bundleIdentifier isEqualToString:sc->application_id]) {
                    target_application = application;
                    break;
                }
            }
            NSArray *target_application_array = [[NSArray alloc] initWithObjects:target_application, nil];

            NSArray *empty = [[NSArray alloc] init];
            content_filter = [[SCContentFilter alloc] initWithDisplay:target_display
                                                includingApplications:target_application_array
                                                     exceptingWindows:empty];
            [target_application_array release];
            [empty release];
        } break;
    }
    os_sem_post(sc->shareable_content_available);
    [sc->stream_properties setQueueDepth:8];

    [sc->stream_properties setCapturesAudio:TRUE];
    [sc->stream_properties setExcludesCurrentProcessAudio:TRUE];

    struct obs_audio_info audio_info;
    BOOL did_get_audio_info = obs_get_audio_info(&audio_info);
    if (!did_get_audio_info) {
        MACCAP_ERR("init_audio_screen_stream: No audio configured, returning %d\n", did_get_audio_info);
        [content_filter release];
        return did_get_audio_info;
    }
    int channel_count = get_audio_channels(audio_info.speakers);
    if (channel_count > 1) {
        [sc->stream_properties setChannelCount:2];
    } else {
        [sc->stream_properties setChannelCount:channel_count];
    }

    sc->disp = [[SCStream alloc] initWithFilter:content_filter configuration:sc->stream_properties
                                       delegate:sc->capture_delegate];
    [content_filter release];

    //add a dummy video stream output to silence errors from SCK. frames are dropped by the delegate
    NSError *error = nil;
    BOOL did_add_output = [sc->disp addStreamOutput:sc->capture_delegate type:SCStreamOutputTypeScreen
                                 sampleHandlerQueue:nil
                                              error:&error];
    if (!did_add_output) {
        MACCAP_ERR("init_audio_screen_stream: Failed to add video stream output with error %s\n",
                   [[error localizedFailureReason] cStringUsingEncoding:NSUTF8StringEncoding]);
        [error release];
        [sc->disp release];
        sc->disp = NULL;
        return !did_add_output;
    }

    did_add_output = [sc->disp addStreamOutput:sc->capture_delegate type:SCStreamOutputTypeAudio sampleHandlerQueue:nil
                                         error:&error];
    if (!did_add_output) {
        MACCAP_ERR("init_audio_screen_stream: Failed to add audio stream output with error %s\n",
                   [[error localizedFailureReason] cStringUsingEncoding:NSUTF8StringEncoding]);
        [error release];
        [sc->disp release];
        sc->disp = NULL;
        return !did_add_output;
    }
    os_event_init(&sc->disp_finished, OS_EVENT_TYPE_MANUAL);
    os_event_init(&sc->stream_start_completed, OS_EVENT_TYPE_MANUAL);

    __block BOOL did_stream_start = false;
    [sc->disp startCaptureWithCompletionHandler:^(NSError *_Nullable error2) {
        did_stream_start = (BOOL) (error2 == nil);
        if (!did_stream_start) {
            MACCAP_ERR("init_audio_screen_stream: Failed to start capture with error %s\n",
                       [[error localizedFailureReason] cStringUsingEncoding:NSUTF8StringEncoding]);
            // Clean up disp so it isn't stopped
            [sc->disp release];
            sc->disp = NULL;
        }
        os_event_signal(sc->stream_start_completed);
    }];
    os_event_wait(sc->stream_start_completed);

    return did_stream_start;
}

static void sck_audio_capture_defaults(obs_data_t *settings)
{
    obs_data_set_default_string(settings, "application", NULL);
    obs_data_set_default_int(settings, "type", ScreenCaptureAudioDesktopStream);
}

static void *sck_audio_capture_create(obs_data_t *settings, obs_source_t *source)
{
    struct screen_capture *sc = bzalloc(sizeof(struct screen_capture));

    sc->source = source;
    sc->audio_only = true;
    sc->audio_capture_type = (unsigned int) obs_data_get_int(settings, "type");

    os_sem_init(&sc->shareable_content_available, 1);
    screen_capture_build_content_list(sc, sc->capture_type == ScreenCaptureAudioDesktopStream);

    sc->capture_delegate = [[ScreenCaptureDelegate alloc] init];
    sc->capture_delegate.sc = sc;

    sc->display = CGMainDisplayID();

    sc->application_id = [[NSString alloc] initWithUTF8String:obs_data_get_string(settings, "application")];
    pthread_mutex_init(&sc->mutex, NULL);

    if (!init_audio_screen_stream(sc))
        goto fail;

    return sc;

fail:
    sck_audio_capture_destroy(sc);
    return NULL;
}

#pragma mark - obs_properties

static bool audio_capture_method_changed(void *data, obs_properties_t *props, obs_property_t *list __unused,
                                         obs_data_t *settings)
{
    struct screen_capture *sc = data;

    unsigned int capture_type_id = (unsigned int) obs_data_get_int(settings, "type");
    obs_property_t *app_list = obs_properties_get(props, "application");

    switch (capture_type_id) {
        case ScreenCaptureAudioDesktopStream: {
            obs_property_set_visible(app_list, false);
            break;
        }
        case ScreenCaptureAudioApplicationStream: {
            obs_property_set_visible(app_list, true);
            screen_capture_build_content_list(sc, capture_type_id == ScreenCaptureAudioDesktopStream);
            build_application_list(sc, props);
            break;
        }
    }

    return true;
}

static bool reactivate_capture(obs_properties_t *props __unused, obs_property_t *property, void *data)
{
    struct screen_capture *sc = data;
    if (!sc->capture_failed) {
        MACCAP_LOG(LOG_WARNING, "Tried to reactivate capture that hadn't failed.");
        return false;
    }

    destroy_audio_screen_stream(sc);
    sc->capture_failed = false;
    init_audio_screen_stream(sc);
    obs_property_set_enabled(property, false);
    return true;
}

static obs_properties_t *sck_audio_capture_properties(void *data)
{
    struct screen_capture *sc = data;

    obs_properties_t *props = obs_properties_create();

    obs_property_t *capture_type = obs_properties_add_list(props, "type", obs_module_text("SCK.Method"),
                                                           OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);

    obs_property_list_add_int(capture_type, obs_module_text("DesktopAudioCapture"), 0);
    obs_property_list_add_int(capture_type, obs_module_text("ApplicationAudioCapture"), 1);

    obs_property_set_modified_callback2(capture_type, audio_capture_method_changed, data);

    obs_property_t *app_list = obs_properties_add_list(props, "application", obs_module_text("Application"),
                                                       OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING);
    obs_property_t *reactivate =
        obs_properties_add_button2(props, "reactivate_capture", obs_module_text("SCK.Restart"), reactivate_capture, sc);
    obs_property_set_enabled(reactivate, sc->capture_failed);

    if (sc) {
        switch (sc->audio_capture_type) {
            case 0: {
                obs_property_set_visible(app_list, false);
                break;
            }
            case 1: {
                obs_property_set_visible(app_list, true);
                break;
            }
        }
    }

    return props;
}

static void sck_audio_capture_update(void *data, obs_data_t *settings)
{
    struct screen_capture *sc = data;

    ScreenCaptureAudioStreamType capture_type = (ScreenCaptureAudioStreamType) obs_data_get_int(settings, "type");
    NSString *application_id = [[NSString alloc] initWithUTF8String:obs_data_get_string(settings, "application")];

    destroy_audio_screen_stream(sc);
    sc->audio_capture_type = capture_type;
    [sc->application_id release];
    sc->application_id = application_id;
    init_audio_screen_stream(sc);
}

#pragma mark - obs_source_info

struct obs_source_info sck_audio_capture_info = {
    .id = "sck_audio_capture",
    .type = OBS_SOURCE_TYPE_INPUT,
    .get_name = sck_audio_capture_getname,

    .create = sck_audio_capture_create,
    .destroy = sck_audio_capture_destroy,

    .output_flags = OBS_SOURCE_DO_NOT_DUPLICATE | OBS_SOURCE_AUDIO,

    .get_defaults = sck_audio_capture_defaults,
    .get_properties = sck_audio_capture_properties,
    .update = sck_audio_capture_update,
    .icon_type = OBS_ICON_TYPE_AUDIO_OUTPUT,
};
