/* SPDX-FileCopyrightText: 2024 Google LLC */
/* SPDX-License-Identifier: Apache-2.0 */

#include "applib/graphics/graphics.h"
#include "applib/graphics/bitblt.h"
#include "applib/graphics/bitblt_private.h"
#include "applib/graphics/8_bit/framebuffer.h"

#include "clar.h"
#include "util.h"

#include <string.h>
#include <stdio.h>

// Stubs
////////////////////////////////////
#include "graphics_common_stubs.h"
#include "stubs_applib_resource.h"
#include "test_graphics.h"

static GContext ctx;
static FrameBuffer framebuffer;

// Helpers
////////////////////////////////////

static void prv_set_opacity(GBitmap *bmp, uint8_t opacity) {
  GRect bounds = bmp->bounds;
  for (uint32_t idx = 0; idx < bounds.size.w * bounds.size.h; idx++) {
    ((GColor *)(bmp->addr + idx))->a = opacity;
  }
}

static GColor prv_next_color(GColor color) {
  GColor8 result = (GColor8){.argb = (color.argb + 1) % 64};
  result.a = color.a;
  return result;
}

// Tests
////////////////////////////////////

// setup and teardown
void test_bitblt__initialize(void) {
  framebuffer_init(&framebuffer, &(GSize) {DISP_COLS, DISP_ROWS});
  test_graphics_context_init(&ctx, &framebuffer);
}

void test_bitblt__cleanup(void) {
}

// Test images reside in "tests/fw/graphics/test_images/".
// The wscript will convert them from PNGs in that directory to PBIs in the build directory.
// Naming conventions of these images tends to be '<test_name>.<bitdepth>.png'.
// For example:
//    test_bitblt__8bit_assign would have:
//      - test_bitblt__8bit_assign.8bit.png
//      - test_bitblt__8bit_assign-expect.8bit.png

// Tests assign, from same size to same size.
// Setup:
//   - Source is 10x10, white.
//   - Dest is 10x10, green.
// Result:
//   - All white.
void test_bitblt__8bit_compop(void) {
  GBitmap *src_bitmap = get_gbitmap_from_pbi("test_bitblt__8bit_assign.8bit.pbi");
  uint8_t dest_data[src_bitmap->bounds.size.w*src_bitmap->bounds.size.h];
  GBitmap dest_bitmap = {
    .addr = dest_data,
    .row_size_bytes = src_bitmap->bounds.size.w,
    .info.format = GBitmapFormat8Bit,
    .info.version = GBITMAP_VERSION_CURRENT,
    .bounds = src_bitmap->bounds
  };
  // All compositing modes except GCompOpSet should be the same as GCompAssign
  memset(dest_data, GColorGreen.argb, sizeof(dest_data));
  bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, GPointZero, GCompOpAssign, GColorWhite);
  cl_assert(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__8bit_assign-expect.8bit.pbi"));

  memset(dest_data, GColorGreen.argb, sizeof(dest_data));
  bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, GPointZero, GCompOpAssignInverted, GColorWhite);
  cl_assert(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__8bit_assign-expect.8bit.pbi"));

  memset(dest_data, GColorGreen.argb, sizeof(dest_data));
  bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, GPointZero, GCompOpOr, GColorWhite);
  cl_assert(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__8bit_assign-expect.8bit.pbi"));

  memset(dest_data, GColorGreen.argb, sizeof(dest_data));
  bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, GPointZero, GCompOpAnd, GColorWhite);
  cl_assert(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__8bit_assign-expect.8bit.pbi"));

  memset(dest_data, GColorGreen.argb, sizeof(dest_data));
  bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, GPointZero, GCompOpClear, GColorWhite);
  cl_assert(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__8bit_assign-expect.8bit.pbi"));

  memset(dest_data, GColorGreen.argb, sizeof(dest_data));
  // Update source bitmap to be semi-transparent
  uint8_t *src_bitmap_buff = src_bitmap->addr;
  for (int index = 0; index < src_bitmap->bounds.size.h * src_bitmap->row_size_bytes; index++) {
    src_bitmap_buff[index] = src_bitmap_buff[index] & 0xbf;
  }
  bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, GPointZero, GCompOpSet, GColorWhite);
  cl_assert(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__8bit_set-expect.8bit.pbi"));

  gbitmap_destroy(src_bitmap);
}

