/*
  libheif integration tests for uncompressed encoder

  MIT License

  Copyright (c) 2023 Brad Hards <bradh@frogmouth.net>

  Permission is hereby granted, free of charge, to any person obtaining a copy
  of this software and associated documentation files (the "Software"), to deal
  in the Software without restriction, including without limitation the rights
  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  copies of the Software, and to permit persons to whom the Software is
  furnished to do so, subject to the following conditions:

  The above copyright notice and this permission notice shall be included in all
  copies or substantial portions of the Software.

  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  SOFTWARE.
*/

#include "catch_amalgamated.hpp"
#include "api_structs.h"
#include "libheif/heif.h"
#include "libheif/heif_uncompressed.h"
#include "libheif/heif_tiling.h"
#include <cstdint>
#include <string.h>
#include "test_utils.h"

TEST_CASE("check have uncompressed")
{
  heif_error err;
  heif_context *ctx = heif_context_alloc();
  heif_encoder *enc;
  err = heif_context_get_encoder_for_format(ctx, heif_compression_uncompressed,
                                            &enc);
  REQUIRE(err.code == heif_error_Ok);

  const char *name = heif_encoder_get_name(enc);
  REQUIRE(strcmp(name, "builtin") == 0);

  heif_encoder_release(enc);

  heif_context_free(ctx);
}


heif_image *createImage_Mono()
{
  heif_image *image;
  heif_error err;
  int w = 1024;
  int h = 768;
  err = heif_image_create(w, h, heif_colorspace_monochrome,
                          heif_chroma_monochrome, &image);
  if (err.code) {
    return nullptr;
  }

  err = heif_image_add_plane(image, heif_channel_Y, w, h, 8);
  REQUIRE(err.code == heif_error_Ok);

  int stride;
  uint8_t *p = heif_image_get_plane(image, heif_channel_Y, &stride);

  int y = 0;
  for (; y < h / 2; y++) {
    int x = 0;
    for (; x < w / 3; x++) {
      p[y * stride + x] = 255;
    }
    for (; x < 2 * w / 3; x++) {
      p[y * stride + x] = 127;
    }
    for (; x < w; x++) {
      p[y * stride + x] = 1;
    }
  }
  for (; y < h; y++) {
    int x = 0;
    for (; x < w / 3; x++) {
      p[y * stride + x] =  (uint8_t) (x % 256);
    }
    for (; x < 2 * w / 3; x++) {
      p[y * stride + x] = (uint8_t) ((255 - x) % 256);
    }
    for (; x < w; x++) {
      p[y * stride + x] =  (uint8_t) ((x + y) % 256);
    }
  }
  if (err.code) {
    heif_image_release(image);
    return nullptr;
  }

  return image;
}


heif_image *createImage_YCbCr()
{
  heif_image *image;
  heif_error err;
  int w = 1024;
  int h = 768;
  err = heif_image_create(w, h, heif_colorspace_YCbCr,
                          heif_chroma_444, &image);
  if (err.code) {
    return nullptr;
  }

  err = heif_image_add_plane(image, heif_channel_Y, w, h, 8);
  REQUIRE(err.code == heif_error_Ok);
  err = heif_image_add_plane(image, heif_channel_Cb, w, h, 8);
  REQUIRE(err.code == heif_error_Ok);
  err = heif_image_add_plane(image, heif_channel_Cr, w, h, 8);
  REQUIRE(err.code == heif_error_Ok);

  int stride;
  uint8_t *p = heif_image_get_plane(image, heif_channel_Y, &stride);
  uint8_t *cb = heif_image_get_plane(image, heif_channel_Cb, &stride);
  uint8_t *cr = heif_image_get_plane(image, heif_channel_Cr, &stride);

  int y = 0;
  for (; y < h / 2; y++)
  {
    int x = 0;
    for (; x < w / 3; x++) {
      p[y * stride + x] = 255;
      cb[y * stride + x] = 0;
      cr[y * stride + x] = 0;
    }
    for (; x < 2 * w / 3; x++) {
      p[y * stride + x] = 127;
      cb[y * stride + x] = 0;
      cr[y * stride + x] = 0;
    }
    for (; x < w; x++) {
      p[y * stride + x] = 1;
      cb[y * stride + x] = 0;
      cr[y * stride + x] = 0;
    }
  }
  for (; y < h; y++) {
    int x = 0;
    for (; x < w / 3; x++) {
      p[y * stride + x] =  255;
      cb[y * stride + x] = 255;
      cr[y * stride + x] = 0;
    }
    for (; x < 2 * w / 3; x++) {
      p[y * stride + x] = 255;
      cb[y * stride + x] = 255;
      cr[y * stride + x] = 255;
    }
    for (; x < w; x++) {
      p[y * stride + x] =  255;
      cb[y * stride + x] = 0;
      cr[y * stride + x] = 255;
    }
  }
  if (err.code) {
    heif_image_release(image);
    return nullptr;
  }

  return image;
}

