/*
 * 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.
 */

#pragma once

#include "applib/graphics/gtypes.h"
#include "applib/graphics/gcontext.h"
#include "applib/graphics/graphics_circle.h"
#include "applib/rockyjs/api/rocky_api_util.h"
#include "applib/tick_timer_service.h"
#include "applib/ui/layer.h"

#include "kernel/events.h"
#include "syscall/syscall.h"

#include <clar.h>
#include <util/attributes.h>

#include <string.h>

#define ASSERT_JS_GLOBAL_EQUALS_B(name, value) \
  cl_assert_equal_i(prv_js_global_get_boolean(name), value);

#define ASSERT_JS_GLOBAL_EQUALS_I(name, value) \
  cl_assert_equal_i(prv_js_global_get_double(name), value);

#define ASSERT_JS_GLOBAL_EQUALS_D(name, value) \
  cl_assert_equal_d(prv_js_global_get_double(name), value);

#define ASSERT_JS_GLOBAL_EQUALS_S(name, value) \
  do { \
    char str_buffer[1024] = {}; \
    prv_js_global_get_string(name, str_buffer, sizeof(str_buffer)); \
    cl_assert_equal_s(str_buffer, value); \
  } while(0)

#define JS_GLOBAL_GET_VALUE(name) \
  prv_js_global_get_value(name)

#define ASSERT_JS_ERROR(error_value, expected_error_string) \
  do { \
    const bool expects_error = (expected_error_string) != NULL; \
    const bool is_error = jerry_value_has_error_flag(error_value); \
    if (is_error) { \
      if (expects_error) { \
        jerry_char_t buffer[100] = {0}; \
        jerry_object_to_string_to_utf8_char_buffer(error_value, buffer, sizeof(buffer)); \
        cl_assert_equal_s((char *)(expected_error_string), (char *)buffer); \
      } else {\
        rocky_log_exception("ASSERT_JS_ERROR", error_value); \
        cl_fail("Error value while no error was expected!"); \
      } \
    } else { \
      if (expects_error) { \
        cl_fail("expected error during JS execution did not occur"); \
      } \
    } \
  } while(0)

#define EXECUTE_SCRIPT_EXPECT_UNDEFINED(script) \
  do { \
    JS_VAR rv = jerry_eval((jerry_char_t *)script, \
                            strlen(script), \
                            false /* is_strict */); \
    cl_assert_equal_b(true, jerry_value_is_undefined(rv)); \
  } while(0)


#define EXECUTE_SCRIPT_EXPECT_ERROR(script, expected_error) \
  do { \
    JS_VAR rv = jerry_eval((jerry_char_t *)script, \
                           strlen(script), \
                           false /* is_strict */); \
    ASSERT_JS_ERROR(rv, expected_error); \
  } while(0)

#define EXECUTE_SCRIPT(script) EXECUTE_SCRIPT_EXPECT_ERROR((script), NULL)

#define EXECUTE_SCRIPT_AND_ASSERT_RV_EQUALS_S(script, expected_c_string) \
  do { \
      JS_VAR rv = jerry_eval((jerry_char_t *)script, \
                             strlen(script), false /* is_strict */); \
      ASSERT_JS_ERROR(rv, NULL); \
      JS_VAR rv_string = jerry_value_to_string(rv); \
      jerry_size_t sz = jerry_get_utf8_string_size(rv_string); \
      cl_assert(sz); \
      jerry_char_t *buffer = malloc(sz + 1); \
      memset(buffer, 0, sz + 1); \
      cl_assert(buffer); \
      cl_assert_equal_i(sz, jerry_string_to_utf8_char_buffer(rv_string, buffer, sz)); \
      cl_assert_equal_s((char *)buffer, expected_c_string); \
      free(buffer); \
    } while(0)

#ifndef DO_NOT_STUB_LEGACY2

UNUSED bool process_manager_compiled_with_legacy2_sdk(void) {
  return false;
}

#endif

UNUSED static jerry_value_t prv_js_global_get_value(char *name) {
  JS_VAR global_obj = jerry_get_global_object();
  cl_assert_equal_b(jerry_value_is_undefined(global_obj), false);

  JS_VAR val = jerry_get_object_field(global_obj, name);
  cl_assert_equal_b(jerry_value_is_undefined(val), false);
  return jerry_acquire_value(val);
}

UNUSED static bool prv_js_global_get_boolean(char *name) {
  JS_VAR val = prv_js_global_get_value(name);
  cl_assert(jerry_value_is_boolean(val));
  double rv = jerry_get_boolean_value(val);
  return rv;
}

UNUSED static double prv_js_global_get_double(char *name) {
  JS_VAR val = prv_js_global_get_value(name);
  cl_assert(jerry_value_is_number(val));
  double rv = jerry_get_number_value(val);
  return rv;
}

