/* Copyright JS Foundation and other contributors, http://js.foundation
 *
 * 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 "jerryscript.h"
#include "test-common.h"

static bool
count_objects (jerry_value_t object, void *user_arg)
{
  (void) object;
  TEST_ASSERT (user_arg != NULL);

  int *counter = (int *) user_arg;

  (*counter)++;
  return true;
} /* count_objects */

static void
test_container (void)
{
  jerry_value_t global = jerry_get_global_object ();
  jerry_value_t map_str = jerry_create_string ((const jerry_char_t *) "Map");
  jerry_value_t map_result = jerry_get_property (global, map_str);
  jerry_type_t type = jerry_value_get_type (map_result);

  jerry_release_value (map_result);
  jerry_release_value (map_str);
  jerry_release_value (global);

  /* If there is no Map function this is not an es.next profile build, skip this test case. */
  if (type != JERRY_TYPE_FUNCTION)
  {
    jerry_port_log (JERRY_LOG_LEVEL_ERROR, "Container based test is disabled!\n");
    return;
  }

  {
    /* Create a "DEMO" array which will be used for the Map below. */
    const char array_str[] = "var DEMO = [[1, 2], [3, 4]]; DEMO";
    jerry_value_t array = jerry_eval ((const jerry_char_t *) array_str, sizeof (array_str) - 1, 0);
    TEST_ASSERT (jerry_value_is_object (array));
    TEST_ASSERT (!jerry_value_is_error (array));
    jerry_release_value (array);
  }

  const char eval_str[] = "new Map (DEMO)";
  {
    /* Make sure that the Map and it's prototype object/function is initialized. */
    jerry_value_t result = jerry_eval ((const jerry_char_t *) eval_str, sizeof (eval_str) - 1, 0);
    TEST_ASSERT (jerry_value_is_object (result));
    TEST_ASSERT (!jerry_value_is_error (result));
    jerry_release_value (result);
  }

  /* Do a bit of cleaning to clear up old objects. */
  jerry_gc (JERRY_GC_PRESSURE_LOW);

  /* Get the number of iterable objects. */
  int start_count = 0;
  jerry_objects_foreach (count_objects, &start_count);

  /* Create another map. */
  jerry_value_t result = jerry_eval ((const jerry_char_t *) eval_str, sizeof (eval_str) - 1, 0);

  /* Remove any old/unused objects. */
  jerry_gc (JERRY_GC_PRESSURE_LOW);

  /* Get the current number of objects. */
  int end_count = 0;
  jerry_objects_foreach (count_objects, &end_count);

  /* As only one Map was created the number of available iterable objects should be incremented only by one. */
  TEST_ASSERT (end_count > start_count);
  TEST_ASSERT ((end_count - start_count) == 1);

  jerry_release_value (result);
} /* test_container */

static void
test_internal_prop (void)
{
  /* Make sure that the object is initialized in the engine. */
  jerry_value_t object_dummy = jerry_create_object ();

  /* Get the number of iterable objects. */
  int before_object_count = 0;
  jerry_objects_foreach (count_objects, &before_object_count);

  jerry_value_t object = jerry_create_object ();

  /* After creating the object, the number of objects is incremented by one. */
  int after_object_count = 0;
  {
    jerry_objects_foreach (count_objects, &after_object_count);

    TEST_ASSERT (after_object_count > before_object_count);
    TEST_ASSERT ((after_object_count - before_object_count) == 1);
  }

  jerry_value_t internal_prop_name = jerry_create_string ((const jerry_char_t *) "hidden_foo");
  jerry_value_t internal_prop_object = jerry_create_object ();
  bool internal_result = jerry_set_internal_property (object, internal_prop_name, internal_prop_object);
  TEST_ASSERT (internal_result == true);
  jerry_release_value (internal_prop_name);
  jerry_release_value (internal_prop_object);

  /* After adding an internal property object, the number of object is incremented by one. */
  {
    int after_internal_count = 0;
    jerry_objects_foreach (count_objects, &after_internal_count);

    TEST_ASSERT (after_internal_count > after_object_count);
    TEST_ASSERT ((after_internal_count - after_object_count) == 1);
  }

  jerry_release_value (object);
  jerry_release_value (object_dummy);
} /* test_internal_prop */

static int test_data = 1;

static void free_test_data (void *data_p)
{
  TEST_ASSERT ((int *) data_p == &test_data);
} /* free_test_data */