heif_image* createImage_Mono_plus_alpha()
{
  heif_image *image;
  heif_error err;
  int w = 1024;
  int h = 768;
  err = heif_image_create(w, h, heif_colorspace_monochrome,
                          heif_chroma_monochrome, &image);
  if (err.code) {
    return nullptr;
  }

  err = heif_image_add_plane(image, heif_channel_Y, w, h, 8);
  REQUIRE(err.code == heif_error_Ok);
  err = heif_image_add_plane(image, heif_channel_Alpha, w, h, 8);
  REQUIRE(err.code == heif_error_Ok);

  int stride;
  uint8_t *p = heif_image_get_plane(image, heif_channel_Y, &stride);
  uint8_t *a = heif_image_get_plane(image, heif_channel_Alpha, &stride);
  int y = 0;
  for (; y < h / 2; y++) {
    int x = 0;
    for (; x < w / 3; x++) {
      p[y * stride + x] = 255;
      a[y * stride + x] = 250;
    }
    for (; x < 2 * w / 3; x++) {
      p[y * stride + x] = 127;
      a[y * stride + x] = 25;
    }
    for (; x < w; x++) {
      p[y * stride + x] = 1;
      a[y * stride + x] = 252;
    }
  }
  for (; y < h; y++) {
    int x = 0;
    for (; x < w / 3; x++) {
      p[y * stride + x] =  (uint8_t) (x % 256);
      a[y * stride + x] = 253;
    }
    for (; x < 2 * w / 3; x++) {
      p[y * stride + x] = (uint8_t) ((255 - x) % 256);
      a[y * stride + x] = 254;
    }
    for (; x < w; x++) {
      p[y * stride + x] =  (uint8_t) ((x + y) % 256);
      a[y * stride + x] = 255;
    }
  }
  if (err.code) {
    heif_image_release(image);
    return nullptr;
  }

  return image;
}

heif_image *createImage_RGB_interleaved()
{
  heif_image *image;
  heif_error err;
  int w = 1024;
  int h = 768;
  err = heif_image_create(w, h, heif_colorspace_RGB,
                          heif_chroma_interleaved_RGB, &image);
  if (err.code) {
    return nullptr;
  }

  err = heif_image_add_plane(image, heif_channel_interleaved, w, h, 8);
  REQUIRE(err.code == heif_error_Ok);

  int stride;
  uint8_t *p = heif_image_get_plane(image, heif_channel_interleaved, &stride);

  int y = 0;
  for (; y < h / 2; y++) {
    int x = 0;
    for (; x < w / 3; x++) {
      p[y * stride + 3 * x] = 1;
      p[y * stride + 3 * x + 1] = 255;
      p[y * stride + 3 * x + 2] = 2;
    }
    for (; x < 2 * w / 3; x++) {
      p[y * stride + 3 * x] = 4;
      p[y * stride + 3 * x + 1] = 5;
      p[y * stride + 3 * x + 2] = 255;
    }
    for (; x < w; x++) {
      p[y * stride + 3 * x] = 255;
      p[y * stride + 3 * x + 1] = 6;
      p[y * stride + 3 * x + 2] = 7;
    }
  }
  for (; y < h; y++) {
    int x = 0;
    for (; x < w / 3; x++) {
      p[y * stride + 3 * x] = 8;
      p[y * stride + 3 * x + 1] = 9;
      p[y * stride + 3 * x + 2] = 255;
    }
    for (; x < 2 * w / 3; x++) {
      p[y * stride + 3 * x] = 253;
      p[y * stride + 3 * x + 1] = 10;
      p[y * stride + 3 * x + 2] = 11;
    }
    for (; x < w; x++) {
      p[y * stride + 3 * x] = 13;
      p[y * stride + 3 * x + 1] = 252;
      p[y * stride + 3 * x + 2] = 12;
    }
  }
  if (err.code) {
    heif_image_release(image);
    return nullptr;
  }

  return image;
}


void set_pixel_on_48bpp(uint8_t* p, int y, int stride, int x, int red, int green, int blue, bool little_endian, int alpha)
{
  uint8_t red_low_byte = (uint8_t)(red & 0xFF);
  uint8_t red_high_byte = (uint8_t)(red >> 8);
  uint8_t green_low_byte = (uint8_t)(green & 0xFF);
  uint8_t green_high_byte = (uint8_t)(green >> 8);
  uint8_t blue_low_byte = (uint8_t)(blue & 0xFF);
  uint8_t blue_high_byte = (uint8_t)(blue >> 8);
  uint8_t alpha_low_byte = (uint8_t)(alpha & 0xFF);
  uint8_t alpha_high_byte = (uint8_t)(alpha >> 8);
  int bytes_per_pixel = 6;
  if (alpha != 0)
  {
    bytes_per_pixel = 8;
  }
  if (little_endian)
  {
    p[y * stride + x * bytes_per_pixel + 0] = red_low_byte;
    p[y * stride + x * bytes_per_pixel + 1] = red_high_byte;
    p[y * stride + x * bytes_per_pixel + 2] = green_low_byte;
    p[y * stride + x * bytes_per_pixel + 3] = green_high_byte;
    p[y * stride + x * bytes_per_pixel + 4] = blue_low_byte;
    p[y * stride + x * bytes_per_pixel + 5] = blue_high_byte;
    if (alpha != 0)
    {
      p[y * stride + x * bytes_per_pixel + 6] = alpha_low_byte;
      p[y * stride + x * bytes_per_pixel + 7] = alpha_high_byte;
    }
  }
  else
  {
    p[y * stride + x * bytes_per_pixel + 0] = red_high_byte;
    p[y * stride + x * bytes_per_pixel + 1] = red_low_byte;
    p[y * stride + x * bytes_per_pixel + 2] = green_high_byte;
    p[y * stride + x * bytes_per_pixel + 3] = green_low_byte;
    p[y * stride + x * bytes_per_pixel + 4] = blue_high_byte;
    p[y * stride + x * bytes_per_pixel + 5] = blue_low_byte;
    if (alpha != 0)
    {
      p[y * stride + x * bytes_per_pixel + 6] = alpha_high_byte;
      p[y * stride + x * bytes_per_pixel + 7] = alpha_low_byte;
    }
  }
}

