/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
/*
   Copyright (C) 2009-2015 Red Hat, Inc.

   This library 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.

   This library 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 this library; if not, see <http://www.gnu.org/licenses/>.
*/
#include <config.h>

#include <glib.h>

#include "image-encoders.h"
#include "spice-bitmap-utils.h"
#include "red-parse-qxl.h" // red_drawable_unref
#include "pixmap-cache.h" // MAX_CACHE_CLIENTS

#define ZLIB_DEFAULT_COMPRESSION_LEVEL 3

#define ENCODER_MESSAGE_SIZE 512

#define MAX_GLZ_DRAWABLE_INSTANCES 2

#if 0
#define COMPRESS_DEBUG(...) g_debug(__VA_ARGS__)
#else
#define COMPRESS_DEBUG(...) G_STMT_START { \
        if (0) g_debug(__VA_ARGS__); \
    } G_STMT_END
#endif

struct RedGlzDrawable;
struct GlzDrawableInstanceItem;

struct GlzSharedDictionary {
    GlzEncDictContext *dict;
    uint32_t refs;
    uint8_t id;
    pthread_rwlock_t encode_lock;
    int migrate_freeze;
    RedClient *client; // channel clients of the same client share the dict
};

/* for each qxl drawable, there may be several instances of lz drawables */
/* TODO - reuse this stuff for the top level. I just added a second level of multiplicity
 * at the Drawable by keeping a ring, so:
 * Drawable -> (ring of) RedGlzDrawable -> (up to 2) GlzDrawableInstanceItem
 * and it should probably (but need to be sure...) be
 * Drawable -> ring of GlzDrawableInstanceItem.
 */
struct GlzDrawableInstanceItem {
    RingItem glz_link;
    RingItem free_link;
    GlzEncDictImageContext *context;
    RedGlzDrawable         *glz_drawable;
};

struct RedGlzDrawable {
    RingItem link;    // ordered by the time it was encoded
    RingItem drawable_link;
    red::shared_ptr<RedDrawable> red_drawable;
    GlzDrawableInstanceItem instances_pool[MAX_GLZ_DRAWABLE_INSTANCES];
    Ring instances;
    uint8_t instances_count;
    gboolean has_drawable;
    ImageEncoders *encoders;
};

#define LINK_TO_GLZ(ptr) SPICE_CONTAINEROF((ptr), RedGlzDrawable, \
                                           drawable_link)
#define DRAWABLE_FOREACH_GLZ_SAFE(drawable, link, next, glz) \
    SAFE_FOREACH(link, next, drawable, &(drawable)->glz_retention.ring, glz, LINK_TO_GLZ(link))

static void glz_drawable_instance_item_free(GlzDrawableInstanceItem *instance);
static void encoder_data_init(EncoderData *data);
static void encoder_data_reset(EncoderData *data);
static void image_encoders_release_glz(ImageEncoders *enc);


static SPICE_GNUC_NORETURN SPICE_GNUC_PRINTF(2, 3) void
quic_usr_error(QuicUsrContext *usr, const char *fmt, ...)
{
    EncoderData *usr_data = &(SPICE_CONTAINEROF(usr, QuicData, usr)->data);
    va_list ap;
    char message_buf[ENCODER_MESSAGE_SIZE];

    va_start(ap, fmt);
    vsnprintf(message_buf, sizeof(message_buf), fmt, ap);
    va_end(ap);
    spice_critical("%s", message_buf);

    longjmp(usr_data->jmp_env, 1);
}

static SPICE_GNUC_NORETURN SPICE_GNUC_PRINTF(2, 3) void
lz_usr_error(LzUsrContext *usr, const char *fmt, ...)
{
    EncoderData *usr_data = &(SPICE_CONTAINEROF(usr, LzData, usr)->data);
    va_list ap;
    char message_buf[ENCODER_MESSAGE_SIZE];

    va_start(ap, fmt);
    vsnprintf(message_buf, sizeof(message_buf), fmt, ap);
    va_end(ap);
    spice_critical("%s", message_buf);

    longjmp(usr_data->jmp_env, 1);
}

static SPICE_GNUC_PRINTF(2, 3) void
glz_usr_error(GlzEncoderUsrContext *usr, const char *fmt, ...)
{
    va_list ap;
    char message_buf[ENCODER_MESSAGE_SIZE];

    va_start(ap, fmt);
    vsnprintf(message_buf, sizeof(message_buf), fmt, ap);
    va_end(ap);

    spice_critical("%s", message_buf); // if global lz fails in the middle
                                        // the consequences are not predictable since the window
                                        // can turn to be unsynchronized between the server and
                                        // and the client
}

static SPICE_GNUC_PRINTF(2, 3) void
quic_usr_warn(QuicUsrContext *usr, const char *fmt, ...)
{
    va_list ap;
    char message_buf[ENCODER_MESSAGE_SIZE];

    va_start(ap, fmt);
    vsnprintf(message_buf, sizeof(message_buf), fmt, ap);
    va_end(ap);
    spice_warning("%s", message_buf);
}

static SPICE_GNUC_PRINTF(2, 3) void
lz_usr_warn(LzUsrContext *usr, const char *fmt, ...)
{
    va_list ap;
    char message_buf[ENCODER_MESSAGE_SIZE];

    va_start(ap, fmt);
    vsnprintf(message_buf, sizeof(message_buf), fmt, ap);
    va_end(ap);
    spice_warning("%s", message_buf);
}

static SPICE_GNUC_PRINTF(2, 3) void
glz_usr_warn(GlzEncoderUsrContext *usr, const char *fmt, ...)
{
    va_list ap;
    char message_buf[ENCODER_MESSAGE_SIZE];

    va_start(ap, fmt);
    vsnprintf(message_buf, sizeof(message_buf), fmt, ap);
    va_end(ap);
    spice_warning("%s", message_buf);
}

static void *quic_usr_malloc(QuicUsrContext *usr, int size)
{
    return g_malloc(size);
}

static void *lz_usr_malloc(LzUsrContext *usr, int size)
{
    return g_malloc(size);
}

static void *glz_usr_malloc(GlzEncoderUsrContext *usr, int size)
{
    return g_malloc(size);
}

static void quic_usr_free(QuicUsrContext *usr, void *ptr)
{
    g_free(ptr);
}

static void lz_usr_free(LzUsrContext *usr, void *ptr)
{
    g_free(ptr);
}

static void glz_usr_free(GlzEncoderUsrContext *usr, void *ptr)
{
    g_free(ptr);
}

static void encoder_data_init(EncoderData *data)
{
    data->bufs_tail = g_new(RedCompressBuf, 1);
    data->bufs_head = data->bufs_tail;
    data->bufs_head->send_next = nullptr;
}

