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

#include "graphics.h"

#include "bitblt.h"
#include "bitblt_private.h"
#include "framebuffer.h"
#include "graphics_private.h"
#include "graphics_private_raw.h"
#include "gtransform.h"

#include "applib/app_logging.h"
#include "applib/applib_malloc.auto.h"
#include "kernel/ui/kernel_ui.h"
#include "process_management/process_manager.h"
#include "process_state/app_state/app_state.h"
#include "system/passert.h"
#include "system/logging.h"
#include "util/bitset.h"
#include "util/graphics.h"
#include "util/math.h"
#include "util/reverse.h"
#include "util/trig.h"

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

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

void graphics_draw_pixel(GContext* ctx, GPoint point) {
  PBL_ASSERTN(ctx);
  if (ctx->lock) {
    return;
  }

  point.x += ctx->draw_state.drawing_box.origin.x;
  point.y += ctx->draw_state.drawing_box.origin.y;

  graphics_private_set_pixel(ctx, point);
}

T_STATIC void prv_fill_rect_legacy2(GContext *ctx, GRect rect, uint16_t radius,
    GCornerMask corner_mask, GColor fill_color) {
  if (gcolor_is_transparent(fill_color)) {
    fill_color = GColorWhite;
  }

  // as this function will only be called with radius 0 to or
  // to support the legacy2 behavior (where the radius is clamped to 8) it's safe to assume 8px here
  PBL_ASSERTN(radius <= 8);
  GBitmap* bitmap = graphics_context_get_bitmap(ctx);

  // translate to absolute bitmap coordinates:
  rect.origin.x += ctx->draw_state.drawing_box.origin.x;
  rect.origin.y += ctx->draw_state.drawing_box.origin.y;

  // clip it to avoid drawing outside of the bitmap memory:
  GRect clipped_rect = rect;
  grect_standardize(&clipped_rect);
  grect_clip(&clipped_rect, &bitmap->bounds);
  grect_clip(&clipped_rect, &ctx->draw_state.clip_box);
  if (grect_is_empty(&clipped_rect)) {
    return;
  }

  // All the row insets are packed into an uint32, taking 4 bits per inset (hence the 8px radius limit):
  static const uint32_t round_top_corner_lookup[] = {
    0x0, 0x01, 0x01, 0x12, 0x113, 0x123, 0x1234, 0x11235, 0x112346,
  };
  static const uint32_t round_bottom_corner_lookup[] = {
    0x0, 0x01, 0x10, 0x210, 0x3110, 0x32100, 0x432100, 0x5321100, 0x64321100,
  };

  // Set up the insets for doing the top corners.
  uint32_t corner_insets_left = (corner_mask & GCornerTopLeft) ? round_top_corner_lookup[radius] : 0;
  uint32_t corner_insets_right = (corner_mask & GCornerTopRight) ? round_top_corner_lookup[radius] : 0;

  const unsigned int top_cropped_rows_count = clipped_rect.origin.y - rect.origin.y;
  const int32_t left_cropped_columns_count = MAX(0, clipped_rect.origin.x - rect.origin.x);
  const int32_t right_cropped_columns_count = MAX(0, rect.size.w - clipped_rect.size.w -
                                                  left_cropped_columns_count);

  if (top_cropped_rows_count) {
    // Skip over rows for each one that's cropped off the top.
    corner_insets_left >>= 4 * MIN(top_cropped_rows_count, 8);
    corner_insets_right >>= 4 * MIN(top_cropped_rows_count, 8);
  }

  // Mark the destination dirty before clipped_rect is modified.
  graphics_context_mark_dirty_rect(ctx, clipped_rect);

  // bit-block fiddling:
  const int16_t max_y = clipped_rect.origin.y + clipped_rect.size.h;
  for (; clipped_rect.origin.y < max_y; ++clipped_rect.origin.y) {

    if ((clipped_rect.origin.y == (rect.origin.y + rect.size.h) - radius) && (corner_mask & GCornersBottom)) {
      if (corner_mask & GCornerBottomLeft) {
        corner_insets_left = round_bottom_corner_lookup[radius];
      }
      if (corner_mask & GCornerBottomRight) {
        corner_insets_right = round_bottom_corner_lookup[radius];
      }
    }

    int32_t left_side = MAX((int32_t)(corner_insets_left & 0xf) - left_cropped_columns_count, 0);
    int32_t right_side = MAX((int32_t)(corner_insets_right & 0xf) - right_cropped_columns_count, 0);

    int32_t corner_insets = left_side + right_side;
    int32_t width = corner_insets < clipped_rect.size.w ? (clipped_rect.size.w - corner_insets) : 0;
    uint32_t x = clipped_rect.origin.x + left_side;
    corner_insets_left >>= 4;
    corner_insets_right >>= 4;

    PBL_ASSERTN(clipped_rect.origin.y < bitmap->bounds.size.h);
    PBL_ASSERTN(clipped_rect.origin.y >= 0);

    const uint16_t y = clipped_rect.origin.y;
    const uint16_t x_end = x + width;
    graphics_private_draw_horizontal_line_integral(ctx, &ctx->dest_bitmap, y, x, x_end, fill_color);
  }
}

