/*
 * 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 "clar.h"
#include "test_jerry_port_common.h"

#define DO_NOT_STUB_LEGACY2 1
#include "test_rocky_common.h"

#include "applib/graphics/gtypes.h"
#include "applib/rockyjs/api/rocky_api.h"
#include "applib/rockyjs/api/rocky_api_global.h"
#include "applib/rockyjs/api/rocky_api_graphics.h"
#include "applib/rockyjs/api/rocky_api_graphics_text.h"
#include "applib/rockyjs/pbl_jerry_port.h"
#include "util/trig.h"
#include "applib/graphics/framebuffer.h"

#include "../graphics/util.h"


// Standard
#include "string.h"

// Fakes
#include "fake_app_timer.h"
#include "fake_time.h"

// Stubs
#include "stubs_app_manager.h"
#include "stubs_app_state.h"
#include "stubs_logging.h"
#include "stubs_passert.h"
#include "stubs_pbl_malloc.h"
#include "stubs_resources.h"
#include "stubs_sleep.h"
#include "stubs_serial.h"
#include "stubs_syscalls.h"
#include "stubs_sys_exit.h"

size_t heap_bytes_free(void) {
  return 123456;
}

bool gbitmap_init_with_png_data(GBitmap *bitmap, const uint8_t *data, size_t data_size) {
  return false;
}

bool gbitmap_png_data_is_png(const uint8_t *data, size_t data_size) {
  return false;
}

void layer_get_unobstructed_bounds(const Layer *layer, GRect *bounds_out) {
  *bounds_out = layer->bounds;
}

void layer_mark_dirty(Layer *layer) {}

static Window s_app_window_stack_get_top_window;
Window *app_window_stack_get_top_window() {
  return &s_app_window_stack_get_top_window;
}

// no text rendering in this test
void rocky_api_graphics_text_init(void) {}
void rocky_api_graphics_text_deinit(void) {}
void rocky_api_graphics_text_add_canvas_methods(jerry_value_t obj) {}
void rocky_api_graphics_text_reset_state(void) {}


//GBitmap s_bitmap;
//uint8_t s_bitmap_data[DISPLAY_FRAMEBUFFER_BYTES];
GContext s_context;
FrameBuffer *s_framebuffer;
GBitmap *s_pixels;

static void prv_init_gcontext(GSize size) {
  graphics_context_init(&s_context, s_framebuffer, GContextInitializationMode_App);
  framebuffer_clear(s_framebuffer);
  if (s_pixels) {
    gbitmap_destroy(s_pixels);
  }
  s_pixels = gbitmap_create_blank(size, GBITMAP_NATIVE_FORMAT);
  memset(s_pixels->addr, 0xff, size.h * s_pixels->row_size_bytes);
  s_context.dest_bitmap = *s_pixels;
  s_context.draw_state.clip_box = (GRect){.size = size};
  s_context.draw_state.drawing_box = s_context.draw_state.clip_box;
  s_app_state_get_graphics_context = &s_context;
}

void test_rocky_api_graphics_rendering__initialize(void) {
  fake_app_timer_init();
  rocky_runtime_context_init();
  jerry_init(JERRY_INIT_EMPTY);

  s_framebuffer = malloc(sizeof(FrameBuffer));
  framebuffer_init(s_framebuffer, &(GSize) { DISP_COLS, DISP_ROWS });
  s_app_window_stack_get_top_window = (Window){};

  prv_init_gcontext((GSize) { DISP_COLS, DISP_ROWS });
  s_app_event_loop_callback = NULL;
}

void test_rocky_api_graphics_rendering__cleanup(void) {
  fake_app_timer_deinit();

  // some tests deinitialize the engine, avoid double de-init
  if (app_state_get_rocky_runtime_context() != NULL) {
    jerry_cleanup();
    rocky_runtime_context_deinit();
  }
  gbitmap_destroy(s_pixels);
  s_pixels = NULL;
  free(s_framebuffer);
}

static const RockyGlobalAPI *s_graphics_api[] = {
  &GRAPHIC_APIS,
  NULL,
};

jerry_value_t prv_create_canvas_context_2d_for_layer(Layer *layer);

static const jerry_value_t prv_global_init_and_set_ctx(void) {
  rocky_global_init(s_graphics_api);

  // make this easily testable by putting it int JS context as global
  Layer l = {.bounds = GRect(0, 0, 144, 168)};
  const jerry_value_t ctx = prv_create_canvas_context_2d_for_layer(&l);
  jerry_set_object_field(jerry_get_global_object(), "ctx", ctx);

  return ctx;
}


void test_rocky_api_graphics_rendering__lines(void) {
  prv_global_init_and_set_ctx();

  // taken from http://fiddle.jshell.net/9298zub9/2/
  EXECUTE_SCRIPT(
    "var t1 = 10;\n"
    "var b1 = 20.5;\n"
    "var t2 = 30.5;\n"
    "var b2 = 40;\n"
    "    \n"
    "for (var i = 1; i <= 5; i++) {\n"
    "  ctx.beginPath();\n"
    "  var x1 = 20 * i;\n"
    "  var x2 = x1 + 10.5;  \n"
    "  ctx.moveTo(x1, t1);\n"
    "  ctx.lineTo(x1, b1);\n"
    "  ctx.moveTo(x2, t1);\n"
    "  ctx.lineTo(x2, b1);\n"
    "\n"
    "  ctx.moveTo(x1, t2);\n"
    "  ctx.lineTo(x1, b2);\n"
    "  ctx.moveTo(x2, t2);\n"
    "  ctx.lineTo(x2, b2);\n"
    "\n"
    "  ctx.lineWidth = i;\n"
    "  ctx.stroke();\n"
    "}\n"
    "for (var i = 1; i <= 5; i++) {\n"
      "  ctx.beginPath();\n"
      "  var y1 = 40 + i * 20;\n"
      "  var y2 = y1 + 10.5;\n"
      "  ctx.moveTo(t1, y1);\n"
      "  ctx.lineTo(b1, y1);\n"
      "  ctx.moveTo(t1, y2);\n"
      "  ctx.lineTo(b1, y2);\n"
      "  \n"
      "  ctx.moveTo(t2, y1);\n"
      "  ctx.lineTo(b2, y1);\n"
      "  ctx.moveTo(t2, y2);\n"
      "  ctx.lineTo(b2, y2);\n"
      "\n"
      "  ctx.lineWidth = i;\n"
      "  ctx.stroke();\n"
      "}\n"
      "for (var i = 1; i <= 5; i++) {\n"
      "  ctx.beginPath();\n"
      "  var xx = 50;\n"
      "  var yy = 50;\n"
      "  var d = 15 * i;\n"
      "  ctx.moveTo(xx, yy + d);\n"
      "  ctx.lineTo(xx + d, yy);\n"
      "\n"
      "  ctx.lineWidth = i;\n"
      "  ctx.stroke();\n"
      "}"
  );

  const bool eq_result =
    gbitmap_pbi_eq(&s_context.dest_bitmap, TEST_NAMED_PBI_FILE("rocky_rendering_lines"));
  cl_check(eq_result);
}

void test_rocky_api_graphics_rendering__rect(void) {
  prv_init_gcontext(GSize(500, 150));
  prv_global_init_and_set_ctx();

  // taken from http://fiddle.jshell.net/a5gjzb7c/6/
  EXECUTE_SCRIPT(
    "function render(x, y, f) {\n"
      "  f(x + 10,   y + 10, 10, 10);\n"
      "  f(x + 30.2, y + 10, 10, 10.2);\n"
      "  f(x + 50.5, y + 10, 10, 10);\n"
      "  f(x + 70.7, y + 10, 10.5, 10.8);\n"
      "  f(x + 10,   y + 30.5, 10, 10);\n"
      "  f(x + 30.2, y + 30.5, 10, 10.2);\n"
      "  f(x + 50.5, y + 30.5, 10, 10);\n"
      "  f(x + 70.7, y + 30.5, 10.5, 10.8);\n"
      "  \n"
      "  f(x + 90,  y + 10, 0, 0);\n"
      "  f(x + 110, y + 10, 0.5, 0.5);\n"
      "  f(x + 90,  y + 30, -2, -2);\n"
      "  f(x + 110, y + 30, -5.5, -6);\n"
      "}"
      "\n"
      "for (var i = 0; i <= 3; i++) {\n"
      "  ctx.lineWidth = i;\n"
      "  var x = 120 * i;\n"
      "  render(x, 0, ctx[i == 0 ? 'fillRect' : 'strokeRect'].bind(ctx));\n"
      "  render(x, 50, function(x,y,w,h) {\n"
      "    ctx.beginPath();\n"
      "    ctx.rect(x, y, w, h);\n"
      "    ctx[i == 0? 'fill' : 'stroke']();  \n"
      "  });\n"
      "  render(x, 100, function r(x, y, w, h) {\n"
      "    ctx.beginPath();\n"
      "    ctx.moveTo(x, y);\n"
      "    ctx.lineTo(x + w, y);\n"
      "    ctx.lineTo(x + w, y + h);\n"
      "    ctx.lineTo(x, y + h);\n"
      "    ctx.lineTo(x, y);\n"
      "    ctx[i == 0? 'fill' : 'stroke']();  \n"
      "  });\n"
      "}"
  );

  const bool eq_result =
    gbitmap_pbi_eq(&s_context.dest_bitmap, TEST_NAMED_PBI_FILE("rocky_rendering_rect"));
  cl_check(eq_result);
}


void test_rocky_api_graphics_rendering__arc(void) {
  prv_init_gcontext(GSize(500, 300));
  prv_global_init_and_set_ctx();

  // http://fiddle.jshell.net/uopr1ez2/2/
  EXECUTE_SCRIPT(
    "var xx = 200;\n"
    "\n"
    "function f(x, y, r, a1, a2) {\n"
    "  ctx.beginPath();\n"
    "  ctx.arc(x, y, r, a1, a2, false);\n"
    "  ctx.stroke();\n"
    "\n"
    "  ctx.rockyFillRadial(x + xx, y, 0, r, a1, a2);\n"
    "}\n"
    "\n"
    "function g(x, y, a1, a2) {\n"
    "  f(x, y, 5, a1, a2);\n"
    "  f(x, y, 15.5, a1, a2);\n"
    "  f(x, y, 25.2, a1, a2);\n"
    "  f(x, y, 34.8, a1, a2);\n"
    "}\n"
    "\n"
    "function h(x, y, a1, a2) {\n"
    "  for (var i = 0; i < 4; i++) {\n"
    "    ctx.lineWidth = i + 1;\n"
    "    g(x, y + 40 * i, a1, a2);\n"
    "  }\n"
    "}\n"
    "\n"
    "h(2, 2, 0, 0.5 * Math.PI);\n"
    "h(50.5, 2.5, 0, 0.5 * Math.PI);\n"
    "h(100.2, 2.2, 0, 0.5 * Math.PI);\n"
    "h(150.8, 2.8, 0, 0.5 * Math.PI);\n"
    "\n"
    "ctx.lineWidth = 1;\n"
    "f(20, 200, 10, 0, 2 * Math.PI);\n"
    "f(60.5, 200, 10, 0, 2 * Math.PI);\n"
    "f(100.5, 200.5, 10, 0, 2 * Math.PI);\n"
    "f(140, 200.5, 10, 0, 2 * Math.PI);\n"
    "\n"
    "f(20, 240, 11, 0, 2 * Math.PI);\n"
    "f(60.5, 240, 11, 0, 2 * Math.PI);\n"
    "f(100.5, 240.5, 11, 0, 2 * Math.PI);\n"
    "f(140, 240.5, 11, 0, 2 * Math.PI);\n"
    "\n"
    "f(20, 280, 11, 0, -0.5 * Math.PI);"
  );

  const bool eq_result =
    gbitmap_pbi_eq(&s_context.dest_bitmap, TEST_NAMED_PBI_FILE("rocky_rendering_arc"));
  cl_check(eq_result);
}