heif_image *createImage_RRGGBB_interleaved(heif_chroma chroma, int bit_depth, bool little_endian, bool with_alpha)
{
  heif_image *image;
  heif_error err;
  int w = 1024;
  int h = 768;
  err = heif_image_create(w, h, heif_colorspace_RGB, chroma, &image);
  if (err.code) {
    return nullptr;
  }

  int max_value = 0;
  for (int i = 0; i < bit_depth; i++)
  {
    max_value |= (1 << i);
  }
  int mid_value = max_value / 2;
  int alpha = 0;
  int alpha_mid = 0;
  if (with_alpha)
  {
    alpha = max_value;
    alpha_mid = mid_value;
  }

  err = heif_image_add_plane(image, heif_channel_interleaved, w, h, bit_depth);
  REQUIRE(err.code == heif_error_Ok);

  int stride;
  uint8_t *p = heif_image_get_plane(image, heif_channel_interleaved, &stride);

  int y = 0;
  for (; y < h / 2; y++) {
    int x = 0;
    for (; x < w / 4; x++)
    {
      set_pixel_on_48bpp(p, y, stride, x, 0x0000, 0x0000, max_value, little_endian, alpha);
    }
    for (; x < 2 * w / 4; x++)
    {
      set_pixel_on_48bpp(p, y, stride, x, 0x0000, max_value, 0x0000, little_endian, alpha);
    }
    for (; x < 3 * w / 4; x++)
    {
      set_pixel_on_48bpp(p, y, stride, x, max_value, 0x0000, 0x0000, little_endian, alpha);
    }
    for (; x < w; x++)
    {
      set_pixel_on_48bpp(p, y, stride, x, max_value - 2, max_value - 1, max_value, little_endian, alpha);
    }
  }
  for (; y < h; y++) {
    int x = 0;
    for (; x < w / 4; x++)
    {
      set_pixel_on_48bpp(p, y, stride, x, 0x0000, max_value, 0x0000, little_endian, alpha_mid);
    }
    for (; x < 2 * w / 4; x++)
    {
      set_pixel_on_48bpp(p, y, stride, x, max_value, 0x0000, 0x0000, little_endian, alpha_mid);
    }
    for (; x < 3 * w / 4; x++)
    {
      set_pixel_on_48bpp(p, y, stride, x, 0x0000, 0x0000, max_value, little_endian, alpha_mid);
    }
    for (; x < w; x++)
    {
      set_pixel_on_48bpp(p, y, stride, x, mid_value - 2, mid_value -1, mid_value, little_endian, alpha_mid);
    }
  }
  if (err.code) {
    heif_image_release(image);
    return nullptr;
  }

  return image;
}

heif_image *createImage_RGBA_interleaved()
{
  heif_image *image;
  heif_error err;
  int w = 1024;
  int h = 768;
  err = heif_image_create(w, h, heif_colorspace_RGB,
                          heif_chroma_interleaved_RGBA, &image);
  if (err.code) {
    return nullptr;
  }

  err = heif_image_add_plane(image, heif_channel_interleaved, w, h, 8);
  REQUIRE(err.code == heif_error_Ok);

  int stride;
  uint8_t *p = heif_image_get_plane(image, heif_channel_interleaved, &stride);

  int y = 0;
  for (; y < h / 2; y++) {
    int x = 0;
    for (; x < w / 3; x++) {
      p[y * stride + 4 * x] = 1;
      p[y * stride + 4 * x + 1] = 255;
      p[y * stride + 4 * x + 2] = 2;
      p[y * stride + 4 * x + 3] = 255;
    }
    for (; x < 2 * w / 3; x++) {
      p[y * stride + 4 * x] = 4;
      p[y * stride + 4 * x + 1] = 5;
      p[y * stride + 4 * x + 2] = 255;
      p[y * stride + 4 * x + 3] = 128;
    }
    for (; x < w; x++) {
      p[y * stride + 4 * x] = 255;
      p[y * stride + 4 * x + 1] = 6;
      p[y * stride + 4 * x + 2] = 7;
      p[y * stride + 4 * x + 3] = 200;
    }
  }
  for (; y < h; y++) {
    int x = 0;
    for (; x < w / 3; x++) {
      p[y * stride + 4 * x] = 8;
      p[y * stride + 4 * x + 1] = 9;
      p[y * stride + 4 * x + 2] = 255;
      p[y * stride + 4 * x + 3] = 254;
    }
    for (; x < 2 * w / 3; x++) {
      p[y * stride + 4 * x] = 253;
      p[y * stride + 4 * x + 1] = 10;
      p[y * stride + 4 * x + 2] = 11;
      p[y * stride + 4 * x + 3] = 253;
    }
    for (; x < w; x++) {
      p[y * stride + 4 * x] = 13;
      p[y * stride + 4 * x + 1] = 252;
      p[y * stride + 4 * x + 2] = 12;
      p[y * stride + 4 * x + 3] = 250;
    }
  }
  if (err.code) {
    heif_image_release(image);
    return nullptr;
  }

  return image;
}

