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

#include "clar.h"

#include "applib/graphics/gtypes.h"
#include "applib/graphics/graphics.h"
#include "applib/graphics/gpath.h"
#include "util/trig.h"
#include "applib/ui/ui.h"

#include <string.h>

// Helper Functions
////////////////////////////////////
#include "util.h"
#include "test_graphics.h"
#include "${BIT_DEPTH_NAME}/test_framebuffer.h"

// Stubs
////////////////////////////////////
#include "graphics_common_stubs.h"
#include "stubs_applib_resource.h"

static const int16_t SCREEN_WIDTH = 144;
static const int16_t SCREEN_HEIGHT = 168;
static FrameBuffer *fb = NULL;

static const GPathInfo s_house_path_info = {
  .num_points = 11,
  .points = (GPoint []) {
    {-40, 0}, {0, -40}, {40, 0}, {28, 0}, {28, 40}, {10, 40},
    {10, 16}, {-10, 16}, {-10, 40}, {-28, 40}, {-28, 0},
  },
};

static const GPathInfo s_bolt_path_info = {
  .num_points = 6,
  .points = (GPoint []) {{21, 0}, {14, 26}, {28, 26}, {7, 60}, {14, 34}, {0, 34}}
};

static const GPathInfo s_duplicates_path_info = {
  6,
  (GPoint[]) {
    {40, 0},
    {40, 0},
    {0, 40},
    {0, 40},
    {80, 40},
    {80, 40}
  }
};

static const GPathInfo s_single_duplicate_path_info = {
  2,
  (GPoint[]) {
    {40, 0},
    {40, 0}
  }
};

static const GPathInfo s_crossing_path_info = {
  6,
  (GPoint[]) {
    {0, 40},
    {20, 20},
    {60, 60},
    {80, 40},
    {60, 20},
    {20, 60}
  }
};

static const GPathInfo s_infinite_path_info = {
  16,
  (GPoint[]) {
    {-50, 0},
    {-50, -60},
    {10, -60},
    {10, -20},
    {-10, -20},
    {-10, -40},
    {-30, -40},
    {-30, -20},
    {50, -20},
    {50, 40},
    {-10, 40},
    {-10, 0},
    {10, 0},
    {10, 20},
    {30, 20},
    {30, 0}
  }
};

static const GPathInfo s_aa_clipping_path_info = {
  .num_points = 4,
  .points = (GPoint []) {{0,0}, {200, 0}, {200, 30}, {0, 30}}
};

static bool s_outline_mode = false;
static int s_path_angle = 0;

static GPath *s_house_path = NULL;
static GPath *s_bolt_path = NULL;
static GPath *s_duplicates_path = NULL;
static GPath *s_single_duplicate_path = NULL;
static GPath *s_crossing_path = NULL;
static GPath *s_infinite_path = NULL;
static GPath *s_current_path = NULL;
static GPath *s_aa_clipping_path = NULL;

static void prv_filled_update_proc(Layer *layer, GContext *ctx) {
  graphics_context_set_stroke_color(ctx, GColorBlack);

#if 0 // Guidelines
  int num_segments = 4;
  int segment_width = SCREEN_WIDTH / num_segments;
  int segment_height = SCREEN_HEIGHT / num_segments;
  for (int i = 1; i < num_segments; ++i) {
    graphics_draw_line(ctx,
        GPoint(segment_width * i, 0),
        GPoint(segment_width * i, SCREEN_HEIGHT));
    graphics_draw_line(ctx,
        GPoint(0, segment_height * i),
        GPoint(SCREEN_WIDTH, segment_height * i));
  }
#endif

  gpath_rotate_to(s_current_path, s_path_angle * TRIG_MAX_ANGLE / 360);

  if (s_outline_mode) {
    graphics_context_set_stroke_color(ctx, GColorBlack);
    gpath_draw_outline(ctx, s_current_path);
  } else {
    graphics_context_set_fill_color(ctx, GColorBlack);
    gpath_draw_filled(ctx, s_current_path);
  }
}

static void prv_reset(void) {
  gpath_move_to(s_house_path, GPoint(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2));
  s_outline_mode = false;
  s_path_angle = 0;
}

