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

#include "gbitmap_sequence.h"

#include "gbitmap_png.h"
#include "util/graphics.h"
#include "util/net.h"
#include "util/time/time.h"
#include "applib/app_logging.h"
#include "applib/applib_malloc.auto.h"
#include "syscall/syscall.h"
#include "system/passert.h"
#include "util/bitset.h"
#include "util/math.h"

#define APNG_DECODE_ERROR "APNG decoding failed"
#define APNG_MEMORY_ERROR "APNG memory allocation failed"
#define APNG_FORMAT_ERROR "Unsupported APNG format, only APNG8 is supported!"
#define APNG_LOAD_ERROR "Failed to load APNG"
#define APNG_UPDATE_ERROR "gbitmap_sequence failed to update bitmap"
#define APNG_ELAPSED_WARNING "invalid elapsed_ms for gbitmap_sequence, forward progression only"

static bool prv_gbitmap_sequence_restart(GBitmapSequence *bitmap_sequence, bool reset_elapsed) {
  if (bitmap_sequence == NULL) {
    return false;
  }

  // can start seeking after SIG + IHDR
  int32_t metadata_bytes = png_seek_chunk_in_resource(bitmap_sequence->resource_id,
                                                      PNG_HEADER_SIZE, false, NULL);

  if (metadata_bytes <= 0) {
    return false;
  }
  metadata_bytes += PNG_HEADER_SIZE;
  bitmap_sequence->png_decoder_data.read_cursor = metadata_bytes;
  bitmap_sequence->current_frame = 0;
  bitmap_sequence->current_frame_delay_ms = 0;

  if (reset_elapsed) {
    bitmap_sequence->elapsed_ms = 0;
    bitmap_sequence->play_index = 0;
  }
  return true;
}

//! Directly modifies dst, blending src into dst using equation
//! dst = src * (alpha_normalized) + dst * (1 - alpha_normalized)
static ALWAYS_INLINE void prv_gbitmap_sequence_blend_over(GColor8 src_color, GColor8 *dst) {
  if (src_color.a == 3) {
// Fast path: 100% opacity
    *dst = src_color;
  } else if (src_color.a == 0) {
// Fast path: 0% opacity, no-op!
  } else {
    const GColor8 dest_color = *dst;
    const uint8_t f_src = src_color.a;
    const uint8_t f_dst = 3 - f_src;
    GColor8 final = {};
    final.r = (src_color.r * f_src + dest_color.r * f_dst) / 3;
    final.g = (src_color.g * f_src + dest_color.g * f_dst) / 3;
    final.b = (src_color.b * f_src + dest_color.b * f_dst) / 3;
    final.a = src_color.a;  // Different than bitblt, required for correct transparency
    *dst = final;
  }
}

GBitmapSequence *gbitmap_sequence_create_with_resource(uint32_t resource_id) {
  ResAppNum app_num = sys_get_current_resource_num();
  return gbitmap_sequence_create_with_resource_system(app_num, resource_id);
}

