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

#include "bitblt_private.h"
#include "graphics.h"
#include "graphics_private.h"
#include "gtypes.h"
#include "system/passert.h"
#include "util/bitset.h"
#include "util/math.h"

// ## Point setting/blending functions

#if PBL_COLOR
T_STATIC inline void set_pixel_raw_8bit(GContext* ctx, GPoint point) {
  if (!grect_contains_point(&ctx->dest_bitmap.bounds, &point)) {
    return;
  }

  const GBitmapDataRowInfo data_row_info = gbitmap_get_data_row_info(&ctx->dest_bitmap, point.y);
  if (!WITHIN(point.x, data_row_info.min_x, data_row_info.max_x)) {
    return;
  }
  uint8_t *line = data_row_info.data;
  GColor color = ctx->draw_state.stroke_color;
  if (!gcolor_is_transparent(color)) {
    // Force alpha to be opaque since that represents how framebuffer discards it in display.
    // Also needed for unit tests since PNG tests interpret alpha
    color.a = 3;
    line[point.x] = color.argb;
  }
}
#endif

#if PBL_BW
static inline void set_pixel_raw_2bit(GContext* ctx, GPoint point) {
  if (!grect_contains_point(&ctx->dest_bitmap.bounds, &point)) {
    return;
  }
  bool black = (gcolor_equal(ctx->draw_state.stroke_color, GColorBlack));

  uint8_t *line = ((uint8_t *)ctx->dest_bitmap.addr) + (ctx->dest_bitmap.row_size_bytes * point.y);
  bitset8_update(line, point.x, !black);
}
#endif

void graphics_private_set_pixel(GContext* ctx, GPoint point) {
  if (!grect_contains_point(&ctx->draw_state.clip_box, &point)) {
    return;
  }

#if PBL_BW
  set_pixel_raw_2bit(ctx, point);
#elif PBL_COLOR
  set_pixel_raw_8bit(ctx, point);
#endif

  const GRect dirty_rect = { point, { 1, 1 } };
  graphics_context_mark_dirty_rect(ctx, dirty_rect);
}

// ## Private blending wrapper functions for non-aa

uint32_t graphics_private_get_1bit_grayscale_pattern(GColor color, uint8_t row_number) {
  const GColor8Component luminance = (color.r + color.g + color.b) / 3;
  switch (luminance) {
    case 0:
      return 0x00000000;
    case 1:
    case 2:
      // This is done to create a checkerboard pattern for gray
      return (row_number % 2) ? 0xAAAAAAAA : 0x55555555;
    case 3:
      return 0xFFFFFFFF;
    default:
      WTF;
  }
}

void prv_assign_line_horizontal_non_aa(GContext* ctx, int16_t y, int16_t x1, int16_t x2) {
  y += ctx->draw_state.drawing_box.origin.y;
  x1 += ctx->draw_state.drawing_box.origin.x;
  x2 += ctx->draw_state.drawing_box.origin.x;

  // Clip results
  const int y_min = ctx->draw_state.clip_box.origin.y;
  const int y_max = grect_get_max_y(&ctx->draw_state.clip_box) - 1;
  const int x_min = ctx->draw_state.clip_box.origin.x;
  const int x_max = grect_get_max_x(&ctx->draw_state.clip_box) - 1;

  x1 = MAX(x1, x_min);
  x2 = MIN(x2, x_max);
  if (!WITHIN(y, y_min, y_max) || x1 > x2) {
    // Outside of drawing bounds..
    return;
  }

  // Capture framebuffer & pass it to drawing implementation
  GBitmap *framebuffer = graphics_capture_frame_buffer(ctx);

  if (!framebuffer) {
    // Couldn't capture framebuffer
    return;
  }

  ctx->draw_state.draw_implementation->blend_horizontal_line(ctx, y, x1, x2,
                                                             ctx->draw_state.stroke_color);

  graphics_release_frame_buffer(ctx, framebuffer);
}

