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

#include "graphics_bitmap.h"

#include "bitblt.h"
#include "bitblt_private.h"
#include "gcontext.h"
#include "graphics.h"
#include "graphics_private.h"

#include "system/passert.h"
#include "util/graphics.h"
#include "util/trig.h"

void graphics_draw_bitmap_in_rect_processed(GContext *ctx, const GBitmap *src_bitmap,
                                            const GRect *rect_ref, GBitmapProcessor *processor) {
  if (!ctx || ctx->lock || !rect_ref) {
    return;
  }

  // Make a copy of the rect and translate it to global screen coordinates
  GRect rect = *rect_ref;
  rect.origin = gpoint_add(rect.origin, ctx->draw_state.drawing_box.origin);

  // Store the bitmap to draw in a new pointer that the processor can modify if it wants to
  const GBitmap *bitmap_to_draw = src_bitmap;

  // Call the processor's pre function, if applicable
  if (processor && processor->pre) {
    processor->pre(processor, ctx, &bitmap_to_draw, &rect);
  }

  // Bail out early if the bitmap to draw is NULL
  if (!bitmap_to_draw) {
    // Set rect to GRectZero so the processor's .post function knows that nothing was drawn
    rect = GRectZero;
    goto call_processor_post_function_and_return;
  }

  // TODO PBL-35694: what if src_bitmap == dest_bitmap....
  // This currently works only if the regions are equal, or the dest region is
  // to the bottom/right of it, since we scan from left to right, top to bottom
  GBitmap *dest_bitmap = graphics_context_get_bitmap(ctx);
  PBL_ASSERTN(dest_bitmap);

  // Save the original origin to compensate the position within src when rect.origin is negative
  const GPoint unclipped_origin = rect.origin;

  // Clip the rect to avoid drawing outside of the bitmap memory
  grect_standardize(&rect);
  grect_clip(&rect, &dest_bitmap->bounds);
  grect_clip(&rect, &ctx->draw_state.clip_box);
  // Bail out early if the clipped drawing rectangle is empty
  if (grect_is_empty(&rect)) {
    goto call_processor_post_function_and_return;
  }

  // Calculate the offset of src_bitmap to use
  const GPoint src_offset = gpoint_sub(rect.origin, unclipped_origin);

  // Blit bitmap_to_draw (which might have been changed by the processor) into dest_bitmap
  bitblt_bitmap_into_bitmap_tiled(dest_bitmap, bitmap_to_draw, rect, src_offset,
                                  ctx->draw_state.compositing_mode, ctx->draw_state.tint_color);
  // Mark the region where the bitmap was drawn as dirty
  graphics_context_mark_dirty_rect(ctx, rect);

call_processor_post_function_and_return:
  // Call the processor's post function, if applicable
  if (processor && processor->post) {
    processor->post(processor, ctx, bitmap_to_draw, &rect);
  }
}

void graphics_draw_bitmap_in_rect(GContext *ctx, const GBitmap *src_bitmap, const GRect *rect_ref) {
  graphics_draw_bitmap_in_rect_processed(ctx, src_bitmap, rect_ref, NULL);
}

void graphics_draw_bitmap_in_rect_by_value(GContext *ctx, const GBitmap *src_bitmap, GRect rect) {
  graphics_draw_bitmap_in_rect_processed(ctx, src_bitmap, &rect, NULL);
}

typedef struct DivResult {
  int32_t quot;
  int32_t rem;
} DivResult;

//! a div and mod operation where any remainder will always be the same direction as the numerator
static DivResult polar_div(int32_t numer, int32_t denom) {
  DivResult res;
  res.quot = numer / denom;
  res.rem = numer % denom;
  if (numer < 0 && res.rem > 0) {
    res.rem -= denom;
    res.quot += denom;
  }
  return res;
}

