/*
 SPDX-License-Identifier: GPL-3.0-or-later
 myMPD (c) 2018-2026 Juergen Mang <mail@jcgames.de>
 https://github.com/jcorporation/mympd
*/

/*! \file
 * \brief MPD sticker functions
 */

#include "compile_time.h"
#include "src/mympd_client/stickerdb.h"

#include "dist/rax/rax.h"
#include "src/lib/config/stickerdb_state.h"
#include "src/lib/convert.h"
#include "src/lib/json/json_rpc.h"
#include "src/lib/log.h"
#include "src/lib/sds/sds_extras.h"
#include "src/lib/sticker.h"
#include "src/lib/utility.h"
#include "src/mympd_api/requests.h"

#include <inttypes.h>
#include <limits.h>
#include <poll.h>
#include <stdlib.h>
#include <string.h>

// Private definitions

static bool get_sticker_types(struct t_stickerdb_state *stickerdb);
static bool sticker_search_add_value_constraint(struct t_stickerdb_state *stickerdb, enum mpd_sticker_operator op, const char *value);
static bool sticker_search_add_sort(struct t_stickerdb_state *stickerdb, enum mpd_sticker_sort sort, bool desc);
static bool sticker_search_add_window(struct t_stickerdb_state *stickerdb, unsigned start, unsigned end);

static struct t_sticker *get_sticker_all(struct t_stickerdb_state *stickerdb, enum mympd_sticker_type type, const char *uri, struct t_sticker *sticker, bool user_defined);
static sds get_sticker_value(struct t_stickerdb_state *stickerdb, enum mympd_sticker_type type, const char *uri, const char *name);
static int64_t get_sticker_int64(struct t_stickerdb_state *stickerdb, enum mympd_sticker_type type, const char *uri, const char *name);
static bool set_sticker_value(struct t_stickerdb_state *stickerdb, enum mympd_sticker_type type, const char *uri, const char *name, const char *value);
static bool set_sticker_int64(struct t_stickerdb_state *stickerdb, enum mympd_sticker_type type, const char *uri, const char *name, int64_t value);
static bool dec_sticker(struct t_stickerdb_state *stickerdb, enum mympd_sticker_type type, const char *uri, const char *name, unsigned value);
static bool inc_sticker(struct t_stickerdb_state *stickerdb, enum mympd_sticker_type type, const char *uri, const char *name, unsigned value);
static bool remove_sticker(struct t_stickerdb_state *stickerdb, enum mympd_sticker_type type, const char *uri, const char *name);
static bool stickerdb_connect_mpd(struct t_stickerdb_state *stickerdb);
static bool check_sticker_support(struct t_stickerdb_state *stickerdb);

// Public functions

/**
 * This function connects to mpd sticker instance on demand
 * or exits the idle mode
 * @param stickerdb pointer to the stickerdb state
 * @return true on success, else false
 */
bool stickerdb_connect(struct t_stickerdb_state *stickerdb) {
    if (stickerdb->config->stickers == false) {
        MYMPD_LOG_WARN("stickerdb", "Stickers are disabled by config");
        return false;
    }
    if (stickerdb->conn_state == MPD_FAILURE) {
        MYMPD_LOG_DEBUG("stickerdb", "Disconnecting from MPD");
        stickerdb_disconnect(stickerdb);
    }
    if (stickerdb->conn_state == MPD_CONNECTED) {
        // already connected
        MYMPD_LOG_DEBUG("stickerdb", "Connected, leaving idle mode");
        if (stickerdb_exit_idle(stickerdb) == true) {
            return true;
        }
        // stickerdb connection broken
        stickerdb_disconnect(stickerdb);
    }
    // try to connect
    MYMPD_LOG_INFO(stickerdb->name, "Creating mpd connection for %s", stickerdb->name);
    if (stickerdb_connect_mpd(stickerdb) == false) {
        MYMPD_LOG_DEBUG("stickerdb", "Connecting to MPD");
        return false;
    }
    // check version
    if (mpd_connection_cmp_server_version(stickerdb->conn, MPD_VERSION_MIN_MAJOR, MPD_VERSION_MIN_MINOR, MPD_VERSION_MIN_PATCH) < 0) {
        MYMPD_LOG_DEBUG("stickerdb", "Checking version");
        MYMPD_LOG_EMERG(stickerdb->name, MPD_TOO_OLD_MSG);
        send_jsonrpc_notify(JSONRPC_FACILITY_MPD, JSONRPC_SEVERITY_CRIT, MPD_PARTITION_ALL, "MPD version is too old");
        stickerdb_disconnect(stickerdb);
        mympd_api_request_sticker_features(false, false);
        return false;
    }
    // check for sticker support
    stickerdb->mpd_state->feat.stickers = check_sticker_support(stickerdb);
    if (stickerdb->mpd_state->feat.stickers == false) {
        MYMPD_LOG_WARN("stickerdb", "MPD does not support stickers");
        stickerdb_disconnect(stickerdb);
        send_jsonrpc_notify(JSONRPC_FACILITY_MPD, JSONRPC_SEVERITY_CRIT, MPD_PARTITION_ALL, "MPD does not support stickers");
        mympd_api_request_sticker_features(false, false);
        return false;
    }
    if (mpd_connection_cmp_server_version(stickerdb->conn, 0, 24, 0) >= 0) {
        MYMPD_LOG_INFO(stickerdb->name, "Enabling advanced sticker commands");
        stickerdb->mpd_state->feat.advsticker = true;
    }
    else {
        MYMPD_LOG_INFO(stickerdb->name, "Disabling advanced sticker commands");
        stickerdb->mpd_state->feat.advsticker = false;
    }
    get_sticker_types(stickerdb);
    mympd_api_request_sticker_features(stickerdb->mpd_state->feat.stickers,
        stickerdb->mpd_state->feat.advsticker);
    *stickerdb->repopulate_pfds = true;
    MYMPD_LOG_DEBUG("stickerdb", "MPD connected and waiting for commands");
    return true;
}