void prv_assign_line_vertical_non_aa(GContext* ctx, int16_t x, int16_t y1, int16_t y2) {
  x += ctx->draw_state.drawing_box.origin.x;
  y1 += ctx->draw_state.drawing_box.origin.y;
  y2 += ctx->draw_state.drawing_box.origin.y;

  // To preserve old behaviour we add one to the end of the line about to be drawn
  y2++;

  // Clip results
  const int y_min = ctx->draw_state.clip_box.origin.y;
  const int y_max = grect_get_max_y(&ctx->draw_state.clip_box) - 1;
  const int x_min = ctx->draw_state.clip_box.origin.x;
  const int x_max = grect_get_max_x(&ctx->draw_state.clip_box) - 1;

  y1 = MAX(y1, y_min);
  y2 = MIN(y2, y_max + 1); // Thats because we added one to end of the line
  if (!WITHIN(x, x_min, x_max) || y1 > y2) {
    // Outside of drawing bounds..
    return;
  }

  // Capture framebuffer & pass it to drawing implementation
  GBitmap *framebuffer = graphics_capture_frame_buffer(ctx);

  if (!framebuffer) {
    // Couldn't capture framebuffer
    return;
  }

  ctx->draw_state.draw_implementation->blend_vertical_line(ctx, x, y1, y2,
                                                           ctx->draw_state.stroke_color);

  graphics_release_frame_buffer(ctx, framebuffer);
}

// ## Line blending wrappers:

void graphics_private_draw_horizontal_line_prepared(GContext *ctx, GBitmap *framebuffer,
                                                    GRect *clip_box, int16_t y, Fixed_S16_3 x1,
                                                    Fixed_S16_3 x2, GColor color) {
  if (gcolor_is_invisible(color)) {
    return;
  }

  // look for clipbox
  if (!WITHIN(y, clip_box->origin.y, grect_get_max_y(clip_box) - 1)) {
    return;
  }

  const int16_t min_valid_x = clip_box->origin.x;
  if (x1.integer < min_valid_x) {
    x1 = (Fixed_S16_3){.integer = min_valid_x, .fraction = 0};
  }

  const int16_t max_valid_x = grect_get_max_x(clip_box) - 1;
  if (x2.integer > max_valid_x) {
    x2 = (Fixed_S16_3){.integer = max_valid_x};
  }

  // last pixel with blending (don't render the pixel if it overflows the framebuffer/clip box)
  if (x2.integer >= max_valid_x) {
    x2.fraction = 0;
  }

  ctx->draw_state.draw_implementation->assign_horizontal_line(ctx, y, x1, x2, color);
}

void graphics_private_draw_horizontal_line_integral(GContext *ctx, GBitmap *framebuffer, int16_t y,
                                                    int16_t x1, int16_t x2, GColor color) {
  // This is a wrapper for prv_draw_horizontal_line_raw for integral coordintaes

  // End of the line is inclusive so we subtract one
  x2--;

  const Fixed_S16_3 x1_fixed = Fixed_S16_3(x1 << FIXED_S16_3_PRECISION);
  const Fixed_S16_3 x2_fixed = Fixed_S16_3(x2 << FIXED_S16_3_PRECISION);

  ctx->draw_state.draw_implementation->assign_horizontal_line(ctx, y, x1_fixed, x2_fixed,
                                                              color);
}

void graphics_private_draw_vertical_line_prepared(GContext *ctx, GBitmap *framebuffer,
                                                  GRect *clip_box, int16_t x, Fixed_S16_3 y1,
                                                  Fixed_S16_3 y2, GColor color) {
  if (gcolor_is_invisible(color)) {
    return;
  }

  // look for clipbox
  if (!WITHIN(x, clip_box->origin.x, grect_get_max_x(clip_box) - 1)) {
    return;
  }

  const int16_t min_valid_y = clip_box->origin.y;
  if (y1.integer < min_valid_y) {
    y1 = (Fixed_S16_3){.integer = min_valid_y, .fraction = 0};
  }

  const int16_t max_valid_y = grect_get_max_y(clip_box) - 1;
  if (y2.integer > max_valid_y) {
    y2 = (Fixed_S16_3){.integer = max_valid_y};
  }

  if (y1.integer > y2.integer) {
    return;
  }

  // last pixel with blending (don't render the pixel if it overflows the framebuffer/clip box)
  if (y2.integer >= max_valid_y) {
    y2.fraction = 0;
  }

  ctx->draw_state.draw_implementation->assign_vertical_line(ctx, x, y1, y2, color);
}

