/* SPDX-License-Identifier: LGPL-2.1-or-later */

#include "bus-util.h"
#include "device-util.h"
#include "hash-funcs.h"
#include "logind-brightness.h"
#include "logind.h"
#include "process-util.h"
#include "stdio-util.h"

/* Brightness and LED devices tend to be very slow to write to (often being I2C and such). Writes to the
 * sysfs attributes are synchronous, and hence will freeze our process on access. We can't really have that,
 * hence we add some complexity: whenever we need to write to the brightness attribute, we do so in a forked
 * off process, which terminates when it is done. Watching that process allows us to watch completion of the
 * write operation.
 *
 * To make this even more complex: clients are likely to send us many write requests in a short time-frame
 * (because they implement reactive brightness sliders on screen). Let's coalesce writes to make this
 * efficient: whenever we get requests to change brightness while we are still writing to the brightness
 * attribute, let's remember the request and restart a new one when the initial operation finished. When we
 * get another request while one is ongoing and one is pending we'll replace the pending one with the new
 * one.
 *
 * The bus messages are answered when the first write operation finishes that started either due to the
 * request or due to a later request that overrode the requested one.
 *
 * Yes, this is complex, but I don't see an easier way if we want to be both efficient and still support
 * completion notification. */

typedef struct BrightnessWriter {
        Manager *manager;

        sd_device *device;
        char *path;

        pid_t child;

        uint32_t brightness;
        bool again;

        Set *current_messages;
        Set *pending_messages;

        sd_event_source* child_event_source;
} BrightnessWriter;

static BrightnessWriter* brightness_writer_free(BrightnessWriter *w) {
        if (!w)
                return NULL;

        if (w->manager && w->path)
                (void) hashmap_remove_value(w->manager->brightness_writers, w->path, w);

        sd_device_unref(w->device);
        free(w->path);

        set_free(w->current_messages);
        set_free(w->pending_messages);

        w->child_event_source = sd_event_source_unref(w->child_event_source);

        return mfree(w);
}

DEFINE_TRIVIAL_CLEANUP_FUNC(BrightnessWriter*, brightness_writer_free);

DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(
                brightness_writer_hash_ops,
                char,
                string_hash_func,
                string_compare_func,
                BrightnessWriter,
                brightness_writer_free);

static void brightness_writer_reply(BrightnessWriter *w, int error) {
        int r;

        assert(w);

        for (;;) {
                _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;

                m = set_steal_first(w->current_messages);
                if (!m)
                        break;

                if (error == 0)
                        r = sd_bus_reply_method_return(m, NULL);
                else
                        r = sd_bus_reply_method_errnof(m, error, "Failed to write to brightness device: %m");
                if (r < 0)
                        log_warning_errno(r, "Failed to send method reply, ignoring: %m");
        }
}

static int brightness_writer_fork(BrightnessWriter *w);

static int on_brightness_writer_exit(sd_event_source *s, const siginfo_t *si, void *userdata) {
        BrightnessWriter *w = ASSERT_PTR(userdata);
        int r;

        assert(s);
        assert(si);

        assert(si->si_pid == w->child);
        w->child = 0;
        w->child_event_source = sd_event_source_unref(w->child_event_source);

        brightness_writer_reply(w,
                                si->si_code == CLD_EXITED &&
                                si->si_status == EXIT_SUCCESS ? 0 : -EPROTO);

        if (w->again) {
                /* Another request to change the brightness has been queued. Act on it, but make the pending
                 * messages the current ones. */
                w->again = false;
                set_free(w->current_messages);
                w->current_messages = TAKE_PTR(w->pending_messages);

                r = brightness_writer_fork(w);
                if (r >= 0)
                        return 0;

                brightness_writer_reply(w, r);
        }

        brightness_writer_free(w);
        return 0;
}

static int brightness_writer_fork(BrightnessWriter *w) {
        int r;

        assert(w);
        assert(w->manager);
        assert(w->child == 0);
        assert(!w->child_event_source);

        r = safe_fork("(sd-bright)", FORK_DEATHSIG|FORK_REARRANGE_STDIO|FORK_CLOSE_ALL_FDS|FORK_LOG|FORK_REOPEN_LOG, &w->child);
        if (r < 0)
                return r;
        if (r == 0) {
                char brs[DECIMAL_STR_MAX(uint32_t)+1];

                /* Child */
                xsprintf(brs, "%" PRIu32, w->brightness);

                r = sd_device_set_sysattr_value(w->device, "brightness", brs);
                if (r < 0) {
                        log_device_error_errno(w->device, r, "Failed to write brightness to device: %m");
                        _exit(EXIT_FAILURE);
                }

                _exit(EXIT_SUCCESS);
        }

        r = sd_event_add_child(w->manager->event, &w->child_event_source, w->child, WEXITED, on_brightness_writer_exit, w);
        if (r < 0)
                return log_error_errno(r, "Failed to watch brightness writer child " PID_FMT ": %m", w->child);

        return 0;
}

static int set_add_message(Set **set, sd_bus_message *message) {
        int r;

        assert(set);

        if (!message)
                return 0;

        r = sd_bus_message_get_expect_reply(message);
        if (r <= 0)
                return r;

        r = set_ensure_put(set, &bus_message_hash_ops, message);
        if (r <= 0)
                return r;
        sd_bus_message_ref(message);

        return 1;
}

int manager_write_brightness(
                Manager *m,
                sd_device *device,
                uint32_t brightness,
                sd_bus_message *message) {

        _cleanup_(brightness_writer_freep) BrightnessWriter *w = NULL;
        BrightnessWriter *existing;
        const char *path;
        int r;

        assert(m);
        assert(device);

        r = sd_device_get_syspath(device, &path);
        if (r < 0)
                return log_device_error_errno(device, r, "Failed to get sysfs path for brightness device: %m");

        existing = hashmap_get(m->brightness_writers, path);
        if (existing) {
                /* There's already a writer for this device. Let's update it with the new brightness, and add
                 * our message to the set of message to reply when done. */

                r = set_add_message(&existing->pending_messages, message);
                if (r < 0)
                        return log_error_errno(r, "Failed to add message to set: %m");

                /* We override any previously requested brightness here: we coalesce writes, and the newest
                 * requested brightness is the one we'll put into effect. */
                existing->brightness = brightness;
                existing->again = true; /* request another iteration of the writer when the current one is
                                         * complete */
                return 0;
        }

        w = new(BrightnessWriter, 1);
        if (!w)
                return log_oom();

        *w = (BrightnessWriter) {
                .device = sd_device_ref(device),
                .path = strdup(path),
                .brightness = brightness,
        };

        if (!w->path)
                return log_oom();

        r = hashmap_ensure_put(&m->brightness_writers, &brightness_writer_hash_ops, w->path, w);
        if (r == -ENOMEM)
                return log_oom();
        if (r < 0)
                return log_error_errno(r, "Failed to add brightness writer to hashmap: %m");

        w->manager = m;

        r = set_add_message(&w->current_messages, message);
        if (r < 0)
                return log_error_errno(r, "Failed to add message to set: %m");

        r = brightness_writer_fork(w);
        if (r < 0)
                return r;

        TAKE_PTR(w);
        return 0;
}
