/****************************************************************************
 * apps/interpreters/quickjs/qjsmini.c
 *
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.  The
 * ASF licenses this file to you 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.
 *
 ****************************************************************************/

/****************************************************************************
 * Included Files
 ****************************************************************************/

#include <stdlib.h>
#include <stdio.h>
#include <stdarg.h>
#include <inttypes.h>
#include <string.h>
#include <assert.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <time.h>
#include <malloc.h>

#include <quickjs.h>
#include <cutils.h>

/****************************************************************************
 * Pre-processor Definitions
 ****************************************************************************/

#define MALLOC_OVERHEAD 8

/****************************************************************************
 * Private Data
 ****************************************************************************/

struct trace_malloc_data
{
  uint8_t *base;
};

/****************************************************************************
 * Private Functions
 ****************************************************************************/

/****************************************************************************
 * Name: help
 ****************************************************************************/

static void qjs_help(void)
{
  printf("QuickJS version " CONFIG_VERSION "\n"
         "usage: "
         "qjs"
         " [options] [file [args]]\n"
         "-h  --help         list options\n"
         "-e  --eval EXPR    evaluate EXPR\n"
         "-T  --trace        trace memory allocation\n"
         "-d  --dump         dump the memory usage stats\n"
         "    --memory-limit n       limit the memory usage to 'n' bytes\n"
         "    --stack-size n         limit the stack size to 'n' bytes\n"
         "    --unhandled-rejection  dump unhandled promise rejections\n"
         "-q  --quit         just instantiate the interpreter and quit\n");
  exit(1);
}

/****************************************************************************
 * Name: js_trace_malloc_ptr_offset
 ****************************************************************************/

static inline unsigned long long
js_trace_malloc_ptr_offset(uint8_t *ptr,
                           struct trace_malloc_data *dp)
{
  return ptr - dp->base;
}

/****************************************************************************
 * Name: js_trace_malloc_usable_size
 ****************************************************************************/

static inline size_t js_trace_malloc_usable_size(void *ptr)
{
  return malloc_usable_size(ptr);
}

/****************************************************************************
 * Name: js_trace_malloc_printf
 ****************************************************************************/

static void __attribute__((format(printf, 2, 3)))
js_trace_malloc_printf(JSMallocState *s, const char *fmt, ...)
{
  va_list ap;
  int c;

  va_start(ap, fmt);
  while ((c = *fmt++) != '\0')
    {
      if (c == '%')
        {
          /* only handle %p and %zd */

          if (*fmt == 'p')
            {
              uint8_t *ptr = va_arg(ap, void *);
              if (ptr == NULL)
                {
                  printf("NULL");
                }
              else
                {
                  printf("H%+06lld.%zd",
                        js_trace_malloc_ptr_offset(ptr, s->opaque),
                        js_trace_malloc_usable_size(ptr));
                }

              fmt++;
              continue;
            }

          if (fmt[0] == 'z' && fmt[1] == 'd')
            {
              size_t sz = va_arg(ap, size_t);
              printf("%zd", sz);
              fmt += 2;
              continue;
            }
        }

      putc(c, stdout);
    }

  va_end(ap);
}

/****************************************************************************
 * Name: js_trace_malloc_init
 ****************************************************************************/

static void js_trace_malloc_init(struct trace_malloc_data *s)
{
  free(s->base = malloc(8));
}

/****************************************************************************
 * Name: js_trace_malloc
 ****************************************************************************/

static void *js_trace_malloc(JSMallocState *s, size_t size)
{
  void *ptr;

  /* Do not allocate zero bytes: behavior is platform dependent */

  assert(size != 0);

  if (unlikely(s->malloc_size + size > s->malloc_limit))
      return NULL;
  ptr = malloc(size);
  js_trace_malloc_printf(s, "A %zd -> %p\n", size, ptr);
  if (ptr)
    {
      s->malloc_count++;
      s->malloc_size += js_trace_malloc_usable_size(ptr) + MALLOC_OVERHEAD;
    }

  return ptr;
}