static void encoder_data_reset(EncoderData *data)
{
    RedCompressBuf *buf = data->bufs_head;
    while (buf) {
        RedCompressBuf *next = buf->send_next;
        g_free(buf);
        buf = next;
    }
    data->bufs_head = data->bufs_tail = nullptr;
}

/* Allocate more space for compressed buffer.
 * The pointer returned in io_ptr is guaranteed to be aligned to 4 bytes.
 */
static int encoder_usr_more_space(EncoderData *enc_data, uint8_t **io_ptr)
{
    RedCompressBuf *buf;

    buf = g_new(RedCompressBuf, 1);
    enc_data->bufs_tail->send_next = buf;
    enc_data->bufs_tail = buf;
    buf->send_next = nullptr;
    *io_ptr = buf->buf.bytes;
    return sizeof(buf->buf);
}

static int quic_usr_more_space(QuicUsrContext *usr, uint32_t **io_ptr, int rows_completed)
{
    EncoderData *usr_data = &(SPICE_CONTAINEROF(usr, QuicData, usr)->data);
    return encoder_usr_more_space(usr_data, reinterpret_cast<uint8_t **>(io_ptr)) /
           sizeof(uint32_t);
}

static int lz_usr_more_space(LzUsrContext *usr, uint8_t **io_ptr)
{
    EncoderData *usr_data = &(SPICE_CONTAINEROF(usr, LzData, usr)->data);
    return encoder_usr_more_space(usr_data, io_ptr);
}

static int glz_usr_more_space(GlzEncoderUsrContext *usr, uint8_t **io_ptr)
{
    EncoderData *usr_data = &(SPICE_CONTAINEROF(usr, GlzData, usr)->data);
    return encoder_usr_more_space(usr_data, io_ptr);
}

static int jpeg_usr_more_space(JpegEncoderUsrContext *usr, uint8_t **io_ptr)
{
    EncoderData *usr_data = &(SPICE_CONTAINEROF(usr, JpegData, usr)->data);
    return encoder_usr_more_space(usr_data, io_ptr);
}

#ifdef USE_LZ4
static int lz4_usr_more_space(Lz4EncoderUsrContext *usr, uint8_t **io_ptr)
{
    EncoderData *usr_data = &(SPICE_CONTAINEROF(usr, Lz4Data, usr)->data);
    return encoder_usr_more_space(usr_data, io_ptr);
}
#endif

static int zlib_usr_more_space(ZlibEncoderUsrContext *usr, uint8_t **io_ptr)
{
    EncoderData *usr_data = &(SPICE_CONTAINEROF(usr, ZlibData, usr)->data);
    return encoder_usr_more_space(usr_data, io_ptr);
}

static inline int encoder_usr_more_lines(EncoderData *enc_data, uint8_t **lines)
{
    struct SpiceChunk *chunk;

    if (enc_data->u.lines_data.reverse) {
        if (!(enc_data->u.lines_data.next >= 0)) {
            return 0;
        }
    } else {
        if (!(enc_data->u.lines_data.next < enc_data->u.lines_data.chunks->num_chunks)) {
            return 0;
        }
    }

    chunk = &enc_data->u.lines_data.chunks->chunk[enc_data->u.lines_data.next];
    if (chunk->len % enc_data->u.lines_data.stride) {
        return 0;
    }

    if (enc_data->u.lines_data.reverse) {
        enc_data->u.lines_data.next--;
        *lines = chunk->data + chunk->len - enc_data->u.lines_data.stride;
    } else {
        enc_data->u.lines_data.next++;
        *lines = chunk->data;
    }

    return chunk->len / enc_data->u.lines_data.stride;
}

static int quic_usr_more_lines(QuicUsrContext *usr, uint8_t **lines)
{
    EncoderData *usr_data = &(SPICE_CONTAINEROF(usr, QuicData, usr)->data);
    return encoder_usr_more_lines(usr_data, lines);
}

static int lz_usr_more_lines(LzUsrContext *usr, uint8_t **lines)
{
    EncoderData *usr_data = &(SPICE_CONTAINEROF(usr, LzData, usr)->data);
    return encoder_usr_more_lines(usr_data, lines);
}

static int glz_usr_more_lines(GlzEncoderUsrContext *usr, uint8_t **lines)
{
    EncoderData *usr_data = &(SPICE_CONTAINEROF(usr, GlzData, usr)->data);
    return encoder_usr_more_lines(usr_data, lines);
}

static int jpeg_usr_more_lines(JpegEncoderUsrContext *usr, uint8_t **lines)
{
    EncoderData *usr_data = &(SPICE_CONTAINEROF(usr, JpegData, usr)->data);
    return encoder_usr_more_lines(usr_data, lines);
}

#ifdef USE_LZ4
static int lz4_usr_more_lines(Lz4EncoderUsrContext *usr, uint8_t **lines)
{
    EncoderData *usr_data = &(SPICE_CONTAINEROF(usr, Lz4Data, usr)->data);
    return encoder_usr_more_lines(usr_data, lines);
}
#endif

static int zlib_usr_more_input(ZlibEncoderUsrContext *usr, uint8_t** input)
{
    EncoderData *usr_data = &(SPICE_CONTAINEROF(usr, ZlibData, usr)->data);
    int buf_size;

    if (!usr_data->u.compressed_data.next) {
        spice_assert(usr_data->u.compressed_data.size_left == 0);
        return 0;
    }

    *input = usr_data->u.compressed_data.next->buf.bytes;
    buf_size = MIN(sizeof(usr_data->u.compressed_data.next->buf),
                   usr_data->u.compressed_data.size_left);

    usr_data->u.compressed_data.next = usr_data->u.compressed_data.next->send_next;
    usr_data->u.compressed_data.size_left -= buf_size;
    return buf_size;
}

static void image_encoders_init_quic(ImageEncoders *enc)
{
    enc->quic_data.usr.error = quic_usr_error;
    enc->quic_data.usr.warn = quic_usr_warn;
    enc->quic_data.usr.info = quic_usr_warn;
    enc->quic_data.usr.malloc = quic_usr_malloc;
    enc->quic_data.usr.free = quic_usr_free;
    enc->quic_data.usr.more_space = quic_usr_more_space;
    enc->quic_data.usr.more_lines = quic_usr_more_lines;

    enc->quic = quic_create(&enc->quic_data.usr);

    if (!enc->quic) {
        spice_critical("create quic failed");
    }
}

static void image_encoders_init_lz(ImageEncoders *enc)
{
    enc->lz_data.usr.error = lz_usr_error;
    enc->lz_data.usr.warn = lz_usr_warn;
    enc->lz_data.usr.info = lz_usr_warn;
    enc->lz_data.usr.malloc = lz_usr_malloc;
    enc->lz_data.usr.free = lz_usr_free;
    enc->lz_data.usr.more_space = lz_usr_more_space;
    enc->lz_data.usr.more_lines = lz_usr_more_lines;

    enc->lz = lz_create(&enc->lz_data.usr);

    if (!enc->lz) {
        spice_critical("create lz failed");
    }
}

