#include <gio/gio.h>
#include <glib-unix.h>
#include <mpv/client.h>
#include <libavformat/avformat.h>
#include <inttypes.h>
#include <string.h>



static const char *introspection_xml =
    "<node>\n"
    "  <interface name=\"org.mpris.MediaPlayer2\">\n"
    "    <method name=\"Raise\">\n"
    "    </method>\n"
    "    <method name=\"Quit\">\n"
    "    </method>\n"
    "    <property name=\"CanQuit\" type=\"b\" access=\"read\"/>\n"
    "    <property name=\"Fullscreen\" type=\"b\" access=\"readwrite\"/>\n"
    "    <property name=\"CanSetFullscreen\" type=\"b\" access=\"read\"/>\n"
    "    <property name=\"CanRaise\" type=\"b\" access=\"read\"/>\n"
    "    <property name=\"HasTrackList\" type=\"b\" access=\"read\"/>\n"
    "    <property name=\"Identity\" type=\"s\" access=\"read\"/>\n"
    "    <property name=\"DesktopEntry\" type=\"s\" access=\"read\"/>\n"
    "    <property name=\"SupportedUriSchemes\" type=\"as\" access=\"read\"/>\n"
    "    <property name=\"SupportedMimeTypes\" type=\"as\" access=\"read\"/>\n"
    "  </interface>\n"
    "  <interface name=\"org.mpris.MediaPlayer2.Player\">\n"
    "    <method name=\"Next\">\n"
    "    </method>\n"
    "    <method name=\"Previous\">\n"
    "    </method>\n"
    "    <method name=\"Pause\">\n"
    "    </method>\n"
    "    <method name=\"PlayPause\">\n"
    "    </method>\n"
    "    <method name=\"Stop\">\n"
    "    </method>\n"
    "    <method name=\"Play\">\n"
    "    </method>\n"
    "    <method name=\"Seek\">\n"
    "      <arg type=\"x\" name=\"Offset\" direction=\"in\"/>\n"
    "    </method>\n"
    "    <method name=\"SetPosition\">\n"
    "      <arg type=\"o\" name=\"TrackId\" direction=\"in\"/>\n"
    "      <arg type=\"x\" name=\"Offset\" direction=\"in\"/>\n"
    "    </method>\n"
    "    <method name=\"OpenUri\">\n"
    "      <arg type=\"s\" name=\"Uri\" direction=\"in\"/>\n"
    "    </method>\n"
    "    <signal name=\"Seeked\">\n"
    "      <arg type=\"x\" name=\"Position\" direction=\"out\"/>\n"
    "    </signal>\n"
    "    <property name=\"PlaybackStatus\" type=\"s\" access=\"read\"/>\n"
    "    <property name=\"LoopStatus\" type=\"s\" access=\"readwrite\"/>\n"
    "    <property name=\"Rate\" type=\"d\" access=\"readwrite\"/>\n"
    "    <property name=\"Shuffle\" type=\"b\" access=\"readwrite\"/>\n"
    "    <property name=\"Metadata\" type=\"a{sv}\" access=\"read\"/>\n"
    "    <property name=\"Volume\" type=\"d\" access=\"readwrite\"/>\n"
    "    <property name=\"Position\" type=\"x\" access=\"read\"/>\n"
    "    <property name=\"MinimumRate\" type=\"d\" access=\"read\"/>\n"
    "    <property name=\"MaximumRate\" type=\"d\" access=\"read\"/>\n"
    "    <property name=\"CanGoNext\" type=\"b\" access=\"read\"/>\n"
    "    <property name=\"CanGoPrevious\" type=\"b\" access=\"read\"/>\n"
    "    <property name=\"CanPlay\" type=\"b\" access=\"read\"/>\n"
    "    <property name=\"CanPause\" type=\"b\" access=\"read\"/>\n"
    "    <property name=\"CanSeek\" type=\"b\" access=\"read\"/>\n"
    "    <property name=\"CanControl\" type=\"b\" access=\"read\"/>\n"
    "  </interface>\n"
    "</node>\n";

typedef struct UserData
{
    mpv_handle *mpv;
    GMainLoop *loop;
    GMainContext *ctx;
    int wakeup_pipe[2];
    gint bus_id;
    GDBusConnection *connection;
    GDBusInterfaceInfo *root_interface_info;
    GDBusInterfaceInfo *player_interface_info;
    guint root_interface_id;
    guint player_interface_id;
    char *client_name;
    const char *status;
    const char *loop_status;
    gboolean shuffle;
    GHashTable *changed_properties;
    GVariant *metadata;
    gboolean seek_expected;
    gboolean idle;
    gboolean paused;
    gboolean events_setup;
    int64_t playlist_count;
    int64_t playlist_pos;
} UserData;

static const char *STATUS_PLAYING = "Playing";
static const char *STATUS_PAUSED = "Paused";
static const char *STATUS_STOPPED = "Stopped";
static const char *LOOP_NONE = "None";
static const char *LOOP_TRACK = "Track";
static const char *LOOP_PLAYLIST = "Playlist";

static void setup_mpv_event_sources(UserData *ud);
static gboolean can_go_next(UserData *ud);
static gboolean can_go_previous(UserData *ud);

static gchar *string_to_utf8(gchar *maybe_utf8)
{
    gchar *attempted_validation;
    attempted_validation = g_utf8_make_valid(maybe_utf8, -1);

    if (g_utf8_validate(attempted_validation, -1, NULL)) {
        return attempted_validation;
    } else {
        g_free(attempted_validation);
        return g_strdup("<invalid utf8>");
    }
}

static void add_metadata_item_string(mpv_handle *mpv, GVariantDict *dict,
                                     const char *property, const char *tag)
{
    char *temp = mpv_get_property_string(mpv, property);
    if (temp) {
        char *utf8 = string_to_utf8(temp);
        g_variant_dict_insert(dict, tag, "s", utf8);
        g_free(utf8);
        mpv_free(temp);
    }
}

static void add_metadata_item_int(mpv_handle *mpv, GVariantDict *dict,
                                  const char *property, const char *tag)
{
    int64_t value;
    int res = mpv_get_property(mpv, property, MPV_FORMAT_INT64, &value);
    if (res >= 0) {
        g_variant_dict_insert(dict, tag, "x", value);
    }
}

