// Copyright (c) the JPEG XL Project Authors. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

#include <jxl/codestream_header.h>
#include <jxl/decode.h>
#include <jxl/encode.h>
#include <jxl/resizable_parallel_runner.h>
#include <jxl/types.h>

#define GDK_PIXBUF_ENABLE_BACKEND
#include <gdk-pixbuf/gdk-pixbuf.h>
#undef GDK_PIXBUF_ENABLE_BACKEND

G_BEGIN_DECLS

// Information about a single frame.
typedef struct {
  uint64_t duration_ms;
  GdkPixbuf *data;
  gboolean decoded;
} GdkPixbufJxlAnimationFrame;

// Represent a whole JPEG XL animation; all its fields are owned; as a GObject,
// the Animation struct itself is reference counted (as are the GdkPixbufs for
// individual frames).
struct _GdkPixbufJxlAnimation {
  GdkPixbufAnimation parent_instance;

  // GDK interface implementation callbacks.
  GdkPixbufModuleSizeFunc image_size_callback;
  GdkPixbufModulePreparedFunc pixbuf_prepared_callback;
  GdkPixbufModuleUpdatedFunc area_updated_callback;
  gpointer user_data;

  // All frames known so far; a frame is added when the JXL_DEC_FRAME event is
  // received from the decoder; initially frame.decoded is FALSE, until
  // the JXL_DEC_IMAGE event is received.
  GArray *frames;

  // JPEG XL decoder and related structures.
  JxlParallelRunner *parallel_runner;
  JxlDecoder *decoder;
  JxlPixelFormat pixel_format;

  // Decoding is `done` when JXL_DEC_SUCCESS is received; calling
  // load_increment afterwards gives an error.
  gboolean done;

  // Image information.
  size_t xsize;
  size_t ysize;
  gboolean alpha_premultiplied;
  gboolean has_animation;
  gboolean has_alpha;
  uint64_t total_duration_ms;
  uint64_t tick_duration_us;
  uint64_t repetition_count;  // 0 = loop forever

  gchar *icc_base64;
};

#define GDK_TYPE_PIXBUF_JXL_ANIMATION (gdk_pixbuf_jxl_animation_get_type())
G_DECLARE_FINAL_TYPE(GdkPixbufJxlAnimation, gdk_pixbuf_jxl_animation, GDK,
                     JXL_ANIMATION, GdkPixbufAnimation);

G_DEFINE_TYPE(GdkPixbufJxlAnimation, gdk_pixbuf_jxl_animation,
              GDK_TYPE_PIXBUF_ANIMATION);

// Iterator to a given point in time in the animation; contains a pointer to the
// full animation.
struct _GdkPixbufJxlAnimationIter {
  GdkPixbufAnimationIter parent_instance;
  GdkPixbufJxlAnimation *animation;
  size_t current_frame;
  uint64_t time_offset;
};

#define GDK_TYPE_PIXBUF_JXL_ANIMATION_ITER \
  (gdk_pixbuf_jxl_animation_iter_get_type())
G_DECLARE_FINAL_TYPE(GdkPixbufJxlAnimationIter, gdk_pixbuf_jxl_animation_iter,
                     GDK, JXL_ANIMATION_ITER, GdkPixbufAnimationIter);
G_DEFINE_TYPE(GdkPixbufJxlAnimationIter, gdk_pixbuf_jxl_animation_iter,
              GDK_TYPE_PIXBUF_ANIMATION_ITER);

static void gdk_pixbuf_jxl_animation_init(GdkPixbufJxlAnimation *obj) {
  // Suppress "unused function" warnings.
  (void)glib_autoptr_cleanup_GdkPixbufJxlAnimation;
  (void)GDK_JXL_ANIMATION;
  (void)GDK_IS_JXL_ANIMATION;
}