//! Return the maximum rounded corner radius allowed for a given rectangle size
T_STATIC uint16_t prv_clamp_corner_radius(GSize size, GCornerMask corner_mask, uint16_t radius) {
  if (corner_mask == GCornerNone) {
    return 0;
  }

  int16_t min_size = MIN(size.w, size.h);

  if (min_size >= 2 * radius) {
    return radius;
  } else {
    return (min_size / 2);
  }
}

typedef void (*FillCircleImplFunc)(GContext *, GPoint pt, uint16_t radius, GCornerMask mask);

//! generic fill_rect implementation to avoid code-duplication between aa and non-aa fill_rect
void prv_fill_rect_internal(GContext *ctx, const GRect *rect, uint16_t radius,
                            GCornerMask corner_mask, GColor fill_color, uint16_t alt_radius,
                            FillCircleImplFunc circle_func) {
  // only draw if there is enough to cover the rounded edges - otherwise round down to largest
  // radius that can be drawn
  radius = prv_clamp_corner_radius(rect->size, corner_mask, radius);

  if (radius <= alt_radius) {
    prv_fill_rect_legacy2(ctx, *rect, radius, corner_mask, fill_color);
  } else {
    // These are used to optimize the rectangles that are drawn such that only three rectangles
    // are drawn always
    int16_t top_rect_origin_x = rect->origin.x;
    int16_t top_rect_size_w = rect->size.w;
    int16_t bottom_rect_origin_x = rect->origin.x;
    int16_t bottom_rect_size_w = rect->size.w;

    // Fill 3 rectangles and 4 quadrants
    if (corner_mask & GCornerTopLeft) {
      circle_func(ctx, GPoint(rect->origin.x + radius, rect->origin.y + radius),
                  radius, GCornerTopLeft);
      top_rect_origin_x += radius;
      top_rect_size_w -= radius;
    }
    if (corner_mask & GCornerBottomLeft) {
      circle_func(ctx, GPoint(rect->origin.x + radius, rect->origin.y + rect->size.h - radius - 1),
                  radius, GCornerBottomLeft);
      bottom_rect_origin_x += radius;
      bottom_rect_size_w -= radius;
    }
    if (corner_mask & GCornerTopRight) {
      circle_func(ctx, GPoint(rect->origin.x + rect->size.w - radius - 1, rect->origin.y + radius),
                  radius, GCornerTopRight);
      top_rect_size_w -= radius;
    }
    if (corner_mask & GCornerBottomRight) {
      circle_func(ctx, GPoint(rect->origin.x + rect->size.w - radius - 1,
                              rect->origin.y + rect->size.h - radius - 1),
                  radius, GCornerBottomRight);
      bottom_rect_size_w -= radius;
    }

    // Top Rect
    prv_fill_rect_legacy2(ctx, GRect(top_rect_origin_x, rect->origin.y, top_rect_size_w, radius),
                          0, GCornerNone, fill_color);

    // Middle Rect
    prv_fill_rect_legacy2(ctx, GRect(rect->origin.x, rect->origin.y + radius,
                                     rect->size.w, rect->size.h - 2 * radius),
                          0, GCornerNone, fill_color);

    // Bottom Rect
    prv_fill_rect_legacy2(ctx, GRect(bottom_rect_origin_x, rect->origin.y + rect->size.h - radius,
                                     bottom_rect_size_w, radius),
                          0, GCornerNone, fill_color);
  }
}