void graphics_private_draw_horizontal_line(GContext *ctx, int16_t y, Fixed_S16_3 x1,
                                            Fixed_S16_3 x2) {
#if PBL_COLOR
  if (ctx->draw_state.antialiased) {
    // apply draw box and clipping
    x1.integer += ctx->draw_state.drawing_box.origin.x;
    x2.integer += ctx->draw_state.drawing_box.origin.x;
    y += ctx->draw_state.drawing_box.origin.y;
    GBitmap *framebuffer = graphics_capture_frame_buffer(ctx);

    if (!framebuffer) {
      // Couldn't capture framebuffer
      return;
    }

    graphics_private_draw_horizontal_line_prepared(ctx, framebuffer, &ctx->draw_state.clip_box, y,
                                                   x1, x2, ctx->draw_state.stroke_color);

    graphics_release_frame_buffer(ctx, framebuffer);
    return;
  }
#endif // PBL_COLOR
  // since x1 is beginning of the line, rounding should work in favor of flooring the value
  //   therefore we substract one from the rounding addition to produce result similar to x2
  int16_t x1_rounded =
    (x1.raw_value + (FIXED_S16_3_ONE.raw_value / 2 - 1)) / FIXED_S16_3_ONE.raw_value;
  int16_t x2_rounded = (x2.raw_value + (FIXED_S16_3_ONE.raw_value / 2)) / FIXED_S16_3_ONE.raw_value;

  if (x1_rounded > x2_rounded) {
    // AA algorithm will draw lines in one way only, so non-AA should reject those too
    return;
  }

  prv_assign_line_horizontal_non_aa(ctx, y, x1_rounded, x2_rounded);
}

void graphics_private_draw_vertical_line(GContext *ctx, int16_t x, Fixed_S16_3 y1, Fixed_S16_3 y2) {
#if PBL_COLOR
  if (ctx->draw_state.antialiased) {
    // apply draw box and clipping
    y1.integer += ctx->draw_state.drawing_box.origin.y;
    y2.integer += ctx->draw_state.drawing_box.origin.y;
    x += ctx->draw_state.drawing_box.origin.x;
    GBitmap *framebuffer = graphics_capture_frame_buffer(ctx);

    if (!framebuffer) {
      // Couldn't capture framebuffer
      return;
    }

    graphics_private_draw_vertical_line_prepared(ctx, framebuffer, &ctx->draw_state.clip_box, x, y1,
                                                 y2, ctx->draw_state.stroke_color);

    graphics_release_frame_buffer(ctx, framebuffer);
    return;
  }
#endif // PBL_COLOR
  // since y1 is beginning of the line, rounding should work in favor of flooring the value
  //   therefore we substract one from the rounding addition to produce result similar to y2
  int16_t y1_rounded =
    (y1.raw_value + (FIXED_S16_3_ONE.raw_value / 2 - 1)) / FIXED_S16_3_ONE.raw_value;
  int16_t y2_rounded = (y2.raw_value + (FIXED_S16_3_ONE.raw_value / 2)) / FIXED_S16_3_ONE.raw_value;

  if (y1_rounded > y2_rounded) {
    // AA algorithm will draw lines in one way only, so non-AA should reject those too
    return;
  }

  prv_assign_line_vertical_non_aa(ctx, x, y1_rounded, y2_rounded);
}

void graphics_private_plot_pixel(GBitmap *framebuffer, GRect *clip_box, int x, int y,
                                 uint16_t opacity, GColor color) {
  // Plots pixel directly to framebuffer
  // Pixel position have to be adjusted to drawing_box before calling this!

  // Checking for clip box
  const GPoint point = GPoint(x, y);
  if (!grect_contains_point(clip_box, &point)) {
    return;
  }

#if PBL_COLOR
  // Checking for data row min/max x
  const GBitmapDataRowInfo data_row_info = gbitmap_get_data_row_info(framebuffer, y);
  if (!WITHIN(x, data_row_info.min_x, data_row_info.max_x)) {
    return;
  }
  GColor *output = (GColor *)(data_row_info.data + x);
  color.a = (uint8_t)(MAX_PLOT_BRIGHTNESS - opacity);
  output->argb = gcolor_alpha_blend(color, (*output)).argb;
#else
  if (opacity <= (MAX_PLOT_BRIGHTNESS / 2)) {
    bool black = (gcolor_equal(color, GColorBlack));
    uint8_t *line = ((uint8_t *)framebuffer->addr) + (framebuffer->row_size_bytes * y);
    bitset8_update(line, x, !black);
  }
#endif // PBL_COLOR
}