GBitmapSequence *gbitmap_sequence_create_with_resource_system(ResAppNum app_num,
                                                              uint32_t resource_id) {
  uint8_t *frame_data_buffer = NULL;

  // Allocate gbitmap
  GBitmapSequence* bitmap_sequence = applib_type_zalloc(GBitmapSequence);
  if (bitmap_sequence == NULL) {
    goto cleanup;
  }

  bitmap_sequence->resource_id = resource_id;
  bitmap_sequence->data_is_loaded_from_flash = true;

  if (!prv_gbitmap_sequence_restart(bitmap_sequence, true)) {
    goto cleanup;
  }
  int32_t frame_bytes = bitmap_sequence->png_decoder_data.read_cursor;

  frame_data_buffer = applib_zalloc(frame_bytes);
  if (frame_data_buffer == NULL) {
    goto cleanup;
  }

  const size_t bytes_read = sys_resource_load_range(app_num, resource_id,
                                                    0, frame_data_buffer, frame_bytes);
  if (bytes_read != (size_t)frame_bytes) {
    goto cleanup;
  }

  upng_t *upng = upng_create();
  if (upng == NULL) {
    goto cleanup;
  }

  bitmap_sequence->png_decoder_data.upng = upng;
  upng_load_bytes(upng, frame_data_buffer, frame_bytes);

  upng_error upng_state = upng_decode_metadata(upng);
  if (upng_state != UPNG_EOK) {
    APP_LOG(APP_LOG_LEVEL_ERROR,
            (upng_state == UPNG_ENOMEM) ? APNG_MEMORY_ERROR : APNG_DECODE_ERROR);
    goto cleanup;
  }
  // Save metadata to bitmap_sequence
  uint32_t play_count = 0;
  // If png is APNG, get num plays, otherwise play count is 0
  if (upng_is_apng(upng)) {
    play_count = upng_apng_num_plays(upng);
    // At the API level 0 is no loops vs APNG specification uses 0 for infinite
    play_count = (play_count == 0) ? PLAY_COUNT_INFINITE : play_count;
  }
  bitmap_sequence->play_count = play_count;
  bitmap_sequence->bitmap_size = (GSize){.w = upng_get_width(upng), .h = upng_get_height(upng)};
  bitmap_sequence->total_frames = upng_apng_num_frames(upng);

  if (!gbitmap_png_is_format_supported(upng)) {
    APP_LOG(APP_LOG_LEVEL_ERROR, APNG_FORMAT_ERROR);
    goto cleanup;
  }

  // Create a color palette in RGBA8 format from RGB24 + ALPHA8 PNG Palettes
  upng_format png_format = upng_get_format(upng);
  if (png_format >= UPNG_INDEXED1 && png_format <= UPNG_INDEXED8) {
    bitmap_sequence->png_decoder_data.palette_entries =
        gbitmap_png_load_palette(upng, &bitmap_sequence->png_decoder_data.palette);
    if (bitmap_sequence->png_decoder_data.palette_entries == 0) {
      APP_LOG(APP_LOG_LEVEL_ERROR, "Failed to load palette");
      goto cleanup;
    }
  }

  bitmap_sequence->header_loaded = true;

cleanup:
  applib_free(frame_data_buffer);  // Free compressed image buffer

  if (!bitmap_sequence || !bitmap_sequence->header_loaded) {
    APP_LOG(APP_LOG_LEVEL_ERROR, APNG_LOAD_ERROR);
    gbitmap_sequence_destroy(bitmap_sequence);
  }

  return bitmap_sequence;
}

bool gbitmap_sequence_restart(GBitmapSequence *bitmap_sequence) {
  return prv_gbitmap_sequence_restart(bitmap_sequence, true);
}

void gbitmap_sequence_destroy(GBitmapSequence *bitmap_sequence) {
  if (bitmap_sequence) {
    upng_destroy(bitmap_sequence->png_decoder_data.upng, true);
    applib_free(bitmap_sequence->png_decoder_data.palette);
    applib_free(bitmap_sequence);
  }
}

static ALWAYS_INLINE GColor8 *prv_target_pixel_addr(GBitmap *bitmap, apng_fctl *fctl,
                                                    uint32_t x, uint32_t y) {
  uint32_t offset = (fctl->y_offset + y + bitmap->bounds.origin.y) * bitmap->row_size_bytes +
      (fctl->x_offset + x + bitmap->bounds.origin.x);
  GColor8 *pixel_data = bitmap->addr;
  return &pixel_data[offset];
}

static void prv_set_pixel_in_row(uint8_t *row_data, GBitmapFormat bitmap_format,
                                 uint32_t x, GColor8 color) {
  if (bitmap_format == GBitmapFormat1Bit) {
    if (!gcolor_is_invisible(color)) {
      const bool pixel_is_white = !gcolor_equal(color, GColorBlack);
      bitset8_update(row_data, x, pixel_is_white);
    }
  } else if ((bitmap_format == GBitmapFormat8Bit) ||
             (bitmap_format == GBitmapFormat8BitCircular)) {
    GColor8 *const destination_pixel = (GColor8 *)(row_data + x);
    *destination_pixel = color;
  } else {
    WTF; // Unsupported destination type
  }
}