T_STATIC void prv_fill_rect_non_aa(GContext* ctx, const GRect *rect, uint16_t radius,
                                   GCornerMask corner_mask, GColor fill_color) {

  // for radii <= 8 we can safely use the legacy2 behavior
  const uint16_t alt_radius = 8;
  FillCircleImplFunc circle_func = graphics_circle_quadrant_fill_non_aa;
  prv_fill_rect_internal(ctx, rect, radius, corner_mask, fill_color, alt_radius, circle_func);
}

#if PBL_COLOR
T_STATIC void prv_fill_rect_aa(GContext* ctx, const GRect *rect, uint16_t radius,
                               GCornerMask corner_mask, GColor fill_color) {
  FillCircleImplFunc circle_func = graphics_internal_circle_quadrant_fill_aa;
  prv_fill_rect_internal(ctx, rect, radius, corner_mask, fill_color, 0, circle_func);
}
#endif // PBL_COLOR

void graphics_fill_round_rect(GContext* ctx, const GRect *rect, uint16_t radius,
                              GCornerMask corner_mask) {
  PBL_ASSERTN(ctx);
  if (!rect || ctx->lock) {
    return;
  }

#if PBL_COLOR
  if (ctx->draw_state.antialiased) {
    // Antialiased (not suppported on 1-bit color)
    prv_fill_rect_aa(ctx, rect, radius, corner_mask, ctx->draw_state.fill_color);
    return;
  }
#endif
  prv_fill_rect_non_aa(ctx, rect, radius, corner_mask, ctx->draw_state.fill_color);
}

void graphics_fill_round_rect_by_value(GContext* ctx, GRect rect, uint16_t radius,
                                 GCornerMask corner_mask) {
  graphics_fill_round_rect(ctx, &rect, radius, corner_mask);
}

void graphics_fill_rect(GContext* ctx, const GRect *rect) {
  graphics_fill_round_rect(ctx, rect, 0, GCornerNone);
}

T_STATIC void prv_draw_rect(GContext *ctx, const GRect *rect) {
  GColor fill_color = ctx->draw_state.fill_color;
  ctx->draw_state.fill_color = ctx->draw_state.stroke_color;
  graphics_fill_rect(ctx, &GRect(rect->origin.x, rect->origin.y, rect->size.w, 1)); // top
  graphics_fill_rect(ctx, &GRect(rect->origin.x, rect->origin.y + rect->size.h - 1,
                                 rect->size.w, 1)); // bottom
  graphics_fill_rect(ctx, &GRect(rect->origin.x, rect->origin.y + 1, 1, rect->size.h - 2)); // left
  graphics_fill_rect(ctx, &GRect(rect->origin.x + rect->size.w - 1,
                                 rect->origin.y + 1, 1, rect->size.h - 2)); // right
  ctx->draw_state.fill_color = fill_color;
}

#if PBL_COLOR
T_STATIC void prv_draw_rect_aa_stroked(GContext *ctx, const GRect *rect, uint8_t stroke_width) {
  const GPoint tl = GPoint(rect->origin.x, rect->origin.y);
  const GPoint tr = GPoint(rect->origin.x + rect->size.w - 1, rect->origin.y);
  const GPoint bl = GPoint(rect->origin.x, rect->origin.y + rect->size.h - 1);
  const GPoint br = GPoint(rect->origin.x + rect->size.w - 1, rect->origin.y + rect->size.h - 1);

  graphics_line_draw_stroked_aa(ctx, tl, tr, stroke_width);
  graphics_line_draw_stroked_aa(ctx, tl, bl, stroke_width);
  graphics_line_draw_stroked_aa(ctx, tr, br, stroke_width);
  graphics_line_draw_stroked_aa(ctx, bl, br, stroke_width);
}
#endif // PBL_COLOR