static const jerry_object_native_info_t test_info =
{
  .free_cb = free_test_data
};

static const jerry_char_t strict_equal_source[] = "var x = function(a, b) {return a === b;}; x";

static bool
find_test_object_by_data (const jerry_value_t candidate,
                          void *object_data_p,
                          void *context_p)
{
  if (object_data_p == &test_data)
  {
    *((jerry_value_t *) context_p) = jerry_acquire_value (candidate);
    return false;
  }
  return true;
} /* find_test_object_by_data */

static bool
find_test_object_by_property (const jerry_value_t candidate,
                              void *context_p)
{
  jerry_value_t *args_p = (jerry_value_t *) context_p;
  jerry_value_t result = jerry_has_property (candidate, args_p[0]);

  bool has_property = (!jerry_value_is_error (result) && jerry_get_boolean_value (result));

  /* If the object has the desired property, store a new reference to it in args_p[1]. */
  if (has_property)
  {
    args_p[1] = jerry_acquire_value (candidate);
  }

  jerry_release_value (result);

  /* Stop iterating if we've found our object. */
  return !has_property;
} /* find_test_object_by_property */

int
main (void)
{
  jerry_init (JERRY_INIT_EMPTY);

  jerry_parse_options_t parse_options;
  parse_options.options = JERRY_PARSE_STRICT_MODE;

  /* Render strict-equal as a function. */
  jerry_value_t parse_result = jerry_parse (strict_equal_source,
                                            sizeof (strict_equal_source) - 1,
                                            &parse_options);
  TEST_ASSERT (!jerry_value_is_error (parse_result));
  jerry_value_t strict_equal = jerry_run (parse_result);
  TEST_ASSERT (!jerry_value_is_error (strict_equal));
  jerry_release_value (parse_result);

  /* Create an object and associate some native data with it. */
  jerry_value_t object = jerry_create_object ();
  jerry_set_object_native_pointer (object, &test_data, &test_info);

  /* Retrieve the object by its native pointer. */

  jerry_value_t found_object;
  TEST_ASSERT (jerry_objects_foreach_by_native_info (&test_info, find_test_object_by_data, &found_object));
  jerry_value_t args[2] = {object, found_object};

  /* Assert that the correct object was retrieved. */
  jerry_value_t undefined = jerry_create_undefined ();
  jerry_value_t strict_equal_result = jerry_call_function (strict_equal, undefined, args, 2);
  TEST_ASSERT (jerry_value_is_boolean (strict_equal_result) && jerry_get_boolean_value (strict_equal_result));
  jerry_release_value (strict_equal_result);
  jerry_release_value (found_object);
  jerry_release_value (object);

  /* Collect garbage. */
  jerry_gc (JERRY_GC_PRESSURE_LOW);

  /* Attempt to retrieve the object by its native pointer again. */
  TEST_ASSERT (!jerry_objects_foreach_by_native_info (&test_info, find_test_object_by_data, &found_object));

  /* Create an object and set a property on it. */
  object = jerry_create_object ();
  jerry_value_t property_name = jerry_create_string ((jerry_char_t *) "xyzzy");
  jerry_value_t property_value = jerry_create_number (42);
  jerry_release_value (jerry_set_property (object, property_name, property_value));
  jerry_release_value (property_value);

  /* Retrieve the object by the presence of its property, placing it at args[1]. */
  args[0] = property_name;
  TEST_ASSERT (jerry_objects_foreach (find_test_object_by_property, args));

  /* Assert that the right object was retrieved and release both the original reference to it and the retrieved one. */
  args[0] = object;
  strict_equal_result = jerry_call_function (strict_equal, undefined, args, 2);
  TEST_ASSERT (jerry_value_is_boolean (strict_equal_result) && jerry_get_boolean_value (strict_equal_result));
  jerry_release_value (strict_equal_result);
  jerry_release_value (args[0]);
  jerry_release_value (args[1]);

  /* Collect garbage. */
  jerry_gc (JERRY_GC_PRESSURE_LOW);

  /* Attempt to retrieve the object by the presence of its property again. */
  args[0] = property_name;
  TEST_ASSERT (!jerry_objects_foreach (find_test_object_by_property, args));

  jerry_release_value (property_name);
  jerry_release_value (undefined);
  jerry_release_value (strict_equal);

  test_container ();
  test_internal_prop ();

  jerry_cleanup ();

  return 0;
} /* main */