bool gbitmap_sequence_update_bitmap_next_frame(GBitmapSequence *bitmap_sequence,
                                               GBitmap *bitmap, uint32_t *delay_ms) {
  bool retval = false;
  uint8_t* buffer = NULL;

  // Disabled if play count is 0 and not the very first frame
  if (!bitmap_sequence ||
      (bitmap_sequence->play_count == 0 && bitmap_sequence->current_frame != 0)) {
    return false;
  }

  GBitmapSequencePNGDecoderData *png_decoder_data = &bitmap_sequence->png_decoder_data;
  upng_t *upng = png_decoder_data->upng;

  // Check bitmap_sequence metadata is loaded, bitmap_sequence size, type & memory constraints
  const GBitmapFormat bitmap_format = gbitmap_get_format(bitmap); // call is NULL-safe
  if (!bitmap_sequence->header_loaded || bitmap == NULL || bitmap->addr == NULL ||
      bitmap_sequence->bitmap_size.w > (bitmap->bounds.size.w) ||
      bitmap_sequence->bitmap_size.h > (bitmap->bounds.size.h)) {
    goto cleanup;
  }

  if (!((bitmap_format == GBitmapFormat1Bit) ||
        (bitmap_format == GBitmapFormat8Bit) ||
        (bitmap_format == GBitmapFormat8BitCircular))) {

    APP_LOG(APP_LOG_LEVEL_ERROR, "Invalid destination bitmap format for APNG");
    goto cleanup;
  }

  // Update current time elapsed using the previous frames current_frame_delay_ms
  bitmap_sequence->elapsed_ms += bitmap_sequence->current_frame_delay_ms;

  // Check if single animation loop is complete, and restart if there are more loops
  if (bitmap_sequence->current_frame >= bitmap_sequence->total_frames) {
    if ((++bitmap_sequence->play_index < bitmap_sequence->play_count) ||
        (bitmap_sequence->play_count == PLAY_COUNT_INFINITE)) {
      prv_gbitmap_sequence_restart(bitmap_sequence, false);
    } else {
      return false;  // animation complete
    }
  }

  const int32_t metadata_bytes =
     png_seek_chunk_in_resource(bitmap_sequence->resource_id,
                                png_decoder_data->read_cursor, true, NULL);

  if (metadata_bytes <= 0) {
    goto cleanup;
  }

  buffer = applib_zalloc(metadata_bytes);
  if (buffer == NULL) {
    goto cleanup;
  }

  ResAppNum app_num = sys_get_current_resource_num();
  const size_t bytes_read = sys_resource_load_range(
      app_num, bitmap_sequence->resource_id,
      png_decoder_data->read_cursor, buffer, metadata_bytes);

  if (bytes_read != (size_t)metadata_bytes) {
    goto cleanup;
  }

  png_decoder_data->read_cursor += metadata_bytes;

  upng_load_bytes(upng, buffer, metadata_bytes);
  upng_error upng_state = upng_decode_image(upng);
  if (upng_state != UPNG_EOK) {
    APP_LOG(APP_LOG_LEVEL_ERROR,
            (upng_state == UPNG_ENOMEM) ? APNG_MEMORY_ERROR : APNG_DECODE_ERROR);
    goto cleanup;
  }
  applib_free(buffer);

  bitmap_sequence->current_frame++;

  const uint32_t width = bitmap_sequence->bitmap_size.w;
  const uint32_t height = bitmap_sequence->bitmap_size.h;

  const bool bitmap_supports_transparency = (bitmap_format != GBitmapFormat1Bit);

  // DISPOSE_OP_BACKGROUND sets the background to black with transparency (0x00)
  // If we don't support tranparency, just do nothing.
  if (bitmap_supports_transparency &&
      (png_decoder_data->last_dispose_op == APNG_DISPOSE_OP_BACKGROUND)) {
    const uint32_t y_origin = bitmap->bounds.origin.y + png_decoder_data->previous_yoffset;
    for (uint32_t y = y_origin; y < y_origin + png_decoder_data->previous_height; y++) {
      const GBitmapDataRowInfo row_info = gbitmap_get_data_row_info(bitmap, y);
      const uint32_t x_origin = bitmap->bounds.origin.x + png_decoder_data->previous_xoffset;
      const int16_t min_x = MAX((uint32_t)row_info.min_x, x_origin);
      const int16_t max_x = MIN((uint32_t)row_info.max_x,
                                (x_origin + png_decoder_data->previous_width - 1));

      const int16_t num_bytes = max_x - min_x + 1;
      if (num_bytes > 0) {
        memset(row_info.data + min_x, 0, num_bytes);
      }
    }
  }

  apng_fctl fctl = {0}; // Defaults work for IDAT frame without fctl data

  // If this frame doesn't have fctl, use the full width & height
  if (!upng_get_apng_fctl(upng, &fctl)) {
    fctl.width = width;
    fctl.height = height;
    // As a PNG image is only a single frame, display it forever
    bitmap_sequence->current_frame_delay_ms = PLAY_DURATION_INFINITE;
  } else {
    png_decoder_data->last_dispose_op = fctl.dispose_op;
    png_decoder_data->previous_xoffset = fctl.x_offset;
    png_decoder_data->previous_yoffset = fctl.y_offset;
    png_decoder_data->previous_width = fctl.width;
    png_decoder_data->previous_height = fctl.height;

    fctl.delay_den = (fctl.delay_den == 0) ? APNG_DEFAULT_DELAY_UNITS : fctl.delay_den;
    // Update the current_frame_delay_ms for this frame
    bitmap_sequence->current_frame_delay_ms =
        ((uint32_t)fctl.delay_num * MS_PER_SECOND) / fctl.delay_den;
  }

  // Return the delay_ms for the new frame
  if (delay_ms != NULL) {
    *delay_ms = bitmap_sequence->current_frame_delay_ms;
  }

  uint32_t bpp = upng_get_bpp(upng);
  upng_format png_format = upng_get_format(upng);
  uint8_t *upng_buffer = (uint8_t*)upng_get_buffer(upng);

  // Byte aligned rows for image at bpp
  uint16_t row_stride_bytes = (fctl.width *  bpp + 7) / 8;

  if (png_format >= UPNG_INDEXED1 && png_format <= UPNG_INDEXED8) {
    const GColor8 *palette = png_decoder_data->palette;

    for (uint32_t y = 0; y < fctl.height; y++) {
      const uint16_t corrected_dst_y = fctl.y_offset + y + bitmap->bounds.origin.y;
      const GBitmapDataRowInfo row_info = gbitmap_get_data_row_info(bitmap, corrected_dst_y);
      int16_t delta_x = fctl.x_offset + bitmap->bounds.origin.x;
      for (int32_t x = MAX(0, row_info.min_x - delta_x);
           x < MIN((int32_t)fctl.width, row_info.max_x - delta_x + 1);
           x++) {
        const uint32_t corrected_dst_x = x + delta_x;
        const uint8_t palette_index = raw_image_get_value_for_bitdepth(upng_buffer, x, y,
            row_stride_bytes, bpp);

        const GColor8 src = palette[palette_index];
        GColor8 *const dst = (GColor8 *)(row_info.data + corrected_dst_x);
        if (fctl.blend_op == APNG_BLEND_OP_OVER) {
          prv_gbitmap_sequence_blend_over(src, dst);
        } else {
          *dst = src;
        }
      }
    }
  } else if (png_format >= UPNG_LUMINANCE1 && png_format <= UPNG_LUMINANCE8) {
    const int32_t transparent_gray = gbitmap_png_get_transparent_gray_value(upng);

    for (uint32_t y = 0; y < fctl.height; y++) {
      const uint16_t corrected_y = fctl.y_offset + y + bitmap->bounds.origin.y;
      const GBitmapDataRowInfo row_info = gbitmap_get_data_row_info(bitmap, corrected_y);

      // delta_x is the first bit of data in this frame relative to the bitmap's coordinate system
      const int16_t delta_x = fctl.x_offset + bitmap->bounds.origin.x;

      // for each pixel in this frame, clipping to the bitmap geometry
      for (int32_t x = MAX(0, row_info.min_x - delta_x);
           x < MIN((int32_t)fctl.width, row_info.max_x - delta_x + 1);
           x++) {

        const uint32_t corrected_dst_x = x + delta_x;
        uint8_t channel = raw_image_get_value_for_bitdepth(upng_buffer, x, y,
                                                           row_stride_bytes, bpp);
        if (transparent_gray >= 0 && channel == transparent_gray) {
          // Grayscale only has fully transparent, so only modify pixels
          // during OP_SOURCE to make the area transparent
          if (fctl.blend_op == APNG_BLEND_OP_SOURCE) {
            prv_set_pixel_in_row(row_info.data, bitmap_format, corrected_dst_x, GColorClear);
          }
        } else {
          channel = (channel * 255) / ~(~0U << bpp);  // Convert to 8-bit value
          const GColor8 color = GColorFromRGB(channel, channel, channel);

          prv_set_pixel_in_row(row_info.data, bitmap_format, corrected_dst_x, color);
        }
      }
    }
  }

  // Successfully updated gbitmap from sequence
  retval = true;

cleanup:
  if (!retval) {
    APP_LOG(APP_LOG_LEVEL_ERROR, APNG_UPDATE_ERROR);
    applib_free(buffer);
  }

  return retval;
}