void graphics_private_plot_horizontal_line_prepared(GContext *ctx, GBitmap *framebuffer,
                                                    GRect *clip_box, int y, int x0, int x1,
                                                    uint16_t opacity, GColor color) {
  // Plots pixel directly to framebuffer
  // Pixel position have to be adjusted to drawing_box before calling this!

  // Checking for clip_box
  if (!WITHIN(y, clip_box->origin.y, grect_get_max_y(clip_box) - 1)) {
    return;
  }
  const int16_t x_min = MAX(MIN(x0, x1), clip_box->origin.x);
  const int16_t x_max = MIN(MAX(x0, x1), grect_get_max_x(clip_box));

#if PBL_COLOR
  color.a = (uint8_t)(MAX_PLOT_BRIGHTNESS - opacity);
#else
  if (opacity > (MAX_PLOT_BRIGHTNESS / 2)) {
    // We're not plotting anything, bail
    return;
  }
#endif // PBL_COLOR

  ctx->draw_state.draw_implementation->blend_horizontal_line(ctx, y, x_min, x_max, color);
}

void graphics_private_plot_vertical_line_prepared(GContext *ctx, GBitmap *framebuffer,
                                                  GRect *clip_box, int x, int y0, int y1,
                                                  uint16_t opacity, GColor color) {
  // Plots pixel directly to framebuffer
  // Pixel position have to be adjusted to drawing_box before calling this!

  // Checking for clip_box
  if (!WITHIN(x, clip_box->origin.x, grect_get_max_x(clip_box) - 1)) {
    return;
  }

  int16_t y_min = MAX(MIN(y0, y1), clip_box->origin.y);
  int16_t y_max = MIN(MAX(y0, y1), clip_box->origin.y + clip_box->size.h);

#if PBL_COLOR
  color.a = (uint8_t)(MAX_PLOT_BRIGHTNESS - opacity);
#else
  if (opacity > (MAX_PLOT_BRIGHTNESS / 2)) {
    // We're not plotting anything, bail
    return;
  }
#endif // PBL_COLOR

  ctx->draw_state.draw_implementation->blend_vertical_line(ctx, x, y_min, y_max, color);
}

void graphics_private_plot_horizontal_line(GContext *ctx, int16_t y, Fixed_S16_3 x1, Fixed_S16_3 x2,
                                           uint16_t opacity) {
#if PBL_COLOR
  if (ctx->draw_state.antialiased) {
    // apply draw box and clipping
    x1.integer += ctx->draw_state.drawing_box.origin.x;
    x2.integer += ctx->draw_state.drawing_box.origin.x;
    y += ctx->draw_state.drawing_box.origin.y;

    // round edges:
    x1.raw_value += (FIXED_S16_3_ONE.raw_value / 2);
    x2.raw_value += (FIXED_S16_3_ONE.raw_value / 2);
    if (x2.fraction > (opacity << 1)) {
      x2.integer++;
    }

    GBitmap *framebuffer = graphics_capture_frame_buffer(ctx);

    if (!framebuffer) {
      // Couldn't capture framebuffer
      return;
    }

    graphics_private_plot_horizontal_line_prepared(ctx, framebuffer, &ctx->draw_state.clip_box, y,
                                                   x1.integer, x2.integer, opacity,
                                                   ctx->draw_state.stroke_color);

    graphics_release_frame_buffer(ctx, framebuffer);
    return;
  }
#endif // PBL_COLOR

  if (opacity <= (MAX_PLOT_BRIGHTNESS / 2)) {
    int16_t x1_rounded = (x1.raw_value + (FIXED_S16_3_ONE.raw_value / 2))
                          / FIXED_S16_3_ONE.raw_value;
    int16_t x2_rounded = (x2.raw_value + (FIXED_S16_3_ONE.raw_value / 2))
                          / FIXED_S16_3_ONE.raw_value;

    prv_assign_line_horizontal_non_aa(ctx, y, x1_rounded, x2_rounded);
  }
}