/**
 * Disconnects from MPD
 * @param stickerdb pointer to stickerdb state
 */
void stickerdb_disconnect(struct t_stickerdb_state *stickerdb) {
    if (stickerdb->conn != NULL) {
        MYMPD_LOG_INFO(stickerdb->name, "Disconnecting from mpd");
        mpd_connection_free(stickerdb->conn);
    }
    stickerdb->conn = NULL;
    stickerdb->conn_state = MPD_DISCONNECTED;
    *stickerdb->repopulate_pfds = true;
}

/**
 * Discards waiting idle events for the stickerdb connection.
 * This prevents the connection to timeout.
 * @param stickerdb pointer to the stickerdb state
 * @return true on success, else false
 */
bool stickerdb_idle(struct t_stickerdb_state *stickerdb) {
    MYMPD_LOG_DEBUG("stickerdb", "Discarding idle events");
    mympd_api_request_trigger_event_emit(TRIGGER_MPD_STICKER, MPD_PARTITION_DEFAULT, NULL, 0);
    return stickerdb_exit_idle(stickerdb) &&
        stickerdb_enter_idle(stickerdb);
}

/**
 * Enters the idle mode
 * @param stickerdb pointer to the stickerdb state
 * @return true on success, else false
 */
bool stickerdb_enter_idle(struct t_stickerdb_state *stickerdb) {
    MYMPD_LOG_DEBUG("stickerdb", "Entering idle mode");
    // the idle events are discarded in the mympd api loop
    if (mpd_send_idle_mask(stickerdb->conn, MPD_IDLE_STICKER) == false) {
        MYMPD_LOG_ERROR("stickerdb", "Error entering idle mode");
        stickerdb_disconnect(stickerdb);
        return false;
    }
    return true;
}

/**
 * Exits the idle mode, ignoring all idle events
 * @param stickerdb pointer to the stickerdb state
 * @return true on success, else false
 */
bool stickerdb_exit_idle(struct t_stickerdb_state *stickerdb) {
    MYMPD_LOG_DEBUG("stickerdb", "Exiting idle mode");
    if (mpd_send_noidle(stickerdb->conn) == false) {
        MYMPD_LOG_ERROR("stickerdb", "Error exiting idle mode");
    }
    return stickerdb_check_error_and_recover(stickerdb, "mpd_send_noidle");
}

/**
 * Calls mpd_response_finish and checks for an mpd error and tries to recover.
 * @param stickerdb pointer to the stickerdb state
 * @param command command to check for the error
 * @return true on success, else false
 */
bool stickerdb_check_error_and_recover(struct t_stickerdb_state *stickerdb, const char *command) {
    if (stickerdb->conn == NULL) {
        stickerdb->conn_state = MPD_FAILURE;
        return false;
    }
    if (mpd_response_finish(stickerdb->conn) == true) {
        return true;
    }
    enum mpd_error error = mpd_connection_get_error(stickerdb->conn);
    const char *error_msg = mpd_connection_get_error_message(stickerdb->conn);
    if (error == MPD_ERROR_SERVER) {
        enum mpd_server_error server_error = mpd_connection_get_server_error(stickerdb->conn);
        MYMPD_LOG_ERROR(stickerdb->name, "MPD error for command %s: %s (%d, %d)", command, error_msg , error, server_error);
    }
    else {
        MYMPD_LOG_ERROR(stickerdb->name, "MPD error for command %s: %s (%d)", command, error_msg , error);
    }
    //try to recover from error
    if (mpd_connection_clear_error(stickerdb->conn) == false ||
        mpd_response_finish(stickerdb->conn) == false)
    {
        MYMPD_LOG_ERROR(stickerdb->name, "Unrecoverable MPD error");
        stickerdb->conn_state = MPD_FAILURE;
    }
    else {
        MYMPD_LOG_WARN(stickerdb->name, "Recovered from MPD error");
    }
    return false;
}

/**
 * Gets a sticker.
 * You must manage the idle state manually.
 * @param stickerdb pointer to the stickerdb state
 * @param type MPD sticker type
 * @param uri sticker uri
 * @param name sticker name
 * @return pointer to sticker value
 */
sds stickerdb_get_batch(struct t_stickerdb_state *stickerdb, enum mympd_sticker_type type, const char *uri, const char *name) {
    if (is_streamuri(uri) == true) {
        return sdsempty();
    }
    sds value = get_sticker_value(stickerdb, type, uri, name);
    return value;
}

/**
 * Gets a sticker.
 * @param stickerdb pointer to the stickerdb state
 * @param type MPD sticker type
 * @param uri sticker uri
 * @param name sticker name
 * @return pointer to sticker value
 */
sds stickerdb_get(struct t_stickerdb_state *stickerdb, enum mympd_sticker_type type, const char *uri, const char *name) {
    if (is_streamuri(uri) == true) {
        return sdsempty();
    }
    if (stickerdb_connect(stickerdb) == false) {
        return sdsempty();
    }
    sds value = get_sticker_value(stickerdb, type, uri, name);
    stickerdb_enter_idle(stickerdb);
    return value;
}

