/***
  This file is part of systemd.

  Copyright 2013 Lennart Poettering

  systemd is free software; you can redistribute it and/or modify it
  under the terms of the GNU Lesser General Public License as published by
  the Free Software Foundation; either version 2.1 of the License, or
  (at your option) any later version.

  systemd is distributed in the hope that it will be useful, but
  WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  Lesser General Public License for more details.

  You should have received a copy of the GNU Lesser General Public License
  along with systemd; If not, see <http://www.gnu.org/licenses/>.
***/

#include "sd-bus.h"

#include "alloc-util.h"
#include "bus-internal.h"
#include "bus-track.h"
#include "bus-util.h"

struct sd_bus_track {
        unsigned n_ref;
        sd_bus *bus;
        sd_bus_track_handler_t handler;
        void *userdata;
        Hashmap *names;
        LIST_FIELDS(sd_bus_track, queue);
        Iterator iterator;
        bool in_queue;
        bool modified;
};

#define MATCH_PREFIX                                        \
        "type='signal',"                                    \
        "sender='org.freedesktop.DBus',"                    \
        "path='/org/freedesktop/DBus',"                     \
        "interface='org.freedesktop.DBus',"                 \
        "member='NameOwnerChanged',"                        \
        "arg0='"

#define MATCH_SUFFIX \
        "'"

#define MATCH_FOR_NAME(name)                                            \
        ({                                                              \
                char *_x;                                               \
                size_t _l = strlen(name);                               \
                _x = alloca(strlen(MATCH_PREFIX)+_l+strlen(MATCH_SUFFIX)+1); \
                strcpy(stpcpy(stpcpy(_x, MATCH_PREFIX), name), MATCH_SUFFIX); \
                _x;                                                     \
        })

static void bus_track_add_to_queue(sd_bus_track *track) {
        assert(track);

        if (track->in_queue)
                return;

        if (!track->handler)
                return;

        LIST_PREPEND(queue, track->bus->track_queue, track);
        track->in_queue = true;
}

static void bus_track_remove_from_queue(sd_bus_track *track) {
        assert(track);

        if (!track->in_queue)
                return;

        LIST_REMOVE(queue, track->bus->track_queue, track);
        track->in_queue = false;
}

_public_ int sd_bus_track_new(
                sd_bus *bus,
                sd_bus_track **track,
                sd_bus_track_handler_t handler,
                void *userdata) {

        sd_bus_track *t;

        assert_return(bus, -EINVAL);
        assert_return(track, -EINVAL);

        if (!bus->bus_client)
                return -EINVAL;

        t = new0(sd_bus_track, 1);
        if (!t)
                return -ENOMEM;

        t->n_ref = 1;
        t->handler = handler;
        t->userdata = userdata;
        t->bus = sd_bus_ref(bus);

        bus_track_add_to_queue(t);

        *track = t;
        return 0;
}

_public_ sd_bus_track* sd_bus_track_ref(sd_bus_track *track) {

        if (!track)
                return NULL;

        assert(track->n_ref > 0);

        track->n_ref++;

        return track;
}

_public_ sd_bus_track* sd_bus_track_unref(sd_bus_track *track) {
        const char *n;

        if (!track)
                return NULL;

        assert(track->n_ref > 0);

        if (track->n_ref > 1) {
                track->n_ref--;
                return NULL;
        }

        while ((n = hashmap_first_key(track->names)))
                sd_bus_track_remove_name(track, n);

        bus_track_remove_from_queue(track);
        hashmap_free(track->names);
        sd_bus_unref(track->bus);
        free(track);

        return NULL;
}

static int on_name_owner_changed(sd_bus_message *message, void *userdata, sd_bus_error *error) {
        sd_bus_track *track = userdata;
        const char *name, *old, *new;
        int r;

        assert(message);
        assert(track);

        r = sd_bus_message_read(message, "sss", &name, &old, &new);
        if (r < 0)
                return 0;

        sd_bus_track_remove_name(track, name);
        return 0;
}

