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

#include "text_render.h"

#include "gcontext.h"
#include "graphics.h"
#include "process_state/app_state/app_state.h"
#include "system/passert.h"
#include "text_resources.h"
#include "util/bitset.h"
#include "util/math.h"

#if !defined(__clang__)
#pragma GCC optimize ("O2")
#endif

static GRect get_glyph_rect(const GlyphData* glyph) {
  GRect r = {
    .size.w = glyph->header.width_px,
    .size.h = glyph->header.height_px,
    .origin.x = glyph->header.left_offset_px,
    .origin.y = glyph->header.top_offset_px
  };

  return r;
}

#if SCREEN_COLOR_DEPTH_BITS == 8
/// This function returns the x coordinate of where to write the contents of a given word (32-bits)
/// of data from the 1-bit frame buffer into the 8-bit framebuffer
/// @param dest_bitmap 8-bit destination frame buffer bitmap
/// @param block_addr source address in 1-bit frame buffer of where the word is being updated
///                   within a given row; assumed to be zero-based
/// @param y_offset row offset within the source 1-bit frame buffer
T_STATIC int32_t prv_convert_1bit_addr_to_8bit_x(GBitmap *dest_bitmap, uint32_t *block_addr,
                                                 int32_t y_offset) {
  // Each byte block_addr corresponds to 8 pixels (i.e. 4-bytes in the 8-bit frame buffer).
  // Thus multiply by 8 to get the word offset within the destination 8-bit frame buffer.
  // Also need to account for the fact that the 1-bit frame buffer has 16 bits of unused space
  // on each row (thus 16 bytes need to be subtracted from the destination address since there is
  // no padding on each row of the 8-bit frame buffer.
  const int32_t padding = (32 - (dest_bitmap->bounds.size.w % 32)) % 32;
  // Calculate the overall offset in the 8-bit bitmap
  const int32_t bitmap_offset_8bit = ((uint32_t)block_addr * 8) - (padding * y_offset);
  // Calculate just the offset from the start of the target row in the 8-bit bitmap (i.e. "x")
  return bitmap_offset_8bit - (dest_bitmap->bounds.size.w * y_offset);
}
#endif