static gboolean gdk_pixbuf_jxl_animation_is_static_image(
    GdkPixbufAnimation *anim) {
  GdkPixbufJxlAnimation *jxl_anim = (GdkPixbufJxlAnimation *)anim;
  return !jxl_anim->has_animation;
}

static GdkPixbuf *gdk_pixbuf_jxl_animation_get_static_image(
    GdkPixbufAnimation *anim) {
  GdkPixbufJxlAnimation *jxl_anim = (GdkPixbufJxlAnimation *)anim;
  if (jxl_anim->frames == NULL || jxl_anim->frames->len == 0) return NULL;
  GdkPixbufJxlAnimationFrame *frame =
      &g_array_index(jxl_anim->frames, GdkPixbufJxlAnimationFrame, 0);
  return frame->decoded ? frame->data : NULL;
}

static void gdk_pixbuf_jxl_animation_get_size(GdkPixbufAnimation *anim,
                                              int *width, int *height) {
  GdkPixbufJxlAnimation *jxl_anim = (GdkPixbufJxlAnimation *)anim;
  if (width) *width = jxl_anim->xsize;
  if (height) *height = jxl_anim->ysize;
}

G_GNUC_BEGIN_IGNORE_DEPRECATIONS
static gboolean gdk_pixbuf_jxl_animation_iter_advance(
    GdkPixbufAnimationIter *iter, const GTimeVal *current_time);

static GdkPixbufAnimationIter *gdk_pixbuf_jxl_animation_get_iter(
    GdkPixbufAnimation *anim, const GTimeVal *start_time) {
  GdkPixbufJxlAnimationIter *iter =
      g_object_new(GDK_TYPE_PIXBUF_JXL_ANIMATION_ITER, NULL);
  iter->animation = (GdkPixbufJxlAnimation *)anim;
  iter->time_offset = start_time->tv_sec * 1000ULL + start_time->tv_usec / 1000;
  g_object_ref(iter->animation);
  gdk_pixbuf_jxl_animation_iter_advance((GdkPixbufAnimationIter *)iter,
                                        start_time);
  return (GdkPixbufAnimationIter *)iter;
}
G_GNUC_END_IGNORE_DEPRECATIONS

static void gdk_pixbuf_jxl_animation_finalize(GObject *obj) {
  GdkPixbufJxlAnimation *decoder_state = (GdkPixbufJxlAnimation *)obj;
  if (decoder_state->frames != NULL) {
    for (size_t i = 0; i < decoder_state->frames->len; i++) {
      g_object_unref(
          g_array_index(decoder_state->frames, GdkPixbufJxlAnimationFrame, i)
              .data);
    }
    g_array_free(decoder_state->frames, /*free_segment=*/TRUE);
  }
  JxlResizableParallelRunnerDestroy(decoder_state->parallel_runner);
  JxlDecoderDestroy(decoder_state->decoder);
  g_free(decoder_state->icc_base64);
}

static void gdk_pixbuf_jxl_animation_class_init(
    GdkPixbufJxlAnimationClass *klass) {
  G_OBJECT_CLASS(klass)->finalize = gdk_pixbuf_jxl_animation_finalize;
  klass->parent_class.is_static_image =
      gdk_pixbuf_jxl_animation_is_static_image;
  klass->parent_class.get_static_image =
      gdk_pixbuf_jxl_animation_get_static_image;
  klass->parent_class.get_size = gdk_pixbuf_jxl_animation_get_size;
  klass->parent_class.get_iter = gdk_pixbuf_jxl_animation_get_iter;
}

static void gdk_pixbuf_jxl_animation_iter_init(GdkPixbufJxlAnimationIter *obj) {
  (void)glib_autoptr_cleanup_GdkPixbufJxlAnimationIter;
  (void)GDK_JXL_ANIMATION_ITER;
  (void)GDK_IS_JXL_ANIMATION_ITER;
}