/**
 * Gets an int64_t value sticker.
 * You must manage the idle state manually.
 * @param stickerdb pointer to the stickerdb state
 * @param type MPD sticker type
 * @param uri sticker uri
 * @param name sticker name
 * @return sticker value or 0 on error
 */
int64_t stickerdb_get_int64_batch(struct t_stickerdb_state *stickerdb, enum mympd_sticker_type type, const char *uri, const char *name) {
    int64_t value = 0;
    if (is_streamuri(uri) == true) {
        return value;
    }
    value = get_sticker_int64(stickerdb, type, uri, name);
    return value;
}

/**
 * Gets an int64_t value sticker.
 * @param stickerdb pointer to the stickerdb state
 * @param type MPD sticker type
 * @param uri sticker uri
 * @param name sticker name
 * @return sticker value or 0 on error
 */
int64_t stickerdb_get_int64(struct t_stickerdb_state *stickerdb, enum mympd_sticker_type type, const char *uri, const char *name) {
    int64_t value = 0;
    if (is_streamuri(uri) == true) {
        return value;
    }
    if (stickerdb_connect(stickerdb) == false) {
        return false;
    }
    value = get_sticker_int64(stickerdb, type, uri, name);
    stickerdb_enter_idle(stickerdb);
    return value;
}

/**
 * Gets all sticker names by type
 * @param stickerdb pointer to the stickerdb state
 * @param type MPD sticker type
 * @param sticker_names List to populate
 * @return true on success, else false
 */
bool stickerdb_get_names(struct t_stickerdb_state *stickerdb, enum mympd_sticker_type type, struct t_list *sticker_names) {
    struct mpd_pair *pair;
    const char *type_name = mympd_sticker_type_name_lookup(type);
    if (type_name == NULL) {
        return false;
    }
    if (stickerdb_connect(stickerdb) == false) {
        return false;
    }
    if (mpd_send_stickernamestypes(stickerdb->conn, mympd_sticker_type_name_lookup(type))) {
        while ((pair = mpd_recv_pair(stickerdb->conn)) != NULL) {
            if (strcmp(pair->name, "name") == 0) {
                list_push(sticker_names, pair->value, 0, NULL, NULL);
            }
            else {
                // ignore type
            }
            mpd_return_pair(stickerdb->conn, pair);
        }
    }
    stickerdb_check_error_and_recover(stickerdb, "mpd_send_stickernames");
    stickerdb_enter_idle(stickerdb);
    return true;
}

/**
 * Gets all stickers.
 * You must manage the idle state manually.
 * @param stickerdb pointer to the stickerdb state
 * @param type MPD sticker type
 * @param uri sticker uri
 * @param sticker pointer to t_sticker struct to populate
 * @param user_defined get user defines stickers?
 * @return Initialized and populated sticker struct or NULL on error
 */
struct t_sticker *stickerdb_get_all_batch(struct t_stickerdb_state *stickerdb, enum mympd_sticker_type type, const char *uri, struct t_sticker *sticker, bool user_defined) {
    if (is_streamuri(uri) == true) {
        return NULL;
    }
    return get_sticker_all(stickerdb, type, uri, sticker, user_defined);
}

/**
 * Gets all stickers
 * @param stickerdb pointer to the stickerdb state
 * @param type MPD sticker type
 * @param uri sticker uri
 * @param sticker pointer to t_sticker struct to populate
 * @param user_defined get user defines stickers?
 * @return Initialized and populated sticker struct or NULL on error
 */
struct t_sticker *stickerdb_get_all(struct t_stickerdb_state *stickerdb, enum mympd_sticker_type type, const char *uri, struct t_sticker *sticker, bool user_defined) {
    if (is_streamuri(uri) == true) {
        return NULL;
    }
    if (stickerdb_connect(stickerdb) == false) {
        return NULL;
    }
    sticker = get_sticker_all(stickerdb, type, uri, sticker, user_defined);
    stickerdb_enter_idle(stickerdb);
    return sticker;
}

/**
 * Gets all stickers by name
 * @param stickerdb pointer to the stickerdb state
 * @param type MPD sticker type
 * @param name sticker name
 * @return newly allocated radix tree or NULL on error
 */
rax *stickerdb_find_stickers_by_name(struct t_stickerdb_state *stickerdb, enum mympd_sticker_type type, const char *name) {
    return stickerdb_find_stickers_by_name_value(stickerdb, type, name, MPD_STICKER_OP_UNKNOWN, NULL);
}

/**
 * Gets all stickers by name and value
 * @param stickerdb pointer to the stickerdb state
 * @param type MPD sticker type
 * @param name sticker name
 * @param op compare operator: MPD_STICKER_OP_EQ, MPD_STICKER_OP_GT, MPD_STICKER_OP_LT
 * @param value sticker value
 * @return newly allocated radix tree or NULL on error
 */