heif_image *createImage_RGBA_planar()
{
  heif_image *image;
  heif_error err;
  int w = 1024;
  int h = 768;
  err = heif_image_create(w, h, heif_colorspace_RGB,
                          heif_chroma_444, &image);
  if (err.code) {
    return nullptr;
  }

  err = heif_image_add_plane(image, heif_channel_R, w, h, 8);
  REQUIRE(err.code == heif_error_Ok);
  err = heif_image_add_plane(image, heif_channel_G, w, h, 8);
  REQUIRE(err.code == heif_error_Ok);
  err = heif_image_add_plane(image, heif_channel_B, w, h, 8);
  REQUIRE(err.code == heif_error_Ok);
  err = heif_image_add_plane(image, heif_channel_Alpha, w, h, 8);
  REQUIRE(err.code == heif_error_Ok);


  int stride;
  uint8_t *r = heif_image_get_plane(image, heif_channel_R, &stride);
  uint8_t *g = heif_image_get_plane(image, heif_channel_G, &stride);
  uint8_t *b = heif_image_get_plane(image, heif_channel_B, &stride);
  uint8_t *a = heif_image_get_plane(image, heif_channel_Alpha, &stride);

  int y = 0;
  for (; y < h / 2; y++) {
    int x = 0;
    for (; x < w / 3; x++) {
      r[y * stride + x] = 1;
      g[y * stride + x] = 255;
      b[y * stride + x] = 2;
      a[y * stride + x] = 240;
    }
    for (; x < 2 * w / 3; x++) {
      r[y * stride + x] = 4;
      g[y * stride + x] = 5;
      b[y * stride + x] = 255;
      a[y * stride + x] = 128;
    }
    for (; x < w; x++) {
      r[y * stride + x] = 255;
      g[y * stride + x] = 6;
      b[y * stride + x] = 7;
      a[y * stride + x] = 241;
    }
  }
  for (; y < h; y++) {
    int x = 0;
    for (; x < w / 3; x++) {
      r[y * stride + x]= 8;
      g[y * stride + x] = 9;
      b[y * stride + x] = 255;
      a[y * stride + x] = 242;
    }
    for (; x < 2 * w / 3; x++) {
      r[y * stride + x] = 253;
      g[y * stride + x] = 10;
      b[y * stride + x] = 11;
      a[y * stride + x] = 243;
    }
    for (; x < w; x++) {
      r[y * stride + x] = 13;
      g[y * stride + x] = 252;
      b[y * stride + x] = 12;
      a[y * stride + x] = 244;
    }
  }
  if (err.code) {
    heif_image_release(image);
    return nullptr;
  }

  return image;
}

static void do_encode(heif_image* input_image, const char* filename, bool check_decode, uint8_t prefer_uncC_short_form = 0)
{
  REQUIRE(input_image != nullptr);

  heif_context *ctx = heif_context_alloc();
  heif_encoder *encoder;
  heif_error err;
  err = heif_context_get_encoder_for_format(ctx, heif_compression_uncompressed, &encoder);
  REQUIRE(err.code == heif_error_Ok);

  heif_encoding_options *options;
  options = heif_encoding_options_alloc();
  options->macOS_compatibility_workaround = false;
  options->macOS_compatibility_workaround_no_nclx_profile = true;
  options->image_orientation = heif_orientation_normal;
  options->prefer_uncC_short_form = prefer_uncC_short_form;
  heif_image_handle *output_image_handle;

  err = heif_context_encode_image(ctx, input_image, encoder, options, &output_image_handle);
  REQUIRE(err.code == heif_error_Ok);
  err = heif_context_write_to_file(ctx, filename);
  REQUIRE(err.code == heif_error_Ok);

  if (check_decode)
  {
    // read file back in
    heif_context* decode_context;
    decode_context = heif_context_alloc();
    err = heif_context_read_from_file(decode_context, filename, NULL);
    REQUIRE(err.code == heif_error_Ok);
    heif_image_handle *decode_image_handle = get_primary_image_handle(decode_context);
    int ispe_width = heif_image_handle_get_ispe_width(decode_image_handle);
    // TODO: check against input_image ispe width and height if we can
    REQUIRE(ispe_width == 1024);
    int ispe_height = heif_image_handle_get_ispe_height(decode_image_handle);
    REQUIRE(ispe_height == 768);
    int width = heif_image_handle_get_width(decode_image_handle);
    REQUIRE(width == heif_image_get_primary_width(input_image));
    int height = heif_image_handle_get_height(decode_image_handle);
    REQUIRE(height == heif_image_get_primary_height(input_image));
    heif_image* decode_image;
    err = heif_decode_image(decode_image_handle, &decode_image, heif_colorspace_undefined, heif_chroma_undefined, NULL);
    REQUIRE(err.code == heif_error_Ok);
    // REQUIRE(heif_image_has_channel(input_image, heif_channel_Y) == heif_image_has_channel(decode_image, heif_channel_Y));
    REQUIRE(heif_image_has_channel(input_image, heif_channel_Cb) == heif_image_has_channel(decode_image, heif_channel_Cb));
    REQUIRE(heif_image_has_channel(input_image, heif_channel_Cr) == heif_image_has_channel(decode_image, heif_channel_Cr));
    // REQUIRE(heif_image_has_channel(input_image, heif_channel_R) == heif_image_has_channel(decode_image, heif_channel_R));
    // REQUIRE(heif_image_has_channel(input_image, heif_channel_G) == heif_image_has_channel(decode_image, heif_channel_G));
    // REQUIRE(heif_image_has_channel(input_image, heif_channel_B) == heif_image_has_channel(decode_image, heif_channel_B));
    // REQUIRE(heif_image_has_channel(input_image, heif_channel_Alpha) == heif_image_has_channel(decode_image, heif_channel_Alpha));
    // REQUIRE(heif_image_has_channel(input_image, heif_channel_interleaved) == heif_image_has_channel(decode_image, heif_channel_interleaved));
    // TODO: make proper test for interleave to component translation

    // TODO: compare values
    heif_image_release(decode_image);
    heif_image_handle_release(decode_image_handle);
    heif_context_free(decode_context);
  }

  heif_image_handle_release(output_image_handle);
  heif_encoding_options_free(options);
  heif_encoder_release(encoder);
  heif_image_release(input_image);

  heif_context_free(ctx);
}