// total elapsed from start of animation
bool gbitmap_sequence_update_bitmap_by_elapsed(GBitmapSequence *bitmap_sequence,
                                               GBitmap *bitmap, uint32_t elapsed_ms) {
  if (!bitmap_sequence) {
    return false;
  }

  // Disabled if play count is 0 and not the very first frame
  if (bitmap_sequence->play_count == 0 && bitmap_sequence->current_frame != 0) {
    return false;
  }

  // If animation has started and specified time is in the past
  if (bitmap_sequence->current_frame_delay_ms != 0 && elapsed_ms <= bitmap_sequence->elapsed_ms) {
    APP_LOG(APP_LOG_LEVEL_WARNING, APNG_ELAPSED_WARNING);
    return false;
  }

  bool retval = false;
  bool frame_updated = true;
  while (frame_updated && ((elapsed_ms > bitmap_sequence->elapsed_ms) ||
                           (bitmap_sequence->current_frame_delay_ms == 0))) {
    frame_updated = gbitmap_sequence_update_bitmap_next_frame(bitmap_sequence, bitmap, NULL);
    // If frame is updated at least once, return true
    if (frame_updated) {
      retval = true;
    }
  }

  return retval;
}

// Helper functions
int32_t gbitmap_sequence_get_current_frame_idx(GBitmapSequence *bitmap_sequence) {
  if (bitmap_sequence) {
    return bitmap_sequence->current_frame;
  }
  return -1;
}