/****************************************************************************
 * Name: js_trace_free
 ****************************************************************************/

static void js_trace_free(JSMallocState *s, void *ptr)
{
  if (!ptr)
    return;

  js_trace_malloc_printf(s, "F %p\n", ptr);
  s->malloc_count--;
  s->malloc_size -= js_trace_malloc_usable_size(ptr) + MALLOC_OVERHEAD;
  free(ptr);
}

/****************************************************************************
 * Name: js_trace_realloc
 ****************************************************************************/

static void *js_trace_realloc(JSMallocState *s, void *ptr, size_t size)
{
  size_t old_size;

  if (!ptr)
    {
      if (size == 0)
          return NULL;
      return js_trace_malloc(s, size);
    }

  old_size = js_trace_malloc_usable_size(ptr);
  if (size == 0)
    {
      js_trace_malloc_printf(s, "R %zd %p\n", size, ptr);
      s->malloc_count--;
      s->malloc_size -= old_size + MALLOC_OVERHEAD;
      free(ptr);
      return NULL;
    }

  if (s->malloc_size + size - old_size > s->malloc_limit)
      return NULL;

  js_trace_malloc_printf(s, "R %zd %p", size, ptr);

  ptr = realloc(ptr, size);
  js_trace_malloc_printf(s, " -> %p\n", ptr);
  if (ptr)
    {
      s->malloc_size += js_trace_malloc_usable_size(ptr) - old_size;
    }

  return ptr;
}

/****************************************************************************
 * Name: js_dump_obj
 ****************************************************************************/

static void js_dump_obj(JSContext *ctx, FILE *f, JSValueConst val)
{
  const char *str;

  str = JS_ToCString(ctx, val);
  if (str)
    {
      fprintf(f, "%s\n", str);
      JS_FreeCString(ctx, str);
    }
  else
    {
      fprintf(f, "[exception]\n");
    }
}

/****************************************************************************
 * Name: js_std_dump_error1
 ****************************************************************************/

static void js_std_dump_error1(JSContext *ctx, JSValueConst exception_val)
{
  JSValue val;
  BOOL is_error;

  is_error = JS_IsError(ctx, exception_val);
  js_dump_obj(ctx, stderr, exception_val);
  if (is_error)
    {
      val = JS_GetPropertyStr(ctx, exception_val, "stack");
      if (!JS_IsUndefined(val))
        {
          js_dump_obj(ctx, stderr, val);
        }

      JS_FreeValue(ctx, val);
    }
}

/****************************************************************************
 * Name: js_std_dump_error
 ****************************************************************************/

static void js_std_dump_error(JSContext *ctx)
{
  JSValue exception_val;

  exception_val = JS_GetException(ctx);
  js_std_dump_error1(ctx, exception_val);
  JS_FreeValue(ctx, exception_val);
}

/****************************************************************************
 * Name: js_eval_buf
 ****************************************************************************/

static int js_eval_buf(JSContext *ctx, const void *buf, int buf_len,
                       const char *filename, int eval_flags)
{
  JSValue val;
  int ret;

  if ((eval_flags & JS_EVAL_TYPE_MASK) == JS_EVAL_TYPE_MODULE)
    {
      val = JS_Eval(ctx, buf, buf_len, filename,
                    eval_flags | JS_EVAL_FLAG_COMPILE_ONLY);
      if (!JS_IsException(val))
        {
          val = JS_EvalFunction(ctx, val);
        }
    }
  else
    {
      val = JS_Eval(ctx, buf, buf_len, filename, eval_flags);
    }

  if (JS_IsException(val))
    {
      js_std_dump_error(ctx);
      ret = -1;
    }
  else
    {
      ret = 0;
    }

  JS_FreeValue(ctx, val);
  return ret;
}