static void add_metadata_item_string_list(mpv_handle *mpv, GVariantDict *dict,
                                          const char *property, const char *tag)
{
    char *temp = mpv_get_property_string(mpv, property);

    if (temp) {
        GVariantBuilder builder;
        char **list = g_strsplit(temp, ", ", 0);
        char **iter = list;
        g_variant_builder_init(&builder, G_VARIANT_TYPE("as"));

        for (; *iter; iter++) {
            char *item = *iter;
            char *utf8 = string_to_utf8(item);
            g_variant_builder_add(&builder, "s", utf8);
            g_free(utf8);
        }

        g_variant_dict_insert(dict, tag, "as", &builder);

        g_strfreev(list);
        mpv_free(temp);
    }
}

static gchar *path_to_uri(mpv_handle *mpv, char *path)
{
    char* working_dir;
    gchar* canonical;
    gchar *uri;

    working_dir = mpv_get_property_string(mpv, "working-directory");
    canonical = g_canonicalize_filename(path, working_dir);
    uri = g_filename_to_uri(canonical, NULL, NULL);

    mpv_free(working_dir);
    g_free(canonical);

    return uri;
}

static void add_metadata_uri(mpv_handle *mpv, GVariantDict *dict)
{
    char *path;
    char *uri;

    path = mpv_get_property_string(mpv, "path");
    if (!path) {
        return;
    }

    uri = g_uri_parse_scheme(path);
    if (uri) {
        g_variant_dict_insert(dict, "xesam:url", "s", path);
        g_free(uri);
    } else {
        gchar *converted = path_to_uri(mpv, path);
        g_variant_dict_insert(dict, "xesam:url", "s", converted);
        g_free(converted);
    }

    mpv_free(path);
}

static gchar* try_get_cover_art_file(mpv_handle *mpv)
{
    char *files_str = mpv_get_property_string(mpv, "cover-art-files");
    if (!files_str || files_str[0] == '\0') {
        mpv_free(files_str);
        return NULL;
    }

    // cover-art-files is a comma-separated list, use the first one
    gchar **files = g_strsplit(files_str, ",", -1);
    mpv_free(files_str);

    gchar *out = NULL;
    if (files[0] && files[0][0] != '\0') {
        out = path_to_uri(mpv, files[0]);
    }

    g_strfreev(files);
    return out;
}

static gchar* try_get_folder_art(mpv_handle *mpv, char *path)
{
    gchar *out = NULL;
    gchar *dirname = g_path_get_dirname(path);

    char *exts_str = mpv_get_property_string(mpv, "image-exts");
    char *names_str = mpv_get_property_string(mpv, "cover-art-whitelist");

    if (!exts_str || !names_str) {
        mpv_free(exts_str);
        mpv_free(names_str);
        g_free(dirname);
        return NULL;
    }

    gchar **exts = g_strsplit(exts_str, ",", -1);
    gchar **names = g_strsplit(names_str, ",", -1);
    mpv_free(exts_str);
    mpv_free(names_str);

    for (gchar **name = names; *name; ++name) {
        for (gchar **ext = exts; *ext; ++ext) {
            gchar *filename = g_strdup_printf("%s/%s.%s", dirname, *name, *ext);
            if (g_file_test(filename, G_FILE_TEST_EXISTS)) {
                out = path_to_uri(mpv, filename);
                g_free(filename);
                goto done;
            }
            g_free(filename);
        }
    }

done:
    g_strfreev(exts);
    g_strfreev(names);
    g_free(dirname);
    return out;
}

static const char *youtube_url_pattern =
    "^https?:\\/\\/(?:youtu.be\\/|(?:www\\.)?youtube\\.com\\/watch\\?v=)(?<id>[a-zA-Z0-9_-]*)\\??.*";

static GRegex *youtube_url_regex;

static gchar* try_get_youtube_thumbnail(char *path)
{
    gchar *out = NULL;
    if (!youtube_url_regex) {
        youtube_url_regex = g_regex_new(youtube_url_pattern, 0, 0, NULL);
    }

    GMatchInfo *match_info;
    gboolean matched = g_regex_match(youtube_url_regex, path, 0, &match_info);

    if (matched) {
        gchar *video_id = g_match_info_fetch_named(match_info, "id");
        out = g_strconcat("https://i1.ytimg.com/vi/",
                                           video_id, "/hqdefault.jpg", NULL);
        g_free(video_id);
    }

    g_match_info_free(match_info);
    return out;
}

static gchar* extract_embedded_art(AVFormatContext *context) {
    AVPacket *packet = NULL;
    for (unsigned int i = 0; i < context->nb_streams; i++) {
        if (context->streams[i]->disposition & AV_DISPOSITION_ATTACHED_PIC) {
            AVPacket *p = &context->streams[i]->attached_pic;

            // Skip the thumbnail if the size is bigger than 25MiB to avoid crashes
            if (p->size <= 25*0x100000) {
                packet = p;
                break;
            }
        }
    }
    if (!packet) {
        return NULL;
    }

    gchar *data = g_base64_encode(packet->data, packet->size);
    gchar *img = g_strconcat("data:image/jpeg;base64,", data, NULL);

    g_free(data);
    return img;
}

static gchar* try_get_embedded_art(char *path)
{
    gchar *out = NULL;
    AVFormatContext *context = NULL;
    if (!avformat_open_input(&context, path, NULL, NULL)) {
        out = extract_embedded_art(context);
        avformat_close_input(&context);
    }

    return out;
}

static gchar* get_art_url(mpv_handle *mpv, char *path)
{
    gchar *url;
    gboolean is_remote = g_str_has_prefix(path, "http");

    if ((url = try_get_cover_art_file(mpv)))
        return url;
    if (is_remote && (url = try_get_youtube_thumbnail(path)))
        return url;
    if (!is_remote && (url = try_get_embedded_art(path)))
        return url;
    if (!is_remote && (url = try_get_folder_art(mpv, path)))
        return url;

    return NULL;
}

// cached last file path, owned by mpv
static char *cached_path = NULL;

