/*
 * 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/gbitmap_sequence.h"
#include "applib/graphics/graphics.h"

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

#include <util/size.h>

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

// Test files are Creative Commons 0 (ie. Public Domain) from
// http://opengameart.org/content/game-character-blue-flappy-bird-sprite-sheets
// http://opengameart.org/content/pixel-puncher-sprites
// http://opengameart.org/content/spike-man-monster
// http://opengameart.org/content/platformer-baddies
// http://opengameart.org/content/greyscale-special-effects-%E2%80%94-various-dimensions-and-flavours
// http://opengameart.org/content/bouncing-ball-guy
// http://opengameart.org/content/medals-3
// http://opengameart.org/content/open-pixel-platformer-tiles-sprites

#if SCREEN_COLOR_DEPTH_BITS == 8
#include "applib/graphics/8_bit/framebuffer.c"
#elif SCREEN_COLOR_DEPTH_BITS == 1
#include "applib/graphics/1_bit/framebuffer.c"
#endif

// Fakes
////////////////////////////////////
#include "fake_resource_syscalls.h"
#include "fake_app_timer.h"

// Stubs
////////////////////////////////////
#include "stubs_applib_resource.h"
#include "stubs_app_state.h"
#include "stubs_logging.h"
#include "stubs_heap.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_pebble_tasks.h"
#include "stubs_print.h"
#include "stubs_queue.h"
#include "stubs_resources.h"
#include "stubs_serial.h"
#include "stubs_ui_window.h"

#define GET_PBI_NAME(x) prv_get_image_name(__func__, x, "pbi")
#define GET_APNG_NAME prv_get_image_name(__func__, 0, "apng")

// Used to work around __func__ not being a string literal (necessary for macro concatenation)
static const char *prv_get_image_name(const char* func_name, int index, const char *extension) {
  char *filename = malloc(PATH_STRING_LENGTH);
  if (index) {
    snprintf(filename, PATH_STRING_LENGTH, "%s_%u.%s", func_name, index, extension);
  } else {
    snprintf(filename, PATH_STRING_LENGTH, "%s.%s", func_name, extension);
  }
  return filename;
}

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

// Reference PNGs reside in "tests/test_images/"
// and are created at build time, with the files copied to TEST_IMAGES_PATH 
// and a separate PBI generated by bitmapgen.py from the PNG copied to TEST_IMAGES_PATH

// Tests APNG file with 2-bit color, 6 frames and loop = infinite 
// Tests start, stop, reset and loop
// Result:
//   - gbitmaps matches platform decoded APNG
void test_gbitmap_sequence__color_2bit_bouncing_ball(void) {
#if PLATFORM_SPALDING
  uint32_t resource_id = sys_resource_load_file_as_resource(TEST_IMAGES_PATH, GET_APNG_NAME);
  cl_assert(resource_id != UINT32_MAX);

  GBitmapSequence *bitmap_sequence = gbitmap_sequence_create_with_resource(resource_id);
  cl_assert(bitmap_sequence);

  GBitmap *bitmap = gbitmap_create_blank(gbitmap_sequence_get_bitmap_size(bitmap_sequence), 
                                         GBitmapFormat8Bit);
  cl_assert(bitmap);

  gbitmap_sequence_update_bitmap_next_frame(bitmap_sequence, bitmap, NULL);
  cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(1)));

  gbitmap_sequence_update_bitmap_next_frame(bitmap_sequence, bitmap, NULL);
  cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(2)));
  
  gbitmap_sequence_update_bitmap_next_frame(bitmap_sequence, bitmap, NULL);
  cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(3)));
  
  gbitmap_sequence_update_bitmap_next_frame(bitmap_sequence, bitmap, NULL);
  cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(4)));
  
  gbitmap_sequence_update_bitmap_next_frame(bitmap_sequence, bitmap, NULL);
  cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(5)));

  gbitmap_sequence_update_bitmap_next_frame(bitmap_sequence, bitmap, NULL);
  cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(6)));

  // Test loop around
  gbitmap_sequence_update_bitmap_next_frame(bitmap_sequence, bitmap, NULL);
  cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(1)));
#endif
}


// Tests APNG file with 8-bit color, 88 frames of delay 100ms each and loop = 2
// Tests gbitmap_sequence_update_bitmap_by_elapsed
// Result:
//   - gbitmaps matches platform decoded APNG
void test_gbitmap_sequence__color_8bit_fight(void) {
#if PLATFORM_SPALDING
  bool status = false;
  uint32_t resource_id = sys_resource_load_file_as_resource(TEST_IMAGES_PATH, GET_APNG_NAME);
  cl_assert(resource_id != UINT32_MAX);

  GBitmapSequence *bitmap_sequence = gbitmap_sequence_create_with_resource(resource_id);
  cl_assert(bitmap_sequence);

  GBitmap *bitmap = gbitmap_create_blank(gbitmap_sequence_get_bitmap_size(bitmap_sequence),
                                 GBitmapFormat8Bit);
  cl_assert(bitmap);

  // first frame always shows at the beginning (0 ms)
  status = gbitmap_sequence_update_bitmap_by_elapsed(bitmap_sequence, bitmap, 0);
  cl_assert_equal_b(status, true);
  cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(1)));
  
  // each frame has delay 100ms, so (33 - 1) * 100 ms shows 33rd frame
  status = gbitmap_sequence_update_bitmap_by_elapsed(bitmap_sequence, bitmap, (33 - 1) * 100);
  cl_assert_equal_b(status, true);
  cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(33)));

  status = gbitmap_sequence_update_bitmap_by_elapsed(bitmap_sequence, bitmap, (60 - 1) * 100);
  cl_assert_equal_b(status, true);
  cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(60)));
  
  // Since we have loop count 2 and 88 frames, see if we can go to elapsed at 2nd loop correctly
  status = gbitmap_sequence_update_bitmap_by_elapsed(bitmap_sequence, bitmap,
                                                     88 * 100 + (49 - 1) * 100);
  cl_assert_equal_b(status, true);
  cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(49)));

  // Since loop only equals 2, make sure that huge elapsed time leaves us at final frame
  status = gbitmap_sequence_update_bitmap_by_elapsed(bitmap_sequence, bitmap, 12345 * 100);
  cl_assert_equal_b(status, true);
  cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(88)));
#endif
}


// Tests APNG file with 8-bit color, 5 frames and loop = 1
// Tests gbitmap_sequence_update_bitmap_by_elapsed with 0 delay frames for 1, 4 and 5
// and 2 & 3 have delay 1000ms
// Result:
//   - gbitmaps matches platform decoded APNG
void test_gbitmap_sequence__color_8bit_coin(void) {
#if PLATFORM_SPALDING
  uint32_t resource_id = sys_resource_load_file_as_resource(TEST_IMAGES_PATH, GET_APNG_NAME);
  cl_assert(resource_id != UINT32_MAX);

  GBitmapSequence *bitmap_sequence = gbitmap_sequence_create_with_resource(resource_id);
  cl_assert(bitmap_sequence);

  GBitmap *bitmap = gbitmap_create_blank(gbitmap_sequence_get_bitmap_size(bitmap_sequence),
                                         GBitmapFormat8Bit);
  cl_assert(bitmap);

  // Since frame 1 has 0 delay, it renders then immediately we render frame 2
  gbitmap_sequence_update_bitmap_by_elapsed(bitmap_sequence, bitmap, 0);
  cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(2)));


  // Frame 2 has delay 1000 ms, so at time 1000 ms, we expect frame 3
  gbitmap_sequence_update_bitmap_by_elapsed(bitmap_sequence, bitmap, 1 * 1000);
  cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(3)));

  // Frame 3 has delay 1000 ms, frame 4 and 5 have zero delay, so at time 2000 ms == frame 5
  gbitmap_sequence_update_bitmap_by_elapsed(bitmap_sequence, bitmap, 2 * 1000);
  cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(5)));

  // Since loop only equals 1, make sure that huge elapsed time leaves us at final frame
  gbitmap_sequence_update_bitmap_by_elapsed(bitmap_sequence, bitmap, 12345 * 1000);
  cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(5)));
#endif
}

// Tests APNG file with 8-bit color, 5 frames and loop = 1
// Tests gbitmap_sequence_update_bitmap_by_elapsed with 0 delay frames for 1, 4 and 5
// and 2 & 3 have delay 1000ms
// Result:
//   - gbitmaps matches platform decoded APNG
void test_gbitmap_sequence__color_8bit_coin_round(void) {
#if PLATFORM_SPALDING
  uint32_t resource_id = sys_resource_load_file_as_resource(
      TEST_IMAGES_PATH, "test_gbitmap_sequence__color_8bit_coin.apng");
  cl_assert(resource_id != UINT32_MAX);

  GBitmapSequence *bitmap_sequence = gbitmap_sequence_create_with_resource(resource_id);
  cl_assert(bitmap_sequence);

  GBitmap *bitmap = gbitmap_create_blank(GSize(180, 180), GBitmapFormat8BitCircular);
  cl_assert(bitmap);

  // Since frame 1 has 0 delay, it renders then immediately we render frame 2
  gbitmap_sequence_update_bitmap_by_elapsed(bitmap_sequence, bitmap, 0);
  cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(2)));

  // Frame 2 has delay 1000 ms, so at time 1000 ms, we expect frame 3
  gbitmap_sequence_update_bitmap_by_elapsed(bitmap_sequence, bitmap, 1 * 1000);
  cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(3)));

  // Frame 3 has delay 1000 ms, frame 4 and 5 have zero delay, so at time 2000 ms == frame 5
  gbitmap_sequence_update_bitmap_by_elapsed(bitmap_sequence, bitmap, 2 * 1000);
  cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(5)));

  // Since loop only equals 1, make sure that huge elapsed time leaves us at final frame
  gbitmap_sequence_update_bitmap_by_elapsed(bitmap_sequence, bitmap, 12345 * 1000);
  cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(5)));
#endif
}

// Tests APNG file with 8-bit color, 5 frames and loop infinite, size 64x64
// Tests gbitmap_sequence for bitmap bounds offset and DISPOSE_OP_BACKGROUND
// Result:
//   - gbitmaps matches platform decoded APNG
void test_gbitmap_sequence__color_8bit_bounds(void) {
#if PLATFORM_SPALDING
  uint32_t resource_id = sys_resource_load_file_as_resource(TEST_IMAGES_PATH, GET_APNG_NAME);
  cl_assert(resource_id != UINT32_MAX);

  GRect orig_bounds = GRect(0, 0, 180, 180);
  int xshift = 71;
  int yshift = 39;
  GRect shift_bounds = GRect(xshift, yshift,
                             orig_bounds.size.w - xshift,
                             orig_bounds.size.h - yshift);

  GBitmapSequence *bitmap_sequence = gbitmap_sequence_create_with_resource(resource_id);
  cl_assert(bitmap_sequence);

  GBitmap *bitmap = gbitmap_create_blank(orig_bounds.size, GBitmapFormat8BitCircular);
  cl_assert(bitmap);

  // Shift the bounds when updating
  gbitmap_set_bounds(bitmap, shift_bounds);
  gbitmap_sequence_update_bitmap_next_frame(bitmap_sequence, bitmap, NULL);
  // set the original bounds to do whole-image comparison
  gbitmap_set_bounds(bitmap, orig_bounds);
  cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(1)));

  gbitmap_set_bounds(bitmap, shift_bounds);
  gbitmap_sequence_update_bitmap_next_frame(bitmap_sequence, bitmap, NULL);
  gbitmap_set_bounds(bitmap, orig_bounds);
  cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(2)));
  
  gbitmap_set_bounds(bitmap, shift_bounds);
  gbitmap_sequence_update_bitmap_next_frame(bitmap_sequence, bitmap, NULL);
  gbitmap_set_bounds(bitmap, orig_bounds);
  cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(3)));
  
  gbitmap_set_bounds(bitmap, shift_bounds);
  gbitmap_sequence_update_bitmap_next_frame(bitmap_sequence, bitmap, NULL);
  gbitmap_set_bounds(bitmap, orig_bounds);
  cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(4)));
  
  gbitmap_set_bounds(bitmap, shift_bounds);
  gbitmap_sequence_update_bitmap_next_frame(bitmap_sequence, bitmap, NULL);
  gbitmap_set_bounds(bitmap, orig_bounds);
  cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(5)));
#endif
}

// Tests APNG file with 8-bit color, originally broke the updated upng+tinflate code
// Result:
//   - gbitmaps matches platform decoded APNG
void test_gbitmap_sequence__color_8bit_yoshi(void) {
#if PLATFORM_SPALDING
  bool status = false;
  uint32_t resource_id = sys_resource_load_file_as_resource(TEST_IMAGES_PATH, GET_APNG_NAME);
  cl_assert(resource_id != UINT32_MAX);
  GBitmapSequence *bitmap_sequence = gbitmap_sequence_create_with_resource(resource_id);
  GBitmap *bitmap = gbitmap_create_blank(gbitmap_sequence_get_bitmap_size(bitmap_sequence), 
                                 GBitmapFormat8Bit);

  status = gbitmap_sequence_update_bitmap_next_frame(bitmap_sequence, bitmap, NULL);
  cl_assert_equal_b(status, true);
  cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(1)));

  status = gbitmap_sequence_update_bitmap_next_frame(bitmap_sequence, bitmap, NULL);
  cl_assert_equal_b(status, true);
  cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(2)));

  status = gbitmap_sequence_update_bitmap_next_frame(bitmap_sequence, bitmap, NULL);
  cl_assert_equal_b(status, true);
  cl_check(gbitmap_pbi_eq(bitmap, GET_PBI_NAME(3)));
#endif
}

void test_gbitmap_sequence__1bit_to_1bit_notification(void) {
  uint32_t resource_id = sys_resource_load_file_as_resource(TEST_IMAGES_PATH, GET_APNG_NAME);
  cl_assert(resource_id != UINT32_MAX);
  GBitmapSequence *bitmap_sequence = gbitmap_sequence_create_with_resource(resource_id);
  GBitmap *bitmap = gbitmap_create_blank(gbitmap_sequence_get_bitmap_size(bitmap_sequence),
                                         GBitmapFormat1Bit);

  // We're interested in checking the following frames
  const int check_frames[] = { 1, 4, 23, 24, 25, 31, 75 };

  int current_frame = 0;
  for (int i = 0; i < ARRAY_LENGTH(check_frames); ++i) {
    // Advance to the next frame we're interested in.
    for (; current_frame < check_frames[i]; ++current_frame) {
      bool status = gbitmap_sequence_update_bitmap_next_frame(bitmap_sequence, bitmap, NULL);
      cl_assert_equal_b(status, true);
    }

    printf("Checking %u\n", current_frame);

    // Don't use GET_PBI_NAME, it doesn't like not using anything other than an integer literal
    char filename_buffer[128];
    snprintf(filename_buffer, sizeof(filename_buffer), "%s_%u.pbi", __func__, current_frame);
    cl_check(gbitmap_pbi_eq(bitmap, filename_buffer));
  }
}