// setup and teardown
void test_graphics_gpath_${BIT_DEPTH_NAME}__initialize(void) {
  fb = malloc(sizeof(FrameBuffer));
  framebuffer_init(fb, &(GSize) {DISP_COLS, DISP_ROWS});
  s_house_path = gpath_create(&s_house_path_info);
  s_bolt_path = gpath_create(&s_bolt_path_info);
  s_duplicates_path = gpath_create(&s_duplicates_path_info);
  s_crossing_path = gpath_create(&s_crossing_path_info);
  s_infinite_path = gpath_create(&s_infinite_path_info);
  s_aa_clipping_path = gpath_create(&s_aa_clipping_path_info);
  prv_reset();
}

void test_graphics_gpath_${BIT_DEPTH_NAME}__cleanup(void) {
  free(fb);
  gpath_destroy(s_house_path);
  gpath_destroy(s_bolt_path);
  gpath_destroy(s_duplicates_path);
  gpath_destroy(s_infinite_path);
  gpath_destroy(s_crossing_path);
  gpath_destroy(s_aa_clipping_path);
}

// tests
void test_graphics_gpath_${BIT_DEPTH_NAME}__filled(void) {
  GContext ctx;
  Layer layer;

  prv_reset();
  s_current_path = s_house_path;
  test_graphics_context_init(&ctx, fb);
  prv_filled_update_proc(NULL, &ctx);
  cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "gpath_filled.${BIT_DEPTH_NAME}.pbi"));
}

void test_graphics_gpath_${BIT_DEPTH_NAME}__filled_clipped(void) {
  GContext ctx;
  Layer layer;

  prv_reset();
  test_graphics_context_init(&ctx, fb);
  s_current_path = s_house_path;
  ctx.draw_state.clip_box = GRect(0, SCREEN_HEIGHT / 2, SCREEN_WIDTH, SCREEN_HEIGHT / 2);
  prv_filled_update_proc(NULL, &ctx);
  cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap,
        "gpath_filled_top_clipped.${BIT_DEPTH_NAME}.pbi"));

  prv_reset();
  test_graphics_context_init(&ctx, fb);
  s_current_path = s_house_path;
  ctx.draw_state.clip_box = GRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT / 2);
  prv_filled_update_proc(NULL, &ctx);
  cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap,
        "gpath_filled_bottom_clipped.${BIT_DEPTH_NAME}.pbi"));

  prv_reset();
  test_graphics_context_init(&ctx, fb);
  s_current_path = s_house_path;
  ctx.draw_state.clip_box = GRect(0, 0, SCREEN_WIDTH / 2, SCREEN_HEIGHT);
  prv_filled_update_proc(NULL, &ctx);
  cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap,
        "gpath_filled_left_clipped.${BIT_DEPTH_NAME}.pbi"));

  prv_reset();
  test_graphics_context_init(&ctx, fb);
  s_current_path = s_house_path;
  ctx.draw_state.clip_box = GRect(SCREEN_WIDTH / 2, 0, SCREEN_WIDTH / 2, SCREEN_HEIGHT);
  prv_filled_update_proc(NULL, &ctx);
  cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap,
        "gpath_filled_right_clipped.${BIT_DEPTH_NAME}.pbi"));
}

// outside with no clipping -- results should be identical to the regular filled test
void test_graphics_gpath_${BIT_DEPTH_NAME}__filled_outside(void) {
  GContext ctx;
  Layer layer;

  printf("-- top\n");
  prv_reset();
  test_graphics_context_init(&ctx, fb);
  s_current_path = s_house_path;
  gpath_move_to(s_house_path, GPoint(SCREEN_WIDTH / 2, 0));
  ctx.draw_state.drawing_box = GRect(0, SCREEN_HEIGHT / 2, SCREEN_WIDTH, SCREEN_HEIGHT);
  ctx.draw_state.clip_box = GRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
  prv_filled_update_proc(NULL, &ctx);
  cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "gpath_filled.${BIT_DEPTH_NAME}.pbi"));

  printf("-- bottom\n");
  prv_reset();
  test_graphics_context_init(&ctx, fb);
  s_current_path = s_house_path;
  gpath_move_to(s_house_path, GPoint(SCREEN_WIDTH / 2, SCREEN_HEIGHT));
  ctx.draw_state.drawing_box = GRect(0, -SCREEN_HEIGHT / 2, SCREEN_WIDTH, SCREEN_HEIGHT);
  ctx.draw_state.clip_box = GRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
  prv_filled_update_proc(NULL, &ctx);
  cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "gpath_filled.${BIT_DEPTH_NAME}.pbi"));

  printf("-- left\n");
  prv_reset();
  prv_reset();
  test_graphics_context_init(&ctx, fb);
  s_current_path = s_house_path;
  gpath_move_to(s_house_path, GPoint(0, SCREEN_HEIGHT / 2));
  ctx.draw_state.drawing_box = GRect(SCREEN_WIDTH / 2, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
  ctx.draw_state.clip_box = GRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
  prv_filled_update_proc(NULL, &ctx);
  cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "gpath_filled.${BIT_DEPTH_NAME}.pbi"));

  printf("-- right\n");
  prv_reset();
  test_graphics_context_init(&ctx, fb);
  s_current_path = s_house_path;
  gpath_move_to(s_house_path, GPoint(SCREEN_WIDTH, SCREEN_HEIGHT / 2));
  ctx.draw_state.drawing_box = GRect(-SCREEN_WIDTH / 2, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
  ctx.draw_state.clip_box = GRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
  prv_filled_update_proc(NULL, &ctx);
  cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "gpath_filled.${BIT_DEPTH_NAME}.pbi"));
}