T_STATIC void prv_draw_rect_stroked(GContext *ctx, const GRect *rect, uint8_t stroke_width) {
  const GPoint tl = GPoint(rect->origin.x, rect->origin.y);
  const GPoint tr = GPoint(rect->origin.x + rect->size.w - 1, rect->origin.y);
  const GPoint bl = GPoint(rect->origin.x, rect->origin.y + rect->size.h - 1);
  const GPoint br = GPoint(rect->origin.x + rect->size.w - 1, rect->origin.y + rect->size.h - 1);

  graphics_line_draw_stroked_non_aa(ctx, tl, tr, stroke_width);
  graphics_line_draw_stroked_non_aa(ctx, tl, bl, stroke_width);
  graphics_line_draw_stroked_non_aa(ctx, tr, br, stroke_width);
  graphics_line_draw_stroked_non_aa(ctx, bl, br, stroke_width);
}

void graphics_draw_rect(GContext* ctx, const GRect *rect) {
  PBL_ASSERTN(ctx);
  if (!rect || ctx->lock) {
    return;
  }

  if (ctx->draw_state.stroke_width <= 2) {
    // Note: stroke width == 2 is rounded down to stroke width of 1
    prv_draw_rect(ctx, rect);
    return;
  }
#if PBL_COLOR
  if (ctx->draw_state.antialiased) {
    // Antialiased and Stroke Width > 2
    prv_draw_rect_aa_stroked(ctx, rect, ctx->draw_state.stroke_width);
    return;
  }
#endif
  // Non-Antialiased and Stroke Width > 2
  // Note: stroke width must be odd and greater than 2
  prv_draw_rect_stroked(ctx, rect, ctx->draw_state.stroke_width);
}

void graphics_draw_rect_by_value(GContext* ctx, GRect rect) {
  graphics_draw_rect(ctx, &rect);
}

void graphics_draw_rect_precise(GContext* ctx, const GRectPrecise *rect) {
  const Fixed_S16_3 right = grect_precise_get_max_x(rect);
  const Fixed_S16_3 bottom = grect_precise_get_max_y(rect);

  const GPointPrecise top_left = rect->origin;
  const GPointPrecise top_right = {right, rect->origin.y};
  const GPointPrecise bottom_right = {right, bottom};
  const GPointPrecise bottom_left = {rect->origin.x, bottom};

  graphics_line_draw_precise_stroked(ctx, top_left, top_right);
  graphics_line_draw_precise_stroked(ctx, top_right, bottom_right);
  graphics_line_draw_precise_stroked(ctx, bottom_right, bottom_left);
  graphics_line_draw_precise_stroked(ctx, bottom_left, top_left);
}

// This takes care of all routines since it re-uses existing AA and SW functionality in draw line
// and draw circle
T_STATIC void prv_draw_round_rect(GContext* ctx, const GRect *rect, uint16_t radius) {
  const GPoint origin = rect->origin;
  const int16_t width = rect->size.w;
  const int16_t height = rect->size.h;

  // Subtract out twice the respective radius values to get the actual width and height of the
  // rectangle lines
  const int16_t width_actual = width - (2 * radius);
  const int16_t height_actual = height - (2 * radius);

  // Take into account the radius values to determine the eight points for each of the four lines
  const GPoint top_l = GPoint(origin.x + radius, origin.y);
  const GPoint top_r = GPoint(origin.x + radius + width_actual - 1, origin.y);

  const GPoint bottom_l = GPoint(origin.x + radius, origin.y + height - 1);
  const GPoint bottom_r = GPoint(origin.x + radius + width_actual - 1, origin.y + height - 1);

  const GPoint left_t = GPoint(origin.x, origin.y + radius);
  const GPoint left_b = GPoint(origin.x, origin.y + radius + height_actual - 1);

  const GPoint right_t = GPoint(origin.x + width - 1, origin.y + radius);
  const GPoint right_b = GPoint(origin.x + width - 1, origin.y + radius + height_actual - 1);

  // Draw lines between each transformed corner point
  graphics_draw_line(ctx, top_l, top_r);       // top
  graphics_draw_line(ctx, bottom_l, bottom_r); // bottom
  graphics_draw_line(ctx, left_t, left_b);     // left
  graphics_draw_line(ctx, right_t, right_b);   // right

  // Draw quadrants
  const GPoint tl = GPoint(origin.x + radius, origin.y + radius);
  const GPoint tr = gpoint_add(tl, GPoint(width_actual - 1, 0));
  const GPoint bl = gpoint_add(tl, GPoint(0, height_actual - 1));
  const GPoint br = gpoint_add(tl, GPoint(width_actual - 1, height_actual - 1));

  graphics_circle_quadrant_draw(ctx, tl, radius, GCornerTopLeft);
  graphics_circle_quadrant_draw(ctx, bl, radius, GCornerBottomLeft);
  graphics_circle_quadrant_draw(ctx, tr, radius, GCornerTopRight);
  graphics_circle_quadrant_draw(ctx, br, radius, GCornerBottomRight);
}