rax *stickerdb_find_stickers_by_name_value(struct t_stickerdb_state *stickerdb, enum mympd_sticker_type type,
        const char *name, enum mpd_sticker_operator op, const char *value)
{
    if (stickerdb_connect(stickerdb) == false) {
        return NULL;
    }
    const char *type_name = mympd_sticker_type_name_lookup(type);
    if (type_name == NULL) {
        return NULL;
    }
    rax *stickers = raxNew();
    struct mpd_pair *pair;
    ssize_t name_len = (ssize_t)strlen(name) + 1;
    sds file = sdsempty();
    if (mpd_sticker_search_begin(stickerdb->conn, type_name, NULL, name) == false ||
        sticker_search_add_value_constraint(stickerdb, op, value) == false)
    {
        mpd_sticker_search_cancel(stickerdb->conn);
        stickerdb_free_find_result(stickers);
        return NULL;
    }
    if (mpd_sticker_search_commit(stickerdb->conn) == true) {
        while ((pair = mpd_recv_pair(stickerdb->conn)) != NULL) {
            if (strcmp(pair->name, "file") == 0) {
                file = sds_replace(file, pair->value);
            }
            else if (strcmp(pair->name, "sticker") == 0) {
                sds sticker_value = sdsnew(pair->value);
                sdsrange(sticker_value, name_len, -1);
                raxInsert(stickers, (unsigned char *)file, sdslen(file), sticker_value, NULL);
            }
            mpd_return_sticker(stickerdb->conn, pair);
        }
    }
    FREE_SDS(file);
    if (stickerdb_check_error_and_recover(stickerdb, "mpd_send_sticker_list") == true) {
        stickerdb_enter_idle(stickerdb);
    }
    else {
        stickerdb_free_find_result(stickers);
        return NULL;
    }
    MYMPD_LOG_DEBUG("stickerdb", "Found %" PRIu64 " stickers for %s",
            stickers->numele, name);
    return stickers;
}

/**
 * Gets a sorted list of stickers by name and value
 * @param stickerdb pointer to the stickerdb state
 * @param type MPD sticker type
 * @param baseuri baseuri for search
 * @param name sticker name
 * @param op mpd sticker compare operator
 * @param value sticker value or NULL to get all stickers with this name
 * @param sort sticker sort type
 * @param sort_desc sort descending?
 * @param start window start (including)
 * @param end window end (excluding), use UINT_MAX for open end
 * @return new allocated struct t_list
 */
struct t_list *stickerdb_find_stickers_sorted(struct t_stickerdb_state *stickerdb, enum mympd_sticker_type type,
        const char *baseuri, const char *name, enum mpd_sticker_operator op, const char *value,
        enum mpd_sticker_sort sort, bool sort_desc, unsigned start, unsigned end)
{
    if (stickerdb_connect(stickerdb) == false) {
        return NULL;
    }
    const char *type_name = mympd_sticker_type_name_lookup(type);
    if (type_name == NULL) {
        return NULL;
    }
    struct t_list *stickers = list_new();
    struct mpd_pair *pair;
    ssize_t name_len = (ssize_t)strlen(name) + 1;
    sds key = sdsempty();
    if (mpd_sticker_search_begin(stickerdb->conn, type_name, baseuri, name) == false ||
        sticker_search_add_value_constraint(stickerdb, op, value) == false ||
        sticker_search_add_sort(stickerdb, sort, sort_desc) == false ||
        sticker_search_add_window(stickerdb, start, end) == false)
    {
        mpd_sticker_search_cancel(stickerdb->conn);
        list_free(stickers);
        return NULL;
    }
    if (mpd_sticker_search_commit(stickerdb->conn) == true) {
        while ((pair = mpd_recv_pair(stickerdb->conn)) != NULL) {
            if (strcmp(pair->name, "sticker") == 0) {
                sds sticker_value = sdsnew(pair->value);
                sdsrange(sticker_value, name_len, -1);
                list_push(stickers, key, 0, sticker_value, NULL);
                FREE_SDS(sticker_value);
            }
            else {
                key = sds_replace(key, pair->value);
            }
            mpd_return_sticker(stickerdb->conn, pair);
        }
    }
    FREE_SDS(key);
    if (stickerdb_check_error_and_recover(stickerdb, "mpd_send_sticker_list") == true) {
        stickerdb_enter_idle(stickerdb);
    }
    else {
        list_free(stickers);
        return NULL;
    }
    MYMPD_LOG_DEBUG("stickerdb", "Found %u stickers for %s", stickers->length, name);
    return stickers;
}

/**
 * Frees the sticker find result
 * @param stickers pointer to stickers rax tree
 */
void stickerdb_free_find_result(rax *stickers) {
    if (stickers == NULL) {
        return;
    }
    raxIterator iter;
    raxStart(&iter, stickers);
    raxSeek(&iter, "^", NULL, 0);
    while (raxNext(&iter)) {
        FREE_SDS(iter.data);
    }
    raxStop(&iter);
    raxFree(stickers);
}

/**
 * Sets a sticker
 * @param stickerdb pointer to the stickerdb state
 * @param type MPD sticker type
 * @param uri sticker uri
 * @param name sticker name
 * @param value sticker value
 * @return true on success, else false
 */
bool stickerdb_set(struct t_stickerdb_state *stickerdb, enum mympd_sticker_type type, const char *uri, const char *name, const char *value) {
    if (is_streamuri(uri) == true) {
        return true;
    }
    if (stickerdb_connect(stickerdb) == false) {
        return false;
    }
    bool rc = set_sticker_value(stickerdb, type, uri, name, value);
    stickerdb_enter_idle(stickerdb);
    return rc;
}

/**
 * Sets a sticker
 * @param stickerdb pointer to the stickerdb state
 * @param type MPD sticker type
 * @param uri sticker uri
 * @param name sticker name
 * @param value sticker value
 * @return true on success, else false
 */