// cached last artwork url, owned by glib
static gchar *cached_art_url = NULL;

static void add_metadata_art(mpv_handle *mpv, GVariantDict *dict)
{
    char *path = mpv_get_property_string(mpv, "path");

    if (!path) {
        return;
    }

    // mpv may call create_metadata multiple times, so cache to save CPU
    if (!cached_path || strcmp(path, cached_path)) {
        mpv_free(cached_path);
        g_free(cached_art_url);
        cached_path = path;
        cached_art_url = get_art_url(mpv, path);
    } else {
        mpv_free(path);
    }

    if (cached_art_url) {
        g_variant_dict_insert(dict, "mpris:artUrl", "s", cached_art_url);
    }
}

static void add_metadata_content_created(mpv_handle *mpv, GVariantDict *dict)
{
    char *date_str = mpv_get_property_string(mpv, "metadata/by-key/Date");

    if (!date_str) {
        return;
    }

    GDate* date = g_date_new();
    if (strlen(date_str) == 4) {
        gint64 year = g_ascii_strtoll(date_str, NULL, 10);
        if (year != 0) {
            g_date_set_dmy(date, 1, 1, year);
        }
    } else {
        g_date_set_parse(date, date_str);
    }

    if (g_date_valid(date)) {
        gchar iso8601[20];
        g_date_strftime(iso8601, 20, "%Y-%m-%dT00:00:00Z", date);
        g_variant_dict_insert(dict, "xesam:contentCreated", "s", iso8601);
    }

    g_date_free(date);
    mpv_free(date_str);
}

static GVariant *create_metadata(UserData *ud)
{
    GVariantDict dict;
    double duration;
    char *temp_str;
    int res;

    g_variant_dict_init(&dict, NULL);

    // mpris:trackid
    // playlist_pos < 0 if there is no playlist or current track
    if (ud->playlist_pos < 0) {
        temp_str = g_strdup("/noplaylist");
    } else {
        temp_str = g_strdup_printf("/%" PRId64, ud->playlist_pos);
    }
    g_variant_dict_insert(&dict, "mpris:trackid", "o", temp_str);
    g_free(temp_str);

    // mpris:length
    res = mpv_get_property(ud->mpv, "duration", MPV_FORMAT_DOUBLE, &duration);
    if (res == MPV_ERROR_SUCCESS) {
        g_variant_dict_insert(&dict, "mpris:length", "x", (int64_t)(duration * 1000000.0));
    }

    add_metadata_item_string(ud->mpv, &dict, "media-title", "xesam:title");
    add_metadata_item_string(ud->mpv, &dict, "metadata/by-key/Album", "xesam:album");
    add_metadata_item_string(ud->mpv, &dict, "metadata/by-key/Genre", "xesam:genre");

    /* Musicbrainz metadata mappings
       (https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html) */

    // IDv3 metadata format
    add_metadata_item_string(ud->mpv, &dict, "metadata/by-key/MusicBrainz Artist Id", "mb:artistId");
    add_metadata_item_string(ud->mpv, &dict, "metadata/by-key/MusicBrainz Track Id", "mb:trackId");
    add_metadata_item_string(ud->mpv, &dict, "metadata/by-key/MusicBrainz Album Artist Id", "mb:albumArtistId");
    add_metadata_item_string(ud->mpv, &dict, "metadata/by-key/MusicBrainz Album Id", "mb:albumId");
    add_metadata_item_string(ud->mpv, &dict, "metadata/by-key/MusicBrainz Release Track Id", "mb:releaseTrackId");
    add_metadata_item_string(ud->mpv, &dict, "metadata/by-key/MusicBrainz Work Id", "mb:workId");

    // Vorbis & APEv2 metadata format
    add_metadata_item_string(ud->mpv, &dict, "metadata/by-key/MUSICBRAINZ_ARTISTID", "mb:artistId");
    add_metadata_item_string(ud->mpv, &dict, "metadata/by-key/MUSICBRAINZ_TRACKID", "mb:trackId");
    add_metadata_item_string(ud->mpv, &dict, "metadata/by-key/MUSICBRAINZ_ALBUMARTISTID", "mb:albumArtistId");
    add_metadata_item_string(ud->mpv, &dict, "metadata/by-key/MUSICBRAINZ_ALBUMID", "mb:albumId");
    add_metadata_item_string(ud->mpv, &dict, "metadata/by-key/MUSICBRAINZ_RELEASETRACKID", "mb:releaseTrackId");
    add_metadata_item_string(ud->mpv, &dict, "metadata/by-key/MUSICBRAINZ_WORKID", "mb:workId");

    add_metadata_item_string_list(ud->mpv, &dict, "metadata/by-key/uploader", "xesam:artist");
    add_metadata_item_string_list(ud->mpv, &dict, "metadata/by-key/Artist", "xesam:artist");
    add_metadata_item_string_list(ud->mpv, &dict, "metadata/by-key/Album_Artist", "xesam:albumArtist");
    add_metadata_item_string_list(ud->mpv, &dict, "metadata/by-key/Composer", "xesam:composer");

    add_metadata_item_int(ud->mpv, &dict, "metadata/by-key/Track", "xesam:trackNumber");
    add_metadata_item_int(ud->mpv, &dict, "metadata/by-key/Disc", "xesam:discNumber");

    add_metadata_uri(ud->mpv, &dict);
    add_metadata_art(ud->mpv, &dict);
    add_metadata_content_created(ud->mpv, &dict);

    return g_variant_dict_end(&dict);
}

static void method_call_root(G_GNUC_UNUSED GDBusConnection *connection,
                             G_GNUC_UNUSED const char *sender,
                             G_GNUC_UNUSED const char *object_path,
                             G_GNUC_UNUSED const char *interface_name,
                             const char *method_name,
                             G_GNUC_UNUSED GVariant *parameters,
                             GDBusMethodInvocation *invocation,
                             gpointer user_data)
{
    UserData *ud = (UserData*)user_data;
    if (g_strcmp0(method_name, "Quit") == 0) {
        const char *cmd[] = {"quit", NULL};
        mpv_command_async(ud->mpv, 0, cmd);
        g_dbus_method_invocation_return_value(invocation, NULL);

    } else if (g_strcmp0(method_name, "Raise") == 0) {
        // Can't raise
        g_dbus_method_invocation_return_value(invocation, NULL);

    } else {
        g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR,
                                              G_DBUS_ERROR_UNKNOWN_METHOD,
                                              "Unknown method");
    }
}