TEST_CASE("Encode RGB")
{
  heif_image *input_image = createImage_RGB_interleaved();
  do_encode(input_image, "encode_rgb.heif", true);
}


TEST_CASE("Encode Mono")
{
  heif_image* input_image = createImage_Mono();
  do_encode(input_image, "encode_mono.heif", true);
}

TEST_CASE("Encode RGB Version1")
{
  heif_image *input_image = createImage_RGB_interleaved();
  do_encode(input_image, "encode_rgb_version1.heif", true, true);
}

TEST_CASE("Encode Mono with alpha")
{
  heif_image* input_image = createImage_Mono_plus_alpha();
  do_encode(input_image, "encode_mono_plus_alpha.heif", true);
}


TEST_CASE("Encode YCBCr")
{
  // TODO: 422 and 420
  heif_image *input_image = createImage_YCbCr();
  do_encode(input_image, "encode_YCbCr.heif", true);
}


TEST_CASE("Encode RRRGGBB_LE 10 bit")
{
  heif_image *input_image = createImage_RRGGBB_interleaved(heif_chroma_interleaved_RRGGBB_LE, 10, true, false);
  do_encode(input_image, "encode_rrggbb_10_le.heif", false);
}


TEST_CASE("Encode RRRGGBB_BE 10 bit ")
{
  heif_image *input_image = createImage_RRGGBB_interleaved(heif_chroma_interleaved_RRGGBB_BE, 10, false, false);
  do_encode(input_image, "encode_rrggbb_10_be.heif", false);
}


TEST_CASE("Encode RRRGGBB_LE 12 bit")
{
  heif_image *input_image = createImage_RRGGBB_interleaved(heif_chroma_interleaved_RRGGBB_LE, 12, true, false);
  do_encode(input_image, "encode_rrggbb_12_le.heif", false);
}


TEST_CASE("Encode RRRGGBB_BE 12 bit ")
{
  heif_image *input_image = createImage_RRGGBB_interleaved(heif_chroma_interleaved_RRGGBB_BE, 12, false, false);
  do_encode(input_image, "encode_rrggbb_12_be.heif", false);
}


TEST_CASE("Encode RRRGGBB_LE 16 bit")
{
  heif_image *input_image = createImage_RRGGBB_interleaved(heif_chroma_interleaved_RRGGBB_LE, 16, true, false);
  do_encode(input_image, "encode_rrggbb_16_le.heif", false);
}


TEST_CASE("Encode RRRGGBB_BE 16 bit ")
{
  heif_image *input_image = createImage_RRGGBB_interleaved(heif_chroma_interleaved_RRGGBB_BE, 16, false, false);
  do_encode(input_image, "encode_rrggbb_16_be.heif", false);
}


// Boundary cases for the RRGGBB block-pixel encoder. 9 bit is the lowest valid
// depth (m_bytes_per_pixel becomes 4); 13 bit is the highest the block encoder
// accepts (m_bytes_per_pixel == 5). Both were adjacent to the out-of-bounds write
// that occurred for the (now rejected) <= 8 bit case (GHSA-46rp-pcq2-rpmr).
TEST_CASE("Encode RRRGGBB_LE 9 bit")
{
  heif_image *input_image = createImage_RRGGBB_interleaved(heif_chroma_interleaved_RRGGBB_LE, 9, true, false);
  do_encode(input_image, "encode_rrggbb_9_le.heif", false);
}