UNUSED static void prv_js_global_get_string(char *name, char *buffer, size_t buffer_size) {
  JS_VAR val = prv_js_global_get_value(name);
  cl_assert(jerry_value_is_string(val));
  ssize_t num_bytes = jerry_string_to_char_buffer(val, (jerry_char_t *)buffer, buffer_size);
  cl_assert(num_bytes <= buffer_size);
}

void (*s_app_event_loop_callback)(void);

void app_event_loop_common(void) {
  if (s_app_event_loop_callback) {
    s_app_event_loop_callback();
  }
}

#define cl_assert_equal_point(a, b) \
  do { \
    const GPoint __pt_a = (a); \
    const GPoint __pt_b = (b); \
    cl_assert_equal_i(__pt_a.x, __pt_b.x); \
    cl_assert_equal_i(__pt_a.y, __pt_b.y); \
  } while(0)

#define cl_assert_equal_point_precise(a, b) \
  do { \
    const GPointPrecise __pt_a = (a); \
    const GPointPrecise __pt_b = (b); \
    cl_assert_equal_i(__pt_a.x.raw_value, __pt_b.x.raw_value); \
    cl_assert_equal_i(__pt_a.y.raw_value, __pt_b.y.raw_value); \
  } while(0)

#define cl_assert_equal_vector_precise(a, b) \
  do { \
    const GVectorPrecise __a = (a); \
    const GVectorPrecise __b = (b); \
    cl_assert_equal_i(__a.dx.raw_value, __b.dx.raw_value); \
    cl_assert_equal_i(__a.dy.raw_value, __b.dy.raw_value); \
  } while(0)

#define cl_assert_equal_size(a, b) \
  do { \
    const GSize __sz_a = (a); \
    const GSize __sz_b = (b); \
    cl_assert_equal_i(__sz_a.w, __sz_b.w); \
    cl_assert_equal_i(__sz_a.h, __sz_b.h); \
  } while(0)

#define cl_assert_equal_size_precise(a, b) \
  do { \
    const GSizePrecise __sz_a = (a); \
    const GSizePrecise __sz_b = (b); \
    cl_assert_equal_i(__sz_a.w.raw_value, __sz_b.w.raw_value); \
    cl_assert_equal_i(__sz_a.h.raw_value, __sz_b.h.raw_value); \
  } while(0)

#define cl_assert_equal_rect(a, b) \
  do { \
    const GRect __a = (a); \
    const GRect __b = (b); \
    cl_assert_equal_point(__a.origin, __b.origin); \
    cl_assert_equal_size(__a.size, __b.size); \
  } while(0)

#define cl_assert_equal_rect_precise(a, b) \
  do { \
    const GRectPrecise __a = (a); \
    const GRectPrecise __b = (b); \
    cl_assert_equal_point_precise(__a.origin, __b.origin); \
    cl_assert_equal_size_precise(__a.size, __b.size); \
  } while(0)


typedef struct {
  union {
    Layer *layer;
    GContext *ctx;
  };
  union {
    GColor color;
    uint8_t width;
    struct {
      GPoint p0;
      GPoint p1;
    };
    struct {
      GPointPrecise pp0;
      GPointPrecise pp1;
    };
    struct {
      GPointPrecise center;
      Fixed_S16_3 radius;
      int32_t angle_start;
      int32_t angle_end;
    } draw_arc;
    struct {
      GRect rect;
      uint16_t radius;
      GCornerMask corner_mask;
    };
    struct {
      GRectPrecise prect;
    };
    TimeUnits tick_units;
    const char *font_key;
    struct {
      char text[200];
      GRect box;
      GColor color;
    } draw_text;
    struct {
      char text[200];
      GFont font;
      GRect box;
      GTextOverflowMode overflow_mode;
      GTextAlignment alignment;
    } max_used_size;
    struct {
      GPoint points[200];
      size_t num_points;
    } path;
    struct {
      GPointPrecise center;
      Fixed_S16_3 radius_inner;
      Fixed_S16_3 radius_outer;
      int32_t angle_start;
      int32_t angle_end;
    } fill_radial_precise;
  };
} MockCallRecording;

typedef struct {
  int call_count;
  MockCallRecording last_call;
} MockCallRecordings;

#define record_mock_call(var) \
    var.call_count++; \
    var.last_call = (MockCallRecording)

// Handy for poking at .js things when debugging a unit test with gdb, for example:
// (gdb) call js_eval("1 + 1")
// 2
// (gdb) call js_eval("Date()")
// Thu Jan 01 1970 00:00:00 GMT+00:00
void js_eval(const char *src) {
  JS_VAR rv = jerry_eval((const jerry_char_t *)src, strlen(src), false);
  char buf[256] = {};
  jerry_object_to_string_to_utf8_char_buffer(rv, (jerry_char_t *)buf, sizeof(buf));
  printf("%s\n", buf);
}

static void (*s_process_manager_callback)(void *data);
static void *s_process_manager_callback_data;
void sys_current_process_schedule_callback(CallbackEventCallback async_cb,
                                           void *ctx) {
  s_process_manager_callback = async_cb;
  s_process_manager_callback_data = ctx;
}