#if PBL_BW
T_STATIC bool get_bitmap_bit(GBitmap *bmp, int x, int y) {
  int byte_num = y * bmp->row_size_bytes + x / 8;
  int bit_num = x % 8;
  uint8_t byte = ((uint8_t*)(bmp->addr))[byte_num];
  return (byte & (1 << bit_num)) ? 1 : 0;
}
#elif PBL_COLOR
T_STATIC GColor get_bitmap_color(GBitmap *bmp, int x, int y) {
  const GBitmapFormat format = gbitmap_get_format(bmp);
  const GBitmapDataRowInfo row_info = gbitmap_get_data_row_info(bmp, y);
  const uint8_t *src = row_info.data;
  const uint8_t src_bpp = gbitmap_get_bits_per_pixel(format);
  uint8_t cindex = raw_image_get_value_for_bitdepth(src, x,
                                                    0,  // y = 0 when using data_row
                                                    bmp->row_size_bytes,
                                                    src_bpp);
  // Default color to be the raw color index - update only if palletized
  GColor src_color = (GColor){.argb = cindex};
  bool palletized = ((format == GBitmapFormat1BitPalette) ||
                     (format == GBitmapFormat2BitPalette) ||
                     (format == GBitmapFormat4BitPalette));
  if (palletized) {
    // Look up color in pallete if palletized
    const GColor *palette = bmp->palette;
    src_color = palette[cindex];
  }
  return src_color;
}
#endif