TEST_CASE("Encode RRRGGBB_BE 9 bit")
{
  heif_image *input_image = createImage_RRGGBB_interleaved(heif_chroma_interleaved_RRGGBB_BE, 9, false, false);
  do_encode(input_image, "encode_rrggbb_9_be.heif", false);
}


TEST_CASE("Encode RRRGGBB_LE 13 bit")
{
  heif_image *input_image = createImage_RRGGBB_interleaved(heif_chroma_interleaved_RRGGBB_LE, 13, true, false);
  do_encode(input_image, "encode_rrggbb_13_le.heif", false);
}


TEST_CASE("Encode RRRGGBB_BE 13 bit")
{
  heif_image *input_image = createImage_RRGGBB_interleaved(heif_chroma_interleaved_RRGGBB_BE, 13, false, false);
  do_encode(input_image, "encode_rrggbb_13_be.heif", false);
}


// Root-cause check: a 16-bit interleaved channel (RRGGBB / RRGGBBAA) with a bit
// depth of <= 8 is self-inconsistent and must be rejected at image construction,
// before any encoder can read/write past the under-sized plane.
TEST_CASE("Reject <=8 bit 16-bit-interleaved channels")
{
  const heif_chroma formats[] = {
    heif_chroma_interleaved_RRGGBB_LE,
    heif_chroma_interleaved_RRGGBB_BE,
    heif_chroma_interleaved_RRGGBBAA_LE,
    heif_chroma_interleaved_RRGGBBAA_BE
  };

  for (heif_chroma chroma : formats) {
    for (int bit_depth = 1; bit_depth <= 8; bit_depth++) {
      heif_image* image;
      heif_error err = heif_image_create(64, 64, heif_colorspace_RGB, chroma, &image);
      REQUIRE(err.code == heif_error_Ok);

      err = heif_image_add_plane(image, heif_channel_interleaved, 64, 64, bit_depth);
      REQUIRE(err.code != heif_error_Ok);

      heif_image_release(image);
    }

    // The lowest valid depth (9 bit) is still accepted.
    heif_image* image;
    heif_error err = heif_image_create(64, 64, heif_colorspace_RGB, chroma, &image);
    REQUIRE(err.code == heif_error_Ok);
    err = heif_image_add_plane(image, heif_channel_interleaved, 64, 64, 9);
    REQUIRE(err.code == heif_error_Ok);
    heif_image_release(image);
  }
}


TEST_CASE("Encode RGBA")
{
  heif_image *input_image = createImage_RGBA_interleaved();
  do_encode(input_image, "encode_rgba.heif", true);
}

TEST_CASE("Encode RGBA Version 1")
{
  heif_image *input_image = createImage_RGBA_interleaved();
  do_encode(input_image, "encode_rgba_version1.heif", true, true);
}


TEST_CASE("Encode RRRGGBBAA_LE 10 bit")
{
  heif_image *input_image = createImage_RRGGBB_interleaved(heif_chroma_interleaved_RRGGBBAA_LE, 10, true, true);
  do_encode(input_image, "encode_rrggbbaa_10_le.heif", false);
}


TEST_CASE("Encode RRRGGBBAA_BE 10 bit ")
{
  heif_image *input_image = createImage_RRGGBB_interleaved(heif_chroma_interleaved_RRGGBBAA_BE, 10, false, true);
  do_encode(input_image, "encode_rrggbbaa_10_be.heif", false);
}


TEST_CASE("Encode RRRGGBBAA_LE 12 bit")
{
  heif_image *input_image = createImage_RRGGBB_interleaved(heif_chroma_interleaved_RRGGBBAA_LE, 12, true, true);
  do_encode(input_image, "encode_rrggbbaa_12_le.heif", false);
}


TEST_CASE("Encode RRRGGBBAA_BE 12 bit ")
{
  heif_image *input_image = createImage_RRGGBB_interleaved(heif_chroma_interleaved_RRGGBBAA_BE, 12, false, true);
  do_encode(input_image, "encode_rrggbbaa_12_be.heif", false);
}


TEST_CASE("Encode RRRGGBBAA_LE 16 bit")
{
  heif_image *input_image = createImage_RRGGBB_interleaved(heif_chroma_interleaved_RRGGBBAA_LE, 16, true, true);
  do_encode(input_image, "encode_rrggbbaa_16_le.heif", false);
}


TEST_CASE("Encode RRRGGBBAA_BE 16 bit ")
{
  heif_image *input_image = createImage_RRGGBB_interleaved(heif_chroma_interleaved_RRGGBBAA_BE, 16, false, true);
  do_encode(input_image, "encode_rrggbbaa_16_be.heif", false);
}


TEST_CASE("Encode RGB planar")
{
  heif_image *input_image = createImage_RGB_planar();
  do_encode(input_image, "encode_rgb_planar.heif", true);
}


TEST_CASE("Encode RGBA planar")
{
  heif_image *input_image = createImage_RGBA_planar();
  do_encode(input_image, "encode_rgba_planar.heif", true);
}