void graphics_private_plot_vertical_line(GContext *ctx, int16_t x, Fixed_S16_3 y1, Fixed_S16_3 y2,
                                         uint16_t opacity) {
#if PBL_COLOR
  if (ctx->draw_state.antialiased) {
    // apply draw box and clipping
    x += ctx->draw_state.drawing_box.origin.x;
    y1.integer += ctx->draw_state.drawing_box.origin.y;
    y2.integer += ctx->draw_state.drawing_box.origin.y;

    // round edges:
    y1.raw_value += (FIXED_S16_3_ONE.raw_value / 2);
    y2.raw_value += (FIXED_S16_3_ONE.raw_value / 2);
    if (y2.fraction > (opacity << 1)) {
      y2.integer++;
    }

    GBitmap *framebuffer = graphics_capture_frame_buffer(ctx);

    if (!framebuffer) {
      // Couldn't capture framebuffer
      return;
    }

    graphics_private_plot_vertical_line_prepared(ctx, framebuffer, &ctx->draw_state.clip_box, x,
                                                 y1.integer, y2.integer, opacity,
                                                 ctx->draw_state.stroke_color);

    graphics_release_frame_buffer(ctx, framebuffer);
    return;
  }
#endif // PBL_COLOR

  if (opacity <= (MAX_PLOT_BRIGHTNESS / 2)) {
    int16_t y1_rounded = (y1.raw_value + (FIXED_S16_3_ONE.raw_value / 2))
                          / FIXED_S16_3_ONE.raw_value;
    int16_t y2_rounded = (y2.raw_value + (FIXED_S16_3_ONE.raw_value / 2))
                          / FIXED_S16_3_ONE.raw_value;

    prv_assign_line_vertical_non_aa(ctx, x, y1_rounded, y2_rounded);
  }
}

#if PBL_COLOR
void graphics_private_draw_horizontal_line_delta_prepared(GContext *ctx, GBitmap *framebuffer,
                                                          GRect *clip_box, int16_t y,
                                                          Fixed_S16_3 x1, Fixed_S16_3 x2,
                                                          Fixed_S16_3 delta1, Fixed_S16_3 delta2,
                                                          GColor color) {

  // Extended sides AA calculations
  uint8_t left_aa_offset = (delta1.integer > 1) ?
        ((delta1.raw_value + (FIXED_S16_3_ONE.raw_value / 2)) / FIXED_S16_3_ONE.raw_value) : 1;

  uint8_t right_aa_offset = (delta2.integer > 1) ?
        ((delta2.raw_value + (FIXED_S16_3_ONE.raw_value / 2)) / FIXED_S16_3_ONE.raw_value) : 1;

  x1.integer -= left_aa_offset / 2;
  x2.integer -= right_aa_offset / 2;

  // look for clipbox
  if (!WITHIN(y, clip_box->origin.y, grect_get_max_y(clip_box) - 1)) {
    return;
  }

  const int16_t min_valid_x = clip_box->origin.x;
  const int16_t max_valid_x = grect_get_max_x(clip_box) - 1;

  // x1/x2 clipping and verification happens in raw drawing function to preserve gradients

  ctx->draw_state.draw_implementation->assign_horizontal_line_delta(ctx, y, x1, x2,
                                                                    left_aa_offset, right_aa_offset,
                                                                    min_valid_x, max_valid_x,
                                                                    color);
}