static int gdk_pixbuf_jxl_animation_iter_get_delay_time(
    GdkPixbufAnimationIter *iter) {
  GdkPixbufJxlAnimationIter *jxl_iter = (GdkPixbufJxlAnimationIter *)iter;
  if (jxl_iter->animation->frames->len <= jxl_iter->current_frame) {
    return 0;
  }
  return g_array_index(jxl_iter->animation->frames, GdkPixbufJxlAnimationFrame,
                       jxl_iter->current_frame)
      .duration_ms;
}

static GdkPixbuf *gdk_pixbuf_jxl_animation_iter_get_pixbuf(
    GdkPixbufAnimationIter *iter) {
  GdkPixbufJxlAnimationIter *jxl_iter = (GdkPixbufJxlAnimationIter *)iter;
  if (jxl_iter->animation->frames->len <= jxl_iter->current_frame) {
    return NULL;
  }
  return g_array_index(jxl_iter->animation->frames, GdkPixbufJxlAnimationFrame,
                       jxl_iter->current_frame)
      .data;
}

static gboolean gdk_pixbuf_jxl_animation_iter_on_currently_loading_frame(
    GdkPixbufAnimationIter *iter) {
  GdkPixbufJxlAnimationIter *jxl_iter = (GdkPixbufJxlAnimationIter *)iter;
  if (jxl_iter->animation->frames->len <= jxl_iter->current_frame) {
    return TRUE;
  }
  return !g_array_index(jxl_iter->animation->frames, GdkPixbufJxlAnimationFrame,
                        jxl_iter->current_frame)
              .decoded;
}

G_GNUC_BEGIN_IGNORE_DEPRECATIONS
static gboolean gdk_pixbuf_jxl_animation_iter_advance(
    GdkPixbufAnimationIter *iter, const GTimeVal *current_time) {
  GdkPixbufJxlAnimationIter *jxl_iter = (GdkPixbufJxlAnimationIter *)iter;
  size_t old_frame = jxl_iter->current_frame;

  uint64_t current_time_ms = current_time->tv_sec * 1000ULL +
                             current_time->tv_usec / 1000 -
                             jxl_iter->time_offset;

  if (jxl_iter->animation->frames->len == 0) {
    jxl_iter->current_frame = 0;
  } else if (!jxl_iter->animation->done &&
             current_time_ms >= jxl_iter->animation->total_duration_ms) {
    jxl_iter->current_frame = jxl_iter->animation->frames->len - 1;
  } else if (jxl_iter->animation->repetition_count != 0 &&
             current_time_ms > jxl_iter->animation->repetition_count *
                                   jxl_iter->animation->total_duration_ms) {
    jxl_iter->current_frame = jxl_iter->animation->frames->len - 1;
  } else {
    uint64_t total_duration_ms = jxl_iter->animation->total_duration_ms;
    // Guard against divide-by-0 in malicious files.
    if (total_duration_ms == 0) total_duration_ms = 1;
    uint64_t loop_offset = current_time_ms % total_duration_ms;
    jxl_iter->current_frame = 0;
    while (TRUE) {
      uint64_t duration =
          g_array_index(jxl_iter->animation->frames, GdkPixbufJxlAnimationFrame,
                        jxl_iter->current_frame)
              .duration_ms;
      if (duration >= loop_offset) {
        break;
      }
      loop_offset -= duration;
      jxl_iter->current_frame++;
    }
  }

  return old_frame != jxl_iter->current_frame;
}
G_GNUC_END_IGNORE_DEPRECATIONS

static void gdk_pixbuf_jxl_animation_iter_finalize(GObject *obj) {
  GdkPixbufJxlAnimationIter *iter = (GdkPixbufJxlAnimationIter *)obj;
  g_object_unref(iter->animation);
}