uint32_t gbitmap_sequence_get_current_frame_delay_ms(GBitmapSequence *bitmap_sequence) {
  if (bitmap_sequence) {
    return bitmap_sequence->current_frame_delay_ms;
  }
  return 0;
}

uint32_t gbitmap_sequence_get_total_num_frames(GBitmapSequence *bitmap_sequence) {
  if (bitmap_sequence) {
    return bitmap_sequence->total_frames;
  }
  return 0;
}

uint32_t gbitmap_sequence_get_play_count(GBitmapSequence *bitmap_sequence) {
  if (bitmap_sequence) {
    return bitmap_sequence->play_count;
  }
  return 0;
}

void gbitmap_sequence_set_play_count(GBitmapSequence *bitmap_sequence, uint32_t play_count) {
  // Loop count is not allowed to be set to 0
  if (bitmap_sequence && play_count) {
    bitmap_sequence->play_count = play_count;
  }
}

GSize gbitmap_sequence_get_bitmap_size(GBitmapSequence *bitmap_sequence) {
  GSize size = (GSize){0, 0};
  if (bitmap_sequence) {
    size = bitmap_sequence->bitmap_size;
  }
  return size;
}

uint32_t gbitmap_sequence_get_total_duration(GBitmapSequence *bitmap_sequence) {
  if (bitmap_sequence) {
    return bitmap_sequence->total_duration_ms;
  }
  return 0;
}