/****************************************************************************
 * Name: js_load_file
 ****************************************************************************/

uint8_t *js_load_file(JSContext *ctx, size_t *pbuf_len, const char *filename)
{
  FILE *f;
  uint8_t *buf;
  size_t buf_len;
  long lret;

  f = fopen(filename, "rb");
  if (!f)
      return NULL;
  if (fseek(f, 0, SEEK_END) < 0)
      goto fail;
  lret = ftell(f);
  if (lret < 0)
      goto fail;

  if (lret == LONG_MAX)
    {
      errno = EISDIR;
      goto fail;
    }

  buf_len = lret;
  if (fseek(f, 0, SEEK_SET) < 0)
      goto fail;
  if (ctx)
      buf = js_malloc(ctx, buf_len + 1);
  else
      buf = malloc(buf_len + 1);
  if (!buf)
      goto fail;
  if (fread(buf, 1, buf_len, f) != buf_len)
    {
      errno = EIO;
      if (ctx)
        js_free(ctx, buf);
      else
        free(buf);
    fail:
      fclose(f);
      return NULL;
    }

  buf[buf_len] = '\0';
  fclose(f);
  *pbuf_len = buf_len;
  return buf;
}

/****************************************************************************
 * Name: js_eval_file
 ****************************************************************************/

static int js_eval_file(JSContext *ctx, const char *filename, int module)
{
  uint8_t *buf;
  int ret, eval_flags;
  size_t buf_len;

  buf = js_load_file(ctx, &buf_len, filename);
  if (!buf)
    {
      perror(filename);
      exit(1);
    }

  if (module < 0)
    {
      module = (has_suffix(filename, ".mjs") ||
                JS_DetectModule((const char *)buf, buf_len));
    }

  if (module)
    eval_flags = JS_EVAL_TYPE_MODULE;
  else
    eval_flags = JS_EVAL_TYPE_GLOBAL;
  ret = js_eval_buf(ctx, buf, buf_len, filename, eval_flags);
  js_free(ctx, buf);
  return ret;
}

static JSValue js_print(JSContext *ctx, JSValueConst this_val,
                        int argc, JSValueConst *argv)
{
  int i;
  const char *str;
  size_t len;

  for (i = 0; i < argc; i++)
    {
      if (i != 0)
        putchar(' ');
      str = JS_ToCStringLen(ctx, &len, argv[i]);
      if (!str)
          return JS_EXCEPTION;
      fwrite(str, 1, len, stdout);
      JS_FreeCString(ctx, str);
    }

  putchar('\n');
  return JS_UNDEFINED;
}

void js_std_add_helpers(JSContext *ctx)
{
  JSValue global_obj, console;
  global_obj = JS_GetGlobalObject(ctx);

  console = JS_NewObject(ctx);
  JS_SetPropertyStr(ctx, console, "log",
                    JS_NewCFunction(ctx, js_print, "log", 1));
  JS_SetPropertyStr(ctx, global_obj, "console", console);

  JS_FreeValue(ctx, global_obj);
}

/****************************************************************************
 * Public Functions Prototypes
 ****************************************************************************/

int js_ext_init(JSContext *ctx);
int js_ext_destroy(JSContext *ctx);

/****************************************************************************
 * Public Functions
 ****************************************************************************/

/****************************************************************************
 * qjs_main
 ****************************************************************************/