static void gdk_pixbuf_jxl_animation_iter_class_init(
    GdkPixbufJxlAnimationIterClass *klass) {
  G_OBJECT_CLASS(klass)->finalize = gdk_pixbuf_jxl_animation_iter_finalize;
  klass->parent_class.get_delay_time =
      gdk_pixbuf_jxl_animation_iter_get_delay_time;
  klass->parent_class.get_pixbuf = gdk_pixbuf_jxl_animation_iter_get_pixbuf;
  klass->parent_class.on_currently_loading_frame =
      gdk_pixbuf_jxl_animation_iter_on_currently_loading_frame;
  klass->parent_class.advance = gdk_pixbuf_jxl_animation_iter_advance;
}

G_END_DECLS

static gpointer begin_load(GdkPixbufModuleSizeFunc size_func,
                           GdkPixbufModulePreparedFunc prepare_func,
                           GdkPixbufModuleUpdatedFunc update_func,
                           gpointer user_data, GError **error) {
  GdkPixbufJxlAnimation *decoder_state =
      g_object_new(GDK_TYPE_PIXBUF_JXL_ANIMATION, NULL);
  if (decoder_state == NULL) {
    g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
                "Creation of the animation state failed");
    return NULL;
  }
  decoder_state->image_size_callback = size_func;
  decoder_state->pixbuf_prepared_callback = prepare_func;
  decoder_state->area_updated_callback = update_func;
  decoder_state->user_data = user_data;
  decoder_state->frames =
      g_array_new(/*zero_terminated=*/FALSE, /*clear_=*/TRUE,
                  sizeof(GdkPixbufJxlAnimationFrame));

  if (decoder_state->frames == NULL) {
    g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
                "Creation of the frame array failed");
    goto cleanup;
  }

  if (!(decoder_state->parallel_runner =
            JxlResizableParallelRunnerCreate(NULL))) {
    g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
                "Creation of the JXL parallel runner failed");
    goto cleanup;
  }

  if (!(decoder_state->decoder = JxlDecoderCreate(NULL))) {
    g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
                "Creation of the JXL decoder failed");
    goto cleanup;
  }

  JxlDecoderStatus status;

  if ((status = JxlDecoderSetParallelRunner(
           decoder_state->decoder, JxlResizableParallelRunner,
           decoder_state->parallel_runner)) != JXL_DEC_SUCCESS) {
    g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
                "JxlDecoderSetParallelRunner failed: %x", status);
    goto cleanup;
  }
  if ((status = JxlDecoderSubscribeEvents(
           decoder_state->decoder, JXL_DEC_BASIC_INFO | JXL_DEC_COLOR_ENCODING |
                                       JXL_DEC_FULL_IMAGE | JXL_DEC_FRAME)) !=
      JXL_DEC_SUCCESS) {
    g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
                "JxlDecoderSubscribeEvents failed: %x", status);
    goto cleanup;
  }

  decoder_state->pixel_format.data_type = JXL_TYPE_FLOAT;
  decoder_state->pixel_format.endianness = JXL_NATIVE_ENDIAN;

  return decoder_state;
cleanup:
  JxlResizableParallelRunnerDestroy(decoder_state->parallel_runner);
  JxlDecoderDestroy(decoder_state->decoder);
  g_object_unref(decoder_state);
  return NULL;
}

static gboolean stop_load(gpointer context, GError **error) {
  g_object_unref(context);
  return TRUE;
}