#if PBL_COLOR
T_STATIC void prv_draw_round_rect_aa(GContext* ctx, const GRect *rect, uint16_t radius) {
  // Assumes AA and stroke_width is set appropriately in ctx
  prv_draw_round_rect(ctx, rect, radius);
}

T_STATIC void prv_draw_round_rect_aa_stroked(GContext* ctx, const GRect *rect,
                                             uint16_t radius, uint8_t stroke_width) {
  // Assumes AA and stroke_width is set appropriately in ctx
  prv_draw_round_rect(ctx, rect, radius);
}
#endif // SCREEN_COLOR_DEPTH_BITS

T_STATIC void prv_draw_round_rect_stroked(GContext* ctx, const GRect *rect, uint16_t radius,
                                          uint8_t stroke_width) {
  // Assumes AA and stroke_width is set appropriately in ctx
  prv_draw_round_rect(ctx, rect, radius);
}

static void prv_graphics_convert_8_bit_to_1_bit(const GBitmap *from, GBitmap *to) {
  const GRect bounds = from->bounds;
  uint8_t *to_buffer = (uint8_t *) to->addr;

  const int y_start = bounds.origin.y;
  const int y_end = y_start + bounds.size.h;
  const int x_start = bounds.origin.x;
  const int x_end = x_start + bounds.size.w;

  for (int y = y_start; y < y_end; ++y) {
    int to_idx_base = y * to->row_size_bytes;
    uint8_t *line = to_buffer + to_idx_base;
    for (int x = x_start; x < x_end; ++x) {
      bitset8_clear(line, x);
    }
  }
}

void graphics_draw_round_rect(GContext* ctx, const GRect *rect, uint16_t radius) {
  PBL_ASSERTN(ctx);
  if (!rect || ctx->lock) {
    return;
  }

  // only draw if there is enough to cover the rounded edges - otherwise round down to largest
  // radius that can be drawn
  radius = prv_clamp_corner_radius(rect->size, GCornersAll, radius);

  if (radius == 0) {
    graphics_draw_rect(ctx, rect);
  } else {
#if PBL_COLOR
    if (ctx->draw_state.antialiased) {
      if (ctx->draw_state.stroke_width > 1) {
        // Antialiased and Stroke Width > 1
        // Note: stroke width == 2 is rounded down to stroke width of 1
        prv_draw_round_rect_aa_stroked(ctx, rect, radius, ctx->draw_state.stroke_width);
        return;
      } else {
        // Antialiased and Stroke Width == 1 (not suppported on 1-bit color)
        // Note: stroke width == 2 is rounded down to stroke width of 1
        prv_draw_round_rect_aa(ctx, rect, radius);
        return;
      }
    }
#endif
    if (ctx->draw_state.stroke_width > 1) {
      // Non-Antialiased and Stroke Width > 1
      prv_draw_round_rect_stroked(ctx, rect, radius, ctx->draw_state.stroke_width);
    } else {
      // Non-Antialiased and Stroke Width == 1
      prv_draw_round_rect(ctx, rect, radius);
    }
  }
}

void graphics_draw_round_rect_by_value(GContext* ctx, GRect rect, uint16_t radius) {
  graphics_draw_round_rect(ctx, &rect, radius);
}