static void glz_usr_free_image(GlzEncoderUsrContext *usr, GlzUsrImageContext *image)
{
    GlzData *lz_data = SPICE_CONTAINEROF(usr, GlzData, usr);
    auto glz_drawable_instance = static_cast<GlzDrawableInstanceItem *>(image);
    ImageEncoders *drawable_enc = glz_drawable_instance->glz_drawable->encoders;
    ImageEncoders *this_enc = SPICE_CONTAINEROF(lz_data, ImageEncoders, glz_data);
    if (this_enc == drawable_enc) {
        glz_drawable_instance_item_free(glz_drawable_instance);
    } else {
        /* The glz dictionary is shared between all DisplayChannelClient
         * instances that belong to the same client, and glz_usr_free_image
         * can be called by the dictionary code
         * (glz_dictionary_window_remove_head). Thus this function can be
         * called from any DisplayChannelClient thread, hence the need for
         * this check.
         */
        pthread_mutex_lock(&drawable_enc->glz_drawables_inst_to_free_lock);
        ring_add_before(&glz_drawable_instance->free_link,
                        &drawable_enc->glz_drawables_inst_to_free);
        pthread_mutex_unlock(&drawable_enc->glz_drawables_inst_to_free_lock);
    }
}

static void image_encoders_init_glz_data(ImageEncoders *enc)
{
    enc->glz_data.usr.error = glz_usr_error;
    enc->glz_data.usr.warn = glz_usr_warn;
    enc->glz_data.usr.info = glz_usr_warn;
    enc->glz_data.usr.malloc = glz_usr_malloc;
    enc->glz_data.usr.free = glz_usr_free;
    enc->glz_data.usr.more_space = glz_usr_more_space;
    enc->glz_data.usr.more_lines = glz_usr_more_lines;
    enc->glz_data.usr.free_image = glz_usr_free_image;
}

static void image_encoders_init_jpeg(ImageEncoders *enc)
{
    enc->jpeg_data.usr.more_space = jpeg_usr_more_space;
    enc->jpeg_data.usr.more_lines = jpeg_usr_more_lines;

    enc->jpeg = jpeg_encoder_create(&enc->jpeg_data.usr);

    if (!enc->jpeg) {
        spice_critical("create jpeg encoder failed");
    }
}

#ifdef USE_LZ4
static inline void image_encoders_init_lz4(ImageEncoders *enc)
{
    enc->lz4_data.usr.more_space = lz4_usr_more_space;
    enc->lz4_data.usr.more_lines = lz4_usr_more_lines;

    enc->lz4 = lz4_encoder_create(&enc->lz4_data.usr);

    if (!enc->lz4) {
        spice_critical("create lz4 encoder failed");
    }
}
#endif

static void image_encoders_init_zlib(ImageEncoders *enc)
{
    enc->zlib_data.usr.more_space = zlib_usr_more_space;
    enc->zlib_data.usr.more_input = zlib_usr_more_input;
}

void image_encoders_init(ImageEncoders *enc, ImageEncoderSharedData *shared_data)
{
    spice_assert(shared_data);
    enc->shared_data = shared_data;

    ring_init(&enc->glz_drawables);
    ring_init(&enc->glz_drawables_inst_to_free);
    pthread_mutex_init(&enc->glz_drawables_inst_to_free_lock, nullptr);

    image_encoders_init_glz_data(enc);
    image_encoders_init_quic(enc);
    image_encoders_init_lz(enc);
    image_encoders_init_jpeg(enc);
#ifdef USE_LZ4
    image_encoders_init_lz4(enc);
#endif
    image_encoders_init_zlib(enc);

    // todo: tune level according to bandwidth
    enc->zlib_level = ZLIB_DEFAULT_COMPRESSION_LEVEL;
}

void image_encoders_free(ImageEncoders *enc)
{
    image_encoders_release_glz(enc);
    quic_destroy(enc->quic);
    enc->quic = nullptr;
    lz_destroy(enc->lz);
    enc->lz = nullptr;
    jpeg_encoder_destroy(enc->jpeg);
    enc->jpeg = nullptr;
#ifdef USE_LZ4
    lz4_encoder_destroy(enc->lz4);
    enc->lz4 = nullptr;
#endif
    if (enc->zlib != nullptr) {
        zlib_encoder_destroy(enc->zlib);
        enc->zlib = nullptr;
    }
    pthread_mutex_destroy(&enc->glz_drawables_inst_to_free_lock);
}

/* Remove from the to_free list and the instances_list.
   When no instance is left - the RedGlzDrawable is released too. (and the qxl drawable too, if
   it is not used by Drawable).
   NOTE - 1) can be called only by the display channel that created the drawable
          2) it is assumed that the instance was already removed from the dictionary*/
static void glz_drawable_instance_item_free(GlzDrawableInstanceItem *instance)
{
    RedGlzDrawable *glz_drawable;

    spice_assert(instance);
    spice_assert(instance->glz_drawable);

    glz_drawable = instance->glz_drawable;

    spice_assert(glz_drawable->instances_count > 0);

    ring_remove(&instance->glz_link);
    glz_drawable->instances_count--;

    // when the remove callback is performed from the channel that the
    // drawable belongs to, the instance is not added to the 'to_free' list
    if (ring_item_is_linked(&instance->free_link)) {
        ring_remove(&instance->free_link);
    }

    if (ring_is_empty(&glz_drawable->instances)) {
        spice_assert(glz_drawable->instances_count == 0);

        if (glz_drawable->has_drawable) {
            ring_remove(&glz_drawable->drawable_link);
        }
        glz_drawable->red_drawable.reset();
        glz_drawable->encoders->shared_data->glz_drawable_count--;
        if (ring_item_is_linked(&glz_drawable->link)) {
            ring_remove(&glz_drawable->link);
        }
        g_free(glz_drawable);
    }
}

/*
 * Releases all the instances of the drawable from the dictionary and the display channel client.
 * The release of the last instance will also release the drawable itself and the qxl drawable
 * if possible.
 * NOTE - the caller should prevent encoding using the dictionary during this operation
 */
static void red_glz_drawable_free(RedGlzDrawable *glz_drawable)
{
    ImageEncoders *enc = glz_drawable->encoders;
    RingItem *head_instance = ring_get_head(&glz_drawable->instances);
    int cont = (head_instance != nullptr);

    while (cont) {
        if (glz_drawable->instances_count == 1) {
            /* Last instance: glz_drawable_instance_item_free will free the glz_drawable */
            cont = FALSE;
        }
        GlzDrawableInstanceItem *instance = SPICE_CONTAINEROF(head_instance,
                                                        GlzDrawableInstanceItem,
                                                        glz_link);
        if (!ring_item_is_linked(&instance->free_link)) {
            // the instance didn't get out from window yet
            glz_enc_dictionary_remove_image(enc->glz_dict->dict,
                                            instance->context,
                                            &enc->glz_data.usr);
        }
        glz_drawable_instance_item_free(instance);

        if (cont) {
            head_instance = ring_get_head(&glz_drawable->instances);
        }
    }
}