static gboolean load_increment(gpointer context, const guchar *buf, guint size,
                               GError **error) {
  GdkPixbufJxlAnimation *decoder_state = context;
  if (decoder_state->done == TRUE) {
    g_warning_once("Trailing data found at end of JXL file");
    return TRUE;
  }

  JxlDecoderStatus status;

  if ((status = JxlDecoderSetInput(decoder_state->decoder, buf, size)) !=
      JXL_DEC_SUCCESS) {
    // Should never happen if things are done properly.
    g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
                "JXL decoder logic error: %x", status);
    return FALSE;
  }

  for (;;) {
    status = JxlDecoderProcessInput(decoder_state->decoder);
    switch (status) {
      case JXL_DEC_NEED_MORE_INPUT: {
        JxlDecoderReleaseInput(decoder_state->decoder);
        return TRUE;
      }

      case JXL_DEC_BASIC_INFO: {
        JxlBasicInfo info;
        if (JxlDecoderGetBasicInfo(decoder_state->decoder, &info) !=
            JXL_DEC_SUCCESS) {
          g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
                      "JXLDecoderGetBasicInfo failed");
          return FALSE;
        }
        decoder_state->pixel_format.num_channels = info.alpha_bits > 0 ? 4 : 3;
        decoder_state->alpha_premultiplied = info.alpha_premultiplied;
        decoder_state->xsize = info.xsize;
        decoder_state->ysize = info.ysize;
        decoder_state->has_animation = info.have_animation;
        decoder_state->has_alpha = info.alpha_bits > 0;
        if (info.have_animation) {
          decoder_state->repetition_count = info.animation.num_loops;
          decoder_state->tick_duration_us = 1000000ULL *
                                            info.animation.tps_denominator /
                                            info.animation.tps_numerator;
        }
        gint width = info.xsize;
        gint height = info.ysize;
        if (decoder_state->image_size_callback) {
          decoder_state->image_size_callback(&width, &height,
                                             decoder_state->user_data);
        }

        // GDK convention for signaling being interested only in the basic info.
        if (width == 0 || height == 0) {
          decoder_state->done = TRUE;
          return TRUE;
        }

        // Set an appropriate number of threads for the image size.
        JxlResizableParallelRunnerSetThreads(
            decoder_state->parallel_runner,
            JxlResizableParallelRunnerSuggestThreads(info.xsize, info.ysize));
        break;
      }

      case JXL_DEC_COLOR_ENCODING: {
        // Get the ICC color profile of the pixel data
        gpointer icc_buff;
        size_t icc_size;
        JxlColorEncoding color_encoding;
        if (JXL_DEC_SUCCESS == JxlDecoderGetColorAsEncodedProfile(
                                   decoder_state->decoder,
                                   JXL_COLOR_PROFILE_TARGET_ORIGINAL,
                                   &color_encoding)) {
          // we don't check the return status here because it's not a problem if
          // this fails
          JxlDecoderSetPreferredColorProfile(decoder_state->decoder,
                                             &color_encoding);
        }
        if (JXL_DEC_SUCCESS != JxlDecoderGetICCProfileSize(
                                   decoder_state->decoder,
                                   JXL_COLOR_PROFILE_TARGET_DATA, &icc_size)) {
          g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
                      "JxlDecoderGetICCProfileSize failed");
          return FALSE;
        }
        if (!(icc_buff = g_malloc(icc_size))) {
          g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
                      "Allocating ICC profile failed");
          return FALSE;
        }
        if (JXL_DEC_SUCCESS !=
            JxlDecoderGetColorAsICCProfile(decoder_state->decoder,
                                           JXL_COLOR_PROFILE_TARGET_DATA,
                                           icc_buff, icc_size)) {
          g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
                      "JxlDecoderGetColorAsICCProfile failed");
          g_free(icc_buff);
          return FALSE;
        }
        decoder_state->icc_base64 = g_base64_encode(icc_buff, icc_size);
        g_free(icc_buff);
        if (!decoder_state->icc_base64) {
          g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
                      "Allocating ICC profile base64 string failed");
          return FALSE;
        }

        break;
      }

      case JXL_DEC_FRAME: {
        // TODO(veluca): support rescaling.
        JxlFrameHeader frame_header;
        if (JxlDecoderGetFrameHeader(decoder_state->decoder, &frame_header) !=
            JXL_DEC_SUCCESS) {
          g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
                      "Failed to retrieve frame info");
          return FALSE;
        }

        {
          GdkPixbufJxlAnimationFrame frame;
          frame.decoded = FALSE;
          frame.duration_ms =
              frame_header.duration * decoder_state->tick_duration_us / 1000;
          decoder_state->total_duration_ms += frame.duration_ms;
          frame.data =
              gdk_pixbuf_new(GDK_COLORSPACE_RGB, decoder_state->has_alpha,
                             /*bits_per_sample=*/8, decoder_state->xsize,
                             decoder_state->ysize);
          if (frame.data == NULL) {
            g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
                        "Failed to allocate output pixel buffer");
            return FALSE;
          }
          gdk_pixbuf_set_option(frame.data, "icc-profile",
                                decoder_state->icc_base64);
          decoder_state->pixel_format.align =
              gdk_pixbuf_get_rowstride(frame.data);
          decoder_state->pixel_format.data_type = JXL_TYPE_UINT8;
          g_array_append_val(decoder_state->frames, frame);
        }
        if (decoder_state->pixbuf_prepared_callback &&
            decoder_state->frames->len == 1) {
          decoder_state->pixbuf_prepared_callback(
              g_array_index(decoder_state->frames, GdkPixbufJxlAnimationFrame,
                            0)
                  .data,
              decoder_state->has_animation ? (GdkPixbufAnimation *)decoder_state
                                           : NULL,
              decoder_state->user_data);
        }
        break;
      }

      case JXL_DEC_NEED_IMAGE_OUT_BUFFER: {
        GdkPixbuf *output =
            g_array_index(decoder_state->frames, GdkPixbufJxlAnimationFrame,
                          decoder_state->frames->len - 1)
                .data;
        decoder_state->pixel_format.align = gdk_pixbuf_get_rowstride(output);
        guint size;
        guchar *dst = gdk_pixbuf_get_pixels_with_length(output, &size);
        if (JXL_DEC_SUCCESS != JxlDecoderSetImageOutBuffer(
                                   decoder_state->decoder,
                                   &decoder_state->pixel_format, dst, size)) {
          g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
                      "JxlDecoderSetImageOutBuffer failed");
          return FALSE;
        }
        break;
      }

      case JXL_DEC_FULL_IMAGE: {
        // TODO(veluca): consider doing partial updates.
        if (decoder_state->area_updated_callback) {
          GdkPixbuf *output = g_array_index(decoder_state->frames,
                                            GdkPixbufJxlAnimationFrame, 0)
                                  .data;
          decoder_state->area_updated_callback(
              output, 0, 0, gdk_pixbuf_get_width(output),
              gdk_pixbuf_get_height(output), decoder_state->user_data);
        }
        g_array_index(decoder_state->frames, GdkPixbufJxlAnimationFrame,
                      decoder_state->frames->len - 1)
            .decoded = TRUE;
        break;
      }

      case JXL_DEC_SUCCESS: {
        decoder_state->done = TRUE;
        return TRUE;
      }

      default: {
        g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
                    "Unexpected JxlDecoderProcessInput return code: %x",
                    status);
        return FALSE;
      }
    }
  }
  return TRUE;
}