static GVariant *get_property_root(G_GNUC_UNUSED GDBusConnection *connection,
                                   G_GNUC_UNUSED const char *sender,
                                   G_GNUC_UNUSED const char *object_path,
                                   G_GNUC_UNUSED const char *interface_name,
                                   const char *property_name,
                                   G_GNUC_UNUSED GError **error,
                                   gpointer user_data)
{
    UserData *ud = (UserData*)user_data;
    GVariant *ret;

    if (g_strcmp0(property_name, "CanQuit") == 0) {
        ret = g_variant_new_boolean(TRUE);

    } else if (g_strcmp0(property_name, "Fullscreen") == 0) {
        int fullscreen;
        mpv_get_property(ud->mpv, "fullscreen", MPV_FORMAT_FLAG, &fullscreen);
        ret = g_variant_new_boolean(fullscreen);

    } else if (g_strcmp0(property_name, "CanSetFullscreen") == 0) {
        int can_fullscreen;
        mpv_get_property(ud->mpv, "vo-configured", MPV_FORMAT_FLAG, &can_fullscreen);
        ret = g_variant_new_boolean(can_fullscreen);

    } else if (g_strcmp0(property_name, "CanRaise") == 0) {
        ret = g_variant_new_boolean(FALSE);

    } else if (g_strcmp0(property_name, "HasTrackList") == 0) {
        ret = g_variant_new_boolean(FALSE);

    } else if (g_strcmp0(property_name, "Identity") == 0) {
        ret = g_variant_new_string("mpv");

    } else if (g_strcmp0(property_name, "DesktopEntry") == 0) {
        ret = g_variant_new_string("mpv");

    } else if (g_strcmp0(property_name, "SupportedUriSchemes") == 0) {
        GVariantBuilder builder;
        g_variant_builder_init(&builder, G_VARIANT_TYPE("as"));
        g_variant_builder_add(&builder, "s", "ftp");
        g_variant_builder_add(&builder, "s", "http");
        g_variant_builder_add(&builder, "s", "https");
        g_variant_builder_add(&builder, "s", "mms");
        g_variant_builder_add(&builder, "s", "rtmp");
        g_variant_builder_add(&builder, "s", "rtsp");
        g_variant_builder_add(&builder, "s", "sftp");
        g_variant_builder_add(&builder, "s", "smb");
        ret = g_variant_builder_end(&builder);

    } else if (g_strcmp0(property_name, "SupportedMimeTypes") == 0) {
        GVariantBuilder builder;
        g_variant_builder_init(&builder, G_VARIANT_TYPE("as"));
        g_variant_builder_add(&builder, "s", "application/ogg");
        g_variant_builder_add(&builder, "s", "audio/mpeg");
        // TODO add the rest
        ret = g_variant_builder_end(&builder);

    } else {
        ret = NULL;
        g_set_error(error, G_DBUS_ERROR,
                    G_DBUS_ERROR_UNKNOWN_PROPERTY,
                    "Unknown property %s", property_name);
    }

    return ret;
}

static gboolean set_property_root(G_GNUC_UNUSED GDBusConnection *connection,
                                  G_GNUC_UNUSED const char *sender,
                                  G_GNUC_UNUSED const char *object_path,
                                  G_GNUC_UNUSED const char *interface_name,
                                  const char *property_name,
                                  GVariant *value,
                                  G_GNUC_UNUSED GError **error,
                                  gpointer user_data)
{
    UserData *ud = (UserData*)user_data;
    if (g_strcmp0(property_name, "Fullscreen") == 0) {
        int fullscreen;
        g_variant_get(value, "b", &fullscreen);
        mpv_set_property(ud->mpv, "fullscreen", MPV_FORMAT_FLAG, &fullscreen);

    } else {
        g_set_error(error, G_DBUS_ERROR,
                    G_DBUS_ERROR_UNKNOWN_PROPERTY,
                    "Cannot set property %s", property_name);
        return FALSE;
    }
    return TRUE;
}

static GDBusInterfaceVTable vtable_root = {
    method_call_root, get_property_root, set_property_root, {0}
};