gboolean image_encoders_glz_encode_lock(ImageEncoders *enc)
{
    if (enc->glz_dict) {
        pthread_rwlock_wrlock(&enc->glz_dict->encode_lock);
        return TRUE;
    }
    return FALSE;
}

void image_encoders_glz_encode_unlock(ImageEncoders *enc)
{
    if (enc->glz_dict) {
        pthread_rwlock_unlock(&enc->glz_dict->encode_lock);
    }
}

/*
 * Remove from the global lz dictionary some glz_drawables that have no reference to
 * Drawable (their qxl drawables are released too).
 * NOTE - the caller should prevent encoding using the dictionary during the operation
 */
int image_encoders_free_some_independent_glz_drawables(ImageEncoders *enc)
{
    RingItem *ring_link;
    int n = 0;

    if (!enc) {
        return 0;
    }
    ring_link = ring_get_head(&enc->glz_drawables);
    while ((n < RED_RELEASE_BUNCH_SIZE) && (ring_link != nullptr)) {
        RedGlzDrawable *glz_drawable = SPICE_CONTAINEROF(ring_link, RedGlzDrawable, link);
        ring_link = ring_next(&enc->glz_drawables, ring_link);
        if (!glz_drawable->has_drawable) {
            red_glz_drawable_free(glz_drawable);
            n++;
        }
    }
    return n;
}

void image_encoders_free_glz_drawables_to_free(ImageEncoders* enc)
{
    RingItem *ring_link;

    if (!enc->glz_dict) {
        return;
    }
    pthread_mutex_lock(&enc->glz_drawables_inst_to_free_lock);
    while ((ring_link = ring_get_head(&enc->glz_drawables_inst_to_free))) {
        GlzDrawableInstanceItem *drawable_instance = SPICE_CONTAINEROF(ring_link,
                                                                 GlzDrawableInstanceItem,
                                                                 free_link);
        glz_drawable_instance_item_free(drawable_instance);
    }
    pthread_mutex_unlock(&enc->glz_drawables_inst_to_free_lock);
}

/* Clear all lz drawables - enforce their removal from the global dictionary.
   NOTE - prevents encoding using the dictionary during the operation*/
void image_encoders_free_glz_drawables(ImageEncoders *enc)
{
    RingItem *ring_link;
    GlzSharedDictionary *glz_dict = enc->glz_dict;

    if (!glz_dict) {
        return;
    }

    // assure no display channel is during global lz encoding
    pthread_rwlock_wrlock(&glz_dict->encode_lock);
    while ((ring_link = ring_get_head(&enc->glz_drawables))) {
        RedGlzDrawable *drawable = SPICE_CONTAINEROF(ring_link, RedGlzDrawable, link);
        // no need to lock the to_free list, since we assured no other thread is encoding and
        // thus not other thread access the to_free list of the channel
        red_glz_drawable_free(drawable);
    }
    pthread_rwlock_unlock(&glz_dict->encode_lock);
}

void glz_retention_free_drawables(GlzImageRetention *ret)
{
    RingItem *glz_item, *next_item;
    RedGlzDrawable *glz;
    SAFE_FOREACH(glz_item, next_item, TRUE, &ret->ring, glz, LINK_TO_GLZ(glz_item)) {
        red_glz_drawable_free(glz);
    }
}

void glz_retention_detach_drawables(GlzImageRetention *ret)
{
    RingItem *item, *next;

    RING_FOREACH_SAFE(item, next, &ret->ring) {
        SPICE_CONTAINEROF(item, RedGlzDrawable, drawable_link)->has_drawable = FALSE;
        ring_remove(item);
    }
}

static void image_encoders_freeze_glz(ImageEncoders *enc)
{
    pthread_rwlock_wrlock(&enc->glz_dict->encode_lock);
    enc->glz_dict->migrate_freeze = TRUE;
    pthread_rwlock_unlock(&enc->glz_dict->encode_lock);
}

void image_encoders_glz_get_restore_data(ImageEncoders *enc,
                                         uint8_t *out_id, GlzEncDictRestoreData *out_data)
{
    spice_assert(enc->glz_dict);
    image_encoders_freeze_glz(enc);
    *out_id = enc->glz_dict->id;
    glz_enc_dictionary_get_restore_data(enc->glz_dict->dict, out_data,
                                        &enc->glz_data.usr);
}

static GlzSharedDictionary *glz_shared_dictionary_new(RedClient *client, uint8_t id,
                                                      GlzEncDictContext *dict)
{
    spice_return_val_if_fail(dict != nullptr, NULL);

    auto shared_dict = g_new0(GlzSharedDictionary, 1);

    shared_dict->dict = dict;
    shared_dict->id = id;
    shared_dict->refs = 1;
    shared_dict->migrate_freeze = FALSE;
    shared_dict->client = client;
    pthread_rwlock_init(&shared_dict->encode_lock, nullptr);

    return shared_dict;
}

static pthread_mutex_t glz_dictionary_list_lock = PTHREAD_MUTEX_INITIALIZER;
static GList *glz_dictionary_list;

static GlzSharedDictionary *find_glz_dictionary(RedClient *client, uint8_t dict_id)
{
    GList *l;
    GlzSharedDictionary *ret = nullptr;

    for (l = glz_dictionary_list; l != nullptr; l = l->next) {
        auto dict = static_cast<GlzSharedDictionary *>(l->data);
        if ((dict->client == client) && (dict->id == dict_id)) {
            ret = dict;
            break;
        }
    }

    return ret;
}

#define MAX_LZ_ENCODERS MAX_CACHE_CLIENTS

static GlzSharedDictionary *create_glz_dictionary(ImageEncoders *enc,
                                                  RedClient *client,
                                                  uint8_t id, int window_size)
{
    spice_debug("Lz Window %d Size=%d", id, window_size);

    GlzEncDictContext *glz_dict =
        glz_enc_dictionary_create(window_size, MAX_LZ_ENCODERS, &enc->glz_data.usr);

    return glz_shared_dictionary_new(client, id, glz_dict);
}