void graphics_private_draw_horizontal_line_delta_aa(GContext *ctx, int16_t y, Fixed_S16_3 x1,
                                                    Fixed_S16_3 x2, Fixed_S16_3 delta1,
                                                    Fixed_S16_3 delta2) {
  // apply draw box and clipping
  x1.integer += ctx->draw_state.drawing_box.origin.x;
  x2.integer += ctx->draw_state.drawing_box.origin.x;
  y += ctx->draw_state.drawing_box.origin.y;
  GBitmap *framebuffer = graphics_capture_frame_buffer(ctx);

  if (!framebuffer) {
    // Couldn't capture framebuffer
    return;
  }

  graphics_private_draw_horizontal_line_delta_prepared(ctx, framebuffer, &ctx->draw_state.clip_box,
                                                       y, x1, x2, delta1, delta2,
                                                       ctx->draw_state.stroke_color);

  graphics_release_frame_buffer(ctx, framebuffer);
}
#endif // PBL_COLOR

void graphics_private_draw_horizontal_line_delta_non_aa(GContext *ctx, int16_t y, Fixed_S16_3 x1,
                                                    Fixed_S16_3 x2, Fixed_S16_3 delta1,
                                                    Fixed_S16_3 delta2) {
  int16_t x1_rounded = (x1.raw_value + (FIXED_S16_3_ONE.raw_value / 2)) / FIXED_S16_3_ONE.raw_value;
  int16_t x2_rounded = (x2.raw_value + (FIXED_S16_3_ONE.raw_value / 2)) / FIXED_S16_3_ONE.raw_value;

  if (x1_rounded > x2_rounded) {
    // AA algorithm will draw lines in one way only, so non-AA should reject those too
    return;
  }

  prv_assign_line_horizontal_non_aa(ctx, y, x1_rounded, x2_rounded);
}

// This function will replicate source column in given area
T_STATIC void prv_replicate_column_row_raw(GBitmap *framebuffer, int16_t src_x, int16_t dst_x1,
                                           int16_t dst_x2) {
  const GRect column_to_replicate = (GRect) {
    .origin = GPoint(src_x, framebuffer->bounds.origin.y),
    .size = GSize(1, framebuffer->bounds.size.h),
  };
  GBitmap column_to_replicate_sub_bitmap;
  gbitmap_init_as_sub_bitmap(&column_to_replicate_sub_bitmap, framebuffer, column_to_replicate);
  for (int16_t x = dst_x1; x <= dst_x2; x++) {
    bitblt_bitmap_into_bitmap(framebuffer, &column_to_replicate_sub_bitmap, GPoint(x, 0),
                              GCompOpAssign, GColorWhite);
  }
}

void graphics_patch_trace_of_moving_rect(GContext *ctx, int16_t *prev_x, GRect current) {
  const int16_t new_x = current.origin.x;
  int16_t src_x = 0; // just so that GCC accepts that src_x is always initialized
  int16_t dst_x1 = INT16_MAX;
  int16_t dst_x2 = INT16_MIN;
  if (*prev_x == INT16_MAX) {
    // do nothing
  } else if (*prev_x > new_x) {
    // move to left
    src_x = new_x + current.size.w - 1;
    dst_x1 = src_x + 1;
    dst_x2 = DISP_COLS - 1;
  } else if (*prev_x < new_x) {
    src_x = new_x;
    dst_x1 = 0;
    dst_x2 = src_x - 1;
  }

  *prev_x = new_x;

  if (dst_x1 > dst_x2) {
    return;
  }

  GBitmap *fb = graphics_capture_frame_buffer(ctx);
  if (!fb) {
    return;
  }

  prv_replicate_column_row_raw(fb, src_x, dst_x1, dst_x2);

  graphics_release_frame_buffer(ctx, fb);
}