_public_ int sd_bus_track_add_name(sd_bus_track *track, const char *name) {
        _cleanup_(sd_bus_slot_unrefp) sd_bus_slot *slot = NULL;
        _cleanup_free_ char *n = NULL;
        const char *match;
        int r;

        assert_return(track, -EINVAL);
        assert_return(service_name_is_valid(name), -EINVAL);

        r = hashmap_ensure_allocated(&track->names, &string_hash_ops);
        if (r < 0)
                return r;

        n = strdup(name);
        if (!n)
                return -ENOMEM;

        /* First, subscribe to this name */
        match = MATCH_FOR_NAME(n);
        r = sd_bus_add_match(track->bus, &slot, match, on_name_owner_changed, track);
        if (r < 0)
                return r;

        r = hashmap_put(track->names, n, slot);
        if (r == -EEXIST)
                return 0;
        if (r < 0)
                return r;

        /* Second, check if it is currently existing, or maybe
         * doesn't, or maybe disappeared already. */
        r = sd_bus_get_name_creds(track->bus, n, 0, NULL);
        if (r < 0) {
                hashmap_remove(track->names, n);
                return r;
        }

        n = NULL;
        slot = NULL;

        bus_track_remove_from_queue(track);
        track->modified = true;

        return 1;
}

_public_ int sd_bus_track_remove_name(sd_bus_track *track, const char *name) {
        _cleanup_(sd_bus_slot_unrefp) sd_bus_slot *slot = NULL;
        _cleanup_free_ char *n = NULL;

        assert_return(name, -EINVAL);

        if (!track)
                return 0;

        slot = hashmap_remove2(track->names, (char*) name, (void**) &n);
        if (!slot)
                return 0;

        if (hashmap_isempty(track->names))
                bus_track_add_to_queue(track);

        track->modified = true;

        return 1;
}

_public_ unsigned sd_bus_track_count(sd_bus_track *track) {
        if (!track)
                return 0;

        return hashmap_size(track->names);
}

_public_ const char* sd_bus_track_contains(sd_bus_track *track, const char *name) {
        assert_return(track, NULL);
        assert_return(name, NULL);

        return hashmap_get(track->names, (void*) name) ? name : NULL;
}

_public_ const char* sd_bus_track_first(sd_bus_track *track) {
        const char *n = NULL;

        if (!track)
                return NULL;

        track->modified = false;
        track->iterator = ITERATOR_FIRST;

        hashmap_iterate(track->names, &track->iterator, NULL, (const void**) &n);
        return n;
}

_public_ const char* sd_bus_track_next(sd_bus_track *track) {
        const char *n = NULL;

        if (!track)
                return NULL;

        if (track->modified)
                return NULL;

        hashmap_iterate(track->names, &track->iterator, NULL, (const void**) &n);
        return n;
}

_public_ int sd_bus_track_add_sender(sd_bus_track *track, sd_bus_message *m) {
        const char *sender;

        assert_return(track, -EINVAL);
        assert_return(m, -EINVAL);

        sender = sd_bus_message_get_sender(m);
        if (!sender)
                return -EINVAL;

        return sd_bus_track_add_name(track, sender);
}

_public_ int sd_bus_track_remove_sender(sd_bus_track *track, sd_bus_message *m) {
        const char *sender;

        assert_return(track, -EINVAL);
        assert_return(m, -EINVAL);

        sender = sd_bus_message_get_sender(m);
        if (!sender)
                return -EINVAL;

        return sd_bus_track_remove_name(track, sender);
}

_public_ sd_bus* sd_bus_track_get_bus(sd_bus_track *track) {
        assert_return(track, NULL);

        return track->bus;
}

void bus_track_dispatch(sd_bus_track *track) {
        int r;

        assert(track);
        assert(track->in_queue);
        assert(track->handler);

        bus_track_remove_from_queue(track);

        sd_bus_track_ref(track);

        r = track->handler(track, track->userdata);
        if (r < 0)
                log_debug_errno(r, "Failed to process track handler: %m");
        else if (r == 0)
                bus_track_add_to_queue(track);

        sd_bus_track_unref(track);
}

_public_ void *sd_bus_track_get_userdata(sd_bus_track *track) {
        assert_return(track, NULL);

        return track->userdata;
}

_public_ void *sd_bus_track_set_userdata(sd_bus_track *track, void *userdata) {
        void *ret;

        assert_return(track, NULL);

        ret = track->userdata;
        track->userdata = userdata;

        return ret;
}