gboolean image_encoders_get_glz_dictionary(ImageEncoders *enc,
                                           RedClient *client,
                                           uint8_t id, int window_size)
{
    GlzSharedDictionary *shared_dict;

    spice_return_val_if_fail(!enc->glz_dict, FALSE);

    pthread_mutex_lock(&glz_dictionary_list_lock);

    shared_dict = find_glz_dictionary(client, id);
    if (shared_dict) {
        shared_dict->refs++;
    } else {
        shared_dict = create_glz_dictionary(enc, client, id, window_size);
        if (shared_dict != nullptr) {
            glz_dictionary_list = g_list_prepend(glz_dictionary_list, shared_dict);
        }
    }

    pthread_mutex_unlock(&glz_dictionary_list_lock);
    enc->glz_dict = shared_dict;
    return shared_dict != nullptr;
}

static GlzSharedDictionary *restore_glz_dictionary(ImageEncoders *enc,
                                                   RedClient *client,
                                                   uint8_t id,
                                                   GlzEncDictRestoreData *restore_data)
{
    GlzEncDictContext *glz_dict =
        glz_enc_dictionary_restore(restore_data, &enc->glz_data.usr);

    return glz_shared_dictionary_new(client, id, glz_dict);
}

gboolean image_encoders_restore_glz_dictionary(ImageEncoders *enc,
                                               RedClient *client,
                                               uint8_t id,
                                               GlzEncDictRestoreData *restore_data)
{
    GlzSharedDictionary *shared_dict = nullptr;

    spice_return_val_if_fail(!enc->glz_dict, FALSE);

    pthread_mutex_lock(&glz_dictionary_list_lock);

    shared_dict = find_glz_dictionary(client, id);

    if (shared_dict) {
        shared_dict->refs++;
    } else {
        shared_dict = restore_glz_dictionary(enc, client, id, restore_data);
        if(shared_dict != nullptr) {
            glz_dictionary_list = g_list_prepend(glz_dictionary_list, shared_dict);
        }
    }

    pthread_mutex_unlock(&glz_dictionary_list_lock);
    enc->glz_dict = shared_dict;
    return shared_dict != nullptr;
}

gboolean image_encoders_glz_create(ImageEncoders *enc, uint8_t id)
{
    enc->glz = glz_encoder_create(id, enc->glz_dict->dict, &enc->glz_data.usr);
    return enc->glz != nullptr;
}

/* destroy encoder, and dictionary if no one uses it*/
static void image_encoders_release_glz(ImageEncoders *enc)
{
    GlzSharedDictionary *shared_dict;

    image_encoders_free_glz_drawables(enc);

    glz_encoder_destroy(enc->glz);
    enc->glz = nullptr;

    if (!(shared_dict = enc->glz_dict)) {
        return;
    }

    enc->glz_dict = nullptr;
    pthread_mutex_lock(&glz_dictionary_list_lock);
    if (--shared_dict->refs != 0) {
        pthread_mutex_unlock(&glz_dictionary_list_lock);
        return;
    }
    glz_dictionary_list = g_list_remove(glz_dictionary_list, shared_dict);
    pthread_mutex_unlock(&glz_dictionary_list_lock);
    glz_enc_dictionary_destroy(shared_dict->dict, &enc->glz_data.usr);
    pthread_rwlock_destroy(&shared_dict->encode_lock);
    g_free(shared_dict);
}

bool image_encoders_compress_quic(ImageEncoders *enc, SpiceImage *dest,
                                  SpiceBitmap *src, compress_send_data_t* o_comp_data)
{
    QuicData *quic_data = &enc->quic_data;
    QuicContext *quic = enc->quic;
    volatile QuicImageType type;
    int size, stride;
    stat_start_time_t start_time;
    stat_start_time_init(&start_time, &enc->shared_data->quic_stat);

    COMPRESS_DEBUG("QUIC compress");

    switch (src->format) {
    case SPICE_BITMAP_FMT_32BIT:
        type = QUIC_IMAGE_TYPE_RGB32;
        break;
    case SPICE_BITMAP_FMT_RGBA:
        type = QUIC_IMAGE_TYPE_RGBA;
        break;
    case SPICE_BITMAP_FMT_16BIT:
        type = QUIC_IMAGE_TYPE_RGB16;
        break;
    case SPICE_BITMAP_FMT_24BIT:
        type = QUIC_IMAGE_TYPE_RGB24;
        break;
    default:
        return FALSE;
    }

    encoder_data_init(&quic_data->data);

    if (setjmp(quic_data->data.jmp_env)) {
        encoder_data_reset(&quic_data->data);
        return FALSE;
    }

    if (src->data->flags & SPICE_CHUNKS_FLAGS_UNSTABLE) {
        spice_chunks_linearize(src->data);
    }

    quic_data->data.u.lines_data.chunks = src->data;
    quic_data->data.u.lines_data.stride = src->stride;
    if ((src->flags & SPICE_BITMAP_FLAGS_TOP_DOWN)) {
        quic_data->data.u.lines_data.next = 0;
        quic_data->data.u.lines_data.reverse = 0;
        stride = src->stride;
    } else {
        quic_data->data.u.lines_data.next = src->data->num_chunks - 1;
        quic_data->data.u.lines_data.reverse = 1;
        stride = -src->stride;
    }
    size = quic_encode(quic, type, src->x, src->y, nullptr, 0, stride,
                       quic_data->data.bufs_head->buf.words,
                       G_N_ELEMENTS(quic_data->data.bufs_head->buf.words));

    // the compressed buffer is bigger than the original data
    if ((size << 2) > (src->y * src->stride)) {
        longjmp(quic_data->data.jmp_env, 1);
    }

    dest->descriptor.type = SPICE_IMAGE_TYPE_QUIC;
    dest->u.quic.data_size = size << 2;

    o_comp_data->comp_buf = quic_data->data.bufs_head;
    o_comp_data->comp_buf_size = size << 2;

    stat_compress_add(&enc->shared_data->quic_stat, start_time, src->stride * src->y,
                      o_comp_data->comp_buf_size);
    return TRUE;
}

static const LzImageType bitmap_fmt_to_lz_image_type[] = {
    LZ_IMAGE_TYPE_INVALID,
    LZ_IMAGE_TYPE_PLT1_LE,
    LZ_IMAGE_TYPE_PLT1_BE,
    LZ_IMAGE_TYPE_PLT4_LE,
    LZ_IMAGE_TYPE_PLT4_BE,
    LZ_IMAGE_TYPE_PLT8,
    LZ_IMAGE_TYPE_RGB16,
    LZ_IMAGE_TYPE_RGB24,
    LZ_IMAGE_TYPE_RGB32,
    LZ_IMAGE_TYPE_RGBA,
    LZ_IMAGE_TYPE_A8
};

