/*
 * Copyright 2024 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "applib/graphics/graphics_circle.h"
#include "util/trig.h"

#include "clar.h"
#include "pebble_asserts.h"

#include <stdio.h>

// stubs
#include "stubs_process_manager.h"
#include "stubs_passert.h"
#include "stubs_logging.h"
#include "stubs_app_state.h"
#include "stubs_heap.h"

GBitmap* graphics_capture_frame_buffer(GContext* ctx) {
  return NULL;
}
bool graphics_release_frame_buffer(GContext* ctx, GBitmap* buffer) {
  return true;
}

void graphics_draw_pixel(){}
void graphics_fill_rect(GContext* ctx, const GRect *rect) {}
void graphics_private_draw_horizontal_line(){}
void graphics_private_draw_vertical_line(){}
void graphics_private_plot_pixel(){}
void graphics_private_set_pixel(){}

/////////////////////////////


static GPointPrecise s_center;
static Fixed_S16_3 s_radius;
static Fixed_S16_3 s_radius_inner;
static Fixed_S16_3 s_radius_outer;
static int32_t s_angle_start;
static int32_t s_angle_end;

////////////////////////////

void test_graphics_circle__initialize(void) {
  s_center = (GPointPrecise){};
  s_radius = (Fixed_S16_3){};
  s_radius_inner = (Fixed_S16_3){};
  s_radius_outer = (Fixed_S16_3){};
}

void test_graphics_circle__gpoint_from_polar_returns_zero_for_null(void) {
  const uint16_t radius = 5;
  const GPoint result = gpoint_from_polar_internal(NULL, radius, 0);
  cl_assert_equal_gpoint(result, GPointZero);
}

void test_graphics_circle__gpoint_from_polar_returns_correct_points(void) {
  const uint16_t radius = 5;
  GPoint result;

  const GPoint origin_center = GPointZero;
  // 90 degrees should be (5, 0)
  result = gpoint_from_polar_internal(&origin_center, radius, (TRIG_MAX_ANGLE / 4));
  cl_assert_equal_gpoint(result, GPoint(5, 0));
  // 270 (90 * 3) degrees should be (-5, 0)
  result = gpoint_from_polar_internal(&origin_center, radius, (TRIG_MAX_ANGLE * 3 / 4));
  cl_assert_equal_gpoint(result, GPoint(-5, 0));

  const GPoint offset_center = GPoint(1, 1);
  // 90 degrees should be (6, 1)
  result = gpoint_from_polar_internal(&offset_center, radius, (TRIG_MAX_ANGLE / 4));
  cl_assert_equal_gpoint(result, GPoint(6, 1));
  // 270 (90 * 3) degrees should be (-4, 1)
  result = gpoint_from_polar_internal(&offset_center, radius, (TRIG_MAX_ANGLE * 3 / 4));
  cl_assert_equal_gpoint(result, GPoint(-4, 1));
}

void test_graphics_circle__gpoint_from_polar_normalizes_input_angles(void) {
  const uint16_t radius = 5;
  const GPoint center = GPointZero;

  GPoint result;

  // -180 degrees should be (0, 5)
  result = gpoint_from_polar_internal(&center, radius, -(TRIG_MAX_ANGLE / 2));
  cl_assert_equal_gpoint(result, GPoint(0, 5));

  // -90 degrees should be (-5, 0)
  result = gpoint_from_polar_internal(&center, radius, -(TRIG_MAX_ANGLE / 4));
  cl_assert_equal_gpoint(result, GPoint(-5, 0));

  // -450 degrees (-90 * 5 -> -90) should be (-5, 0)
  result = gpoint_from_polar_internal(&center, radius, -(TRIG_MAX_ANGLE * 5 / 4));
  cl_assert_equal_gpoint(result, GPoint(-5, 0));

  // 450 degrees (90 * 5 -> 90) should be (0, 5)
  result = gpoint_from_polar_internal(&center, radius, (TRIG_MAX_ANGLE * 5 / 4));
  cl_assert_equal_gpoint(result, GPoint(5, 0));

  // 360 degrees (-> 0 degrees) should be (0, -5)
  result = gpoint_from_polar_internal(&center, radius, TRIG_MAX_ANGLE);
  cl_assert_equal_gpoint(result, GPoint(0, -5));

  // -360 degrees (-> 0 degrees) should be (0, -5)
  result = gpoint_from_polar_internal(&center, radius, -TRIG_MAX_ANGLE);
  cl_assert_equal_gpoint(result, GPoint(0, -5));
}

void test_graphics_circle__gpoint_from_polar_correct_scale(void) {
  // edge cases are covered above, this test only verifies that
  // the internal implementation correctly scales
  GPoint result = gpoint_from_polar(GRect(0, 0, 10, 10), GOvalScaleModeFillCircle, 0);
  cl_assert_equal_gpoint(result, GPoint(4, 0));
}

void test_graphics_circle__grect_centered_from_polar(void) {
  GOvalScaleMode mode = GOvalScaleModeFillCircle;

  // Basic container rect with GPointZero origin
  const GRect container_rect1 = GRect(0, 0, 10, 10);
  const GRect resulting_rect1 = grect_centered_from_polar(container_rect1, mode, 0, GSize(3, 5));
  cl_assert_equal_grect(resulting_rect1, GRect(3, -2, 3, 5));

  // Container rect is offset from GPointZero
  const GRect container_rect2 = GRect(2, 2, 4, 4);
  const GRect resulting_rect2 = grect_centered_from_polar(container_rect2, mode, 0, GSize(2, 4));
  cl_assert_equal_grect(resulting_rect2, GRect(3, 0, 2, 4));

  // Odd-length width and height for container rect, 180 degree angle
  const GRect container_rect3 = GRect(2, 2, 5, 5);
  const GRect resulting_rect3 = grect_centered_from_polar(container_rect3, mode,
                                                          DEG_TO_TRIGANGLE(180), GSize(2, 4));
  cl_assert_equal_grect(resulting_rect3, GRect(3, 4, 2, 4));
}

void test_graphics_circle__grect_centered_internal(void) {
  GPointPrecise p1 = GPointPrecise(0, 0);
  // GRectZero + standardize
  cl_assert_equal_grect(GRect(0, 0, 0, 0), grect_centered_internal(&p1, GSize(0, 0)));
  cl_assert_equal_grect(GRect(0, -1, 1, 2), grect_centered_internal(&p1, GSize(-1, -2)));

  // handles fixed point
  cl_assert_equal_grect(GRect(-1, -1, 2, 2), grect_centered_internal(&p1, GSize(2, 2)));
  p1.x.raw_value += FIXED_S16_3_HALF.raw_value;
  cl_assert_equal_grect(GRect(0, -1, 2, 2), grect_centered_internal(&p1, GSize(2, 2)));

  // Repeat for an offset center point
  GPointPrecise p2 = GPointPreciseFromGPoint(GPoint(5, 5));

  // GRectZero + standardize
  cl_assert_equal_grect(GRect(5, 5, 0, 0), grect_centered_internal(&p2, GSize(0, 0)));
  cl_assert_equal_grect(GRect(5, 4, 1, 2), grect_centered_internal(&p2, GSize(-1, -2)));

  // handles fixed point
  cl_assert_equal_grect(GRect(4, 4, 2, 2), grect_centered_internal(&p2, GSize(2, 2)));
  p2.x.raw_value += FIXED_S16_3_HALF.raw_value;
  cl_assert_equal_grect(GRect(5, 4, 2, 2), grect_centered_internal(&p2, GSize(2, 2)));

  // Repeat for a positive offset center point with 0.5 fractions
  GPointPrecise p3 = GPointPreciseFromGPoint(GPoint(5, 5));
  p3.x.raw_value += FIXED_S16_3_HALF.raw_value;
  p3.y.raw_value += FIXED_S16_3_HALF.raw_value;

  // GRectZero + standardize
  cl_assert_equal_grect(GRect(6, 6, 0, 0), grect_centered_internal(&p3, GSize(0, 0)));
  cl_assert_equal_grect(GRect(5, 5, 1, 2), grect_centered_internal(&p3, GSize(-1, -2)));

  // handles fixed point
  cl_assert_equal_grect(GRect(5, 5, 2, 2), grect_centered_internal(&p3, GSize(2, 2)));
  p3.x.raw_value += FIXED_S16_3_HALF.raw_value;
  cl_assert_equal_grect(GRect(5, 5, 2, 2), grect_centered_internal(&p3, GSize(2, 2)));

  // Repeat for a negative offset center point with 0.5 fractions
  GPointPrecise p4 = GPointPreciseFromGPoint(GPoint(-5, -5));
  p4.x.raw_value -= FIXED_S16_3_HALF.raw_value;
  p4.y.raw_value -= FIXED_S16_3_HALF.raw_value;

  // GRectZero + standardize
  cl_assert_equal_grect(GRect(-5, -5, 0, 0), grect_centered_internal(&p4, GSize(0, 0)));
  cl_assert_equal_grect(GRect(-6, -6, 1, 2), grect_centered_internal(&p4, GSize(-1, -2)));

  // handles fixed point
  cl_assert_equal_grect(GRect(-6, -6, 2, 2), grect_centered_internal(&p4, GSize(2, 2)));
  p4.x.raw_value += FIXED_S16_3_HALF.raw_value;
  cl_assert_equal_grect(GRect(-6, -6, 2, 2), grect_centered_internal(&p4, GSize(2, 2)));
}

#define cl_assert_fixedS16_3(v, f) \
  cl_assert_equal_i((v).raw_value, (int)((f) * FIXED_S16_3_ONE.raw_value))

#define cl_assert_gpoint_precise(p, px, py) \
  do { \
    cl_assert_fixedS16_3(p.x, px); \
    cl_assert_fixedS16_3(p.y, py); \
  } while(0)


void test_graphics_circle__grect_polar_calc_values_handles_null(void) {
  GPointPrecise center = {};
  Fixed_S16_3 radius = {};
  const GRect r = GRect(0, 0, 3, 5);
  const GOvalScaleMode mode = GOvalScaleModeFitCircle;

  grect_polar_calc_values(&r, mode, NULL, NULL);
  grect_polar_calc_values(&r, mode, &center, NULL);
  grect_polar_calc_values(&r, mode, NULL, &radius);

  cl_assert_gpoint_precise(center, 1, 2);
  cl_assert_fixedS16_3(radius, 1);

  grect_polar_calc_values(NULL, mode, &center, &radius);
  cl_assert_gpoint_precise(center, 1, 2);
  cl_assert_fixedS16_3(radius, 1);
}

void test_graphics_circle__grect_polar_calc_values_edge_cases(void) {
  GPointPrecise center = {};
  Fixed_S16_3 radius = {};
  const GOvalScaleMode mode = GOvalScaleModeFillCircle;

  GRect r = GRect(0, 5, 0, 0);
  grect_polar_calc_values(&r, mode, &center, &radius);
  cl_assert_gpoint_precise(center, 0, 5);
  cl_assert_fixedS16_3(radius, 0);

  // 1 pixel width means radius of 0
  r = GRect(-1, -5, 1, 1);
  grect_polar_calc_values(&r, mode, &center, &radius);
  cl_assert_gpoint_precise(center, -1, -5);
  cl_assert_fixedS16_3(radius, 0);

  // 2 pixel width means: center is 1px from side, 0.5 pixels to center of outer pixels
  r = GRect(-1, -5, 2, 2);
  grect_polar_calc_values(&r, mode, &center, &radius);
  cl_assert_gpoint_precise(center, -0.5, -4.5);
  cl_assert_fixedS16_3(radius, 0.5);
}

void test_graphics_circle__grect_polar_calc_values_standardizes(void) {
  GPointPrecise center = {};
  Fixed_S16_3 radius = {};
  const GOvalScaleMode mode = GOvalScaleModeFitCircle;

  GRect r = GRect(0, 0, 10, 20);
  grect_polar_calc_values(&r, mode, &center, &radius);
  cl_assert_gpoint_precise(center, 4.5, 9.5);
  cl_assert_fixedS16_3(radius, 4.5);

  r = GRect(0, 0, -10, -20);
  grect_polar_calc_values(&r, mode, &center, &radius);
  cl_assert_gpoint_precise(center, -5.5, -10.5);
  cl_assert_fixedS16_3(radius, 4.5);
}

void test_graphics_circle__grect_polar_calc_values_square(void) {
  GPointPrecise center = {};
  Fixed_S16_3 radius = {};
  GRect r = GRect(0, 0, 5, 5);
  GOvalScaleMode mode = GOvalScaleModeFitCircle; // irrelevant as we deal with squares

  grect_polar_calc_values(&r, mode, &center, &radius);
  cl_assert_gpoint_precise(center, 2, 2);
  cl_assert_fixedS16_3(radius, 2);

  r = GRect(0, 0, 6, 6);
  grect_polar_calc_values(&r, mode, &center, &radius);
  cl_assert_gpoint_precise(center, 2.5, 2.5);
  cl_assert_fixedS16_3(radius, 2.5);

  r = GRect(0, 0, 10, 10);
  grect_polar_calc_values(&r, mode, &center, &radius);
  cl_assert_gpoint_precise(center, 4.5, 4.5);
  cl_assert_fixedS16_3(radius, 4.5);

  r = GRect(1, 1, 9, 9);
  grect_polar_calc_values(&r, mode, &center, &radius);
  cl_assert_gpoint_precise(center, 5, 5);
  cl_assert_fixedS16_3(radius, 4);

  r = GRect(2, 2, 8, 8);
  grect_polar_calc_values(&r, mode, &center, &radius);
  cl_assert_gpoint_precise(center, 5.5, 5.5);
  cl_assert_fixedS16_3(radius, 3.5);
}

void test_graphics_circle__grect_polar_calc_values_mode(void) {
  GPointPrecise center = {};
  Fixed_S16_3 radius = {};
  GRect r = GRect(0, 0, 144, 168);

  grect_polar_calc_values(&r, GOvalScaleModeFitCircle, &center, &radius);
  cl_assert_gpoint_precise(center, 144 / 2 - 0.5, 168 / 2 - 0.5);
  cl_assert_fixedS16_3(radius, 144 / 2 - 0.5);

  grect_polar_calc_values(&r, GOvalScaleModeFillCircle, &center, &radius);
  cl_assert_gpoint_precise(center, 144 / 2 - 0.5, 168 / 2 - 0.5);
  cl_assert_fixedS16_3(radius, 168 / 2 - 0.5);
}

void graphics_draw_arc_precise_internal(GContext *ctx, GPointPrecise center,
                                        Fixed_S16_3 radius,
                                        int32_t angle_start, int32_t angle_end) {
  s_center = center;
  s_radius = radius;
}

void test_graphics_circle__draw_arc(void) {
  cl_assert_gpoint_precise(s_center, 0, 0);
  cl_assert_fixedS16_3(s_radius, 0);

  graphics_draw_arc(NULL, GRect(0, 0, 10, 12), GOvalScaleModeFitCircle, 0, 0);
  cl_assert_gpoint_precise(s_center, 4.5, 5.5);
  cl_assert_fixedS16_3(s_radius, 4.5);
}

void test_graphics_circle__fill_oval(void) {
  cl_assert_gpoint_precise(s_center, 0, 0);
  cl_assert_fixedS16_3(s_radius_inner, 0);
  cl_assert_fixedS16_3(s_radius_outer, 0);
  cl_assert_equal_i(s_angle_start, 0);
  cl_assert_equal_i(s_angle_end, 0);

  graphics_fill_oval(NULL, GRect(0, 0, 10, 12), GOvalScaleModeFitCircle);

  cl_assert_gpoint_precise(s_center, 4.5, 5.5);
  cl_assert_fixedS16_3(s_radius_outer, 4.5);
  cl_assert(s_radius_inner.integer <= 0);
  cl_assert_equal_i(s_angle_start, 0);
  cl_assert_equal_i(s_angle_end, TRIG_MAX_ANGLE);

  graphics_fill_oval(NULL, GRect(10, 12, -10, -12), GOvalScaleModeFitCircle);

  cl_assert_gpoint_precise(s_center, 4.5, 5.5);
  cl_assert_fixedS16_3(s_radius_outer, 4.5);
  cl_assert(s_radius_inner.integer <= 0);
  cl_assert_equal_i(s_angle_start, 0);
  cl_assert_equal_i(s_angle_end, TRIG_MAX_ANGLE);

  graphics_fill_oval(NULL, GRect(0, 0, 0, 0), GOvalScaleModeFillCircle);
  cl_assert_gpoint_precise(s_center, 0.0, 0.0);
  cl_assert_fixedS16_3(s_radius_outer, 0.0);
  cl_assert(s_radius_inner.integer <= 0);
  cl_assert_equal_i(s_angle_start, 0);
  cl_assert_equal_i(s_angle_end, TRIG_MAX_ANGLE);
}

void graphics_fill_radial_precise_internal(GContext *ctx, GPointPrecise center,
                                           Fixed_S16_3 radius_inner, Fixed_S16_3 radius_outer,
                                           int32_t angle_start, int32_t angle_end) {
  s_center = center;
  s_radius_inner = radius_inner;
  s_radius_outer = radius_outer;
  s_angle_start = angle_start;
  s_angle_end = angle_end;
}


void test_graphics_circle__fill_radial(void) {
  GContext *ctx = (GContext *)&ctx;
  cl_assert_gpoint_precise(s_center, 0, 0);
  cl_assert_fixedS16_3(s_radius_inner, 0);
  cl_assert_fixedS16_3(s_radius_outer, 0);

  graphics_fill_radial(NULL, GRect(0, 0, 10, 12), GOvalScaleModeFitCircle, 3, 0, 0);
  cl_assert_gpoint_precise(s_center, 4.5, 5.5);
  cl_assert_fixedS16_3(s_radius_outer, 4.5);
  cl_assert_fixedS16_3(s_radius_inner, 1.5);
}

void test_graphics_circle__DEG_TO_TRIGANGLE(void) {
  cl_assert_equal_i(DEG_TO_TRIGANGLE(720), TRIG_MAX_ANGLE * 2);
  cl_assert_equal_i(DEG_TO_TRIGANGLE(-720), -TRIG_MAX_ANGLE * 2);

  cl_assert_equal_i(DEG_TO_TRIGANGLE(360), TRIG_MAX_ANGLE);
  cl_assert_equal_i(DEG_TO_TRIGANGLE(-360), -TRIG_MAX_ANGLE);

  cl_assert_equal_i(DEG_TO_TRIGANGLE(180), TRIG_PI);
  cl_assert_equal_i(DEG_TO_TRIGANGLE(-180), -TRIG_PI);

  cl_assert_equal_i(DEG_TO_TRIGANGLE(90), TRIG_PI / 2);
  cl_assert_equal_i(DEG_TO_TRIGANGLE(-90), -TRIG_PI / 2);

  cl_assert_equal_i(DEG_TO_TRIGANGLE(0), 0);
}

void test_graphics_circle__TRIGANGLE_TO_DEG(void) {
  cl_assert_equal_i(TRIGANGLE_TO_DEG(TRIG_MAX_ANGLE * 2), 720);
  cl_assert_equal_i(TRIGANGLE_TO_DEG(-TRIG_MAX_ANGLE * 2), -720);

  cl_assert_equal_i(TRIGANGLE_TO_DEG(TRIG_MAX_ANGLE), 360);
  cl_assert_equal_i(TRIGANGLE_TO_DEG(-TRIG_MAX_ANGLE), -360);

  cl_assert_equal_i(TRIGANGLE_TO_DEG(TRIG_PI / 2), 90);
  cl_assert_equal_i(TRIGANGLE_TO_DEG(-TRIG_PI / 2), -90);

  cl_assert_equal_i(TRIGANGLE_TO_DEG(TRIG_MAX_ANGLE / 2), 180);
  cl_assert_equal_i(TRIGANGLE_TO_DEG(-TRIG_MAX_ANGLE / 2), -180);

  cl_assert_equal_i(TRIGANGLE_TO_DEG(TRIG_PI), 180);
  cl_assert_equal_i(TRIGANGLE_TO_DEG(-TRIG_PI), -180);

  cl_assert_equal_i(TRIGANGLE_TO_DEG(0), 0);
}