// AA section
void test_graphics_gpath_${BIT_DEPTH_NAME}__filled_aa(void) {
  GContext ctx;
  Layer layer;

  // House path - tests horizontal line edge case
  prv_reset();
  s_current_path = s_house_path;
  test_graphics_context_init(&ctx, fb);
  graphics_context_set_antialiased(&ctx, true);
  prv_filled_update_proc(NULL, &ctx);
  cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "gpath_filled_aa.${BIT_DEPTH_NAME}.pbi"));

  // Special case for two points that are duplicates...
  prv_reset();
  test_graphics_context_init(&ctx, fb);
  s_current_path = s_single_duplicate_path;
  ctx.draw_state.clip_box = GRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT / 2);
  graphics_context_set_antialiased(&ctx, true);
  prv_filled_update_proc(NULL, &ctx);
  cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap,
        "gpath_filled_single_duplicate_aa.${BIT_DEPTH_NAME}.pbi"));

}

void test_graphics_gpath_${BIT_DEPTH_NAME}__filled_clipped_aa(void) {
  GContext ctx;
  Layer layer;

  prv_reset();
  test_graphics_context_init(&ctx, fb);
  s_current_path = s_house_path;
  ctx.draw_state.clip_box = GRect(0, SCREEN_HEIGHT / 2, SCREEN_WIDTH, SCREEN_HEIGHT / 2);
  graphics_context_set_antialiased(&ctx, true);
  prv_filled_update_proc(NULL, &ctx);
  cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap,
        "gpath_filled_top_clipped_aa.${BIT_DEPTH_NAME}.pbi"));

  prv_reset();
  test_graphics_context_init(&ctx, fb);
  s_current_path = s_house_path;
  ctx.draw_state.clip_box = GRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT / 2);
  graphics_context_set_antialiased(&ctx, true);
  prv_filled_update_proc(NULL, &ctx);
  cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap,
        "gpath_filled_bottom_clipped_aa.${BIT_DEPTH_NAME}.pbi"));

  prv_reset();
  test_graphics_context_init(&ctx, fb);
  s_current_path = s_house_path;
  ctx.draw_state.clip_box = GRect(0, 0, SCREEN_WIDTH / 2, SCREEN_HEIGHT);
  graphics_context_set_antialiased(&ctx, true);
  prv_filled_update_proc(NULL, &ctx);
  cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap,
        "gpath_filled_left_clipped_aa.${BIT_DEPTH_NAME}.pbi"));

  prv_reset();
  test_graphics_context_init(&ctx, fb);
  s_current_path = s_house_path;
  ctx.draw_state.clip_box = GRect(SCREEN_WIDTH / 2, 0, SCREEN_WIDTH / 2, SCREEN_HEIGHT);
  graphics_context_set_antialiased(&ctx, true);
  prv_filled_update_proc(NULL, &ctx);
  cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap,
        "gpath_filled_right_clipped_aa.${BIT_DEPTH_NAME}.pbi"));
}