bool stickerdb_set_int64(struct t_stickerdb_state *stickerdb, enum mympd_sticker_type type, const char *uri, const char *name, int64_t value) {
    if (is_streamuri(uri) == true) {
        return true;
    }
    if (stickerdb_connect(stickerdb) == false) {
        return false;
    }
    bool rc = set_sticker_int64(stickerdb, type, uri, name, value);
    stickerdb_enter_idle(stickerdb);
    return rc;
}

/**
 * Decrements a sticker
 * @param stickerdb pointer to the stickerdb state
 * @param type MPD sticker type
 * @param uri sticker uri
 * @param name sticker name
 * @param value value to decrement
 * @return true on success, else false
 */
bool stickerdb_dec(struct t_stickerdb_state *stickerdb, enum mympd_sticker_type type, const char *uri, const char *name, unsigned value) {
    if (is_streamuri(uri) == true) {
        return true;
    }
    if (stickerdb_connect(stickerdb) == false) {
        return false;
    }
    bool rc = dec_sticker(stickerdb, type, uri, name, value);
    stickerdb_enter_idle(stickerdb);
    return rc;
}

/**
 * Increments a sticker
 * @param stickerdb pointer to the stickerdb state
 * @param type MPD sticker type
 * @param uri sticker uri
 * @param name sticker name
 * @param value value to increment
 * @return true on success, else false
 */
bool stickerdb_inc(struct t_stickerdb_state *stickerdb, enum mympd_sticker_type type, const char *uri, const char *name, unsigned value) {
    if (is_streamuri(uri) == true) {
        return true;
    }
    if (stickerdb_connect(stickerdb) == false) {
        return false;
    }
    bool rc = inc_sticker(stickerdb, type, uri, name, value);
    stickerdb_enter_idle(stickerdb);
    return rc;
}

/**
 * Sets the myMPD elapsed timestamp sticker
 * @param stickerdb pointer to the stickerdb state
 * @param type MPD sticker type
 * @param uri sticker uri
 * @param elapsed timestamp
 * @return true on success, else false
 */
bool stickerdb_set_elapsed(struct t_stickerdb_state *stickerdb, enum mympd_sticker_type type, const char *uri, time_t elapsed) {
    return stickerdb_set_int64(stickerdb, type, uri, sticker_name_lookup(STICKER_ELAPSED), (int64_t)elapsed);
}

/**
 * Increments a counter and sets a timestamp
 * @param stickerdb pointer to the stickerdb state
 * @param type MPD sticker type
 * @param uri sticker uri
 * @param name_inc sticker name for counter
 * @param name_timestamp sticker name for timestamp
 * @param timestamp timestamp to set
 * @return true on success, else false
 */
bool stickerdb_inc_set(struct t_stickerdb_state *stickerdb, enum mympd_sticker_type type, const char *uri,
        enum mympd_sticker_names name_inc, enum mympd_sticker_names name_timestamp, time_t timestamp)
{
    if (is_streamuri(uri) == true) {
        return true;
    }
    if (stickerdb_connect(stickerdb) == false) {
        return false;
    }
    bool rc = set_sticker_int64(stickerdb, type, uri, sticker_name_lookup(name_timestamp), (int64_t)timestamp) &&
        inc_sticker(stickerdb, type, uri, sticker_name_lookup(name_inc), 1);
    stickerdb_enter_idle(stickerdb);
    return rc;
}

/**
 * Increments the myMPD play count and sets the last played time
 * @param stickerdb pointer to the stickerdb state
 * @param type MPD sticker type
 * @param uri sticker uri
 * @param timestamp timestamp to set
 * @return true on success, else false
 */
bool stickerdb_inc_play_count(struct t_stickerdb_state *stickerdb, enum mympd_sticker_type type, const char *uri, time_t timestamp) {
    return stickerdb_inc_set(stickerdb, type, uri, STICKER_PLAY_COUNT, STICKER_LAST_PLAYED, timestamp);
}

/**
 * Increments the myMPD skip count and sets the last skipped time
 * @param stickerdb pointer to the stickerdb state
 * @param type MPD sticker type
 * @param uri sticker uri
 * @return true on success, else false
 */
bool stickerdb_inc_skip_count(struct t_stickerdb_state *stickerdb, enum mympd_sticker_type type, const char *uri) {
    time_t timestamp = time(NULL);
    return stickerdb_inc_set(stickerdb, type, uri, STICKER_SKIP_COUNT, STICKER_LAST_SKIPPED, timestamp);
}

/**
 * Sets the myMPD like sticker
 * @param stickerdb pointer to the stickerdb state
 * @param type MPD sticker type
 * @param uri sticker uri
 * @param value 0 = hate, 1 = neutral, 2 = like
 * @return true on success, else false
 */
bool stickerdb_set_like(struct t_stickerdb_state *stickerdb, enum mympd_sticker_type type, const char *uri, enum sticker_like value) {
    if (value < STICKER_LIKE_MIN || value > STICKER_LIKE_MAX) {
        return false;
    }
    return stickerdb_set_int64(stickerdb, type, uri, sticker_name_lookup(STICKER_LIKE), (int64_t)value);
}

/**
 * Sets the myMPD rating sticker
 * @param stickerdb pointer to the stickerdb state
 * @param type MPD sticker type
 * @param uri sticker uri
 * @param value 0 - 10 stars
 * @return true on success, else false
 */