// Test GCompOpTint, from same size to size.
// Setup:
//   - Source is a 10x10 square, white.
//   - Destination is either a black or white square.
// Result:
//   - When source is transparent or tint color is clear, dest is black.
//   - When source is opaque, dest is blended blue
void test_bitblt__8bit_to_8bit_comptint(void) {
  GBitmap *src_bitmap = get_gbitmap_from_pbi("test_bitblt__8bit_assign.8bit.pbi");
  GRect bounds = gbitmap_get_bounds(src_bitmap);

  uint8_t dest_data[bounds.size.w * bounds.size.h];
  GBitmap dest_bitmap = {
    .addr = dest_data,
    .row_size_bytes = bounds.size.w,
    .info.format = GBitmapFormat8Bit,
    .info.version = GBITMAP_VERSION_CURRENT,
    .bounds = src_bitmap->bounds
  };

  uint8_t expect_bmp_data[bounds.size.w * bounds.size.h];
  GBitmap expect_bmp = {
    .addr = expect_bmp_data,
    .row_size_bytes = bounds.size.w,
    .info.format = GBitmapFormat8Bit,
    .info.version = GBITMAP_VERSION_CURRENT,
    .bounds = src_bitmap->bounds
  };

  // Verify that the compositing mode is correctly applied when the source is opaque and
  // the tint color is not clear.
  memset(dest_data, GColorClear.argb, sizeof(dest_data));
  memset(expect_bmp_data, GColorBlue.argb, sizeof(expect_bmp_data));
  bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, GPointZero, GCompOpTint, GColorBlue);
  cl_assert(gbitmap_eq(&dest_bitmap, &expect_bmp, "test_bitblt__8bit_comptint-expect.8bit.pbi"));

  // Rewrite the destination bitmap to be all black.
  memset(dest_data, GColorBlack.argb, sizeof(dest_data));

  // Verify that if the tint color is clear, than the source bitmap should not affect the destination bitmap
  memset(expect_bmp_data, GColorBlack.argb, sizeof(expect_bmp_data));
  bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, GPointZero, GCompOpTint, GColorClear);
  cl_assert(gbitmap_eq(&dest_bitmap, &expect_bmp, "test_bitblt__8bit_comptint_clear-expect.8bit.pbi"));

  // Verify that if the source is transparent, it should not affect the destination bitmap
  prv_set_opacity(src_bitmap, 0);

  bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, GPointZero, GCompOpTint, GColorRed);
  cl_assert(gbitmap_eq(&dest_bitmap, &expect_bmp, "test_bitblt__8bit_comptint_clear-expect.8bit.pbi"));

  gbitmap_destroy(src_bitmap);
}