// Regression test for the heap OOB write reported as GHSA-5x55-x5pf-9c6g
// (https://github.com/strukturag/libheif/security/advisories/GHSA-5x55-x5pf-9c6g).
// Chroma destination offsets in unc_decoder_component_interleave and
// unc_decoder_mixed_interleave used the full-resolution tile origin against
// the subsampled chroma plane stride, so the second tile row in a 4:2:0 tiled
// unci image was written past the end of the chroma plane. The fix scales the
// tile origin into the chroma grid; this test encodes a 2x2-tiled YCbCr 4:2:0
// unci with a distinctive per-tile chroma pattern and verifies the round-trip
// places each tile correctly.
static heif_image *createImage_YCbCr_420_tiled_pattern(int w, int h)
{
  heif_image *image;
  heif_error err;
  err = heif_image_create(w, h, heif_colorspace_YCbCr, heif_chroma_420, &image);
  REQUIRE(err.code == heif_error_Ok);

  err = heif_image_add_plane(image, heif_channel_Y, w, h, 8);
  REQUIRE(err.code == heif_error_Ok);
  err = heif_image_add_plane(image, heif_channel_Cb, w / 2, h / 2, 8);
  REQUIRE(err.code == heif_error_Ok);
  err = heif_image_add_plane(image, heif_channel_Cr, w / 2, h / 2, 8);
  REQUIRE(err.code == heif_error_Ok);

  int y_stride, cb_stride, cr_stride;
  uint8_t *Y = heif_image_get_plane(image, heif_channel_Y, &y_stride);
  uint8_t *Cb = heif_image_get_plane(image, heif_channel_Cb, &cb_stride);
  uint8_t *Cr = heif_image_get_plane(image, heif_channel_Cr, &cr_stride);

  // Each tile (in a 2x2 tile grid) gets a unique chroma constant so that
  // misplaced chroma tiles are detected.
  for (int row = 0; row < h; row++) {
    for (int col = 0; col < w; col++) {
      Y[row * y_stride + col] = static_cast<uint8_t>((row * 3 + col) & 0xFF);
    }
  }
  int cw = w / 2;
  int ch = h / 2;
  for (int row = 0; row < ch; row++) {
    int tile_row = (row < ch / 2) ? 0 : 1;
    for (int col = 0; col < cw; col++) {
      int tile_col = (col < cw / 2) ? 0 : 1;
      // Distinct per-tile chroma constants.
      Cb[row * cb_stride + col] = static_cast<uint8_t>(0x10 + tile_row * 2 + tile_col);
      Cr[row * cr_stride + col] = static_cast<uint8_t>(0xA0 + tile_row * 2 + tile_col);
    }
  }

  return image;
}