bool stickerdb_set_rating(struct t_stickerdb_state *stickerdb, enum mympd_sticker_type type, const char *uri, int value) {
    if (value < STICKER_RATING_MIN || value > STICKER_RATING_MAX) {
        return false;
    }
    return stickerdb_set_int64(stickerdb, type, uri, sticker_name_lookup(STICKER_RATING), (int64_t)value);
}

/**
 * Removes a sticker
 * @param stickerdb pointer to the stickerdb state
 * @param type MPD sticker type
 * @param uri sticker uri
 * @param name sticker name
 * @return bool true on success, else false
 */
bool stickerdb_remove(struct t_stickerdb_state *stickerdb, enum mympd_sticker_type type, const char *uri, const char *name) {
    if (is_streamuri(uri) == true) {
        return true;
    }
    if (stickerdb_connect(stickerdb) == false) {
        return false;
    }
    bool rc = remove_sticker(stickerdb, type, uri, name);
    stickerdb_enter_idle(stickerdb);
    return rc;
}

/**
 * Checks if stickers should be fetched
 * @param featSticker Flag indicating enabled sticker support
 * @param stickers Pointer to t_stickers struct
 * @return bool true if stickers should be fetched, else false
 */
bool check_get_sticker(bool featSticker, const struct t_stickers *stickers) {
    if (featSticker == false || 
        (stickers->len == 0 && stickers->user_defined == false))
    {
        return false;
    }
    return true;
}

// Private functions

/**
 * Populates the sticker_types list
 * @param stickerdb pointer to the stickerdb state
 * @return true on success, else false
 */
static bool get_sticker_types(struct t_stickerdb_state *stickerdb) {
    list_clear(&stickerdb->mpd_state->sticker_types);
    if (stickerdb->mpd_state->feat.advsticker == false) {
        list_push(&stickerdb->mpd_state->sticker_types, "song", 0, NULL, NULL);
        return true;
    }
    struct mpd_pair *pair;
    if (mpd_send_stickertypes(stickerdb->conn)) {
        while ((pair = mpd_recv_pair(stickerdb->conn)) != NULL) {
            list_push(&stickerdb->mpd_state->sticker_types, pair->value, 0, NULL, NULL);
            mpd_return_pair(stickerdb->conn, pair);
        }
    }
    stickerdb_check_error_and_recover(stickerdb, "mpd_send_stickertypes");
    return true;
}

/**
 * Adds a mpd sticker search value constraint if value is not NULL
 * @param stickerdb pointer to the stickerdb state
 * @param op compare operator
 * @param value sticker value to search
 * @return true on success, else false
 */
static bool sticker_search_add_value_constraint(struct t_stickerdb_state *stickerdb, enum mpd_sticker_operator op, const char *value) {
    if (value != NULL) {
        return mpd_sticker_search_add_value_constraint(stickerdb->conn, op, value);
    }
    return true;
}

/**
 * Adds a mpd sticker sort definition, if supported by MPD
 * @param stickerdb pointer to the stickerdb state
 * @param sort mpd sticker sort type
 * @param desc sort descending?
 * @return true on success, else false
 */
static bool sticker_search_add_sort(struct t_stickerdb_state *stickerdb, enum mpd_sticker_sort sort, bool desc) {
    if (stickerdb->mpd_state->feat.advsticker == true &&
        sort != MPD_STICKER_SORT_UNKNOWN)
    {
        return mpd_sticker_search_add_sort(stickerdb->conn, sort, desc);
    }
    return true;
}

/**
 * Adds a mpd sticker window definition, if supported by MPD
 * @param stickerdb pointer to the stickerdb state
 * @param start window start (including)
 * @param end window end (excluding)
 * @return true on success, else false
 */
static bool sticker_search_add_window(struct t_stickerdb_state *stickerdb, unsigned start, unsigned end) {
    if (stickerdb->mpd_state->feat.advsticker == true) {
        return mpd_sticker_search_add_window(stickerdb->conn, start, end);
    }
    return true;
}

/**
 * Initializes the sticker struct and gets all stickers.
 * You must manage the idle state manually.
 * @param stickerdb pointer to the stickerdb state
 * @param type MPD sticker type
 * @param uri sticker uri
 * @param sticker pointer to t_sticker struct to populate
 * @param user_defined get user defines stickers?
 * @return the initialized and populated sticker struct
 */
static struct t_sticker *get_sticker_all(struct t_stickerdb_state *stickerdb, enum mympd_sticker_type type,
        const char *uri, struct t_sticker *sticker, bool user_defined)
{
    struct mpd_pair *pair;
    sticker_struct_init(sticker);
    const char *type_name = mympd_sticker_type_name_lookup(type);
    if (type_name == NULL) {
        return sticker;
    }
    if (mpd_send_sticker_list(stickerdb->conn, type_name, uri)) {
        while ((pair = mpd_recv_sticker(stickerdb->conn)) != NULL) {
            enum mympd_sticker_names sticker_name = sticker_name_parse(pair->name);
            if (sticker_name != STICKER_UNKNOWN) {
                int num;
                enum str2int_errno rc = str2int(&num, pair->value);
                sticker->mympd[sticker_name] = rc == STR2INT_SUCCESS
                    ? num
                    : 0;
            }
            else if (user_defined == true) {
                list_push(&sticker->user, pair->name, 0, pair->value, NULL);
            }
            mpd_return_sticker(stickerdb->conn, pair);
        }
    }
    stickerdb_check_error_and_recover(stickerdb, "mpd_send_sticker_list");
    return sticker;
}