bool image_encoders_compress_lz(ImageEncoders *enc,
                                SpiceImage *dest, SpiceBitmap *src,
                                compress_send_data_t* o_comp_data)
{
    LzData *lz_data = &enc->lz_data;
    LzContext *lz = enc->lz;
    LzImageType type = bitmap_fmt_to_lz_image_type[src->format];
    int size;            // size of the compressed data

    stat_start_time_t start_time;
    stat_start_time_init(&start_time, &enc->shared_data->lz_stat);

    COMPRESS_DEBUG("LZ LOCAL compress");

    encoder_data_init(&lz_data->data);

    if (setjmp(lz_data->data.jmp_env)) {
        encoder_data_reset(&lz_data->data);
        return FALSE;
    }

    lz_data->data.u.lines_data.chunks = src->data;
    lz_data->data.u.lines_data.stride = src->stride;
    lz_data->data.u.lines_data.next = 0;
    lz_data->data.u.lines_data.reverse = 0;

    size = lz_encode(lz, type, src->x, src->y,
                     !!(src->flags & SPICE_BITMAP_FLAGS_TOP_DOWN),
                     nullptr, 0, src->stride,
                     lz_data->data.bufs_head->buf.bytes,
                     sizeof(lz_data->data.bufs_head->buf));

    // the compressed buffer is bigger than the original data
    if (size > (src->y * src->stride)) {
        longjmp(lz_data->data.jmp_env, 1);
    }

    if (bitmap_fmt_is_rgb(src->format)) {
        dest->descriptor.type = SPICE_IMAGE_TYPE_LZ_RGB;
        dest->u.lz_rgb.data_size = size;

        o_comp_data->comp_buf = lz_data->data.bufs_head;
        o_comp_data->comp_buf_size = size;
    } else {
        /* masks are 1BIT bitmaps without palettes, but they are not compressed
         * (see fill_mask) */
        spice_assert(src->palette);
        dest->descriptor.type = SPICE_IMAGE_TYPE_LZ_PLT;
        dest->u.lz_plt.data_size = size;
        dest->u.lz_plt.flags = src->flags & SPICE_BITMAP_FLAGS_TOP_DOWN;
        dest->u.lz_plt.palette = src->palette;
        dest->u.lz_plt.palette_id = src->palette->unique;
        o_comp_data->comp_buf = lz_data->data.bufs_head;
        o_comp_data->comp_buf_size = size;

        o_comp_data->lzplt_palette = dest->u.lz_plt.palette;
    }

    stat_compress_add(&enc->shared_data->lz_stat, start_time, src->stride * src->y,
                      o_comp_data->comp_buf_size);
    return TRUE;
}

bool image_encoders_compress_jpeg(ImageEncoders *enc, SpiceImage *dest,
                                  SpiceBitmap *src, compress_send_data_t* o_comp_data)
{
    JpegData *jpeg_data = &enc->jpeg_data;
    LzData *lz_data = &enc->lz_data;
    JpegEncoderContext *jpeg = enc->jpeg;
    LzContext *lz = enc->lz;
    volatile JpegEncoderImageType jpeg_in_type;
    int jpeg_size = 0;
    volatile int has_alpha = FALSE;
    int alpha_lz_size = 0;
    int comp_head_filled;
    int comp_head_left;
    int stride;
    uint8_t *lz_out_start_byte;
    stat_start_time_t start_time;
    stat_start_time_init(&start_time, &enc->shared_data->jpeg_alpha_stat);

    COMPRESS_DEBUG("JPEG compress");

    switch (src->format) {
    case SPICE_BITMAP_FMT_16BIT:
        jpeg_in_type = JPEG_IMAGE_TYPE_RGB16;
        break;
    case SPICE_BITMAP_FMT_24BIT:
        jpeg_in_type = JPEG_IMAGE_TYPE_BGR24;
        break;
    case SPICE_BITMAP_FMT_32BIT:
        jpeg_in_type = JPEG_IMAGE_TYPE_BGRX32;
        break;
    case SPICE_BITMAP_FMT_RGBA:
        jpeg_in_type = JPEG_IMAGE_TYPE_BGRX32;
        has_alpha = TRUE;
        break;
    default:
        return FALSE;
    }

    encoder_data_init(&jpeg_data->data);

    if (setjmp(jpeg_data->data.jmp_env)) {
        encoder_data_reset(&jpeg_data->data);
        return FALSE;
    }

    if (src->data->flags & SPICE_CHUNKS_FLAGS_UNSTABLE) {
        spice_chunks_linearize(src->data);
    }

    jpeg_data->data.u.lines_data.chunks = src->data;
    jpeg_data->data.u.lines_data.stride = src->stride;
    if ((src->flags & SPICE_BITMAP_FLAGS_TOP_DOWN)) {
        jpeg_data->data.u.lines_data.next = 0;
        jpeg_data->data.u.lines_data.reverse = 0;
        stride = src->stride;
    } else {
        jpeg_data->data.u.lines_data.next = src->data->num_chunks - 1;
        jpeg_data->data.u.lines_data.reverse = 1;
        stride = -src->stride;
    }
    jpeg_size = jpeg_encode(jpeg, enc->jpeg_quality, jpeg_in_type,
                            src->x, src->y, nullptr,
                            0, stride, jpeg_data->data.bufs_head->buf.bytes,
                            sizeof(jpeg_data->data.bufs_head->buf));

    // the compressed buffer is bigger than the original data
    if (jpeg_size > (src->y * src->stride)) {
        longjmp(jpeg_data->data.jmp_env, 1);
    }

    if (!has_alpha) {
        dest->descriptor.type = SPICE_IMAGE_TYPE_JPEG;
        dest->u.jpeg.data_size = jpeg_size;

        o_comp_data->comp_buf = jpeg_data->data.bufs_head;
        o_comp_data->comp_buf_size = jpeg_size;
        o_comp_data->is_lossy = TRUE;

        stat_compress_add(&enc->shared_data->jpeg_stat, start_time, src->stride * src->y,
                          o_comp_data->comp_buf_size);
        return TRUE;
    }

    lz_data->data.bufs_head = jpeg_data->data.bufs_tail;
    lz_data->data.bufs_tail = lz_data->data.bufs_head;

    comp_head_filled = jpeg_size % sizeof(lz_data->data.bufs_head->buf);
    comp_head_left = sizeof(lz_data->data.bufs_head->buf) - comp_head_filled;
    lz_out_start_byte = lz_data->data.bufs_head->buf.bytes + comp_head_filled;

    lz_data->data.u.lines_data.chunks = src->data;
    lz_data->data.u.lines_data.stride = src->stride;
    lz_data->data.u.lines_data.next = 0;
    lz_data->data.u.lines_data.reverse = 0;

    alpha_lz_size = lz_encode(lz, LZ_IMAGE_TYPE_XXXA, src->x, src->y,
                               !!(src->flags & SPICE_BITMAP_FLAGS_TOP_DOWN),
                               nullptr, 0, src->stride,
                               lz_out_start_byte,
                               comp_head_left);

    // the compressed buffer is bigger than the original data
    if ((jpeg_size + alpha_lz_size) > (src->y * src->stride)) {
        longjmp(jpeg_data->data.jmp_env, 1);
    }

    dest->descriptor.type = SPICE_IMAGE_TYPE_JPEG_ALPHA;
    dest->u.jpeg_alpha.flags = 0;
    if (src->flags & SPICE_BITMAP_FLAGS_TOP_DOWN) {
        dest->u.jpeg_alpha.flags |= SPICE_JPEG_ALPHA_FLAGS_TOP_DOWN;
    }

    dest->u.jpeg_alpha.jpeg_size = jpeg_size;
    dest->u.jpeg_alpha.data_size = jpeg_size + alpha_lz_size;

    o_comp_data->comp_buf = jpeg_data->data.bufs_head;
    o_comp_data->comp_buf_size = jpeg_size + alpha_lz_size;
    o_comp_data->is_lossy = TRUE;
    stat_compress_add(&enc->shared_data->jpeg_alpha_stat, start_time, src->stride * src->y,
                      o_comp_data->comp_buf_size);
    return TRUE;
}