TEST_CASE("Encode tiled YCbCr 4:2:0 unci - chroma placement round-trip")
{
  const int W = 32;
  const int H = 32;
  const int TW = 16;
  const int TH = 16;

  heif_unci_image_parameters params{};
  params.version = 1;
  params.image_width = W;
  params.image_height = H;
  params.tile_width = TW;
  params.tile_height = TH;
  params.compression = heif_unci_compression_off;

  heif_image *input_image = createImage_YCbCr_420_tiled_pattern(W, H);
  REQUIRE(input_image != nullptr);

  heif_context *ctx = heif_context_alloc();

  heif_encoder *encoder;
  heif_error err = heif_context_get_encoder_for_format(ctx, heif_compression_uncompressed, &encoder);
  REQUIRE(err.code == heif_error_Ok);

  heif_encoding_options *options = heif_encoding_options_alloc();
  options->macOS_compatibility_workaround_no_nclx_profile = true;

  heif_image_handle *tiled_image;
  err = heif_context_add_empty_unci_image(ctx, &params, options, input_image, &tiled_image);
  REQUIRE(err.code == heif_error_Ok);

  // Add each 16x16 tile carrying the corresponding sub-region of the input.
  for (uint32_t ty = 0; ty < 2; ty++) {
    for (uint32_t tx = 0; tx < 2; tx++) {
      heif_image *tile;
      err = heif_image_create(TW, TH, heif_colorspace_YCbCr, heif_chroma_420, &tile);
      REQUIRE(err.code == heif_error_Ok);
      err = heif_image_add_plane(tile, heif_channel_Y, TW, TH, 8);
      REQUIRE(err.code == heif_error_Ok);
      err = heif_image_add_plane(tile, heif_channel_Cb, TW / 2, TH / 2, 8);
      REQUIRE(err.code == heif_error_Ok);
      err = heif_image_add_plane(tile, heif_channel_Cr, TW / 2, TH / 2, 8);
      REQUIRE(err.code == heif_error_Ok);

      int y_stride, cb_stride, cr_stride;
      uint8_t *tY = heif_image_get_plane(tile, heif_channel_Y, &y_stride);
      uint8_t *tCb = heif_image_get_plane(tile, heif_channel_Cb, &cb_stride);
      uint8_t *tCr = heif_image_get_plane(tile, heif_channel_Cr, &cr_stride);

      int src_y_stride, src_cb_stride, src_cr_stride;
      const uint8_t *sY = heif_image_get_plane(input_image, heif_channel_Y, &src_y_stride);
      const uint8_t *sCb = heif_image_get_plane(input_image, heif_channel_Cb, &src_cb_stride);
      const uint8_t *sCr = heif_image_get_plane(input_image, heif_channel_Cr, &src_cr_stride);

      for (int row = 0; row < TH; row++) {
        memcpy(tY + row * y_stride,
               sY + (ty * TH + row) * src_y_stride + tx * TW,
               TW);
      }
      for (int row = 0; row < TH / 2; row++) {
        memcpy(tCb + row * cb_stride,
               sCb + (ty * (TH / 2) + row) * src_cb_stride + tx * (TW / 2),
               TW / 2);
        memcpy(tCr + row * cr_stride,
               sCr + (ty * (TH / 2) + row) * src_cr_stride + tx * (TW / 2),
               TW / 2);
      }

      err = heif_context_add_image_tile(ctx, tiled_image, tx, ty, tile, encoder);
      REQUIRE(err.code == heif_error_Ok);
      heif_image_release(tile);
    }
  }

  err = heif_context_write_to_file(ctx, "encode_ycbcr420_tiled.heif");
  REQUIRE(err.code == heif_error_Ok);

  heif_image_handle_release(tiled_image);

  // --- decode and check chroma placement (the bug placed second-tile-row
  //     chroma far past the end of the chroma plane).

  heif_context *decode_context = heif_context_alloc();
  err = heif_context_read_from_file(decode_context, "encode_ycbcr420_tiled.heif", NULL);
  REQUIRE(err.code == heif_error_Ok);

  heif_image_handle *decode_handle = get_primary_image_handle(decode_context);
  heif_image *decoded;
  err = heif_decode_image(decode_handle, &decoded, heif_colorspace_YCbCr, heif_chroma_420, NULL);
  REQUIRE(err.code == heif_error_Ok);

  int stride;
  const uint8_t *cb = heif_image_get_plane_readonly(decoded, heif_channel_Cb, &stride);
  REQUIRE(cb != nullptr);
  for (int row = 0; row < H / 2; row++) {
    int expected_tile_row = (row < H / 4) ? 0 : 1;
    for (int col = 0; col < W / 2; col++) {
      int expected_tile_col = (col < W / 4) ? 0 : 1;
      uint8_t expected = static_cast<uint8_t>(0x10 + expected_tile_row * 2 + expected_tile_col);
      INFO("Cb row=" << row << " col=" << col);
      REQUIRE(((int) cb[row * stride + col]) == expected);
    }
  }

  const uint8_t *cr = heif_image_get_plane_readonly(decoded, heif_channel_Cr, &stride);
  REQUIRE(cr != nullptr);
  for (int row = 0; row < H / 2; row++) {
    int expected_tile_row = (row < H / 4) ? 0 : 1;
    for (int col = 0; col < W / 2; col++) {
      int expected_tile_col = (col < W / 4) ? 0 : 1;
      uint8_t expected = static_cast<uint8_t>(0xA0 + expected_tile_row * 2 + expected_tile_col);
      INFO("Cr row=" << row << " col=" << col);
      REQUIRE(((int) cr[row * stride + col]) == expected);
    }
  }

  heif_image_release(decoded);
  heif_image_handle_release(decode_handle);
  heif_context_free(decode_context);

  heif_encoding_options_free(options);
  heif_encoder_release(encoder);
  heif_image_release(input_image);
  heif_context_free(ctx);
}


// Regression test for the heap OOB write reported as GHSA-xpw3-9rhw-482x
// (https://github.com/strukturag/libheif/security/advisories/GHSA-xpw3-9rhw-482x).
// The uncompressed encoder sized its output buffer from the primary image
// dimensions but copied each component plane using that plane's actual size.
// A component plane larger than the primary image (e.g. an alpha plane that
// does not match the color planes, as produced by an image sequence with a
// mismatched alpha auxiliary track) was memcpy'd past the end of the buffer.
// The encoder now rejects such inconsistent images instead of overflowing.
TEST_CASE("Encode rejects oversized component plane")
{
  heif_image *image;
  heif_error err = heif_image_create(2, 2, heif_colorspace_monochrome,
                                     heif_chroma_monochrome, &image);
  REQUIRE(err.code == heif_error_Ok);

  err = heif_image_add_plane(image, heif_channel_Y, 2, 2, 8);
  REQUIRE(err.code == heif_error_Ok);

  // Alpha plane much larger than the 2x2 primary image. heif_image_add_plane
  // does not validate the plane size against the image size, so this builds the
  // same inconsistent image that the sequence decoder used to produce.
  err = heif_image_add_plane(image, heif_channel_Alpha, 256, 256, 8);
  REQUIRE(err.code == heif_error_Ok);

  heif_context *ctx = heif_context_alloc();
  heif_encoder *encoder;
  err = heif_context_get_encoder_for_format(ctx, heif_compression_uncompressed, &encoder);
  REQUIRE(err.code == heif_error_Ok);

  heif_image_handle *output_image_handle = nullptr;
  err = heif_context_encode_image(ctx, image, encoder, nullptr, &output_image_handle);
  // Must fail cleanly rather than overflow the heap.
  REQUIRE(err.code != heif_error_Ok);

  if (output_image_handle) {
    heif_image_handle_release(output_image_handle);
  }
  heif_encoder_release(encoder);
  heif_image_release(image);
  heif_context_free(ctx);
}