/**
 * Gets a string value from sticker
 * @param stickerdb pointer to the stickerdb state
 * @param type MPD sticker type
 * @param uri sticker uri
 * @param name sticker name
 * @return string
 */
static sds get_sticker_value(struct t_stickerdb_state *stickerdb, enum mympd_sticker_type type, const char *uri, const char *name) {
    struct mpd_pair *pair;
    sds value = sdsempty();
    const char *type_name = mympd_sticker_type_name_lookup(type);
    if (type_name == NULL) {
        return value;
    }
    if (mpd_send_sticker_list(stickerdb->conn, type_name, uri)) {
        while ((pair = mpd_recv_sticker(stickerdb->conn)) != NULL) {
            if (strcmp(pair->name, name) == 0) {
                value = sdscat(value, pair->value);
            }
            mpd_return_sticker(stickerdb->conn, pair);
        }
    }
    stickerdb_check_error_and_recover(stickerdb, "mpd_send_sticker_list");
    return value;
}

/**
 * Gets an int64t value from sticker
 * @param stickerdb pointer to the stickerdb state
 * @param type MPD sticker type
 * @param uri sticker uri
 * @param name sticker name
 * @return number
 */
int64_t get_sticker_int64(struct t_stickerdb_state *stickerdb, enum mympd_sticker_type type, const char *uri, const char *name) {
    struct mpd_pair *pair;
    int64_t value = 0;
    const char *type_name = mympd_sticker_type_name_lookup(type);
    if (type_name == NULL) {
        return value;
    }
    if (mpd_send_sticker_list(stickerdb->conn, type_name, uri)) {
        while ((pair = mpd_recv_sticker(stickerdb->conn)) != NULL) {
            if (strcmp(pair->name, name) == 0) {
                str2int64(&value, pair->value);
            }
            mpd_return_sticker(stickerdb->conn, pair);
        }
    }
    stickerdb_check_error_and_recover(stickerdb, "mpd_send_sticker_list");
    return value;
}

/**
 * Sets a sticker string value
 * @param stickerdb pointer to the stickerdb state
 * @param type MPD sticker type
 * @param uri sticker uri
 * @param name sticker name
 * @param value string to set
 * @return true on success, else false
 */
static bool set_sticker_value(struct t_stickerdb_state *stickerdb, enum mympd_sticker_type type, const char *uri, const char *name, const char *value) {
    const char *type_name = mympd_sticker_type_name_lookup(type);
    if (type_name == NULL) {
        return false;
    }
    MYMPD_LOG_INFO(stickerdb->name, "Setting sticker %s: \"%s\" -> %s: %s", type_name, uri, name, value);
    mpd_run_sticker_set(stickerdb->conn, type_name, uri, name, value);
    return stickerdb_check_error_and_recover(stickerdb, "mpd_run_sticker_set");
}

/**
 * Sets an int64_t sticker value
 * @param stickerdb pointer to the stickerdb state
 * @param type MPD sticker type
 * @param uri sticker uri
 * @param name sticker name
 * @param value number to set
 * @return true on success, else false
 */
static bool set_sticker_int64(struct t_stickerdb_state *stickerdb, enum mympd_sticker_type type, const char *uri, const char *name, int64_t value) {
    sds value_str = sdsfromlonglong((long long)value);
    if (stickerdb->config->stickers_pad_int == true &&
        stickerdb->mpd_state->feat.advsticker == false)
    {
        sds pad_str = sdsempty();
        size_t value_len = sdslen(value_str);
        if (value_len < PADDING_LENGTH) {
            for (size_t i = 0, j = PADDING_LENGTH - value_len; i < j; i++) {
                pad_str = sds_catchar(pad_str, '0');
            }
        }
        pad_str = sdscatsds(pad_str, value_str);
        FREE_SDS(value_str);
        value_str = pad_str;
    }
    bool rc = set_sticker_value(stickerdb, type, uri, name, value_str);
    FREE_SDS(value_str);
    return rc;
}

/**
 * Decrements a sticker
 * @param stickerdb pointer to the stickerdb state
 * @param type MPD sticker type
 * @param uri sticker uri
 * @param name sticker name
 * @param value valute to decrement
 * @return true on success, else false
 */
static bool dec_sticker(struct t_stickerdb_state *stickerdb, enum mympd_sticker_type type, const char *uri, const char *name, unsigned value) {
    if (stickerdb->mpd_state->feat.advsticker == true) {
        const char *type_name = mympd_sticker_type_name_lookup(type);
        if (type_name == NULL) {
            return false;
        }
        mpd_run_sticker_dec(stickerdb->conn, type_name, uri, name, value);
        return stickerdb_check_error_and_recover(stickerdb, "mpd_run_sticker_dec");
    }
    // MPD < 0.24
    int64_t new_value = get_sticker_int64(stickerdb, type, uri, name) - value;
    return set_sticker_int64(stickerdb, type, uri, name, new_value);
}

/**
 * Increments a sticker
 * @param stickerdb pointer to the stickerdb state
 * @param type MPD sticker type
 * @param uri sticker uri
 * @param name sticker name
 * @param value valute to increment
 * @return true on success, else false
 */
