/*
 * 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 "rocky_api_global.h"

#include "applib/app_logging.h"
#include "jerry-api.h"
#include "kernel/pbl_malloc.h"
#include "system/passert.h"
#include "syscall/syscall.h"
#include "util/size.h"

#include "rocky_api.h"
#include "rocky_api_errors.h"
#include "rocky_api_util.h"

#define ROCKY_LISTENERS "_listeners"

#define ROCKY_ON "on"
#define ROCKY_ADD_EVENT_LISTENER "addEventListener"
#define ROCKY_OFF "off"
#define ROCKY_REMOVE_EVENT_LISTENER "removeEventListener"
#define ROCKY_EVENT_CONSTRUCTOR "Event"
#define ROCKY_EVENT_TYPE "type"

// TODO: PBL-35780 make this part of app_state_get_rocky_runtime_context()
SECTION(".rocky_bss") static const RockyGlobalAPI *const *s_global_apis;

#define API_REFS_FOREACH(var_name) \
  for (RockyGlobalAPI const *const *var_name = s_global_apis; *var_name != NULL; var_name++)


static jerry_value_t prv_get_or_create_listener_array(const char *event_name) {
  JS_VAR rocky = rocky_get_rocky_singleton();
  JS_VAR all_listeners = rocky_get_or_create_object(rocky, ROCKY_LISTENERS,
                                                           rocky_creator_object,
                                                           NULL, NULL);
  return rocky_get_or_create_object(all_listeners, event_name,
                                    rocky_creator_empty_array,
                                    NULL, NULL);
}

// callback while iterating over listeners
// @param event_listeners the array this listener is part of
// @param idx position within the array at which the listener exists
// @param listener the JS function that's registered as listener
// @param data pointer provided when calling prv_iterate_event_listeners()
// @return true, if you interested in further iterations. False to stop iterating.
typedef bool (*EventListenerIteratorCb)(jerry_value_t event_listeners, uint32_t idx,
                                        jerry_value_t listener, void *data);

static void prv_iterate_event_listeners(const char *event_name,
                                        EventListenerIteratorCb callback,
                                        void *data) {
  PBL_ASSERTN(callback);

  JS_VAR rocky = rocky_get_rocky_singleton();
  JS_VAR all_listeners = jerry_get_object_field(rocky, "_listeners");
  JS_VAR event_listeners = jerry_get_object_field(all_listeners, event_name);
//  printf("event_listeners.refcount: %d", jerry_port_)

  const uint32_t len = jerry_get_array_length(event_listeners);
  for (uint32_t idx = 0; idx < len; idx++) {
    JS_VAR listener = jerry_get_property_by_index(event_listeners, idx);
    if (jerry_value_is_function(listener)) {
      bool wants_more = callback(event_listeners, idx, listener, data);
      if (!wants_more) {
        break;
      }
    }
  }
}

typedef struct {
  jerry_value_t listener;
  bool found;
} ListenerQueryData;

static bool prv_find_listener(jerry_value_t event_listeners, uint32_t idx,
                       jerry_value_t listener, void *data) {
  ListenerQueryData *query_data = data;
  if (query_data->listener == listener) {
    query_data->found = true;
    return false;
  }
  return true;
}

static bool prv_listener_is_registered(const char *event_name, jerry_value_t listener) {
  ListenerQueryData data = {
    .listener = listener,
  };
  prv_iterate_event_listeners(event_name, prv_find_listener, &data);
  return data.found;
}

T_STATIC void prv_add_event_listener_to_list(const char *event_name, jerry_value_t listener) {
  if (prv_listener_is_registered(event_name, listener)) {
    // we won't register the same listener twice
    return;
  }
  JS_VAR listeners = prv_get_or_create_listener_array(event_name);
  const uint32_t num_entries = jerry_get_array_length(listeners);
  JS_UNUSED_VAL = jerry_set_property_by_index(listeners, num_entries, listener);
}

static bool prv_remove_listener(jerry_value_t event_listeners, uint32_t idx,
                                jerry_value_t listener, void *data) {
  ListenerQueryData *query_data = data;
  if (query_data->listener == listener) {
    // calling `event_listeners.splice(idx, 1)` to remove item at idx
    // wow, this is a lot of code for this
    JS_VAR splice = jerry_get_object_field(event_listeners, "splice");
    const jerry_value_t args[] = {jerry_create_number(idx), jerry_create_number(1)};
    JS_VAR remove_result = jerry_call_function(splice, event_listeners,
                                               args, ARRAY_LENGTH(args));
    if (jerry_value_has_error_flag(remove_result)) {
      rocky_log_exception("removing event listener", remove_result);
    }
    for (size_t i = 0; i < ARRAY_LENGTH(args); i++) {
      jerry_release_value(args[i]);
    }

    // mark item as removed and stop iterating
    query_data->found = true;
    return false;
  }
  return true;
}

T_STATIC bool prv_remove_event_listener_from_list(const char *event_name, jerry_value_t listener) {
  ListenerQueryData data = {
    .listener = listener,
  };
  prv_iterate_event_listeners(event_name, prv_remove_listener, &data);
  return data.found;
}


// implementation of .on(event_name, handler) and stores them in a new property
//
//  rocky._listeners = {
//    "event_1" : [ function_1, function_2, ... ],
//    "event_2" ; [ ... ] ,
//    ...
//  }
//
// please note that we ignore events, no API is interested in by asking each of them
// via .add_handler(event_name, func)

static jerry_value_t prv_event_listener_extract_args(const jerry_length_t argc,
                                                     const jerry_value_t argv[],
                                                     char *event_name, size_t event_name_size,
                                                     jerry_value_t *func) {
  PBL_ASSERTN(event_name && func);

  if (argc < 2) {
    return rocky_error_arguments_missing();
  }

  memset(event_name, 0, event_name_size);
  const jerry_size_t len = jerry_string_to_utf8_char_buffer(argv[0],
                                                            (jerry_char_t *)event_name,
                                                            event_name_size);
  if (len == 0 || len == event_name_size) {
    return rocky_error_argument_invalid("Not a valid event");
  }

  if (!jerry_value_is_function(argv[1])) {
    return rocky_error_argument_invalid("Not a valid handler");
  }
  *func = argv[1];

  return jerry_create_undefined();
}

JERRY_FUNCTION(prv_add_event_listener) {
  char event_name[32];
  jerry_value_t func; // out parameter &func will not be acquired
  JS_VAR arg_result = prv_event_listener_extract_args(argc, argv,
                                                      event_name, sizeof(event_name),
                                                      &func);
  if (jerry_value_has_error_flag(arg_result)) {
    return jerry_acquire_value(arg_result);
  }

  bool is_relevant = false;
  API_REFS_FOREACH(api_ref) {
    if ((*api_ref)->add_handler) {
      is_relevant = (*api_ref)->add_handler(event_name, func);
      if (is_relevant) {
        break;
      }
    }
  }

  if (!is_relevant) {
    APP_LOG(LOG_LEVEL_WARNING, "Unknown event '%s'", event_name);
    return jerry_create_undefined();
  }

  prv_add_event_listener_to_list((char *)event_name, func);

  return jerry_create_undefined();
}

JERRY_FUNCTION(prv_remove_event_listener) {
  char event_name[32];
  jerry_value_t func; // out parameter &func will not be acquired
  JS_VAR arg_result = prv_event_listener_extract_args(argc, argv,
                                                      event_name, sizeof(event_name),
                                                      &func);
  if (jerry_value_has_error_flag(arg_result)) {
    return jerry_acquire_value(arg_result);
  }

  const bool removed = prv_remove_event_listener_from_list(event_name, func);
  if (removed) {
    API_REFS_FOREACH(api_ref) {
      if ((*api_ref)->remove_handler) {
        (*api_ref)->remove_handler(event_name, func);
      }
    }
  } else {
    APP_LOG(LOG_LEVEL_WARNING, "Unknown handler for event '%s'", event_name);
  }

  return jerry_create_undefined();
}

JERRY_FUNCTION(prv_event_constructor) {
  if (argc < 1) {
    return rocky_error_arguments_missing();
  }
  if (!jerry_value_is_string(argv[0])) {
    return rocky_error_unexpected_type(0, "String");
  }
  jerry_set_object_field(this_val, ROCKY_EVENT_TYPE, argv[0]);
  return jerry_create_undefined();
}

static void prv_copy_property(const jerry_value_t rocky,
                              const char *name_from, const char *name_to) {
  JS_VAR on = jerry_get_object_field(rocky, name_from);
  jerry_set_object_field(rocky, name_to, on);
}

void rocky_global_init(const RockyGlobalAPI *const *global_apis) {
  PBL_ASSERTN(global_apis);
  s_global_apis = global_apis;

  JS_VAR rocky = jerry_create_object();
  // this keeps a permanent reference to the singleton
  rocky_set_rocky_singleton(rocky);

  rocky_add_function(rocky, ROCKY_ON, prv_add_event_listener);
  prv_copy_property(rocky, ROCKY_ON, ROCKY_ADD_EVENT_LISTENER);
  rocky_add_function(rocky, ROCKY_OFF, prv_remove_event_listener);
  prv_copy_property(rocky, ROCKY_OFF, ROCKY_REMOVE_EVENT_LISTENER);

  JS_UNUSED_VAL = rocky_add_constructor(ROCKY_EVENT_CONSTRUCTOR, prv_event_constructor);

  API_REFS_FOREACH(api_ref) {
    if ((*api_ref)->init) {
      (*api_ref)->init();
    }
  }
}

void rocky_global_deinit(void) {
  API_REFS_FOREACH(api_ref) {
    if ((*api_ref)->deinit) {
      (*api_ref)->deinit();
    }
  }

#if APPLIB_EMSCRIPTEN
  rocky_delete_singleton();
#endif
}

static bool prv_has_any(jerry_value_t event_listeners, uint32_t idx,
                        jerry_value_t listener, void *data) {
  *(bool *)data = true;
  return false;
}

bool rocky_global_has_event_handlers(const char *event_name) {
  bool result = false;
  prv_iterate_event_listeners(event_name, prv_has_any, &result);
  return result;
}

typedef struct {
  const jerry_value_t this_arg;
  const jerry_value_t *args_p;
  jerry_size_t args_count;
} EventHandlersCallArgs;

static bool prv_call_event_handlers_cb(jerry_value_t event_listeners, uint32_t idx,
                                       jerry_value_t listener, void *data) {
  EventHandlersCallArgs * const args = data;
  rocky_util_call_user_function_and_log_uncaught_error(listener, args->this_arg, args->args_p,
                                                       args->args_count);
  return true;
}

void rocky_global_call_event_handlers(jerry_value_t event) {
  EventHandlersCallArgs args = {
    .this_arg =  jerry_create_undefined(),
    .args_p = &event,
    .args_count = 1,
  };

  JS_VAR event_str = jerry_get_object_field(event, "type");
  char *event_name = rocky_string_alloc_and_copy(event_str);
  prv_iterate_event_listeners(event_name, prv_call_event_handlers_cb, &args);

  task_free(event_name);
  jerry_release_value(args.this_arg);
}

static void prv_call_event_handlers_async_cb(void *ctx) {
  jerry_value_t event = (jerry_value_t)(uintptr_t) ctx;
  rocky_global_call_event_handlers(event);
  jerry_release_value(event);  // was acquired in rocky_global_call_event_handlers_async() call
}

void rocky_global_call_event_handlers_async(jerry_value_t event) {
  sys_current_process_schedule_callback(prv_call_event_handlers_async_cb,
                                        (void *)(uintptr_t)jerry_acquire_value(event));
}

jerry_value_t rocky_global_create_event(const char *type_str) {
  JS_VAR jerry_type_str = jerry_create_string_utf8((const jerry_char_t *)type_str);
  JS_VAR event = rocky_create_with_constructor(ROCKY_EVENT_CONSTRUCTOR, &jerry_type_str, 1);
  return jerry_acquire_value(event);
}
