/*
 * Copyright 2024 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#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 <string.h>

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

// Setup
////////////////////////////////////
static GContext ctx;
static FrameBuffer framebuffer;
static uint8_t dest_bitmap_data[FRAMEBUFFER_SIZE_BYTES];
static GBitmap dest_bitmap = {
  .addr = dest_bitmap_data,
  .row_size_bytes = FRAMEBUFFER_BYTES_PER_ROW,
  .info.is_bitmap_heap_allocated = false,
  .info.format = GBitmapFormat8Bit,
  .info.version = GBITMAP_VERSION_CURRENT,
  .bounds = {
    .size = {
      .w = DISP_COLS,
      .h = DISP_ROWS
    },
    .origin = { 0, 0 },
  },
};

// Utilities
////////////////////////////////////
#if SCREEN_COLOR_DEPTH_BITS == 8
extern GColor get_bitmap_color(GBitmap *bmp, int x, int y);
#endif

// @param color_index the color index (the value) to put into the bitmap at (x, y).
// @param bpp Bits per pixel.
// @para line_stride how many bytes per line in the bitmap data.
void packed_pixel_set(uint8_t *buf, uint8_t color_index, int16_t x, int16_t y,
                      uint8_t bpp, int16_t line_stride) {
  const uint8_t ppb = 8 / bpp;
  uint8_t idx = y*line_stride + (x/(ppb));
  const uint8_t shift = (8 - bpp) - bpp * (x % ppb);
  const uint8_t mask = ~(((1 << bpp) - 1) << shift);
  buf[idx] = buf[idx] & mask;
  buf[idx] |= ((color_index & ((1 << bpp) - 1)) << shift);
}

static bool prv_check_source_stripe_blit(const uint8_t *data,
                                         const GBitmap *src_bmp, GColor surround_color) {
  for (uint8_t y = 0; y < (FRAMEBUFFER_SIZE_BYTES / FRAMEBUFFER_BYTES_PER_ROW); ++y) {
    for (uint8_t x = 0; x < FRAMEBUFFER_BYTES_PER_ROW; ++x) {
      uint8_t color = data[y*FRAMEBUFFER_BYTES_PER_ROW + x];
      if (y < src_bmp->bounds.size.h && x < src_bmp->bounds.size.w) {
        if (color != src_bmp->palette[x].argb) {
          return false;
        }
      } else {
        if (color != surround_color.argb) {
          return false;
        }
      }
    }
  }
  return true;
}

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

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

void test_bitblt_palette__cleanup(void) {
}

void test_bitblt_palette__1Bit_color(void) {
  const int BITS_PER_PIXEL = 1;
  const int PIXELS_PER_BYTE = (8 / BITS_PER_PIXEL);
  const int WIDTH = 2;
  const int HEIGHT = 2;
  const int ROW_STRIDE = (WIDTH + (PIXELS_PER_BYTE - 1)) / PIXELS_PER_BYTE;

  uint8_t s_data[ROW_STRIDE * HEIGHT];
  GColor s_palette[1 << 1] = {
    GColorMelon, GColorIcterine
  };
  cl_assert(sizeof(s_palette) == (1 << BITS_PER_PIXEL));
  GBitmap s_bmp = (GBitmap) {
    .addr = s_data,
    .row_size_bytes = ROW_STRIDE,
    .info.format = GBitmapFormat1BitPalette,
    .info.version = GBITMAP_VERSION_CURRENT,
    .bounds = { .size = { WIDTH, HEIGHT } },
    .palette = s_palette,
  };
  memset(s_data, 0, sizeof(s_data));

  for (int y = 0; y < HEIGHT; ++y) {
    for (int x = 0; x < WIDTH; ++x) {
      packed_pixel_set(s_data, x /* color_index */, x, y, BITS_PER_PIXEL, s_bmp.row_size_bytes);
    }
  }
  memset(dest_bitmap_data, GColorWhite.argb, sizeof(dest_bitmap_data));

#if SCREEN_COLOR_DEPTH_BITS == 8
  char print_buf[20];
  for (int y = 0; y < HEIGHT; ++y) {
    for (int x = 0; x < WIDTH; ++x) {
      snprintf(print_buf, sizeof(print_buf), "Failed index = %d, %d", x, y);
      cl_check_(gcolor_equal(get_bitmap_color(&s_bmp, x, y), s_palette[x]), print_buf);
      cl_check_(gcolor_equal(get_bitmap_color(&s_bmp, x, y), s_palette[x]), print_buf);
    }
  }
#endif
}