static bool inc_sticker(struct t_stickerdb_state *stickerdb, enum mympd_sticker_type type, const char *uri, const char *name, unsigned value) {
    if (stickerdb->mpd_state->feat.advsticker == true) {
        const char *type_name = mympd_sticker_type_name_lookup(type);
        if (type_name == NULL) {
            return false;
        }
        mpd_run_sticker_inc(stickerdb->conn, type_name, uri, name, value);
        return stickerdb_check_error_and_recover(stickerdb, "mpd_run_sticker_inc");
    }
    // MPD < 0.24
    int64_t new_value = get_sticker_int64(stickerdb, type, uri, name) + value;
    return set_sticker_int64(stickerdb, type, uri, name, new_value);
}

/**
 * Removes a sticker
 * @param stickerdb pointer to the stickerdb state
 * @param type MPD sticker type
 * @param uri sticker uri
 * @param name sticker name
 * @return true on success, else false
 */
static bool remove_sticker(struct t_stickerdb_state *stickerdb, enum mympd_sticker_type type, const char *uri, const char *name) {
    const char *type_name = mympd_sticker_type_name_lookup(type);
    if (type_name == NULL) {
        return false;
    }
    MYMPD_LOG_INFO(stickerdb->name, "Removing sticker: \"%s\" -> %s", uri, name);
    mpd_run_sticker_delete(stickerdb->conn, type_name, uri, name);
    return stickerdb_check_error_and_recover(stickerdb, "mpd_run_sticker_delete");
}

/**
 * Creates the connection to MPD
 * @param stickerdb pointer to the stickerdb state
 * @return true on success, else false
 */
static bool stickerdb_connect_mpd(struct t_stickerdb_state *stickerdb) {
    if (stickerdb->mpd_state->mpd_host[0] == '/') {
        MYMPD_LOG_NOTICE(stickerdb->name, "Connecting to socket \"%s\"", stickerdb->mpd_state->mpd_host);
    }
    else {
        MYMPD_LOG_NOTICE(stickerdb->name, "Connecting to \"%s:%d\"", stickerdb->mpd_state->mpd_host, stickerdb->mpd_state->mpd_port);
    }
    stickerdb->conn = mpd_connection_new(stickerdb->mpd_state->mpd_host, stickerdb->mpd_state->mpd_port, stickerdb->mpd_state->mpd_timeout);
    if (stickerdb->conn == NULL) {
        MYMPD_LOG_ERROR(stickerdb->name, "Connection failed: out-of-memory");
        stickerdb->conn_state = MPD_FAILURE;
        sds buffer = jsonrpc_event(sdsempty(), JSONRPC_EVENT_MPD_DISCONNECTED);
        ws_notify(buffer, MPD_PARTITION_ALL);
        FREE_SDS(buffer);
        return false;
    }
    if (mpd_connection_get_error(stickerdb->conn) != MPD_ERROR_SUCCESS) {
        MYMPD_LOG_ERROR(stickerdb->name, "Connection: %s", mpd_connection_get_error_message(stickerdb->conn));
        sds buffer = jsonrpc_notify_phrase(sdsempty(), JSONRPC_FACILITY_MPD,
            JSONRPC_SEVERITY_ERROR, "MPD connection error: %{error}", 2,
            "error", mpd_connection_get_error_message(stickerdb->conn));
        ws_notify(buffer, MPD_PARTITION_ALL);
        FREE_SDS(buffer);
        mpd_connection_free(stickerdb->conn);
        stickerdb->conn = NULL;
        stickerdb->conn_state = MPD_FAILURE;
        return false;
    }
    if (sdslen(stickerdb->mpd_state->mpd_pass) > 0) {
        MYMPD_LOG_DEBUG(stickerdb->name, "Password set, authenticating to MPD");
        if (mpd_run_password(stickerdb->conn, stickerdb->mpd_state->mpd_pass) == false) {
            MYMPD_LOG_ERROR(stickerdb->name, "MPD connection: %s", mpd_connection_get_error_message(stickerdb->conn));
            stickerdb->conn_state = MPD_FAILURE;
            sds buffer = jsonrpc_notify_phrase(sdsempty(), JSONRPC_FACILITY_MPD,
                JSONRPC_SEVERITY_ERROR, "MPD connection error: %{error}", 2,
                "error", mpd_connection_get_error_message(stickerdb->conn));
            ws_notify(buffer, MPD_PARTITION_ALL);
            FREE_SDS(buffer);
            return false;
        }
        MYMPD_LOG_INFO(stickerdb->name, "Successfully authenticated to MPD");
    }
    else {
        MYMPD_LOG_DEBUG(stickerdb->name, "No password set");
    }

    MYMPD_LOG_NOTICE(stickerdb->name, "Connected to MPD");
    stickerdb->conn_state = MPD_CONNECTED;
    return true;
}

/**
 * Check for sticker support
 * @param stickerdb pointer to the stickerdb state
 * @return true if stickers are supported, else false
 */
static bool check_sticker_support(struct t_stickerdb_state *stickerdb) {
    bool supported = false;
    if (mpd_send_allowed_commands(stickerdb->conn) == true) {
        struct mpd_pair *pair;
        while ((pair = mpd_recv_command_pair(stickerdb->conn)) != NULL) {
            if (strcmp(pair->value, "sticker") == 0) {
                mpd_return_pair(stickerdb->conn, pair);
                supported = true;
                break;
            }
            mpd_return_pair(stickerdb->conn, pair);
        }
    }
    if (stickerdb_check_error_and_recover(stickerdb, "mpd_send_allowed_commands") == false) {
        return false;
    }
    return supported;
}