void graphics_private_move_pixels_horizontally(GBitmap *bitmap, int16_t delta_x,
                                               bool patch_garbage) {
  if (!bitmap || delta_x == 0) {
    return;
  }

  const int bpp = gbitmap_get_bits_per_pixel(bitmap->info.format);

  const int16_t abs_delta = ABS(delta_x);
  const bool delta_neg = (delta_x < 0);
  const int16_t min_y = bitmap->bounds.origin.y;
  const int16_t max_y = grect_get_max_y(&bitmap->bounds) - 1;
  for (int16_t y = min_y; y <= max_y; y++) {
    const GBitmapDataRowInfo row_info = gbitmap_get_data_row_info(bitmap, y);
    const int16_t min_x = MAX(row_info.min_x, bitmap->bounds.origin.x);
    const int16_t max_x = MIN(row_info.max_x, grect_get_max_x(&bitmap->bounds) - 1);
    const int16_t num_pix_data_row = max_x - min_x + 1;
    const int16_t pixels_to_move = num_pix_data_row - abs_delta;
    switch (bpp) {
      case 1: {
        // Note: this doesn't care about the bounding, because we don't have any round 1bpp
        // devices to support, and it simplifies the code.
#if PBL_ROUND
        WTF;
#endif
        uint8_t *const buf = row_info.data;
        const int delta_bytes = abs_delta / 8;
        const int delta_bits = abs_delta % 8;
        // Subtract two bytes to account for the 16-bit padding at the end of each row
        const int bytes = bitmap->row_size_bytes - 2;

        uint8_t *const left_pixel = buf;
        uint8_t *const right_pixel = buf + delta_bytes;

        const uint8_t fill_byte = (delta_neg ? (buf[bytes - 1] & 0x80) : buf[0] & 1) ? 0xFF : 0;

        if (pixels_to_move <= 0) {
          // on this row, the delta is wider than the available pixels
          if (patch_garbage) {
            memset(left_pixel, fill_byte, bytes);
          }
          break;
        }

        uint8_t *const from = delta_neg ? right_pixel : left_pixel;
        uint8_t *const to = delta_neg ? left_pixel : right_pixel;
        uint8_t *const garbage_start = delta_neg ? left_pixel + bytes - delta_bytes : left_pixel;

        if (delta_bytes) {
          memmove(to, from, bytes - delta_bytes);
          if (patch_garbage) {
            memset(garbage_start, fill_byte, delta_bytes);
          }
        }

        if (delta_neg) {
          if (delta_bits) {
            const int rshift = delta_bits;
            const int lshift = 8 - rshift;
            for (int i = 0; i < bytes - 1; i++) {
              buf[i] = (buf[i] >> rshift) | (buf[i+1] << lshift);
            }
            if (patch_garbage) {
              buf[bytes - 1] >>= rshift;
              buf[bytes - 1] |= fill_byte << lshift;
            } else {
              // Leave shifted-out areas alone
              buf[bytes - 1] = (buf[bytes - 1] >> rshift) | (buf[bytes - 1] & (0xFF << lshift));
            }
          }
        } else {
          if (delta_bits) {
            const int lshift = delta_bits;
            const int rshift = 8 - lshift;
            for (int i = bytes - 1; i >= 1; i--) {
              buf[i] = (buf[i] << lshift) | (buf[i-1] >> rshift);
            }
            if (patch_garbage) {
              buf[0] <<= lshift;
              buf[0] |= fill_byte >> rshift;
            } else {
              // Leave shifted-out areas alone
              buf[0] = (buf[0] << lshift) | (buf[0] & (0xFF >> rshift));
            }
          }
        }
        break;
      }
      case 8: {
        uint8_t *const left_pixel = row_info.data + min_x;
        uint8_t *const right_pixel = left_pixel + abs_delta;

        if (pixels_to_move <= 0) {
          // on this row, the delta is wider than the available pixels
          if (patch_garbage) {
            const uint8_t fill_byte = delta_neg ? left_pixel[num_pix_data_row - 1] :
                                                  left_pixel[0];
            memset(left_pixel, fill_byte, num_pix_data_row);
          }
          break;
        }

        uint8_t *const from = delta_neg ? right_pixel : left_pixel;
        uint8_t *const to = delta_neg ? left_pixel : right_pixel;
        uint8_t *const garbage_start = delta_neg ? left_pixel + pixels_to_move : left_pixel;
        const uint8_t fill_byte = delta_neg ? right_pixel[pixels_to_move - 1] : left_pixel[0];

        memmove(to, from, (size_t)pixels_to_move);
        if (patch_garbage) {
          memset(garbage_start, fill_byte, abs_delta);
        }
        break;
      }
      default:
        WTF;
    }
  }
}