void test_bitblt_palette__4Bit_assign(void) {
  const int BITS_PER_PIXEL = 4;
  const int PIXELS_PER_BYTE = (8 / BITS_PER_PIXEL);
  const int WIDTH = 16;
  const int HEIGHT = 16;
  const int ROW_STRIDE = (WIDTH + (PIXELS_PER_BYTE - 1)) / PIXELS_PER_BYTE;

  uint8_t s_data[ROW_STRIDE * HEIGHT];
  GColor s_palette[1 << 4] = {
    GColorMelon,         GColorIcterine,   GColorYellow, GColorSunsetOrange,
    GColorScreaminGreen, GColorMagenta,    GColorOrange, GColorFolly,
    GColorLimerick,      GColorPictonBlue, GColorPurple, GColorCadetBlue,
    GColorMalachite,     GColorGreen,      GColorIndigo, GColorVividCerulean
  };
  cl_assert(sizeof(s_palette) == (1 << BITS_PER_PIXEL));
  GBitmap s_bmp = (GBitmap) {
    .addr = s_data,
    .row_size_bytes = ROW_STRIDE,
    .info.format = GBitmapFormat4BitPalette,
    .info.version = GBITMAP_VERSION_CURRENT,
    .bounds = { .size = { WIDTH, HEIGHT } },
    .palette = s_palette,
  };
  memset(s_data, 0, sizeof(s_data));

  for (int y = 0; y < HEIGHT; ++y) {
    for (int x = 0; x < WIDTH; ++x) {
      packed_pixel_set(s_data, x /* color_index */, x, y, BITS_PER_PIXEL, s_bmp.row_size_bytes);
    }
  }
  memset(dest_bitmap_data, GColorWhite.argb, sizeof(dest_bitmap_data));

#if SCREEN_COLOR_DEPTH_BITS == 8
  char print_buf[20];
  for (int y = 0; y < HEIGHT; ++y) {
    for (int x = 0; x < WIDTH; ++x) {
      snprintf(print_buf, sizeof(print_buf), "Failed index = %d, %d", x, y);
      cl_check_(gcolor_equal(get_bitmap_color(&s_bmp, x, y), s_palette[x]), print_buf);
      cl_check_(gcolor_equal(get_bitmap_color(&s_bmp, x, y), s_palette[x]), print_buf);
    }
  }
#endif

  bitblt_bitmap_into_bitmap(&dest_bitmap, &s_bmp, GPointZero, GCompOpAssign, GColorWhite);

  cl_assert(prv_check_source_stripe_blit(dest_bitmap_data, &s_bmp, GColorWhite));
}

static void prv_opaque_2bit_simple(GCompOp compositing_mode) {
  const int BITS_PER_PIXEL = 2;
  const int PIXELS_PER_BYTE = (8 / BITS_PER_PIXEL);
  const int WIDTH = 4;
  const int HEIGHT = 4;
  const int ROW_STRIDE = (WIDTH + (PIXELS_PER_BYTE - 1)) / PIXELS_PER_BYTE;

  uint8_t s_data[ROW_STRIDE * HEIGHT];
  GColor s_palette[] = {
    GColorRed, GColorWhite, GColorBlack, GColorBlue
  };

  cl_assert(sizeof(s_palette) == (1 << BITS_PER_PIXEL));

  GBitmap s_bmp = (GBitmap) {
    .addr = s_data,
    .row_size_bytes = ROW_STRIDE,
    .info.format = GBitmapFormat2BitPalette,
    .info.version = GBITMAP_VERSION_CURRENT,
    .bounds = { .size = { WIDTH, HEIGHT } },
    .palette = s_palette,
  };

  memset(s_data, 0, sizeof(s_data));

  for (int y = 0; y < HEIGHT; ++y) {
    for (int x = 0; x < WIDTH; ++x) {
      packed_pixel_set(s_data, x /* color_index */, x, y, BITS_PER_PIXEL, s_bmp.row_size_bytes);
    }
  }

  memset(dest_bitmap_data, GColorWhite.argb, sizeof(dest_bitmap_data));

#if SCREEN_COLOR_DEPTH_BITS == 8
  char print_buf[20];
  for (int y = 0; y < HEIGHT; ++y) {
    for (int x = 0; x < WIDTH; ++x) {
      snprintf(print_buf, sizeof(print_buf), "Failed index = %d, %d", x, y);
      cl_check_(gcolor_equal(get_bitmap_color(&s_bmp, x, y), s_palette[x]), print_buf);
      cl_check_(gcolor_equal(get_bitmap_color(&s_bmp, x, y), s_palette[x]), print_buf);
    }
  }
#endif

  bitblt_bitmap_into_bitmap(&dest_bitmap, &s_bmp, GPointZero, compositing_mode, GColorWhite);

  switch (compositing_mode) {
    case GCompOpTint: {
      // Since this is opaque, the tint_color will be used for the RGB value of the destination
      // image, so we want to use that as our palette.
      for (uint8_t idx = 0; idx < WIDTH; idx++) {
        if (s_palette[idx].a == 3) {
          s_palette[idx] = GColorWhite;
        }
      }
      break;
    }
    default:
      break;
  }

  cl_assert(prv_check_source_stripe_blit(dest_bitmap_data, &s_bmp, GColorWhite));
}