int main(int argc, char *argv[])
{
  JSRuntime *rt = NULL;
  JSContext *ctx = NULL;
  struct trace_malloc_data trace_data =
  {
    NULL
  };

  static const JSMallocFunctions trace_mf =
  {
      js_trace_malloc,
      js_trace_free,
      js_trace_realloc,
      malloc_usable_size,
  };

  int optind;
  char *expr = NULL;
  int dump_memory = 0;
  int trace_memory = 0;
  int empty_run = 0;
  size_t memory_limit = 0;
  size_t stack_size = 0;

  optind = 1;
  while (optind < argc && *argv[optind] == '-')
    {
      char *arg = argv[optind] + 1;
      const char *longopt = "";

      if (!*arg)
        break;
      optind++;
      if (*arg == '-')
        {
          longopt = arg + 1;
          arg += strlen(arg);
          if (!*longopt)
            break;
        }

      for (; *arg || *longopt; longopt = "")
        {
          char opt = *arg;
          if (opt)
            {
              arg++;
            }

          if (opt == 'h' || opt == '?' || !strcmp(longopt, "help"))
            {
              qjs_help();
              continue;
            }

          if (opt == 'e' || !strcmp(longopt, "eval"))
            {
              if (*arg)
                {
                  expr = arg;
                  break;
                }

              if (optind < argc)
                {
                  expr = argv[optind++];
                  break;
                }

              fprintf(stderr, "qjs: missing expression for -e\n");
              exit(2);
            }

          if (opt == 'd' || !strcmp(longopt, "dump"))
            {
              dump_memory++;
              continue;
            }

          if (opt == 'T' || !strcmp(longopt, "trace"))
            {
              trace_memory++;
              continue;
            }

          if (opt == 'q' || !strcmp(longopt, "quit"))
            {
              empty_run++;
              continue;
            }

          if (!strcmp(longopt, "memory-limit"))
            {
              if (optind >= argc)
                {
                  fprintf(stderr, "expecting memory limit");
                  exit(1);
                }

              memory_limit = (size_t)strtod(argv[optind++], NULL);
              continue;
            }

          if (!strcmp(longopt, "stack-size"))
            {
              if (optind >= argc)
                {
                  fprintf(stderr, "expecting stack size");
                  exit(1);
                }

              stack_size = (size_t)strtod(argv[optind++], NULL);
              continue;
            }

          if (opt)
            {
              fprintf(stderr, "qjs: unknown option '-%c'\n", opt);
            }
          else
            {
              fprintf(stderr, "qjs: unknown option '--%s'\n", longopt);
            }

          qjs_help();
        }
    }

  if (trace_memory)
    {
      js_trace_malloc_init(&trace_data);
      rt = JS_NewRuntime2(&trace_mf, &trace_data);
    }
  else
    {
      rt = JS_NewRuntime();
    }

  if (!rt)
    {
      fprintf(stderr, "qjs: cannot allocate JS runtime\n");
      goto fail;
    }

  if (memory_limit != 0)
    JS_SetMemoryLimit(rt, memory_limit);
  if (stack_size != 0)
    JS_SetMaxStackSize(rt, stack_size);

  ctx = JS_NewContext(rt);

  if (!ctx)
    {
      fprintf(stderr, "qjs: cannot allocate JS context\n");
      goto fail;
    }

  js_std_add_helpers(ctx);

#ifdef CONFIG_INTERPRETERS_QUICKJS_EXT_HOOK
  if (OK != js_ext_init(ctx))
    {
      fprintf(stderr, "qjs: external context init failed\n");
      goto fail;
    }
#endif

  if (!empty_run)
    {
      if (expr)
        {
          if (js_eval_buf(ctx, expr, strlen(expr), "<cmdline>", 0))
            {
              goto fail;
            }
        }
      else if (optind < argc)
        {
          const char *filename;
          filename = argv[optind];
          if (js_eval_file(ctx, filename, 1))
            {
              goto fail;
            }
        }
    }

  if (dump_memory)
    {
      JSMemoryUsage stats;
      JS_ComputeMemoryUsage(rt, &stats);
      JS_DumpMemoryUsage(stdout, &stats, rt);
    }

fail:
  if (ctx)
    {
#ifdef CONFIG_INTERPRETERS_QUICKJS_EXT_HOOK
      js_ext_destroy(ctx);
#endif
      JS_FreeContext(ctx);
    }

  if (rt)
    JS_FreeRuntime(rt);

  return 0;
}