static gboolean jxl_is_save_option_supported(const gchar *option_key) {
  if (g_strcmp0(option_key, "quality") == 0) {
    return TRUE;
  }

  return FALSE;
}

static gboolean jxl_image_saver(FILE *f, GdkPixbuf *pixbuf, gchar **keys,
                                gchar **values, GError **error) {
  long quality = 90; /* default; must be between 0 and 100 */
  double distance;
  gboolean save_alpha;
  JxlEncoder *encoder;
  void *parallel_runner;
  JxlEncoderFrameSettings *frame_settings;
  JxlBasicInfo output_info;
  JxlPixelFormat pixel_format;
  JxlColorEncoding color_profile;
  JxlEncoderStatus status;

  GByteArray *compressed;
  size_t offset = 0;
  uint8_t *next_out;
  size_t avail_out;

  if (f == NULL || pixbuf == NULL) {
    return FALSE;
  }

  if (keys && *keys) {
    gchar **kiter = keys;
    gchar **viter = values;

    while (*kiter) {
      if (strcmp(*kiter, "quality") == 0) {
        char *endptr = NULL;
        quality = strtol(*viter, &endptr, 10);

        if (endptr == *viter) {
          g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_BAD_OPTION,
                      "JXL quality must be a value between 0 and 100; value "
                      "\"%s\" could not be parsed.",
                      *viter);

          return FALSE;
        }

        if (quality < 0 || quality > 100) {
          g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_BAD_OPTION,
                      "JXL quality must be a value between 0 and 100; value "
                      "\"%ld\" is not allowed.",
                      quality);

          return FALSE;
        }
      } else {
        g_warning("Unrecognized parameter (%s) passed to JXL saver.", *kiter);
      }

      ++kiter;
      ++viter;
    }
  }

  if (gdk_pixbuf_get_bits_per_sample(pixbuf) != 8) {
    g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_UNKNOWN_TYPE,
                "Sorry, only 8bit images are supported by this JXL saver");
    return FALSE;
  }

  JxlEncoderInitBasicInfo(&output_info);
  output_info.have_container = JXL_FALSE;
  output_info.xsize = gdk_pixbuf_get_width(pixbuf);
  output_info.ysize = gdk_pixbuf_get_height(pixbuf);
  output_info.bits_per_sample = 8;
  output_info.orientation = JXL_ORIENT_IDENTITY;
  output_info.num_color_channels = 3;

  if (output_info.xsize == 0 || output_info.ysize == 0) {
    g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_CORRUPT_IMAGE,
                "Empty image, nothing to save");
    return FALSE;
  }

  save_alpha = gdk_pixbuf_get_has_alpha(pixbuf);

  pixel_format.data_type = JXL_TYPE_UINT8;
  pixel_format.endianness = JXL_NATIVE_ENDIAN;
  pixel_format.align = gdk_pixbuf_get_rowstride(pixbuf);

  if (save_alpha) {
    if (gdk_pixbuf_get_n_channels(pixbuf) != 4) {
      g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_UNKNOWN_TYPE,
                  "Unsupported number of channels");
      return FALSE;
    }

    output_info.num_extra_channels = 1;
    output_info.alpha_bits = 8;
    pixel_format.num_channels = 4;
  } else {
    if (gdk_pixbuf_get_n_channels(pixbuf) != 3) {
      g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_UNKNOWN_TYPE,
                  "Unsupported number of channels");
      return FALSE;
    }

    output_info.num_extra_channels = 0;
    output_info.alpha_bits = 0;
    pixel_format.num_channels = 3;
  }

  encoder = JxlEncoderCreate(NULL);
  if (!encoder) {
    g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
                "Creation of the JXL encoder failed");
    return FALSE;
  }

  parallel_runner = JxlResizableParallelRunnerCreate(NULL);
  if (!parallel_runner) {
    JxlEncoderDestroy(encoder);
    g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
                "Creation of the JXL decoder failed");
    return FALSE;
  }

  JxlResizableParallelRunnerSetThreads(
      parallel_runner, JxlResizableParallelRunnerSuggestThreads(
                           output_info.xsize, output_info.ysize));

  status = JxlEncoderSetParallelRunner(encoder, JxlResizableParallelRunner,
                                       parallel_runner);
  if (status != JXL_ENC_SUCCESS) {
    JxlResizableParallelRunnerDestroy(parallel_runner);
    JxlEncoderDestroy(encoder);
    g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
                "JxlDecoderSetParallelRunner failed: %x", status);
    return FALSE;
  }

  if (quality > 99) {
    output_info.uses_original_profile = JXL_TRUE;
    distance = 0;
  } else {
    output_info.uses_original_profile = JXL_FALSE;
    distance = JxlEncoderDistanceFromQuality((float)quality);
  }

  status = JxlEncoderSetBasicInfo(encoder, &output_info);
  if (status != JXL_ENC_SUCCESS) {
    JxlResizableParallelRunnerDestroy(parallel_runner);
    JxlEncoderDestroy(encoder);
    g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
                "JxlEncoderSetBasicInfo failed: %x", status);
    return FALSE;
  }

  JxlColorEncodingSetToSRGB(&color_profile, JXL_FALSE);
  status = JxlEncoderSetColorEncoding(encoder, &color_profile);
  if (status != JXL_ENC_SUCCESS) {
    JxlResizableParallelRunnerDestroy(parallel_runner);
    JxlEncoderDestroy(encoder);
    g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
                "JxlEncoderSetColorEncoding failed: %x", status);
    return FALSE;
  }

  frame_settings = JxlEncoderFrameSettingsCreate(encoder, NULL);
  JxlEncoderSetFrameDistance(frame_settings, distance);
  JxlEncoderSetFrameLossless(frame_settings, output_info.uses_original_profile);

  status = JxlEncoderAddImageFrame(frame_settings, &pixel_format,
                                   gdk_pixbuf_read_pixels(pixbuf),
                                   gdk_pixbuf_get_byte_length(pixbuf));
  if (status != JXL_ENC_SUCCESS) {
    JxlResizableParallelRunnerDestroy(parallel_runner);
    JxlEncoderDestroy(encoder);
    g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
                "JxlEncoderAddImageFrame failed: %x", status);
    return FALSE;
  }

  JxlEncoderCloseInput(encoder);

  compressed = g_byte_array_sized_new(4096);
  g_byte_array_set_size(compressed, 4096);
  do {
    next_out = compressed->data + offset;
    avail_out = compressed->len - offset;
    status = JxlEncoderProcessOutput(encoder, &next_out, &avail_out);

    if (status == JXL_ENC_NEED_MORE_OUTPUT) {
      offset = next_out - compressed->data;
      g_byte_array_set_size(compressed, compressed->len * 2);
    } else if (status == JXL_ENC_ERROR) {
      JxlResizableParallelRunnerDestroy(parallel_runner);
      JxlEncoderDestroy(encoder);
      g_set_error(error, G_FILE_ERROR, 0, "JxlEncoderProcessOutput failed: %x",
                  status);
      return FALSE;
    }
  } while (status != JXL_ENC_SUCCESS);

  JxlResizableParallelRunnerDestroy(parallel_runner);
  JxlEncoderDestroy(encoder);

  g_byte_array_set_size(compressed, next_out - compressed->data);
  if (compressed->len > 0) {
    fwrite(compressed->data, 1, compressed->len, f);
    g_byte_array_free(compressed, TRUE);
    return TRUE;
  }

  return FALSE;
}

void fill_vtable(GdkPixbufModule *module) {
  module->begin_load = begin_load;
  module->stop_load = stop_load;
  module->load_increment = load_increment;
  module->is_save_option_supported = jxl_is_save_option_supported;
  module->save = jxl_image_saver;
}

void fill_info(GdkPixbufFormat *info) {
  static GdkPixbufModulePattern signature[] = {
      {"\xFF\x0A", "  ", 100},
      {"...\x0CJXL \x0D\x0A\x87\x0A", "zzz         ", 100},
      {NULL, NULL, 0},
  };

  static gchar *mime_types[] = {"image/jxl", NULL};

  static gchar *extensions[] = {"jxl", NULL};

  info->name = "jxl";
  info->signature = signature;
  info->description = "JPEG XL image";
  info->mime_types = mime_types;
  info->extensions = extensions;
  info->flags = GDK_PIXBUF_FORMAT_WRITABLE | GDK_PIXBUF_FORMAT_THREADSAFE;
  info->license = "BSD-3";
}