#ifdef USE_LZ4
bool image_encoders_compress_lz4(ImageEncoders *enc, SpiceImage *dest,
                                 SpiceBitmap *src, compress_send_data_t* o_comp_data)
{
    Lz4Data *lz4_data = &enc->lz4_data;
    Lz4EncoderContext *lz4 = enc->lz4;
    int lz4_size = 0;
    stat_start_time_t start_time;
    stat_start_time_init(&start_time, &enc->shared_data->lz4_stat);

    COMPRESS_DEBUG("LZ4 compress");

    encoder_data_init(&lz4_data->data);

    if (setjmp(lz4_data->data.jmp_env)) {
        encoder_data_reset(&lz4_data->data);
        return FALSE;
    }

    if (src->data->flags & SPICE_CHUNKS_FLAGS_UNSTABLE) {
        spice_chunks_linearize(src->data);
    }

    lz4_data->data.u.lines_data.chunks = src->data;
    lz4_data->data.u.lines_data.stride = src->stride;
    lz4_data->data.u.lines_data.next = 0;
    lz4_data->data.u.lines_data.reverse = 0;

    lz4_size = lz4_encode(lz4, src->y, src->stride, lz4_data->data.bufs_head->buf.bytes,
                          sizeof(lz4_data->data.bufs_head->buf),
                          src->flags & SPICE_BITMAP_FLAGS_TOP_DOWN, src->format);

    // the compressed buffer is bigger than the original data
    if (lz4_size > (src->y * src->stride)) {
        longjmp(lz4_data->data.jmp_env, 1);
    }

    dest->descriptor.type = SPICE_IMAGE_TYPE_LZ4;
    dest->u.lz4.data_size = lz4_size;

    o_comp_data->comp_buf = lz4_data->data.bufs_head;
    o_comp_data->comp_buf_size = lz4_size;

    stat_compress_add(&enc->shared_data->lz4_stat, start_time, src->stride * src->y,
                      o_comp_data->comp_buf_size);
    return TRUE;
}
#endif

/* if already exists, returns it. Otherwise allocates and adds it (1) to the ring tail
   in the channel (2) to the Drawable*/
static RedGlzDrawable *get_glz_drawable(ImageEncoders *enc, RedDrawable *red_drawable,
                                        GlzImageRetention *glz_retention)
{
    RedGlzDrawable *ret;
    RingItem *item, *next;

    // TODO - I don't really understand what's going on here, so doing the technical equivalent
    // now that we have multiple glz_dicts, so the only way to go from dcc to drawable glz is to go
    // over the glz_ring (unless adding some better data structure then a ring)
    SAFE_FOREACH(item, next, TRUE, &glz_retention->ring, ret, LINK_TO_GLZ(item)) {
        if (ret->encoders == enc) {
            return ret;
        }
    }

    ret = g_new0(RedGlzDrawable, 1);

    ret->encoders = enc;
    ret->red_drawable.reset(red_drawable);
    ret->has_drawable = TRUE;
    ret->instances_count = 0;
    ring_init(&ret->instances);

    ring_item_init(&ret->link);
    ring_item_init(&ret->drawable_link);
    ring_add_before(&ret->link, &enc->glz_drawables);
    ring_add(&glz_retention->ring, &ret->drawable_link);
    enc->shared_data->glz_drawable_count++;
    return ret;
}

/* allocates new instance and adds it to instances in the given drawable.
   NOTE - the caller should set the glz_instance returned by the encoder by itself.*/
static GlzDrawableInstanceItem *add_glz_drawable_instance(RedGlzDrawable *glz_drawable)
{
    spice_assert(glz_drawable->instances_count < MAX_GLZ_DRAWABLE_INSTANCES);
    // NOTE: We assume the additions are performed consecutively, without removals in the middle
    GlzDrawableInstanceItem *ret = glz_drawable->instances_pool + glz_drawable->instances_count;
    glz_drawable->instances_count++;

    ring_item_init(&ret->free_link);
    ring_item_init(&ret->glz_link);
    ring_add(&glz_drawable->instances, &ret->glz_link);
    ret->context = nullptr;
    ret->glz_drawable = glz_drawable;

    return ret;
}

#define MIN_GLZ_SIZE_FOR_ZLIB 100