void graphics_private_move_pixels_vertically(GBitmap *bitmap, int16_t delta_y) {
  if (!bitmap || (delta_y == 0)) {
    return;
  }

  const int bpp = gbitmap_get_bits_per_pixel(bitmap->info.format);

  const bool delta_neg = (delta_y < 0);
  const int16_t abs_delta = ABS(delta_y);
  const int16_t min_y = bitmap->bounds.origin.y;
  const int16_t max_y = grect_get_max_y(&bitmap->bounds) - 1;
  const int16_t max_x = grect_get_max_x(&bitmap->bounds) - 1;
  const int16_t iterate_dir = delta_neg ? -1 : 1;
  const int16_t end_y = delta_neg ? max_y : min_y;

  const int16_t start_y = delta_neg ? min_y + abs_delta : max_y - abs_delta;
  if ((!delta_neg && (start_y < end_y)) || (delta_neg && (start_y > end_y))) {
    return;
  }
  for (int16_t y = start_y; y != end_y; y -= iterate_dir) {
    const GBitmapDataRowInfo dst_row_info = gbitmap_get_data_row_info(bitmap, y + delta_y);
    const GBitmapDataRowInfo src_row_info = gbitmap_get_data_row_info(bitmap, y);

    switch (bpp) {
      case 1: {
        // Note: this doesn't care about the bounding, because we don't have any round 1bpp
        // devices to support, and it simplifies the code.
#if PBL_ROUND
        WTF;
#endif
        memmove(dst_row_info.data, src_row_info.data, bitmap->row_size_bytes);
        break;
      }
      case 8: {
        const int16_t dst_min_x = MAX(dst_row_info.min_x, bitmap->bounds.origin.x);
        const int16_t dst_max_x = MIN(dst_row_info.max_x, max_x);
        const int16_t dst_pixels = dst_max_x - dst_min_x + 1;

        const int16_t src_min_x = MAX(src_row_info.min_x, bitmap->bounds.origin.x);
        const int16_t src_max_x = MIN(src_row_info.max_x, max_x);
        const int16_t src_pixels = src_max_x - src_min_x + 1;

        const int16_t x_offset = src_min_x - dst_min_x;
        const int16_t copy_pixels = MIN(src_pixels, dst_pixels);
        memmove(dst_row_info.data + dst_min_x + x_offset, src_row_info.data + src_min_x,
                (size_t)copy_pixels);
        break;
      }
      default:
        WTF;
    }
  }
}

GColor graphics_private_sample_line_color(const GBitmap *bitmap, GColorSampleEdge edge,
                                          GColor fallback) {
  if (!bitmap) {
    return fallback;
  }

  GColor color = fallback;

  const int bpp = gbitmap_get_bits_per_pixel(bitmap->info.format);

  const int16_t min_x = bitmap->bounds.origin.x;
  const int16_t min_y = bitmap->bounds.origin.y;
  const int16_t end_x = grect_get_max_x(&bitmap->bounds);
  const int16_t end_y = grect_get_max_y(&bitmap->bounds);

  const bool horiz_advance = (edge == GColorSampleEdgeUp) || (edge == GColorSampleEdgeDown);
  const bool edge_is_max_position = (edge == GColorSampleEdgeDown) ||
                                    (edge == GColorSampleEdgeRight);

  const int16_t length = horiz_advance ? (end_x - min_x) : (end_y - min_y);

  for (int16_t i = 0; i < length; i++) {
    const int16_t x = horiz_advance ? min_x + i : (edge_is_max_position ? end_x - 1 : min_x);
    const int16_t y = !horiz_advance ? min_y + i : (edge_is_max_position ? end_y - 1 : min_y);

    const GBitmapDataRowInfo data_row_info = gbitmap_get_data_row_info(bitmap, y);
    GColor this_color;
    switch (bpp) {
      case 1:
        this_color = (data_row_info.data[x / 8] & (0x1 << (x % 8))) ? GColorWhite : GColorBlack;
        break;
      case 8:
        this_color.argb = data_row_info.data[x];
        break;
      default:
        WTF;
    }

    if (i == 0) {
      color.argb = this_color.argb;
    } else if (color.argb != this_color.argb) {
      return fallback;
    }
  }
  return color;
}