void graphics_draw_rotated_bitmap(GContext* ctx, GBitmap *src, GPoint src_ic, int rotation,
                                  GPoint dest_ic) {
  PBL_ASSERTN(ctx);
  if (rotation == 0) {
    graphics_draw_bitmap_in_rect(
      ctx, src, &(GRect){ .origin = { dest_ic.x - src_ic.x, dest_ic.y - src_ic.y },
        .size = src->bounds.size });
    return;
  }

  GBitmap *dest_bitmap = graphics_capture_frame_buffer(ctx);
  if (dest_bitmap == NULL) {
    return;
  }

  GRect dest_clip = ctx->draw_state.clip_box;
  dest_ic.x += ctx->draw_state.drawing_box.origin.x;
  dest_ic.y += ctx->draw_state.drawing_box.origin.y;

  GCompOp compositing_mode = ctx->draw_state.compositing_mode;
#if PBL_BW
  GColor foreground, background;
  switch (compositing_mode) {
    case GCompOpAssign:
      foreground = GColorWhite;
      background = GColorBlack;
      break;
    case GCompOpAssignInverted:
      foreground = GColorBlack;
      background = GColorWhite;
      break;
    case GCompOpOr:
      foreground = GColorWhite;
      background = GColorClear;
      break;
    case GCompOpAnd:
      foreground = GColorClear;
      background = GColorBlack;
      break;
    case GCompOpClear:
      foreground = GColorBlack;
      background = GColorClear;
      break;
    case GCompOpSet:
      foreground = GColorClear;
      background = GColorWhite;
      break;
    default:
      PBL_ASSERT(0, "unknown coposting mode %d", compositing_mode);
      return;
  }
#endif

  // Backup context color
  const GColor ctx_color = ctx->draw_state.stroke_color;

  if (grect_contains_point(&src->bounds, &src_ic)) {
    // TODO: Optimize further (PBL-15657)
    // If src_ic is within the bounds of the source image, do the following performance
    // optimization:
    // Create a clipping rectangle based on the max distance away from the pivot point
    // that the destination image could be located at:
    // max distance from the pivot point = sqrt(x^2 + y^2), where x and y are at max twice the width
    // and height of the source image
    // i.e. in case the anchor point is on the edge then it would be twice
    // Also need to account for the dest_ic offset

    const int16_t max_width = MAX(src->bounds.origin.x + src->bounds.size.w - src_ic.x,
                                  src_ic.x - src->bounds.origin.x);
    const int16_t max_height = MAX(src->bounds.origin.y + src->bounds.size.h - src_ic.y,
                                   src_ic.y - src->bounds.origin.y);
    const int32_t width = 2 * (max_width + 1);   // Add one more pixel in case on the edge
    const int32_t height = 2 * (max_height + 1); // Add one more pixel in case on the edge

    // add two pixels just in case of rounding isssues
    const int32_t max_distance = integer_sqrt((width * width) + (height * height)) + 2;
    const int32_t min_x = src_ic.x - max_distance;
    const int32_t min_y = src_ic.y - max_distance;

    const int32_t size_x = max_distance*2;
    const int32_t size_y = size_x;

    const GRect dest_clip_min = GRect(dest_ic.x + min_x, dest_ic.y + min_y, size_x, size_y);
    grect_clip(&dest_clip, &dest_clip_min);
  }

  for (int y = dest_clip.origin.y; y < dest_clip.origin.y + dest_clip.size.h; ++y) {
    for (int x = dest_clip.origin.x; x < dest_clip.origin.x + dest_clip.size.w; ++x) {
      // only draw if within the dest range
      const GBitmapDataRowInfo dest_info = gbitmap_get_data_row_info(dest_bitmap, y);
      if (!WITHIN(x, dest_info.min_x, dest_info.max_x)) {
        continue;
      }

      const int32_t cos_value = cos_lookup(-rotation);
      const int32_t sin_value = sin_lookup(-rotation);
      const int32_t src_numerator_x = cos_value * (x - dest_ic.x) - sin_value * (y - dest_ic.y);
      const int32_t src_numerator_y = cos_value * (y - dest_ic.y) + sin_value * (x - dest_ic.x);

      const DivResult src_vector_x = polar_div(src_numerator_x, TRIG_MAX_RATIO);
      const DivResult src_vector_y = polar_div(src_numerator_y, TRIG_MAX_RATIO);

      const int32_t src_x = src_ic.x + src_vector_x.quot;
      const int32_t src_y = src_ic.y + src_vector_y.quot;

      // only draw if within the src range
      const GBitmapDataRowInfo src_info = gbitmap_get_data_row_info(src, src_y);
      if (!(WITHIN(src_x, 0, src->bounds.size.w - 1) &&
        WITHIN(src_y, 0, src->bounds.size.h - 1) &&
        WITHIN(src_x, src_info.min_x, src_info.max_x))) {
        continue;
      }

#if PBL_BW
      // dividing by 8 to avoid overflows of <thresh> in the next loop
      const int32_t horiz_contrib[3] = {
        src_vector_x.rem < 0 ? (-src_vector_x.rem) >> 3 : 0,
        src_vector_x.rem < 0 ? (TRIG_MAX_RATIO + src_vector_x.rem) >> 3 :
                               (TRIG_MAX_RATIO - src_vector_x.rem) >> 3,
        src_vector_x.rem < 0 ? 0 : (src_vector_x.rem) >> 3
      };

      const int32_t vert_contrib[3] = {
        src_vector_y.rem < 0 ? (-src_vector_y.rem) >> 3 : 0,
        src_vector_y.rem < 0 ? (TRIG_MAX_RATIO + src_vector_y.rem) >> 3 :
                               (TRIG_MAX_RATIO - src_vector_y.rem) >> 3,
        src_vector_y.rem < 0 ? 0 : (src_vector_y.rem) >> 3
      };

      int32_t thresh = 0;

      for (int i = -1; i <= 1; ++i) {
        for (int j = -1; j <= 1; ++j) {
          if (src_x + i >= 0 && src_x + i < src->bounds.size.w
              && src_y + j >= 0 && src_y + j < src->bounds.size.h) {
            // I'm within bounds
            if (get_bitmap_bit(src, src_x + i , src_y + j)) {
              // more color
              thresh += (horiz_contrib[i+1] * vert_contrib[j+1]);
            } else {
              // less color
              thresh -= (horiz_contrib[i+1] * vert_contrib[j+1]);
            }
          }
        }
      }

      if (thresh > 0) {
        ctx->draw_state.stroke_color = foreground;
      } else {
        ctx->draw_state.stroke_color = background;
      }

      if (!gcolor_is_transparent(ctx->draw_state.stroke_color)) {
        graphics_private_set_pixel(ctx, GPoint(x, y));
      }
#elif PBL_COLOR
      const GColor src_color = get_bitmap_color(src, src_x, src_y);
      const GColor tint_color = ctx->draw_state.tint_color;
      switch (compositing_mode) {
        case GCompOpSet: {
          const GColor dst_color = get_bitmap_color(dest_bitmap, x, y);
          ctx->draw_state.stroke_color = gcolor_alpha_blend(src_color, dst_color);
          break;
        }
        case GCompOpOr: {
          const GColor dst_color = get_bitmap_color(dest_bitmap, x, y);
          if (tint_color.a != 0) {
            GColor actual_color = tint_color;
            actual_color.a = src_color.a;
            ctx->draw_state.stroke_color = gcolor_alpha_blend(actual_color, dst_color);
            break;
          }
        }
        /* FALLTHRU */
        case GCompOpAssign:
        default:
          // Do assign by default
          ctx->draw_state.stroke_color = src_color;
          break;
      }
      ctx->draw_state.stroke_color.a = 3; // Force to be opaque

      graphics_private_set_pixel(ctx, GPoint(x, y));
#endif
    }
  }

  // Restore context color
  ctx->draw_state.stroke_color = ctx_color;
  graphics_release_frame_buffer(ctx, dest_bitmap);
}