static void method_call_player(G_GNUC_UNUSED GDBusConnection *connection,
                               G_GNUC_UNUSED const char *sender,
                               G_GNUC_UNUSED const char *_object_path,
                               G_GNUC_UNUSED const char *interface_name,
                               const char *method_name,
                               G_GNUC_UNUSED GVariant *parameters,
                               GDBusMethodInvocation *invocation,
                               gpointer user_data)
{
    UserData *ud = (UserData*)user_data;
    if (g_strcmp0(method_name, "Pause") == 0) {
        int paused = TRUE;
        mpv_set_property(ud->mpv, "pause", MPV_FORMAT_FLAG, &paused);
        g_dbus_method_invocation_return_value(invocation, NULL);

    } else if (g_strcmp0(method_name, "PlayPause") == 0) {
        int paused;
        if (ud->status == STATUS_PAUSED) {
            paused = FALSE;
        } else {
            paused = TRUE;
        }
        mpv_set_property(ud->mpv, "pause", MPV_FORMAT_FLAG, &paused);
        g_dbus_method_invocation_return_value(invocation, NULL);

    } else if (g_strcmp0(method_name, "Play") == 0) {
        int paused = FALSE;
        mpv_set_property(ud->mpv, "pause", MPV_FORMAT_FLAG, &paused);
        g_dbus_method_invocation_return_value(invocation, NULL);

    } else if (g_strcmp0(method_name, "Stop") == 0) {
        const char *cmd[] = {"stop", NULL};
        mpv_command_async(ud->mpv, 0, cmd);
        g_dbus_method_invocation_return_value(invocation, NULL);

    } else if (g_strcmp0(method_name, "Next") == 0) {
        const char *cmd[] = {"playlist_next", NULL};
        mpv_command_async(ud->mpv, 0, cmd);
        g_dbus_method_invocation_return_value(invocation, NULL);

    } else if (g_strcmp0(method_name, "Previous") == 0) {
        const char *cmd[] = {"playlist_prev", NULL};
        mpv_command_async(ud->mpv, 0, cmd);
        g_dbus_method_invocation_return_value(invocation, NULL);

    } else if (g_strcmp0(method_name, "Seek") == 0) {
        int64_t offset_us; // in microseconds
        char *offset_str;
        g_variant_get(parameters, "(x)", &offset_us);
        double offset_s = offset_us / 1000000.0;
        offset_str = g_strdup_printf("%f", offset_s);

        const char *cmd[] = {"seek", offset_str, NULL};
        mpv_command_async(ud->mpv, 0, cmd);
        g_dbus_method_invocation_return_value(invocation, NULL);
        g_free(offset_str);

    } else if (g_strcmp0(method_name, "SetPosition") == 0) {
        char *object_path;
        double new_position_s;
        int64_t new_position_us;

        g_variant_get(parameters, "(&ox)", &object_path, &new_position_us);
        new_position_s = ((float)new_position_us) / 1000000.0; // us -> s

        if (ud->playlist_pos == g_ascii_strtoll(object_path + 1, NULL, 10)) {
            mpv_set_property(ud->mpv, "time-pos", MPV_FORMAT_DOUBLE, &new_position_s);
        }

        g_dbus_method_invocation_return_value(invocation, NULL);

    } else if (g_strcmp0(method_name, "OpenUri") == 0) {
        char *uri;
        g_variant_get(parameters, "(&s)", &uri);
        const char *cmd[] = {"loadfile", uri, NULL};
        mpv_command_async(ud->mpv, 0, cmd);
        g_dbus_method_invocation_return_value(invocation, NULL);

    } else {
        g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR,
                                              G_DBUS_ERROR_UNKNOWN_METHOD,
                                              "Unknown method");
    }
}

static GVariant *get_property_player(G_GNUC_UNUSED GDBusConnection *connection,
                                     G_GNUC_UNUSED const char *sender,
                                     G_GNUC_UNUSED const char *object_path,
                                     G_GNUC_UNUSED const char *interface_name,
                                     const char *property_name,
                                     GError **error,
                                     gpointer user_data)
{
    UserData *ud = (UserData*)user_data;
    GVariant *ret;
    if (g_strcmp0(property_name, "PlaybackStatus") == 0) {
        ret = g_variant_new_string(ud->status);

    } else if (g_strcmp0(property_name, "LoopStatus") == 0) {
        ret = g_variant_new_string(ud->loop_status);

    } else if (g_strcmp0(property_name, "Rate") == 0) {
        double rate;
        mpv_get_property(ud->mpv, "speed", MPV_FORMAT_DOUBLE, &rate);
        ret = g_variant_new_double(rate);

    } else if (g_strcmp0(property_name, "Shuffle") == 0) {
        int shuffle;
        mpv_get_property(ud->mpv, "shuffle", MPV_FORMAT_FLAG, &shuffle);
        ret = g_variant_new_boolean(shuffle);

    } else if (g_strcmp0(property_name, "Metadata") == 0) {
        if (!ud->metadata) {
            ud->metadata = create_metadata(ud);
        }
        // Increase reference count to prevent it from being freed after returning
        g_variant_ref(ud->metadata);
        ret = ud->metadata;

    } else if (g_strcmp0(property_name, "Volume") == 0) {
        double volume;
        mpv_get_property(ud->mpv, "volume", MPV_FORMAT_DOUBLE, &volume);
        volume /= 100;
        ret = g_variant_new_double(volume);

    } else if (g_strcmp0(property_name, "Position") == 0) {
        double position_s;
        int64_t position_us;
        mpv_get_property(ud->mpv, "time-pos", MPV_FORMAT_DOUBLE, &position_s);
        position_us = position_s * 1000000.0; // s -> us
        ret = g_variant_new_int64(position_us);

    } else if (g_strcmp0(property_name, "MinimumRate") == 0) {
        ret = g_variant_new_double(0.01);

    } else if (g_strcmp0(property_name, "MaximumRate") == 0) {
        ret = g_variant_new_double(100);

    } else if (g_strcmp0(property_name, "CanGoNext") == 0) {
        ret = g_variant_new_boolean(can_go_next(ud));

    } else if (g_strcmp0(property_name, "CanGoPrevious") == 0) {
        ret = g_variant_new_boolean(can_go_previous(ud));

    } else if (g_strcmp0(property_name, "CanPlay") == 0) {
        ret = g_variant_new_boolean(TRUE);

    } else if (g_strcmp0(property_name, "CanPause") == 0) {
        ret = g_variant_new_boolean(TRUE);

    } else if (g_strcmp0(property_name, "CanSeek") == 0) {
        ret = g_variant_new_boolean(TRUE);

    } else if (g_strcmp0(property_name, "CanControl") == 0) {
        ret = g_variant_new_boolean(TRUE);

    } else {
        ret = NULL;
        g_set_error(error, G_DBUS_ERROR,
                    G_DBUS_ERROR_UNKNOWN_PROPERTY,
                    "Unknown property %s", property_name);
    }

    return ret;
}