// PRO TIP: if you have to modify this function, expect to waste the rest of your day on it
void render_glyph(GContext* const ctx, const uint32_t codepoint, FontInfo* const font,
                  const GRect cursor) {
  if (codepoint_is_special(codepoint)) {
    TextRenderState *state = app_state_get_text_render_state();
    if (state->special_codepoint_handler_cb) {
      state->special_codepoint_handler_cb(ctx, codepoint, cursor,
          state->special_codepoint_handler_context);
    }
    return;
  }

  const GlyphData* glyph = text_resources_get_glyph(&ctx->font_cache, codepoint, font);

  PBL_ASSERTN(glyph);
  // Bitfiddle the metrics data:
  GRect glyph_metrics = get_glyph_rect(glyph);

  // Calculate the box that we intend to draw to the screen, in screen coordinates
  GRect glyph_target = {
    .origin = { .x = cursor.origin.x + glyph_metrics.origin.x,
                .y = cursor.origin.y + glyph_metrics.origin.y },
    .size = { .w = glyph_metrics.size.w,
              .h = glyph_metrics.size.h }
  };


  // The destination bitmap's x-coordinate and row advance. Used in the loop below.
  GBitmap* dest_bitmap = graphics_context_get_bitmap(ctx);
  const int32_t x = (int32_t)((int16_t)cursor.origin.x + (int16_t)glyph_metrics.origin.x);

  // Now clip that box against the screen/other UI elements. This rect will be the rect that we
  // actually fill with bits on the screen.
  GRect clipped_glyph_target = glyph_target;
  grect_clip(&clipped_glyph_target, &ctx->draw_state.clip_box);

  // The number of bits to be clipped off the edges
  const int left_clip = clipped_glyph_target.origin.x - glyph_target.origin.x;
  const int right_clip = MIN(glyph_target.size.w,
                             MAX(0, glyph_target.size.w - clipped_glyph_target.size.w - left_clip));

#if SCREEN_COLOR_DEPTH_BITS == 8
  // Set base address to 0 for 8-bit as this will be later translated to the destination bitmap
  // address - so do all calculations so everything is offset from 0
  uint32_t * base_addr = 0;
#else
  uint32_t * base_addr = ((uint32_t*)dest_bitmap->addr);
#endif

  const uint32_t * const dest_block_x_begin = base_addr +
                                              (left_clip ?
                                               MAX(0, (((x + left_clip + 31)/ 32) - 1)) : (x / 32));

  if (clipped_glyph_target.size.h == 0 || clipped_glyph_target.size.w == 0) {
    return;
  }

#if SCREEN_COLOR_DEPTH_BITS == 8
  // NOTE: Since all calculations are based on 1-bit calculation - use the row size from
  // the 1-bit frame buffer
  const int row_size_bytes = 4 * ((dest_bitmap->bounds.size.w / 32) +
                                  ((dest_bitmap->bounds.size.w % 32) ? 1 : 0));
#else
  const int row_size_bytes = dest_bitmap->row_size_bytes;
#endif // SCREEN_COLOR_DEPTH_BITS == 8

  // Number of blocks (i.e. 32-bit chunks)
  const int dest_row_length = row_size_bytes / 4;

  // The number of bits between the beginning of dest_block and glyph_block.
  // If x is negative we need to be fancy to get the rounded down remainder. This
  // is the number of bits to the right of the next 32-bit boundry to the left.
  // For example, if x is -5 we want this shift to be 27, since -32 (the nearest
  // boundry) + 27 = -5
  const uint8_t dest_shift_at_line_begin = (x >= 0) ?
      x % 32 :
      (x - ((x / 32) * 32));

  uint8_t dest_shift = dest_shift_at_line_begin;

  // The glyph bitmap starts the block after the metrics data:
  uint32_t const* glyph_block = glyph->data;

  // Set up the first piece of source glyph bitmap:
  int8_t glyph_block_bits_left = 32;
  uint32_t src = *glyph_block;

  // Use bit-rotate to align to shift the bitmap to align with the destination.
  // The advantage of rotate vs. bitwise shift is that we can use
  // the bits that wrapped around for the next dest_block
  rotl32(src, dest_shift);
  int8_t src_rotated = dest_shift;
  // how many 32-bit blocks do we need to bitblt on each row. If we're not word aligned we'll need to
  // modify an extra partial word, as we'll have an incomplete word on either side of the line segment
  // we're modifying.
  // For 1-bit, each pixel goes into one bit in dest bitmap - so 32 pixels per block
  const uint8_t num_dest_blocks_per_row = (clipped_glyph_target.size.w / 32) +
                                          (((dest_shift + left_clip) % 32) ? 1 : 0);

  // Handle clipping at the top of the character. We need to skip a number of bits in our source data.
  const unsigned int bits_to_skip = glyph_metrics.size.w * (clipped_glyph_target.origin.y - glyph_target.origin.y);
  if (bits_to_skip) {
    glyph_block += bits_to_skip / 32;
    src = *glyph_block;

    // Simulate the rotate that happens at the bottom of the bitblt loop so our source value is set
    // up just as if we actually rendered those first few lines.
    rotl32(src, (dest_shift_at_line_begin + ((0 - ((uint8_t)glyph_metrics.size.w)) % 32) * (clipped_glyph_target.origin.y - glyph_target.origin.y)) % 32);
    src_rotated = (dest_shift_at_line_begin + ((0 - ((uint8_t)glyph_metrics.size.w)) % 32) * (clipped_glyph_target.origin.y - glyph_target.origin.y)) % 32;
    glyph_block_bits_left -= bits_to_skip % 32;
  }

  for (int dest_y = clipped_glyph_target.origin.y; dest_y != clipped_glyph_target.origin.y + clipped_glyph_target.size.h; ++dest_y) {
    dest_shift = dest_shift_at_line_begin;

    // Number of bits to render on this line.
    uint8_t glyph_line_bits_left = clipped_glyph_target.size.w;

    uint32_t *dest_block = (uint32_t *)dest_block_x_begin + (dest_y * dest_row_length);
    const uint32_t *dest_block_end = dest_block + num_dest_blocks_per_row + 1;

    if (left_clip) {
      const int left_clip_shift = left_clip % 32;
      const int clipped_blocks = left_clip / 32;

      dest_shift = (dest_shift + left_clip_shift) % 32;
      glyph_block_bits_left -= left_clip_shift;

      glyph_block += clipped_blocks;

      if (glyph_block_bits_left <= 0) {
        src = *(++glyph_block);
        glyph_block_bits_left += 32;
        // Need to account for the dest_shift when loading up the new glyph block
        rotl32(src, glyph_block_bits_left + dest_shift);
        src_rotated = glyph_block_bits_left + dest_shift;
      }

      dest_block += clipped_blocks;
    }

    while (dest_block != dest_block_end && glyph_line_bits_left) {
      PBL_ASSERT(dest_block < dest_block_end, "DB=<%p> DBE=<%p>", dest_block, dest_block_end);
      PBL_ASSERTN(dest_block >= (uint32_t*) base_addr);
      PBL_ASSERTN(dest_block < (uint32_t*) base_addr + row_size_bytes *
                  (dest_bitmap->bounds.origin.y + dest_bitmap->bounds.size.h));

      // bitblt part of glyph_block:
      const uint8_t number_of_bits = MIN(32 - dest_shift, MIN(glyph_line_bits_left, glyph_block_bits_left));
      const uint32_t mask = (((1 << number_of_bits) - 1) << dest_shift);

#if SCREEN_COLOR_DEPTH_BITS == 8
      // dest_block points to the block if the dest image was a 1-bit buffer
      // translate this to an x coordinate in the 8-bit buffer
      const int32_t block_start_x = prv_convert_1bit_addr_to_8bit_x(dest_bitmap, dest_block,
                                                                    dest_y);
      const GBitmapDataRowInfo data_row = gbitmap_get_data_row_info(dest_bitmap, dest_y);
      // Only enter the loop if the current block is within the valid data row range
      if (block_start_x + 31 >= data_row.min_x && block_start_x <= data_row.max_x) {
        uint8_t *dest_addr = data_row.data + block_start_x;

        // For each bit in block, write that bit to the dest_bitmap
        for (unsigned int bitindex = 0; bitindex < 32; bitindex++) {
          const int32_t current_x = block_start_x + bitindex;
          // Stop iteration early if we have reached the end of the data row
          if (current_x > data_row.max_x) {
            break;
          }
          // Skip over pixels outside of the bitmap data's x coordinate range
          if (current_x < data_row.min_x) {
            continue;
          }
          // Find position in dest_bitmap that corresponds to the bit index
          // Write to that position if mask for that bit is 1
          if ((mask & src) & (1 << bitindex)) {
            GColor dest_color;
            if (ctx->draw_state.compositing_mode == GCompOpSet) {
              // Blend (i.e. for transparency) if GCompOpSet
              dest_color = gcolor_alpha_blend(ctx->draw_state.text_color,
                                              (GColor) {.argb = dest_addr[bitindex]});
            } else {
              dest_color = ctx->draw_state.text_color;
              dest_color.a = 3;
            }
            dest_addr[bitindex] = dest_color.argb;
          }
        }
      }
#else
      if (gcolor_equal(ctx->draw_state.text_color, GColorBlack)) {
        *(dest_block) &= ~(mask & src);
      } else {
        *(dest_block) |= mask & src;
      }
#endif

      dest_shift = (dest_shift + number_of_bits) % 32;
      glyph_block_bits_left -= number_of_bits;
      glyph_line_bits_left -= number_of_bits;

      if (glyph_block_bits_left <= 0) {
        // We ran out of bits in the current glyph block. Get the next glyph blob:
        src = *(++glyph_block);
        glyph_block_bits_left += 32;
        rotl32(src, dest_shift);
        src_rotated = dest_shift;
        // Continue with this dest_block if there is still space left:
        if (dest_shift) {
          continue;
        }
      }

      ++dest_block;
    }

    dest_shift += right_clip % 32;

    // emulate having drawn the right clip
    if (glyph_block_bits_left <= right_clip) {
      int jump_words = (right_clip - glyph_block_bits_left) / 32 + 1;
      glyph_block += jump_words;
      src = *glyph_block;
      rotl32(src, src_rotated);
      glyph_block_bits_left += 32 * jump_words;
    }
    glyph_block_bits_left -= right_clip;


    // Rotate the bits into the right position for the next row:
    dest_shift = dest_shift_at_line_begin - dest_shift;
    rotl32(src, dest_shift % 32);
    src_rotated = (src_rotated + dest_shift) % 32;
  }

  graphics_context_mark_dirty_rect(ctx, clipped_glyph_target);
}


void text_render_set_special_codepoint_cb(SpecialCodepointHandlerCb handler, void *context) {
  TextRenderState *state = app_state_get_text_render_state();
  state->special_codepoint_handler_cb = handler;
  state->special_codepoint_handler_context = context;
}