void graphics_context_init(GContext *context, FrameBuffer *framebuffer,
                           GContextInitializationMode init_mode) {
  PBL_ASSERTN(context);
  PBL_ASSERTN(framebuffer);

  *context = (GContext) {
    // For apps, this is run before the app has a chance to run, so there's no concern here of the
    // app changing its framebuffer size.
    .dest_bitmap = framebuffer_get_as_bitmap(framebuffer, &framebuffer->size),
    .parent_framebuffer = framebuffer,
    .parent_framebuffer_vertical_offset = 0,
    .lock = false
  };

  // init the font cache
  FontCache *font_cache = &context->font_cache;
  memset(font_cache->cache_keys, 0, sizeof(font_cache->cache_keys));
  memset(font_cache->cache_data, 0, sizeof(font_cache->cache_data));
  keyed_circular_cache_init(&font_cache->line_cache, font_cache->cache_keys,
                            font_cache->cache_data, sizeof(LineCacheData), LINE_CACHE_SIZE);

  graphics_context_set_default_drawing_state(context, init_mode);
}

void graphics_context_set_default_drawing_state(GContext *ctx,
                                                GContextInitializationMode init_mode) {
  PBL_ASSERTN(ctx);
  GBitmap* bitmap = graphics_context_get_bitmap(ctx);

  ctx->draw_state = (GDrawState) {
    .stroke_color = GColorBlack,
    .fill_color = GColorBlack,
    .text_color = GColorWhite,
    .tint_color = GColorWhite,
    .compositing_mode = GCompOpAssign,
    .clip_box = bitmap->bounds,
    .drawing_box = bitmap->bounds,
#if PBL_COLOR
    .antialiased = !process_manager_compiled_with_legacy2_sdk(),
#endif
    .stroke_width = 1,
    .draw_implementation = &g_default_draw_implementation,
    .avoid_text_orphans = (init_mode == GContextInitializationMode_System),
  };
}

GDrawState graphics_context_get_drawing_state(GContext* ctx) {
  PBL_ASSERTN(ctx);
  return ctx->draw_state;
}

void graphics_context_set_drawing_state(GContext* ctx, GDrawState draw_state) {
  PBL_ASSERTN(ctx);
  ctx->draw_state = draw_state;
}

void graphics_context_move_draw_box(GContext* ctx, GPoint offset) {
  PBL_ASSERTN(ctx);
  ctx->draw_state.drawing_box.origin = gpoint_add(ctx->draw_state.drawing_box.origin, offset);
}

void graphics_context_set_stroke_color(GContext* ctx, GColor color) {
  PBL_ASSERTN(ctx);
  if (ctx->lock) {
    return;
  }

  #if PBL_BW
    color = gcolor_get_bw(color);
  #else
    color = gcolor_closest_opaque(color);
  #endif
  ctx->draw_state.stroke_color = color;
}

void graphics_context_set_stroke_color_2bit(GContext* ctx, GColor2 color) {
  graphics_context_set_stroke_color(ctx, get_native_color(color));
}

void graphics_context_set_fill_color(GContext* ctx, GColor color) {
  PBL_ASSERTN(ctx);
  if (ctx->lock) {
    return;
  }

  #if PBL_BW
    color = gcolor_get_grayscale(color);
  #else
    color = gcolor_closest_opaque(color);
  #endif
  ctx->draw_state.fill_color = color;
}

void graphics_context_set_fill_color_2bit(GContext* ctx, GColor2 color) {
  graphics_context_set_fill_color(ctx, get_native_color(color));
}

void graphics_context_set_text_color(GContext* ctx, GColor color) {
  PBL_ASSERTN(ctx);
  if (ctx->lock) {
    return;
  }

  #if PBL_BW
    color = gcolor_get_bw(color);
  #else
    color = gcolor_closest_opaque(color);
  #endif
  ctx->draw_state.text_color = color;
}

void graphics_context_set_text_color_2bit(GContext* ctx, GColor2 color) {
  graphics_context_set_text_color(ctx, get_native_color(color));
}

void graphics_context_set_tint_color(GContext *ctx, GColor color) {
  PBL_ASSERTN(ctx);
  if (ctx->lock) {
    return;
  }
  ctx->draw_state.tint_color = gcolor_closest_opaque(color);
}

void graphics_context_set_compositing_mode(GContext* ctx, GCompOp mode) {
  PBL_ASSERTN(ctx);
  if (ctx->lock) {
    return;
  }

  ctx->draw_state.compositing_mode = mode;
}