static gboolean set_property_player(G_GNUC_UNUSED GDBusConnection *connection,
                                    G_GNUC_UNUSED const char *sender,
                                    G_GNUC_UNUSED const char *object_path,
                                    G_GNUC_UNUSED const char *interface_name,
                                    const char *property_name,
                                    GVariant *value,
                                    G_GNUC_UNUSED GError **error,
                                    gpointer user_data)
{
    UserData *ud = (UserData*)user_data;
    if (g_strcmp0(property_name, "LoopStatus") == 0) {
        const char *status;
        int t = TRUE;
        int f = FALSE;
        status = g_variant_get_string(value, NULL);
        if (g_strcmp0(status, "Track") == 0) {
            mpv_set_property(ud->mpv, "loop-file", MPV_FORMAT_FLAG, &t);
            mpv_set_property(ud->mpv, "loop-playlist", MPV_FORMAT_FLAG, &f);
        } else if (g_strcmp0(status, "Playlist") == 0) {
            mpv_set_property(ud->mpv, "loop-file", MPV_FORMAT_FLAG, &f);
            mpv_set_property(ud->mpv, "loop-playlist", MPV_FORMAT_FLAG, &t);
        } else {
            mpv_set_property(ud->mpv, "loop-file", MPV_FORMAT_FLAG, &f);
            mpv_set_property(ud->mpv, "loop-playlist", MPV_FORMAT_FLAG, &f);
        }

    } else if (g_strcmp0(property_name, "Rate") == 0) {
        double rate = g_variant_get_double(value);
        mpv_set_property(ud->mpv, "speed", MPV_FORMAT_DOUBLE, &rate);

    } else if (g_strcmp0(property_name, "Shuffle") == 0) {
        int shuffle = g_variant_get_boolean(value);
        if (shuffle && !ud->shuffle) {
            const char *cmd[] = {"playlist-shuffle", NULL};
            mpv_command_async(ud->mpv, 0, cmd);
        } else if (!shuffle && ud->shuffle) {
            const char *cmd[] = {"playlist-unshuffle", NULL};
            mpv_command_async(ud->mpv, 0, cmd);
        }
        mpv_set_property(ud->mpv, "shuffle", MPV_FORMAT_FLAG, &shuffle);

    } else if (g_strcmp0(property_name, "Volume") == 0) {
        double volume = g_variant_get_double(value);
        volume *= 100;
        mpv_set_property(ud->mpv, "volume", MPV_FORMAT_DOUBLE, &volume);

    } else {
        g_set_error(error, G_DBUS_ERROR,
                    G_DBUS_ERROR_UNKNOWN_PROPERTY,
                    "Cannot set property %s", property_name);
        return FALSE;
    }

    return TRUE;
}

static GDBusInterfaceVTable vtable_player = {
    method_call_player, get_property_player, set_property_player, {0}
};

static gboolean emit_property_changes(gpointer data)
{
    UserData *ud = (UserData*)data;
    GError *error = NULL;
    gpointer prop_name, prop_value;
    GHashTableIter iter;

    if (g_hash_table_size(ud->changed_properties) > 0) {
        GVariant *params;
        GVariantBuilder *properties = g_variant_builder_new(G_VARIANT_TYPE("a{sv}"));
        GVariantBuilder *invalidated = g_variant_builder_new(G_VARIANT_TYPE("as"));
        g_hash_table_iter_init(&iter, ud->changed_properties);
        while (g_hash_table_iter_next(&iter, &prop_name, &prop_value)) {
            if (prop_value) {
                g_variant_builder_add(properties, "{sv}", prop_name, prop_value);
            } else {
                g_variant_builder_add(invalidated, "s", prop_name);
            }
        }
        params = g_variant_new("(sa{sv}as)",
                               "org.mpris.MediaPlayer2.Player", properties, invalidated);
        g_variant_builder_unref(properties);
        g_variant_builder_unref(invalidated);

        g_dbus_connection_emit_signal(ud->connection, NULL,
                                      "/org/mpris/MediaPlayer2",
                                      "org.freedesktop.DBus.Properties",
                                      "PropertiesChanged",
                                      params, &error);
        if (error != NULL) {
            g_printerr("%s", error->message);
            g_clear_error(&error);
        }

        g_hash_table_remove_all(ud->changed_properties);
    }
    return TRUE;
}

static void emit_seeked_signal(UserData *ud)
{
    GVariant *params;
    double position_s;
    int64_t position_us;
    GError *error = NULL;
    mpv_get_property(ud->mpv, "time-pos", MPV_FORMAT_DOUBLE, &position_s);
    position_us = position_s * 1000000.0; // s -> us
    params = g_variant_new("(x)", position_us);

    g_dbus_connection_emit_signal(ud->connection, NULL,
                                  "/org/mpris/MediaPlayer2",
                                  "org.mpris.MediaPlayer2.Player",
                                  "Seeked",
                                  params, &error);

    if (error != NULL) {
        g_printerr("%s", error->message);
        g_clear_error(&error);
    }
}

static gboolean can_go_next(UserData *ud)
{
    if (ud->playlist_pos < 0 || ud->playlist_count <= 0)
        return FALSE;
    if (ud->playlist_count == 1)
        return FALSE;
    if (g_strcmp0(ud->loop_status, LOOP_PLAYLIST) == 0)
        return TRUE;
    return ud->playlist_pos < ud->playlist_count - 1;
}

static gboolean can_go_previous(UserData *ud)
{
    if (ud->playlist_pos < 0 || ud->playlist_count <= 0)
        return FALSE;
    if (ud->playlist_count == 1)
        return FALSE;
    if (g_strcmp0(ud->loop_status, LOOP_PLAYLIST) == 0)
        return TRUE;
    return ud->playlist_pos > 0;
}

static GVariant * set_playback_status(UserData *ud)
{
    if (ud->idle) {
        ud->status = STATUS_STOPPED;
    } else if (ud->paused) {
        ud->status = STATUS_PAUSED;
    } else {
        ud->status = STATUS_PLAYING;
    }
    return g_variant_new_string(ud->status);
}

static void set_stopped_status(UserData *ud)
{
  const char *prop_name = "PlaybackStatus";
  GVariant *prop_value = g_variant_new_string(STATUS_STOPPED);

  ud->status = STATUS_STOPPED;

  g_hash_table_insert(ud->changed_properties,
                      (gpointer)prop_name, prop_value);

  emit_property_changes(ud);
}