static void prv_4bit_simple(GCompOp compositing_mode, GColor color, bool transparent) {
  const int BITS_PER_PIXEL = 4;
  const int PIXELS_PER_BYTE = (8 / BITS_PER_PIXEL);
  const int WIDTH = 16;
  const int HEIGHT = 16;
  const int ROW_STRIDE = (WIDTH + (PIXELS_PER_BYTE - 1)) / PIXELS_PER_BYTE;

  uint8_t s_data[ROW_STRIDE * HEIGHT];
  GColor s_palette[] = {
    GColorMelon,         GColorIcterine,   GColorYellow, GColorSunsetOrange,
    GColorScreaminGreen, GColorMagenta,    GColorOrange, GColorFolly,
    GColorLimerick,      GColorPictonBlue, GColorPurple, GColorCadetBlue,
    GColorMalachite,     GColorGreen,      GColorIndigo, GColorVividCerulean
  };

  cl_assert(sizeof(s_palette) == (1 << BITS_PER_PIXEL));

  if (transparent) {
    // Make all of those colors fully transparent
    for (int i = 0; i < sizeof(s_palette); ++i) {
      s_palette[i].a = 0;
    }
  }

  GBitmap s_bmp = (GBitmap) {
    .addr = s_data,
    .row_size_bytes = ROW_STRIDE,
    .info.format = GBitmapFormat4BitPalette,
    .info.version = GBITMAP_VERSION_CURRENT,
    .bounds = { .size = { WIDTH, HEIGHT } },
    .palette = s_palette,
  };

  memset(s_data, 0, sizeof(s_data));

  for (int y = 0; y < HEIGHT; ++y) {
    for (int x = 0; x < WIDTH; ++x) {
      packed_pixel_set(s_data, x /* color_index */, x, y, BITS_PER_PIXEL, s_bmp.row_size_bytes);
    }
  }

  memset(dest_bitmap_data, GColorWhite.argb, sizeof(dest_bitmap_data));

  bitblt_bitmap_into_bitmap(&dest_bitmap, &s_bmp, GPointZero, compositing_mode, color);

  switch (compositing_mode) {
    case GCompOpSet: {
      if (transparent) {
        memset(s_palette, color.argb, sizeof(s_palette));
      }
      break;
    }
    case GCompOpAssign:
      break;
    case GCompOpTint: {
      for (uint8_t idx = 0; idx < WIDTH; idx++) {
        if (s_palette[idx].a == 3 || transparent) {
          s_palette[idx] = color;
        }
      }
      break;
    }
    default:
      break;
  }

  cl_assert(prv_check_source_stripe_blit(dest_bitmap_data, &s_bmp, GColorWhite));
}

void test_bitblt_palette__2Bit_assign_opaque(void) {
  prv_opaque_2bit_simple(GCompOpAssign);
}

void test_bitblt_palette__2Bit_set_opaque(void) {
  prv_opaque_2bit_simple(GCompOpSet);
}

void test_bitblt_palette__2Bit_comptint_opaque(void) {
  prv_opaque_2bit_simple(GCompOpTint);
}

void test_bitblt_palette__4Bit_assign_opaque(void) {
  prv_4bit_simple(GCompOpAssign, GColorWhite, false /* opaque */);
}

void test_bitblt_palette__4Bit_assign_transparent(void) {
  prv_4bit_simple(GCompOpAssign, GColorWhite, true /* transparent */);
}

void test_bitblt_palette__4Bit_set_opaque(void) {
  prv_4bit_simple(GCompOpSet, GColorWhite, false /* opaque */);
}

void test_bitblt_palette__4Bit_set_transparent(void) {
  prv_4bit_simple(GCompOpSet, GColorWhite, true /* transparent */);
}

void test_bitblt_palette__4Bit_comptint_opaque(void) {
  prv_4bit_simple(GCompOpTint, GColorBlack, false /* opaque */);
  prv_4bit_simple(GCompOpTint, GColorBlue, false /* opaque */);
}

void test_bitblt_palette__4Bit_comptint_transparent(void) {
  prv_4bit_simple(GCompOpTint, GColorWhite, true /* transparent */);
}