// Tests comptint, multiple tint colors and varying opacity of the source image.
// Setup:
//   - Destination is two 128 by 64 blocks expressing the 64 colors; each row expresses
//     one of the colors stack vertically.
//   - Source is a set of 4 by 4 blocks each with a 1 pixel wide vertical strip of a color
//     with an opacity in [0,3] inclusive.
// Result:
//   - Destination should be blended properly
void test_bitblt__8bit_comptint_blend(void) {
  const uint8_t NUM_COLORS = 64;
  const uint8_t WIDTH = NUM_COLORS * 2;
  const uint8_t LEGEND_WIDTH = 4;
  const uint8_t LEGEND_HEIGHT = 4;
  const uint8_t OFFSET = 1;
  const uint8_t HEIGHT = WIDTH;
  const uint8_t NUM_OPACITIES = 4;
  const uint8_t TOTAL_WIDTH = LEGEND_WIDTH * 2 + WIDTH;
  const uint8_t TOTAL_HEIGHT = HEIGHT + OFFSET + LEGEND_HEIGHT * 2;

  uint8_t dest_data[TOTAL_WIDTH * TOTAL_HEIGHT];
  memset(dest_data, GColorWhite.argb, sizeof(dest_data));

  GBitmap dest_bitmap = {
    .addr = dest_data,
    .row_size_bytes = TOTAL_WIDTH,
    .info.format = GBitmapFormat8Bit,
    .info.version = GBITMAP_VERSION_CURRENT,
    .bounds = (GRect){GPointZero, (GSize){TOTAL_WIDTH, TOTAL_HEIGHT}}
  };

  GColor color = (GColor){ .a = 3, .r = 0, .g = 0, .b = 0 };
  uint8_t src_data[] = { color.argb };
  GBitmap src_bmp = (GBitmap){
    .addr = src_data,
    .row_size_bytes = 1,
    .info.format = GBitmapFormat8Bit,
    .info.version = GBITMAP_VERSION_CURRENT,
    .bounds = GRect(0, 0, 1, 1)
  };

  for (uint8_t offset_y = 0; offset_y < NUM_COLORS; offset_y++) {
    const int16_t offset_x = OFFSET + LEGEND_WIDTH + WIDTH;
    const int16_t x = LEGEND_WIDTH;

    memset(src_data, color.argb, sizeof(src_data));
    color = prv_next_color(color);

    const int16_t y_upper = LEGEND_HEIGHT + offset_y;
    const GRect upper_rect = GRect(x, y_upper, WIDTH, 1);
    const GRect upper_left_legend_rect = GRect(0, y_upper, 3, 1);
    const GRect upper_right_legend_rect = GRect(offset_x, y_upper, 3, 1);
    bitblt_bitmap_into_bitmap_tiled(&dest_bitmap, &src_bmp, upper_rect,
                                    GPointZero, GCompOpAssign, GColorWhite);
    bitblt_bitmap_into_bitmap_tiled(&dest_bitmap, &src_bmp, upper_left_legend_rect,
                                    GPointZero, GCompOpAssign, GColorWhite);
    bitblt_bitmap_into_bitmap_tiled(&dest_bitmap, &src_bmp, upper_right_legend_rect,
                                    GPointZero, GCompOpAssign, GColorWhite);

    const int16_t y_lower = y_upper + LEGEND_HEIGHT + NUM_COLORS + OFFSET;
    const GRect lower_rect = GRect(x, y_lower, WIDTH, 1);
    const GRect lower_left_legend_rect = GRect(0, y_lower, 3, 1);
    const GRect lower_right_legend_rect = GRect(offset_x, y_lower, 3, 1);
    bitblt_bitmap_into_bitmap_tiled(&dest_bitmap, &src_bmp, lower_rect,
                                    GPointZero, GCompOpAssign, GColorWhite);
    bitblt_bitmap_into_bitmap_tiled(&dest_bitmap, &src_bmp, lower_left_legend_rect,
                                    GPointZero, GCompOpAssign, GColorWhite);
    bitblt_bitmap_into_bitmap_tiled(&dest_bitmap, &src_bmp, lower_right_legend_rect,
                                    GPointZero, GCompOpAssign, GColorWhite);
  }

  // RGB value should be discarded later on adding them here might relveal bugs.
  // .a is the important part
  GColor8 test_blend_colors[] = {
    (GColor8){.a = 0, .r = 3, .g = 2, .b = 1},
    (GColor8){.a = 1, .r = 0, .g = 3, .b = 2},
    (GColor8){.a = 2, .r = 1, .g = 0, .b = 3},
    (GColor8){.a = 3, .r = 2, .g = 1, .b = 0}
  };

  // Test image with four pixels of all our suported alpha values
  GBitmap test_bmp = (GBitmap){
    .addr = test_blend_colors,
    .row_size_bytes = 4,
    .bounds = GRect(0, 0, 4, 1),
    .info.format = GBitmapFormat8Bit,
    .info.version = GBITMAP_VERSION_CURRENT
  };

  for (uint8_t rgb_half = 0; rgb_half < NUM_COLORS / 2; rgb_half++) {
    const int16_t x = rgb_half * NUM_OPACITIES + LEGEND_WIDTH;
    const int8_t legend_height = 3;

    // Upper row with destination colors from 0..31
    const GColor upper_tint_color = (GColor){.argb = 0b11000000 | rgb_half};
    const int16_t y_upper = LEGEND_HEIGHT;
    const GRect upper_rect = GRect(x, y_upper, test_bmp.bounds.size.w, NUM_COLORS);
    const GRect upper_legend_rect = GRect(x, y_upper - LEGEND_HEIGHT, test_bmp.bounds.size.w, legend_height);
    GColor8 upper_legend[4] = { [0 ... 3] = upper_tint_color };
    test_bmp.addr = upper_legend;
    bitblt_bitmap_into_bitmap_tiled(&dest_bitmap, &test_bmp, upper_legend_rect, GPointZero, GCompOpAssign, GColorWhite);
    test_bmp.addr = test_blend_colors;
    bitblt_bitmap_into_bitmap_tiled(&dest_bitmap, &test_bmp, upper_rect, GPointZero, GCompOpTint, upper_tint_color);

    // Lower row with destination colors from 31..63
    const int16_t y_lower = y_upper + NUM_COLORS + OFFSET + LEGEND_HEIGHT;
    const GRect lower_rect = GRect(x, y_lower, test_bmp.bounds.size.w, NUM_COLORS);
    const GColor lower_tint_color = (GColor){.argb = 0b11000000 | (rgb_half + (NUM_COLORS / 2))};
    const GRect lower_legend_rect = GRect(x, y_lower - LEGEND_HEIGHT, test_bmp.bounds.size.w, legend_height);
    GColor8 lower_legend[4] = { [0 ... 3] = lower_tint_color };
    test_bmp.addr = lower_legend;
    bitblt_bitmap_into_bitmap_tiled(&dest_bitmap, &test_bmp, lower_legend_rect, GPointZero, GCompOpAssign, GColorWhite);
    test_bmp.addr = test_blend_colors;
    bitblt_bitmap_into_bitmap_tiled(&dest_bitmap, &test_bmp, lower_rect, GPointZero, GCompOpTint, lower_tint_color);
  }

  cl_assert(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__8bit_comptint_blend-expect.8bit.pbi"));
}

// Tests assign, clipping, makes sure in bottom right corner.
// Setup:
//   - Source is 10x15, black box ((0, 0), (5, 10)), rest is red.
//   - Dest is White, 50x50.
//   - Dest offset is set to 5x10 pixels in bottom right corner.
// Result:
//   - White, with 5x10 black box in bottom right corner.
void test_bitblt__8bit_clipping(void) {
  GBitmap *src_bitmap = get_gbitmap_from_pbi("test_bitblt__8bit_clipping.8bit.pbi");
  uint8_t dest_data[50*50];
  GBitmap dest_bitmap = {
    .addr = dest_data,
    .row_size_bytes = 50,
    .info.format = GBitmapFormat8Bit,
    .info.version = GBITMAP_VERSION_CURRENT,
    .bounds = { { 0, 0 }, { 50, 50 } }
  };
  memset(dest_data, GColorWhite.argb, sizeof(dest_data));

  GPoint dest_offset = { dest_bitmap.bounds.size.w-5, dest_bitmap.bounds.size.h-10 };
  bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, dest_offset, GCompOpAssign, GColorWhite);

  cl_assert(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__8bit_clipping-expect.8bit.pbi"));

  gbitmap_destroy(src_bitmap);
}

// Test horizontal wrapping when dest_rect wider than src_bitmap.
// Setup:
//   - Source 15 x 10, each row has the folling pattern:
//       - 2px  Red
//       - 13px Black
//   - Dest Green 50x50
//   - Dest rect (17, 10) at (0, 0)
// Result:
//   - 2px  Red
//   - 13px Black
//   - 2px  Red
//   - Rest Blue
void test_bitblt__8bit_wrap_x(void) {
  GBitmap *src_bitmap = get_gbitmap_from_pbi("test_bitblt__8bit_wrap_x.8bit.pbi");

  uint8_t dest_data[50*50];
  GBitmap dest_bitmap = {
    .addr = dest_data,
    .row_size_bytes = 50,
    .info.format = GBitmapFormat8Bit,
    .info.version = GBITMAP_VERSION_CURRENT,
    .bounds = { { 0, 0 }, { 50, 50 } }
  };
  memset(dest_data, GColorBlue.argb, sizeof(dest_data));

  // 2 wider than src_bitmap, so 2 columns of red will repeat again.
  GRect dest_rect = GRect(0, 0, src_bitmap->bounds.size.w + 2, src_bitmap->bounds.size.h);

  bitblt_bitmap_into_bitmap_tiled_8bit_to_8bit(
      &dest_bitmap, src_bitmap, dest_rect, GPointZero, GCompOpAssign, GColorWhite);

  cl_assert(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__8bit_wrap_x-expect.8bit.pbi"));

  gbitmap_destroy(src_bitmap);
}

// Test vertical wrapping when dest_rect taller than src_bitmap.
// Setup:
//   - Source is 25 x 10
//   - 4 rows red, 2 rows blue, 4 rows black.
//   - Dest is Green, 50 x 50
//   - Dest Rect is 10 x 24 at (0, 0)
// Result:
//   - Pattern repeated vertically x2, plus 4 rows of red.
void test_bitblt__8bit_wrap_y(void) {
  GBitmap *src_bitmap = get_gbitmap_from_pbi("test_bitblt__8bit_wrap_y.8bit.pbi");

  uint8_t dest_data[50*50];
  GBitmap dest_bitmap = {
    .addr = dest_data,
    .row_size_bytes = 50,
    .info.format = GBitmapFormat8Bit,
    .info.version = GBITMAP_VERSION_CURRENT,
    .bounds = { { 0, 0 }, { 50, 50 } }
  };
  memset(dest_data, GColorGreen.argb, sizeof(dest_data));
  GRect dest_rect = GRect(0, 0, src_bitmap->bounds.size.w, src_bitmap->bounds.size.h*2 + 4);

  bitblt_bitmap_into_bitmap_tiled_8bit_to_8bit(
      &dest_bitmap, src_bitmap, dest_rect, GPointZero, GCompOpAssign, GColorWhite);

  cl_assert(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__8bit_wrap_y-expect.8bit.pbi"));

  gbitmap_destroy(src_bitmap);
}

// Test src_origin_offset, shouldn't see any red in dest_bitmap.
// This covers src_origin_offset, y-axis and x-axis wraparound.
// Setup:
//   - Source 25x25, 2 columns, 2 rows red, rest is black.
//   - Source offset starts at (2, 2)
//   - Dest is blue, 100x100.
//   - Dest rect is 50x50 at (0,0).
// Result:
//   - No red in dest_bitmap.
//   - 50x50 black square at (0,0), rest is blue.
void test_bitblt__8bit_src_origin_offset_wrap(void) {
  GBitmap *src_bitmap = get_gbitmap_from_pbi("test_bitblt__8bit_src_origin_offset_wrap.8bit.pbi");
  uint8_t dest_data[100*100];
  GBitmap dest_bitmap = {
    .addr = dest_data,
    .row_size_bytes = 100,
    .info.format = GBitmapFormat8Bit,
    .info.version = GBITMAP_VERSION_CURRENT,
    .bounds = { { 0, 0 }, { 100, 100 } }
  };
  memset(dest_data, GColorBlue.argb, sizeof(dest_data));
  GRect dest_rect = GRect(0, 0, src_bitmap->bounds.size.w*2, src_bitmap->bounds.size.h*2);
  GPoint src_origin_offset = { 2, 2 }; // Offset past the 2 red rows

  bitblt_bitmap_into_bitmap_tiled_8bit_to_8bit(
      &dest_bitmap, src_bitmap, dest_rect, src_origin_offset, GCompOpAssign, GColorWhite);

  cl_assert(gbitmap_pbi_eq(&dest_bitmap,
                           "test_bitblt__8bit_src_origin_offset_wrap-expect.8bit.pbi"));

  gbitmap_destroy(src_bitmap);
}


//
// Test 1-bit to 8-bit blitting
///////////////////////////////

// Setup:
//   - Source is 25x25.
//   - Source has alternating white / black lines.
//   - Dest is Blue, 100x100.
//   - Dest offset is (0,0) to blit to top left corner.
// Result:
//   - 25x25 alternating black / white lines in top left corner.
void test_bitblt__1bit_to_8bit_compop(void) {
  GBitmap *src_bitmap = get_gbitmap_from_pbi("test_bitblt__1bit_to_8bit_assign.1bit.pbi");
  uint8_t dest_data[100*100];
  GBitmap dest_bitmap = {
    .addr = dest_data,
    .row_size_bytes = 100,
    .info.format = GBitmapFormat8Bit,
    .info.version = GBITMAP_VERSION_CURRENT,
    .bounds = { { 0, 0 }, { 100, 100 } }
  };
  memset(dest_data, GColorBlue.argb, sizeof(dest_data));
  bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, GPointZero, GCompOpAssign, GColorWhite);
  cl_assert(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__1bit_to_8bit_assign-expect.8bit.pbi"));

  memset(dest_data, GColorBlue.argb, sizeof(dest_data));
  bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, GPointZero, GCompOpAssignInverted, GColorWhite);
  cl_check(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__1bit_to_8bit_assigninverted-expect.8bit.pbi"));

  memset(dest_data, GColorBlue.argb, sizeof(dest_data));
  bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, GPointZero, GCompOpOr, GColorWhite);
  cl_check(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__1bit_to_8bit_or-expect.8bit.pbi"));

  memset(dest_data, GColorBlue.argb, sizeof(dest_data));
  bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, GPointZero, GCompOpAnd, GColorWhite);
  cl_check(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__1bit_to_8bit_and-expect.8bit.pbi"));

  memset(dest_data, GColorBlue.argb, sizeof(dest_data));
  bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, GPointZero, GCompOpClear, GColorWhite);
  cl_check(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__1bit_to_8bit_clear-expect.8bit.pbi"));

  memset(dest_data, GColorBlue.argb, sizeof(dest_data));
  bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, GPointZero, GCompOpSet, GColorWhite);
  cl_check(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__1bit_to_8bit_set-expect.8bit.pbi"));

  gbitmap_destroy(src_bitmap);
}

// Setup:
//   - Source is an image of a white cross
//   - Dest is blue, same size as source
// Result:
//   - Destination should be written with a White cross
// Description:
//   - This test verifies that when the bitmap is 1-bit, we treat white as a
//     non-transparent color
void test_bitblt__1bit_to_8bit_compor(void) {
  GBitmap *src_bitmap = get_gbitmap_from_pbi("test_bitblt__1bit_to_8bit_compor.1bit.pbi");
  GRect bounds = gbitmap_get_bounds(src_bitmap);

  cl_assert_equal_i(src_bitmap->info.format, GBitmapFormat1Bit);

  uint8_t dest_data[bounds.size.w * bounds.size.h];
  GBitmap dest_bitmap = {
    .addr = dest_data,
    .row_size_bytes = bounds.size.w,
    .info.format = GBitmapFormat8Bit,
    .info.version = GBITMAP_VERSION_CURRENT,
    .bounds = src_bitmap->bounds
  };

  memset(dest_data, GColorBlue.argb, sizeof(dest_data));
  bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, (GPoint){0}, GCompOpOr, GColorLightGray);

  cl_check(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__1bit_to_8bit_compor-expect.8bit.pbi"));

  gbitmap_destroy(src_bitmap);
}

// Setup:
//   - Source is a 1bit image of a white cross with a black background
// Result:
//   - The image names describe the expected result of each destination color / tint color
//     combination
void test_bitblt__1bit_to_8bit_comptint(void) {
  GBitmap *src_bitmap = get_gbitmap_from_pbi("test_bitblt__1bit_to_8bit_comptint.1bit.pbi");
  const GRect bounds = gbitmap_get_bounds(src_bitmap);

  cl_assert_equal_i(src_bitmap->info.format, GBitmapFormat1Bit);

  uint8_t dest_data[bounds.size.w * bounds.size.h];
  GBitmap dest_bitmap = {
    .addr = dest_data,
    .row_size_bytes = bounds.size.w,
    .info.format = GBitmapFormat8Bit,
    .info.version = GBITMAP_VERSION_CURRENT,
    .bounds = src_bitmap->bounds
  };

  // Image name descriptions
  // comptint_<cross_color>_on_<background_color>.8bit

  // Destination White
  memset(dest_data, GColorWhite.argb, sizeof(dest_data));
  bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, (GPoint){0}, GCompOpTint, GColorWhite);
  cl_check(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__1bit_to_8bit_comptint_white_cross_white_corners-expect.8bit.pbi"));

  memset(dest_data, GColorWhite.argb, sizeof(dest_data));
  bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, (GPoint){0}, GCompOpTint, GColorBlack);
  cl_check(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__1bit_to_8bit_comptint_white_cross_black_corners-expect.8bit.pbi"));

  memset(dest_data, GColorWhite.argb, sizeof(dest_data));
  bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, (GPoint){0}, GCompOpTint, GColorLightGray);
  cl_check(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__1bit_to_8bit_comptint_white_cross_lightgray_corners-expect.8bit.pbi"));

  // Destination Black
  memset(dest_data, GColorBlack.argb, sizeof(dest_data));
  bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, (GPoint){0}, GCompOpTint, GColorWhite);
  cl_check(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__1bit_to_8bit_comptint_black_cross_white_corners-expect.8bit.pbi"));

  memset(dest_data, GColorBlack.argb, sizeof(dest_data));
  bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, (GPoint){0}, GCompOpTint, GColorBlack);
  cl_check(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__1bit_to_8bit_comptint_black_cross_black_corners-expect.8bit.pbi"));

  memset(dest_data, GColorBlack.argb, sizeof(dest_data));
  bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, (GPoint){0}, GCompOpTint, GColorLightGray);
  cl_check(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__1bit_to_8bit_comptint_black_cross_lightgray_corners-expect.8bit.pbi"));

  gbitmap_destroy(src_bitmap);
}

// Setup:
//   - Source is 25x25.
//   - Source has alternating white / black lines.
//   - Dest is Blue, 100x100.
//   - Dest offset is set to 8x10 clipped in the bottom right corner.
// Result:
//   - There should be an 8x10 alternating black & white lines in the bottom right corner.
void test_bitblt__1bit_to_8bit_clipping(void) {
  GBitmap *src_bitmap = get_gbitmap_from_pbi("test_bitblt__1bit_to_8bit_clipping.1bit.pbi");

  uint8_t dest_data[100*100];
  GBitmap dest_bitmap = {
    .addr = dest_data,
    .row_size_bytes = 100,
    .info.format = GBitmapFormat8Bit,
    .info.version = GBITMAP_VERSION_CURRENT,
    .bounds = { { 0, 0 }, { 100, 100 } }
  };
  memset(dest_data, GColorBlue.argb, sizeof(dest_data));
  GPoint dest_offset = { dest_bitmap.bounds.size.w-8, dest_bitmap.bounds.size.h-10 };
  bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, dest_offset, GCompOpAssign, GColorWhite);

  cl_assert(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__1bit_to_8bit_clipping-expect.8bit.pbi"));

  gbitmap_destroy(src_bitmap);
}

// Setup:
//   - Source is 25 x 25.
//   - Source rows alternating: 1px black, 1px white.
//   - Dest is Blue, 100x100
//   - Dest rect is 50 x 25, at (0, 0)
// Result:
//   - 50x20 of alternating stripes, rest is blue.
void test_bitblt__1bit_to_8bit_wrap_x(void) {
  GBitmap *src_bitmap = get_gbitmap_from_pbi("test_bitblt__1bit_to_8bit_wrap_x.1bit.pbi");

  uint8_t dest_data[100*100];
  GBitmap dest_bitmap = {
    .addr = dest_data,
    .row_size_bytes = 100,
    .info.format = GBitmapFormat8Bit,
    .info.version = GBITMAP_VERSION_CURRENT,
    .bounds = { { 0, 0 }, { 100, 100 } }
  };
  memset(dest_data, GColorBlue.argb, sizeof(dest_data));
  GRect dest_rect = GRect(0, 0, src_bitmap->bounds.size.w*2, src_bitmap->bounds.size.h);

  bitblt_bitmap_into_bitmap_tiled_1bit_to_8bit(
      &dest_bitmap, src_bitmap, dest_rect, GPointZero, GCompOpAssign, GColorWhite);

  cl_assert(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__1bit_to_8bit_wrap_x-expect.8bit.pbi"));

  gbitmap_destroy(src_bitmap);
}

// Setup:
//   - Source is 40 x 30.
//   - Source has 2 columns, 4 rows of black, rest is white.
//   - Dest is all blue.
//   - Source offset (2, 4) past black.
//   - Destination is 100 x 100
//   - Destination rect is size of white portion of source.
// Result:
//   - Blue bitmap with white square at 0,0 of dest_rect size.
void test_bitblt__1bit_to_8bit_src_origin_offset(void) {
  GBitmap *src_bitmap =
      get_gbitmap_from_pbi("test_bitblt__1bit_to_8bit_src_origin_offset.1bit.pbi");
  uint8_t dest_data[100*100];
  GBitmap dest_bitmap = {
    .addr = dest_data,
    .row_size_bytes = 100,
    .info.format = GBitmapFormat8Bit,
    .info.version = GBITMAP_VERSION_CURRENT,
    .bounds = { { 0, 0 }, { 100, 100 } }
  };
  memset(dest_data, GColorBlue.argb, sizeof(dest_data));
  GRect dest_rect = GRect(0, 0, src_bitmap->bounds.size.w - 2, src_bitmap->bounds.size.h - 4);
  GPoint src_origin_offset = { 2, 4 }; // Offset past the black

  bitblt_bitmap_into_bitmap_tiled_1bit_to_8bit(
      &dest_bitmap, src_bitmap, dest_rect, src_origin_offset, GCompOpAssign, GColorWhite);

  cl_assert(gbitmap_pbi_eq(&dest_bitmap,
                           "test_bitblt__1bit_to_8bit_src_origin_offset-expect.8bit.pbi"));

  gbitmap_destroy(src_bitmap);
}

// Setup:
//   - Source is a 10x32 white square.
//   - Dest is all black.
//   - Dest origin offset set to 15, 18.
//   - Dest clipped to 10x10
// Result:
//   - Black bitmap with 10x10 white square starting at (15, 10)
void test_bitblt__1bit_to_8bit_dest_origin_offset_clip(void) {
  GBitmap *src_bitmap =
      get_gbitmap_from_pbi("test_bitblt__1bit_to_8bit_dest_origin_offset_clip.1bit.pbi");

  uint8_t dest_data[100*100];
  GBitmap dest_bitmap = {
    .addr = dest_data,
    .row_size_bytes = 100,
    .info.format = GBitmapFormat8Bit,
    .info.version = GBITMAP_VERSION_CURRENT,
    .bounds = { { 0, 0 }, { 100, 100 } }
  };
  memset(dest_data, GColorBlack.argb, sizeof(dest_data));
  GRect dest_rect = GRect(15, 10, 10, 10);
  GPoint src_origin_offset = { 0, 0 };

  bitblt_bitmap_into_bitmap_tiled(&dest_bitmap, src_bitmap, dest_rect, src_origin_offset,
                                  GCompOpAssign, GColorWhite);

  cl_assert(gbitmap_pbi_eq(&dest_bitmap,
                           "test_bitblt__1bit_to_8bit_dest_origin_offset_clip-expect.8bit.pbi"));

  gbitmap_destroy(src_bitmap);
}


// Setup:
//   - Source width is 32 pixels (ie. a word in source)
//   - Source starts with 2 rows and 4 columns of black pixels.
//   - Dest is all blue.
//   - Src origin is set to (4, 2)
//   - dest origin is set to 10, 25
//   - dest size is twice the height / width of the source.
void test_bitblt__1bit_to_8bit_src_origin_offset_wrap(void) {
  GBitmap *src_bitmap =
      get_gbitmap_from_pbi("test_bitblt__1bit_to_8bit_src_origin_offset_wrap.1bit.pbi");

  uint8_t dest_data[100*100];
  GBitmap dest_bitmap = {
    .addr = dest_data,
    .row_size_bytes = 100,
    .info.format = GBitmapFormat8Bit,
    .info.version = GBITMAP_VERSION_CURRENT,
    .bounds = { { 0, 0 }, { 100, 100 } }
  };
  memset(dest_data, GColorBlue.argb, sizeof(dest_data));
  GRect dest_rect = GRect(10, 25, src_bitmap->bounds.size.w*2, src_bitmap->bounds.size.h*2);
  GPoint src_origin_offset = { .x = 4, .y = 2 }; // Offset past the black

  bitblt_bitmap_into_bitmap_tiled(&dest_bitmap, src_bitmap, dest_rect, src_origin_offset,
                                  GCompOpAssign, GColorWhite);

  cl_assert(gbitmap_pbi_eq(&dest_bitmap,
                           "test_bitblt__1bit_to_8bit_src_origin_offset_wrap-expect.8bit.pbi"));

  gbitmap_destroy(src_bitmap);
}

// Setup:
//   - Source width is not a multiple of 8 (ie. not byte aligned in source)
//   - Source starts with 2 rows and 4 columns of black pixels.
//   - Dest is all blue.
//   - Src origin is set to (4, 2)
//   - dest origin is set to 22, 6
//   - dest size is twice the height / width of the source.
void test_bitblt__1bit_to_8bit_src_origin_offset_wrap2(void) {
  GBitmap *src_bitmap =
      get_gbitmap_from_pbi("test_bitblt__1bit_to_8bit_src_origin_offset_wrap2.1bit.pbi");

  uint8_t dest_data[100*100];
  GBitmap dest_bitmap = {
    .addr = dest_data,
    .row_size_bytes = 100,
    .info.format = GBitmapFormat8Bit,
    .info.version = GBITMAP_VERSION_CURRENT,
    .bounds = { { 0, 0 }, { 100, 100 } }
  };
  memset(dest_data, GColorBlue.argb, sizeof(dest_data));
  GRect dest_rect = GRect(21, 6, src_bitmap->bounds.size.w*2, src_bitmap->bounds.size.h*2);
  GPoint src_origin_offset = { .x = 4, .y = 2 }; // Offset past the black

  bitblt_bitmap_into_bitmap_tiled(&dest_bitmap, src_bitmap, dest_rect, src_origin_offset,
                                  GCompOpAssign, GColorWhite);

  cl_assert(gbitmap_pbi_eq(&dest_bitmap,
                           "test_bitblt__1bit_to_8bit_src_origin_offset_wrap2-expect.8bit.pbi"));

  gbitmap_destroy(src_bitmap);
}

// Setup:
//   - Source has 2 lines of white and the rest black.
//   - Destination all blue.
//   - Use gbitmap_init_as_sub_bitmap to get a sub-bitmap that starts at y = 2
// Result:
//   - A 48 x 50 black box starting at y=2 in dest, rest is blue.
void test_bitblt__bitmap_into_bitmap_sub_bitmap(void) {
  GBitmap *src_bitmap = get_gbitmap_from_pbi("test_bitblt__bitmap_into_bitmap_sub_bitmap.8bit.pbi");

  GBitmap cropped_src_bitmap;
  gbitmap_init_as_sub_bitmap(&cropped_src_bitmap, src_bitmap,
      (GRect) { { 0, 2 }, src_bitmap->bounds.size });

  uint8_t dest_data[50*50];
  GBitmap dest_bitmap = {
    .addr = dest_data,
    .row_size_bytes = 50,
    .info.format = GBitmapFormat8Bit,
    .info.version = GBITMAP_VERSION_CURRENT,
    .bounds = { { 0, 0 }, { 50, 50 } }
  };
  memset(dest_data, GColorBlue.argb, sizeof(dest_data));
  bitblt_bitmap_into_bitmap(&dest_bitmap, &cropped_src_bitmap, (GPoint) { 0, 2 },
                            GCompOpAssign, GColorWhite);

  cl_assert(gbitmap_pbi_eq(&dest_bitmap,
                           "test_bitblt__bitmap_into_bitmap_sub_bitmap-expect.8bit.pbi"));

  gbitmap_destroy(src_bitmap);
}

// Test:
//   - source origin offset
//   - source bounds origin and size
//   - wrapping into larger destination
//   - dest rect not at { 0, 0 }
// Setup:
//   - Source has non-zero bounds origin, { 5, 5 }, outside of this is red.
//   - Source has non-full bounds size, { 10, 10 }, outside of this is white.
//   - Source has an origin offset, { 3, 6 }, outside of this is blue.
//   - Rest is black
//
//   - Destination has non-zero dest_rect origin, { 4, 4 }
//   - Destination has non-full dest_rect size larger than source, { 10, 10 }
//   - Destination has full size of { 20, 20 }
// Result:
//   - Expect a 10 x 10 black box at { 4, 4 } that is of size { 10, 10 }
//   - Remainder should be green.
void test_bitblt__8bit_bounds_and_origin_offset(void) {
  GBitmap *src_bitmap = get_gbitmap_from_pbi("test_bitblt__bounds_and_origin_offset.8bit.pbi");
  src_bitmap->bounds = (GRect) { { 5, 5}, { 10, 10 } };

  uint8_t dest_data[50*50];
  GBitmap dest_bitmap = {
    .addr = dest_data,
    .row_size_bytes = 50,
    .info.format = GBitmapFormat8Bit,
    .info.version = GBITMAP_VERSION_CURRENT,
    .bounds = { { 0, 0 }, { 50, 50 } }
  };
  memset(dest_data, GColorGreen.argb, sizeof(dest_data));
  const GRect dest_rect = GRect(4, 4, 10, 10);
  const GPoint src_origin_offset = GPoint(3, 6);
  bitblt_bitmap_into_bitmap_tiled_8bit_to_8bit(&dest_bitmap, src_bitmap, dest_rect,
                                               src_origin_offset, GCompOpAssign, GColorWhite);
  cl_assert(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__bounds_and_origin_offset-expect.8bit.pbi"));

  gbitmap_destroy(src_bitmap);
}

// Setup:
//   - Source is a 1bit image of a white cross with a black background
// Result:
//   - The image names describe the expected result of each destination color / tint color
//     combination
void test_bitblt__1bit_to_1bit_comptint(void) {
  GBitmap *src_bitmap = get_gbitmap_from_pbi("test_bitblt__1bit_to_1bit_comptint.1bit.pbi");
  const GRect bounds = gbitmap_get_bounds(src_bitmap);

  cl_assert_equal_i(src_bitmap->info.format, GBitmapFormat1Bit);

  uint8_t dest_data[bounds.size.w * bounds.size.h];
  GBitmap dest_bitmap = {
    .addr = dest_data,
    .row_size_bytes = src_bitmap->row_size_bytes,
    .info.format = GBitmapFormat1Bit,
    .info.version = GBITMAP_VERSION_CURRENT,
    .bounds = src_bitmap->bounds
  };

  // Image name descriptions
  // comptint_<cross_color>_on_<background_color>.1bit

  // Destination White
  memset(dest_data, 0b11111111, sizeof(dest_data));
  bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, (GPoint){0}, GCompOpTint, GColorClear);
  cl_check(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__1bit_to_1bit_comptint_white_on_white-expect.1bit.pbi"));

  memset(dest_data, 0b11111111, sizeof(dest_data));
  bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, (GPoint){0}, GCompOpTint, GColorWhite);
  cl_check(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__1bit_to_1bit_comptint_white_on_white-expect.1bit.pbi"));

  memset(dest_data, 0b11111111, sizeof(dest_data));
  bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, (GPoint){0}, GCompOpTint, GColorBlack);
  cl_check(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__1bit_to_1bit_comptint_black_on_white-expect.1bit.pbi"));

  // Destination Black
  memset(dest_data, 0b00000000, sizeof(dest_data));
  bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, (GPoint){0}, GCompOpTint, GColorClear);
  cl_check(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__1bit_to_1bit_comptint_black_on_black-expect.1bit.pbi"));

  memset(dest_data, 0b00000000, sizeof(dest_data));
  bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, (GPoint){0}, GCompOpTint, GColorWhite);
  cl_check(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__1bit_to_1bit_comptint_white_on_black-expect.1bit.pbi"));

  memset(dest_data, 0b00000000, sizeof(dest_data));
  bitblt_bitmap_into_bitmap(&dest_bitmap, src_bitmap, (GPoint){0}, GCompOpTint, GColorBlack);
  cl_check(gbitmap_pbi_eq(&dest_bitmap, "test_bitblt__1bit_to_1bit_comptint_black_on_black-expect.1bit.pbi"));

  gbitmap_destroy(src_bitmap);
}

// Test:
//   - source origin offset
//   - source bounds origin and size
//     - Source bounds origin is beyond (32, y) to pass word boundary.
//     - Source origin offset is beyond (32, y) to pass another word boundary.
//   - wrapping into larger destination
//   - dest rect not at { 0, 0 }
// Setup:
//   - Dest rect at {4, 4}, repeat twice and a bit: {140, 55}
//   - Source bounds origin at {37, 3), size {63, 23}
//   - Source origin offset (39, 11)
void DISABLED_test_bitblt__1bit_to_8bit_bounds_and_origin_offset(void) {
  GBitmap *src_bitmap =
      get_gbitmap_from_pbi("test_bitblt__1bit_to_8bit_bounds_and_origin_offset.1bit.pbi");

  uint8_t dest_data[144*168];
  GBitmap dest_bitmap = {
    .addr = dest_data,
    .row_size_bytes = 144,
    .info.format = GBitmapFormat8Bit,
    .info.version = GBITMAP_VERSION_CURRENT,
    .bounds = { { 0, 0 }, { 144, 168 } }
  };
  memset(dest_data, GColorGreen.argb, sizeof(dest_data));
  const GRect dest_rect = GRect(4, 4, 140, 55);
  const GPoint src_origin_offset = GPoint(39, 11);
  src_bitmap->bounds = (GRect) { { 37, 3}, { 63, 23 } };

  bitblt_bitmap_into_bitmap_tiled_1bit_to_8bit(&dest_bitmap, src_bitmap, dest_rect,
                                               src_origin_offset, GCompOpAssign, GColorWhite);

  cl_assert(gbitmap_pbi_eq(&dest_bitmap,
      "test_bitblt__1bit_to_8bit_bounds_and_origin_offset-expect.8bit.pbi"));

  gbitmap_destroy(src_bitmap);
}

// This test is the same as test_bitblt__1bit_to_1bit_bounds_and_origin_offset, except it is for
// 1-bit to 1-bit.
// FIXME: This is a known legacy-broken case in 1-bit bitblt. See PBL-14671 for more information
void DISABLED__test_bitblt__1bit_to_1bit_bounds_and_origin_offset(void) {
  GBitmap *src_bitmap =
      get_gbitmap_from_pbi("test_bitblt__1bit_to_1bit_bounds_and_origin_offset.1bit.pbi");

  uint8_t dest_data[src_bitmap->row_size_bytes * src_bitmap->bounds.size.h];
  GBitmap dest_bitmap = *src_bitmap;
  dest_bitmap.addr = dest_data;
  dest_bitmap.bounds = src_bitmap->bounds;
  memset(dest_data, 0xff, sizeof(dest_data));

  const GRect dest_rect = GRect(4, 4, 140, 55);
  const GPoint src_origin_offset = GPoint(39, 11);

  src_bitmap->bounds = (GRect) { { 37, 3}, { 63, 23 } };
  bitblt_bitmap_into_bitmap_tiled_1bit_to_1bit(&dest_bitmap, src_bitmap, dest_rect,
                                               src_origin_offset, GCompOpAssign, GColorWhite);

  cl_assert(gbitmap_pbi_eq(&dest_bitmap,
      "test_bitblt__1bit_to_1bit_bounds_and_origin_offset-expect.8bit.pbi"));

  gbitmap_destroy(src_bitmap);
}