// Register D-Bus object and interfaces, then set up mpv event handlers
static void on_bus_acquired(GDBusConnection *connection,
                            G_GNUC_UNUSED const char *name,
                            gpointer user_data)
{
    GError *error = NULL;
    UserData *ud = user_data;
    ud->connection = connection;

    if (ud->root_interface_id == 0) {
        ud->root_interface_id =
            g_dbus_connection_register_object(connection, "/org/mpris/MediaPlayer2",
                                              ud->root_interface_info,
                                              &vtable_root,
                                              user_data, NULL, &error);
        if (error != NULL) {
            g_printerr("Failed to register root interface: %s\n", error->message);
            g_clear_error(&error);
        }
    }

    if (ud->player_interface_id == 0) {
        ud->player_interface_id =
            g_dbus_connection_register_object(connection, "/org/mpris/MediaPlayer2",
                                              ud->player_interface_info,
                                              &vtable_player,
                                              user_data, NULL, &error);
        if (error != NULL) {
            g_printerr("Failed to register player interface: %s\n", error->message);
            g_clear_error(&error);
        }
    }

    if (!ud->events_setup) {
        setup_mpv_event_sources(ud);
        ud->events_setup = TRUE;
    }
}

static char *generate_random_id(void)
{
    static const char alpha[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
    const int len = 8;

    char *id = g_malloc(len + 1);
    for (int i = 0; i < len; i++) {
        id[i] = alpha[g_random_int_range(0, sizeof(alpha) - 1)];
    }
    id[len] = '\0';
    return id;
}

static char *build_bus_name(const char *client_name, gboolean unique_suffix)
{
    GString *name = g_string_new("org.mpris.MediaPlayer2.mpv");

    // don't append client name if it is the default value 'mpv'
    if (g_strcmp0(client_name, "mpv") != 0) {
        g_string_append_printf(name, ".%s", client_name);
    }

    if (unique_suffix) {
        char *id = generate_random_id();
        g_string_append_printf(name, ".instance-%s", id);
        g_free(id);
    }

    return g_string_free(name, FALSE);
}

static void on_name_lost(GDBusConnection *connection,
                         G_GNUC_UNUSED const char *_name,
                         gpointer user_data)
{
    UserData *ud = user_data;

    if (connection) {
        char *name = build_bus_name(ud->client_name, TRUE);
        ud->bus_id = g_bus_own_name(G_BUS_TYPE_SESSION,
                                    name,
                                    G_BUS_NAME_OWNER_FLAGS_NONE,
                                    NULL, NULL, NULL,
                                    &ud, NULL);
        g_free(name);
    } else {
      ud->root_interface_id = 0;
      ud->player_interface_id = 0;
    }
}

static void handle_property_change(const char *name, void *data, UserData *ud)
{
    const char *prop_name = NULL;
    GVariant *prop_value = NULL;
    gboolean update_can_go_next_prev = FALSE;

    if (g_strcmp0(name, "pause") == 0) {
        ud->paused = *(int*)data;
        prop_name = "PlaybackStatus";
        prop_value = set_playback_status(ud);

    } else if (g_strcmp0(name, "idle-active") == 0) {
        ud->idle = *(int*)data;
        prop_name = "PlaybackStatus";
        prop_value = set_playback_status(ud);

    } else if (g_strcmp0(name, "media-title") == 0 ||
               g_strcmp0(name, "duration") == 0) {
        // Free existing metadata object
        if (ud->metadata) {
            g_variant_unref(ud->metadata);
        }
        ud->metadata = create_metadata(ud);
        prop_name = "Metadata";
        prop_value = ud->metadata;

    } else if (g_strcmp0(name, "speed") == 0) {
        double *rate = data;
        prop_name = "Rate";
        prop_value = g_variant_new_double(*rate);

    } else if (g_strcmp0(name, "volume") == 0) {
        double *volume = data;
        *volume /= 100;
        prop_name = "Volume";
        prop_value = g_variant_new_double(*volume);

    } else if (g_strcmp0(name, "loop-file") == 0) {
        char *status = *(char **)data;
        if (g_strcmp0(status, "no") != 0) {
            ud->loop_status = LOOP_TRACK;
        } else {
            char *playlist_status;
            mpv_get_property(ud->mpv, "loop-playlist", MPV_FORMAT_STRING, &playlist_status);
            if (g_strcmp0(playlist_status, "no") != 0) {
                ud->loop_status = LOOP_PLAYLIST;
            } else {
                ud->loop_status = LOOP_NONE;
            }
            mpv_free(playlist_status);
        }
        prop_name = "LoopStatus";
        prop_value = g_variant_new_string(ud->loop_status);
        update_can_go_next_prev = TRUE;

    } else if (g_strcmp0(name, "loop-playlist") == 0) {
        char *status = *(char **)data;
        if (g_strcmp0(status, "no") != 0) {
            ud->loop_status = LOOP_PLAYLIST;
        } else {
            char *file_status;
            mpv_get_property(ud->mpv, "loop-file", MPV_FORMAT_STRING, &file_status);
            if (g_strcmp0(file_status, "no") != 0) {
                ud->loop_status = LOOP_TRACK;
            } else {
                ud->loop_status = LOOP_NONE;
            }
            mpv_free(file_status);
        }
        prop_name = "LoopStatus";
        prop_value = g_variant_new_string(ud->loop_status);
        update_can_go_next_prev = TRUE;

    } else if (g_strcmp0(name, "shuffle") == 0) {
        int shuffle = *(int*)data;
        ud->shuffle = shuffle;
        prop_name = "Shuffle";
        prop_value = g_variant_new_boolean(shuffle);

    } else if (g_strcmp0(name, "fullscreen") == 0) {
        gboolean *status = data;
        prop_name = "Fullscreen";
        prop_value = g_variant_new_boolean(*status);

    } else if (g_strcmp0(name, "playlist-count") == 0) {
      ud->playlist_count = *(int64_t *)data;
      update_can_go_next_prev = TRUE;

    } else if (g_strcmp0(name, "playlist-pos") == 0) {
      ud->playlist_pos = *(int64_t *)data;
      update_can_go_next_prev = TRUE;
    }

    if (prop_name) {
        if (prop_value) {
            g_variant_ref(prop_value);
        }
        g_hash_table_insert(ud->changed_properties,
                            (gpointer)prop_name, prop_value);
    }

    if (update_can_go_next_prev) {
        g_hash_table_insert(ud->changed_properties, "CanGoNext",
                            g_variant_new_boolean(can_go_next(ud)));
        g_hash_table_insert(ud->changed_properties, "CanGoPrevious",
                            g_variant_new_boolean(can_go_previous(ud)));
    }
}

static gboolean event_handler(int fd, G_GNUC_UNUSED GIOCondition condition, gpointer data)
{
    UserData *ud = data;
    gboolean has_event = TRUE;

    // Discard data in pipe
    char unused[16];
    while (read(fd, unused, sizeof(unused)) > 0);

    while (has_event) {
        mpv_event *event = mpv_wait_event(ud->mpv, 0);
        switch (event->event_id) {
        case MPV_EVENT_NONE:
            has_event = FALSE;
            break;
        case MPV_EVENT_SHUTDOWN:
            set_stopped_status(ud);
            g_main_loop_quit(ud->loop);
            break;
        case MPV_EVENT_PROPERTY_CHANGE: {
            mpv_event_property *prop_event = (mpv_event_property*)event->data;
            handle_property_change(prop_event->name, prop_event->data, ud);
        } break;
        case MPV_EVENT_SEEK:
            ud->seek_expected = TRUE;
            break;
        case MPV_EVENT_PLAYBACK_RESTART: {
            if (ud->seek_expected) {
                emit_seeked_signal(ud);
                ud->seek_expected = FALSE;
            }
         } break;
        default:
            break;
        }
    }

    return TRUE;
}

static void wakeup_handler(void *fd)
{
    (void)!write(*((int*)fd), "0", 1);
}

static void setup_mpv_event_sources(UserData *ud)
{
    GError *error = NULL;
    GSource *mpv_pipe_source;
    GSource *timeout_source;

    g_unix_open_pipe(ud->wakeup_pipe, 0, &error);
    if (error != NULL) {
        g_printerr("%s", error->message);
        g_clear_error(&error);
    }
    fcntl(ud->wakeup_pipe[0], F_SETFL, O_NONBLOCK);
    mpv_set_wakeup_callback(ud->mpv, wakeup_handler, &ud->wakeup_pipe[1]);

    mpv_pipe_source = g_unix_fd_source_new(ud->wakeup_pipe[0], G_IO_IN);
    g_source_set_callback(mpv_pipe_source,
                          G_SOURCE_FUNC(event_handler),
                          ud,
                          NULL);
    g_source_attach(mpv_pipe_source, ud->ctx);
    g_source_unref(mpv_pipe_source);

    timeout_source = g_timeout_source_new(100);
    g_source_set_callback(timeout_source,
                          G_SOURCE_FUNC(emit_property_changes),
                          ud,
                          NULL);
    g_source_attach(timeout_source, ud->ctx);
    g_source_unref(timeout_source);
}

// Plugin entry point
int mpv_open_cplugin(mpv_handle *mpv)
{
    GMainContext *ctx;
    GMainLoop *loop;
    UserData ud = {0};
    GError *error = NULL;
    GDBusNodeInfo *introspection_data = NULL;

    ctx = g_main_context_new();
    loop = g_main_loop_new(ctx, FALSE);

    // Load introspection data and split into separate interfaces
    introspection_data = g_dbus_node_info_new_for_xml(introspection_xml, &error);
    if (error != NULL) {
        g_printerr("%s", error->message);
        g_clear_error(&error);
    }
    ud.root_interface_info =
        g_dbus_node_info_lookup_interface(introspection_data, "org.mpris.MediaPlayer2");
    ud.player_interface_info =
        g_dbus_node_info_lookup_interface(introspection_data, "org.mpris.MediaPlayer2.Player");

    ud.mpv = mpv;
    ud.loop = loop;
    ud.ctx = ctx;
    ud.status = STATUS_STOPPED;
    ud.loop_status = LOOP_NONE;
    ud.changed_properties = g_hash_table_new(g_str_hash, g_str_equal);
    ud.seek_expected = FALSE;
    ud.idle = FALSE;
    ud.paused = FALSE;
    ud.shuffle = FALSE;
    ud.client_name = mpv_get_property_string(mpv, "audio-client-name");
    mpv_get_property(mpv, "playlist-count", MPV_FORMAT_INT64, &ud.playlist_count);
    mpv_get_property(mpv, "playlist-pos", MPV_FORMAT_INT64, &ud.playlist_pos);

    char *bus_name = build_bus_name(ud.client_name, FALSE);
    g_main_context_push_thread_default(ctx);
    ud.bus_id = g_bus_own_name(G_BUS_TYPE_SESSION,
                               bus_name,
                               G_BUS_NAME_OWNER_FLAGS_DO_NOT_QUEUE,
                               on_bus_acquired,
                               NULL,
                               on_name_lost,
                               &ud, NULL);
    g_main_context_pop_thread_default(ctx);
    g_free(bus_name);

    // Receive event for property changes
    mpv_observe_property(mpv, 0, "pause", MPV_FORMAT_FLAG);
    mpv_observe_property(mpv, 0, "idle-active", MPV_FORMAT_FLAG);
    mpv_observe_property(mpv, 0, "media-title", MPV_FORMAT_STRING);
    mpv_observe_property(mpv, 0, "speed", MPV_FORMAT_DOUBLE);
    mpv_observe_property(mpv, 0, "volume", MPV_FORMAT_DOUBLE);
    mpv_observe_property(mpv, 0, "loop-file", MPV_FORMAT_STRING);
    mpv_observe_property(mpv, 0, "loop-playlist", MPV_FORMAT_STRING);
    mpv_observe_property(mpv, 0, "duration", MPV_FORMAT_INT64);
    mpv_observe_property(mpv, 0, "shuffle", MPV_FORMAT_FLAG);
    mpv_observe_property(mpv, 0, "fullscreen", MPV_FORMAT_FLAG);
    mpv_observe_property(mpv, 0, "playlist-count", MPV_FORMAT_INT64);
    mpv_observe_property(mpv, 0, "playlist-pos", MPV_FORMAT_INT64);

    g_main_loop_run(loop);

    g_dbus_connection_unregister_object(ud.connection, ud.root_interface_id);
    g_dbus_connection_unregister_object(ud.connection, ud.player_interface_id);

    g_bus_unown_name(ud.bus_id);
    g_main_loop_unref(loop);
    g_main_context_unref(ctx);
    g_dbus_node_info_unref(introspection_data);
    mpv_free(ud.client_name);

    return 0;
}