void graphics_context_set_antialiased(GContext* ctx, bool enable) {
  PBL_ASSERTN(ctx);
  if (ctx->lock) {
    return;
  }
#if PBL_COLOR
  ctx->draw_state.antialiased = enable;
#endif
}

bool graphics_context_get_antialiased(GContext *ctx) {
  PBL_ASSERTN(ctx);
  return PBL_IF_COLOR_ELSE(ctx->draw_state.antialiased, false);
}

void graphics_context_set_stroke_width(GContext* ctx, uint8_t stroke_width) {
  PBL_ASSERTN(ctx);
  if (ctx->lock) {
    return;
  }

  // Ignore if stroke width == 0
  if (stroke_width >= 1) {
    ctx->draw_state.stroke_width = stroke_width;
  }
}

GSize graphics_context_get_framebuffer_size(GContext *ctx) {
  if (ctx && ctx->parent_framebuffer) {
    return ctx->parent_framebuffer->size;
  } else {
    return GSize(DISP_COLS, DISP_ROWS);
  }
}

GBitmap* graphics_context_get_bitmap(GContext* ctx) {
  PBL_ASSERTN(ctx);
  return &ctx->dest_bitmap;
}

void graphics_context_mark_dirty_rect(GContext* ctx, GRect rect) {
  PBL_ASSERTN(ctx);
  if (ctx->parent_framebuffer) {
    framebuffer_mark_dirty_rect(ctx->parent_framebuffer, rect);
  }
}

bool graphics_frame_buffer_is_captured(GContext* ctx) {
  PBL_ASSERTN(ctx);
  return ctx->lock;
}

GBitmap* graphics_capture_frame_buffer_format(GContext *ctx, GBitmapFormat format) {
  PBL_ASSERTN(ctx);
  if (ctx->lock) {
    APP_LOG(APP_LOG_LEVEL_WARNING,
            "Frame buffer has already been captured; it cannot be captured again until "
            "graphics_release_frame_buffer has been called.");
    return NULL;
  }
  ctx->lock = true;

  GBitmap *native =  graphics_context_get_bitmap(ctx);

  if (format == native->info.format) {
    return native;
  }

  GBitmap *result = NULL;
  if (format == GBitmapFormat1Bit && native->info.format == GBitmapFormat8Bit) {
    // Create a new blank gbitmap in the correct format.
    const GBitmap *native_framebuffer = graphics_context_get_bitmap(ctx);

    if (process_manager_compiled_with_legacy2_sdk()) {
      result = app_state_legacy2_get_2bit_framebuffer();
    } else {
      result = gbitmap_create_blank(native_framebuffer->bounds.size, GBitmapFormat1Bit);
    }

    if (result) {
      prv_graphics_convert_8_bit_to_1_bit(native_framebuffer, result);
    }
  }

  if (!result) {
    ctx->lock = false;
  }

  return result;
}

GBitmap* graphics_capture_frame_buffer_2bit(GContext *ctx) {
  return graphics_capture_frame_buffer_format(ctx, GBitmapFormat1Bit);
}

MOCKABLE GBitmap *graphics_capture_frame_buffer(GContext *ctx) {
  PBL_ASSERTN(ctx);
  return graphics_capture_frame_buffer_format(ctx, GBITMAP_NATIVE_FORMAT);
}

#include "system/profiler.h"
MOCKABLE bool graphics_release_frame_buffer(GContext *ctx, GBitmap *buffer) {
  PBL_ASSERTN(ctx);
  GBitmap *native_framebuffer = graphics_context_get_bitmap(ctx);
  if (gbitmap_get_format(buffer) != GBITMAP_NATIVE_FORMAT) {
    ctx->lock = false;
    bitblt_bitmap_into_bitmap(native_framebuffer, buffer, GPointZero,
        GCompOpAssign, GColorWhite);
    framebuffer_dirty_all(ctx->parent_framebuffer);

    // Don't destroy the bitmap we got from app_state_legacy2_get_2bit_framebuffer()
    if (!process_manager_compiled_with_legacy2_sdk()) {
      gbitmap_destroy(buffer);
    }
    return true;
  }

  if (buffer == native_framebuffer) {
    ctx->lock = false;
    framebuffer_dirty_all(ctx->parent_framebuffer);
    return true;
  }

  return false;
}