bool image_encoders_compress_glz(ImageEncoders *enc,
                                 SpiceImage *dest, SpiceBitmap *src,
                                 RedDrawable *red_drawable,
                                 GlzImageRetention *glz_retention,
                                 compress_send_data_t* o_comp_data,
                                 gboolean enable_zlib_glz_wrap)
{
    stat_start_time_t start_time;
    stat_start_time_init(&start_time, &enc->shared_data->zlib_glz_stat);
    spice_assert(bitmap_fmt_is_rgb(src->format));
    GlzData *glz_data = &enc->glz_data;
    ZlibData *zlib_data;
    LzImageType type = bitmap_fmt_to_lz_image_type[src->format];
    RedGlzDrawable *glz_drawable;
    GlzDrawableInstanceItem *glz_drawable_instance;
    int glz_size;
    int zlib_size;

    COMPRESS_DEBUG("LZ global compress fmt=%d", src->format);

    if ((src->x * src->y) >= glz_enc_dictionary_get_size(enc->glz_dict->dict)) {
        return FALSE;
    }

    pthread_rwlock_rdlock(&enc->glz_dict->encode_lock);
    /* using the global dictionary only if it is not frozen */
    if (enc->glz_dict->migrate_freeze) {
        pthread_rwlock_unlock(&enc->glz_dict->encode_lock);
        return FALSE;
    }

    encoder_data_init(&glz_data->data);

    glz_drawable = get_glz_drawable(enc, red_drawable, glz_retention);
    glz_drawable_instance = add_glz_drawable_instance(glz_drawable);

    glz_data->data.u.lines_data.chunks = src->data;
    glz_data->data.u.lines_data.stride = src->stride;
    glz_data->data.u.lines_data.next = 0;
    glz_data->data.u.lines_data.reverse = 0;

    glz_size = glz_encode(enc->glz, type, src->x, src->y,
                          (src->flags & SPICE_BITMAP_FLAGS_TOP_DOWN), nullptr, 0,
                          src->stride, glz_data->data.bufs_head->buf.bytes,
                          sizeof(glz_data->data.bufs_head->buf),
                          glz_drawable_instance,
                          &glz_drawable_instance->context);

    stat_compress_add(&enc->shared_data->glz_stat, start_time, src->stride * src->y, glz_size);

    if (!enable_zlib_glz_wrap || (glz_size < MIN_GLZ_SIZE_FOR_ZLIB)) {
        goto glz;
    }
    if (enc->zlib == nullptr) {
        enc->zlib = zlib_encoder_create(&enc->zlib_data.usr, ZLIB_DEFAULT_COMPRESSION_LEVEL);
        if (enc->zlib == nullptr) {
            g_warning("creating zlib encoder failed");
            goto glz;
        }
    }
    stat_start_time_init(&start_time, &enc->shared_data->zlib_glz_stat);
    zlib_data = &enc->zlib_data;

    encoder_data_init(&zlib_data->data);

    zlib_data->data.u.compressed_data.next = glz_data->data.bufs_head;
    zlib_data->data.u.compressed_data.size_left = glz_size;

    zlib_size = zlib_encode(enc->zlib, enc->zlib_level,
                            glz_size, zlib_data->data.bufs_head->buf.bytes,
                            sizeof(zlib_data->data.bufs_head->buf));

    // the compressed buffer is bigger than the original data
    if (zlib_size >= glz_size) {
        encoder_data_reset(&zlib_data->data);
        goto glz;
    } else {
        encoder_data_reset(&glz_data->data);
    }

    dest->descriptor.type = SPICE_IMAGE_TYPE_ZLIB_GLZ_RGB;
    dest->u.zlib_glz.glz_data_size = glz_size;
    dest->u.zlib_glz.data_size = zlib_size;

    o_comp_data->comp_buf = zlib_data->data.bufs_head;
    o_comp_data->comp_buf_size = zlib_size;

    stat_compress_add(&enc->shared_data->zlib_glz_stat, start_time, glz_size, zlib_size);
    pthread_rwlock_unlock(&enc->glz_dict->encode_lock);
    return TRUE;

glz:
    pthread_rwlock_unlock(&enc->glz_dict->encode_lock);

    dest->descriptor.type = SPICE_IMAGE_TYPE_GLZ_RGB;
    dest->u.lz_rgb.data_size = glz_size;

    o_comp_data->comp_buf = glz_data->data.bufs_head;
    o_comp_data->comp_buf_size = glz_size;

    return TRUE;
}

void image_encoder_shared_init(ImageEncoderSharedData *shared_data)
{
    clockid_t stat_clock = CLOCK_THREAD_CPUTIME_ID;

    stat_compress_init(&shared_data->off_stat, "off", stat_clock);
    stat_compress_init(&shared_data->lz_stat, "lz", stat_clock);
    stat_compress_init(&shared_data->glz_stat, "glz", stat_clock);
    stat_compress_init(&shared_data->quic_stat, "quic", stat_clock);
    stat_compress_init(&shared_data->jpeg_stat, "jpeg", stat_clock);
    stat_compress_init(&shared_data->zlib_glz_stat, "zlib", stat_clock);
    stat_compress_init(&shared_data->jpeg_alpha_stat, "jpeg_alpha", stat_clock);
    stat_compress_init(&shared_data->lz4_stat, "lz4", stat_clock);
}

void image_encoder_shared_stat_reset(ImageEncoderSharedData *shared_data)
{
    stat_reset(&shared_data->off_stat);
    stat_reset(&shared_data->quic_stat);
    stat_reset(&shared_data->lz_stat);
    stat_reset(&shared_data->glz_stat);
    stat_reset(&shared_data->jpeg_stat);
    stat_reset(&shared_data->zlib_glz_stat);
    stat_reset(&shared_data->jpeg_alpha_stat);
    stat_reset(&shared_data->lz4_stat);
}

#define STAT_FMT "%s\t%8u\t%13.8g\t%12.8g\t%12.8g"

#ifdef COMPRESS_STAT
static void stat_print_one(const char *name, const stat_info_t *stat)
{
    spice_info(STAT_FMT, name, stat->count,
               stat_byte_to_mega(stat->orig_size),
               stat_byte_to_mega(stat->comp_size),
               stat_cpu_time_to_sec(stat->total));
}

static void stat_sum(stat_info_t *total, const stat_info_t *stat)
{
    total->count += stat->count;
    total->orig_size += stat->orig_size;
    total->comp_size += stat->comp_size;
    total->total += stat->total;
}
#endif

void image_encoder_shared_stat_print(const ImageEncoderSharedData *shared_data)
{
#ifdef COMPRESS_STAT
    /* sum all statistics */
    stat_info_t total = {
        .count = 0,
        .total = 0,
        .orig_size = 0,
        .comp_size = 0,
    };
    stat_sum(&total, &shared_data->off_stat);
    stat_sum(&total, &shared_data->quic_stat);
    stat_sum(&total, &shared_data->glz_stat);
    stat_sum(&total, &shared_data->lz_stat);
    stat_sum(&total, &shared_data->jpeg_stat);
    stat_sum(&total, &shared_data->jpeg_alpha_stat);
    stat_sum(&total, &shared_data->lz4_stat);

    /* fix for zlib glz */
    total.total += shared_data->zlib_glz_stat.total;
    if (shared_data->zlib_glz_stat.count) {
        total.comp_size = total.comp_size - shared_data->glz_stat.comp_size +
                          shared_data->zlib_glz_stat.comp_size;
    }

    spice_info("Method   \t  count  \torig_size(MB)\tenc_size(MB)\tenc_time(s)");
    stat_print_one("OFF      ", &shared_data->off_stat);
    stat_print_one("QUIC     ", &shared_data->quic_stat);
    stat_print_one("GLZ      ", &shared_data->glz_stat);
    stat_print_one("ZLIB GLZ ", &shared_data->zlib_glz_stat);
    stat_print_one("LZ       ", &shared_data->lz_stat);
    stat_print_one("JPEG     ", &shared_data->jpeg_stat);
    stat_print_one("JPEG-RGBA", &shared_data->jpeg_alpha_stat);
    stat_print_one("LZ4      ", &shared_data->lz4_stat);
    spice_info("-------------------------------------------------------------------");
    stat_print_one("Total    ", &total);
#endif
}