// Additional test to check AA on edges - works only on 8bit
void test_graphics_gpath_8bit__filled_bolt_aa(void) {
  // NOTE: Those tests are being performed only in 8bits due to differences caused by AA,
  //   performing them in both 1bit and 8bit would create differences on the edges
  //   and fail unit tests as a result
  GContext ctx;
  Layer layer;

  // Bolt path - test antialiased edges
  prv_reset();
  test_graphics_context_init(&ctx, fb);
  s_current_path = s_bolt_path;
  ctx.draw_state.clip_box = GRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT / 2);
  graphics_context_set_antialiased(&ctx, true);
  prv_filled_update_proc(NULL, &ctx);
  cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap,
                          "gpath_filled_bolt_aa.8bit.pbi"));

  // Duplicate points in GPath test - makes sure theres no division by zero ;)
  prv_reset();
  test_graphics_context_init(&ctx, fb);
  s_current_path = s_duplicates_path;
  ctx.draw_state.clip_box = GRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT / 2);
  graphics_context_set_antialiased(&ctx, true);
  prv_filled_update_proc(NULL, &ctx);
  cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap,
                          "gpath_filled_duplicates_aa.8bit.pbi"));

  // Crossing path - makes sure algorithm works for path that crosses itself
  prv_reset();
  s_current_path = s_crossing_path;
  test_graphics_context_init(&ctx, fb);
  graphics_context_set_antialiased(&ctx, true);
  prv_filled_update_proc(NULL, &ctx);
  cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "gpath_filled_crossing_aa.8bit.pbi"));

  // Infinite path - shows an example where path seems to be crossing itself but
  //   in fact it does not
  prv_reset();
  s_current_path = s_infinite_path;
  gpath_move_to(s_infinite_path, GPoint(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2));
  test_graphics_context_init(&ctx, fb);
  graphics_context_set_antialiased(&ctx, true);
  prv_filled_update_proc(NULL, &ctx);
  cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "gpath_filled_infinite_aa.8bit.pbi"));

  // An angle of infinite path - here we see the spacing between the parts
  prv_reset();
  s_current_path = s_infinite_path;
  gpath_move_to(s_infinite_path, GPoint(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2));
  s_path_angle = 45;
  test_graphics_context_init(&ctx, fb);
  graphics_context_set_antialiased(&ctx, true);
  prv_filled_update_proc(NULL, &ctx);
  cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "gpath_filled_infinite_45_aa.8bit.pbi"));

  // Another angle of infinite path
  prv_reset();
  s_current_path = s_infinite_path;
  gpath_move_to(s_infinite_path, GPoint(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2));
  s_path_angle = 70;
  test_graphics_context_init(&ctx, fb);
  graphics_context_set_antialiased(&ctx, true);
  prv_filled_update_proc(NULL, &ctx);
  cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "gpath_filled_infinite_70_aa.8bit.pbi"));

  // House path - two edge cases for tipping points of the path
  prv_reset();
  s_current_path = s_house_path;
  gpath_move_to(s_house_path, GPoint(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2));
  s_path_angle = 20;
  test_graphics_context_init(&ctx, fb);
  graphics_context_set_antialiased(&ctx, true);
  prv_filled_update_proc(NULL, &ctx);
  cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "gpath_filled_house_20_aa.8bit.pbi"));

  // This case demonstrates tipping point that is also the starting point of the path
  prv_reset();
  s_current_path = s_house_path;
  gpath_move_to(s_house_path, GPoint(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2));
  s_path_angle = 105;
  test_graphics_context_init(&ctx, fb);
  graphics_context_set_antialiased(&ctx, true);
  prv_filled_update_proc(NULL, &ctx);
  cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap, "gpath_filled_house_105_aa.8bit.pbi"));
  
  // Safety
  s_path_angle = 0;
}

void test_graphics_gpath_8bit__clipping_aa(void) {
  // NOTE: This test verifies correct clipping of anti-aliased edges on gpaths, therefore
  //         it works only on 8bit
  GContext ctx;
  Layer layer;

  prv_reset();
  test_graphics_context_init(&ctx, fb);
  s_current_path = s_aa_clipping_path;
  s_path_angle = 17;
  ctx.draw_state.clip_box = GRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT / 2);
  graphics_context_set_antialiased(&ctx, true);
  prv_filled_update_proc(NULL, &ctx);
  // NOTE: Expected result of this test is to have an antialiased stripe go through the screen,
  //         where antialiased edges are being nicely cut off on top and bottom of the stripe
  //         (antialiased gradient would dive into the stripe near screen edges), also top
  //         left corner is intentinally ending just before screen cuts it out to verify that
  //         fractional anti-aliasing is not bleeding into row before (would occur as pixels on
  //         right side of the screen)
  cl_check(gbitmap_pbi_eq(&ctx.dest_bitmap,
                          "gpath_clipping_aa.8bit.pbi"));

  // Safety
  s_path_angle = 0;
}
