#include <assert.h>
#include <bare.h>
#include <js.h>
#include <stdlib.h>
#include <utf.h>
#include <uv.h>

typedef struct {
  uv_process_t handle;

  js_env_t *env;
  js_ref_t *ctx;
  js_ref_t *on_exit;

  bool killed;
  bool exiting;

  js_deferred_teardown_t *teardown;
} bare_subprocess_t;

typedef struct {
  uv_pipe_t pipe;
  uv_buf_t read;
  size_t written;
} bare_subprocess_buffered_pipe_t;

typedef utf8_t bare_subprocess_path_t[4096 + 1 /* NULL */];

static void
bare_subprocess__on_close(uv_handle_t *handle) {
  int err;

  bare_subprocess_t *subprocess = (bare_subprocess_t *) handle;

  js_env_t *env = subprocess->env;

  js_deferred_teardown_t *teardown = subprocess->teardown;

  err = js_delete_reference(env, subprocess->on_exit);
  assert(err == 0);

  err = js_delete_reference(env, subprocess->ctx);
  assert(err == 0);

  err = js_finish_deferred_teardown_callback(teardown);
  assert(err == 0);
}

static void
bare_subprocess__on_exit(uv_process_t *handle, int64_t exit_status, int term_signal) {
  int err;

  bare_subprocess_t *subprocess = (bare_subprocess_t *) handle;

  subprocess->killed = true;

  if (subprocess->exiting) goto close;

  js_env_t *env = subprocess->env;

  js_handle_scope_t *scope;
  err = js_open_handle_scope(env, &scope);
  assert(err == 0);

  js_value_t *ctx;
  err = js_get_reference_value(env, subprocess->ctx, &ctx);
  assert(err == 0);

  js_value_t *on_exit;
  err = js_get_reference_value(env, subprocess->on_exit, &on_exit);
  assert(err == 0);

  js_value_t *argv[2];

  err = js_create_int64(env, exit_status, &argv[0]);
  assert(err == 0);

  err = js_create_int32(env, term_signal, &argv[1]);
  assert(err == 0);

  js_call_function(env, ctx, on_exit, 2, argv, NULL);

  err = js_close_handle_scope(env, scope);
  assert(err == 0);

close:
  uv_close((uv_handle_t *) &subprocess->handle, bare_subprocess__on_close);
}

static void
bare_subprocess__on_teardown(js_deferred_teardown_t *handle, void *data) {
  int err;

  bare_subprocess_t *subprocess = (bare_subprocess_t *) data;

  subprocess->exiting = true;

  if (subprocess->killed) return;

  err = uv_process_kill(&subprocess->handle, SIGTERM);
  assert(err == 0);
}

static void
bare_subprocess__on_alloc(uv_handle_t *handle, size_t suggested_size, uv_buf_t *buf) {
  bare_subprocess_buffered_pipe_t *pipe = (bare_subprocess_buffered_pipe_t *) handle;

  *buf = pipe->read;
}

static void
bare_subprocess__on_read(uv_stream_t *stream, ssize_t nread, const uv_buf_t *buf) {
  if (nread == 0) return;

  if (nread == UV_EOF) {
    uv_close((uv_handle_t *) stream, NULL);
  } else {
    int err;

    bare_subprocess_buffered_pipe_t *pipe = (bare_subprocess_buffered_pipe_t *) stream;

    pipe->read.base += nread;
    pipe->read.len -= nread;

    pipe->written += nread;
  }
}

static js_value_t *
bare_subprocess_init(js_env_t *env, js_callback_info_t *info) {
  int err;

  size_t argc = 2;
  js_value_t *argv[2];

  err = js_get_callback_info(env, info, &argc, argv, NULL, NULL);
  assert(err == 0);

  assert(argc == 2);

  js_value_t *handle;

  bare_subprocess_t *subprocess;
  err = js_create_arraybuffer(env, sizeof(bare_subprocess_t), (void **) &subprocess, &handle);
  assert(err == 0);

  subprocess->env = env;
  subprocess->killed = false;
  subprocess->exiting = false;

  err = js_create_reference(env, argv[0], 1, &subprocess->ctx);
  assert(err == 0);

  err = js_create_reference(env, argv[1], 1, &subprocess->on_exit);
  assert(err == 0);

  err = js_add_deferred_teardown_callback(env, bare_subprocess__on_teardown, (void *) subprocess, &subprocess->teardown);
  assert(err == 0);

  return handle;
}

static js_value_t *
bare_subprocess_spawn(js_env_t *env, js_callback_info_t *info) {
  int err;

  size_t argc = 11;
  js_value_t *argv[11];

  err = js_get_callback_info(env, info, &argc, argv, NULL, NULL);
  assert(err == 0);

  assert(argc == 11);

  uv_loop_t *loop;
  err = js_get_env_loop(env, &loop);
  assert(err == 0);

  bare_subprocess_t *subprocess;
  err = js_get_arraybuffer_info(env, argv[0], (void **) &subprocess, NULL);
  assert(err == 0);

  bare_subprocess_path_t file;
  err = js_get_value_string_utf8(env, argv[1], file, sizeof(bare_subprocess_path_t), NULL);
  assert(err == 0);

  uint32_t args_len;
  err = js_get_array_length(env, argv[2], &args_len);
  assert(err == 0);

  utf8_t **args = calloc(1 /* file */ + args_len + 1 /* NULL */, sizeof(utf8_t *));

  args[0] = file;

  for (uint32_t i = 0; i < args_len; i++) {
    js_value_t *value;
    err = js_get_element(env, argv[2], i, &value);
    assert(err == 0);

    size_t arg_len;
    err = js_get_value_string_utf8(env, value, NULL, 0, &arg_len);
    assert(err == 0);

    arg_len += 1 /* NULL */;

    utf8_t *arg = malloc(arg_len);
    err = js_get_value_string_utf8(env, value, arg, arg_len, NULL);
    assert(err == 0);

    args[i + 1] = arg;
  }

  bare_subprocess_path_t cwd;
  err = js_get_value_string_utf8(env, argv[3], cwd, sizeof(bare_subprocess_path_t), NULL);
  assert(err == 0);

  uint32_t pairs_len;
  err = js_get_array_length(env, argv[4], &pairs_len);
  assert(err == 0);

  utf8_t **pairs = calloc(pairs_len + 1 /* NULL */, sizeof(utf8_t *));

  for (uint32_t i = 0; i < pairs_len; i++) {
    js_value_t *value;
    err = js_get_element(env, argv[4], i, &value);
    assert(err == 0);

    size_t pair_len;
    err = js_get_value_string_utf8(env, value, NULL, 0, &pair_len);
    assert(err == 0);

    pair_len += 1 /* NULL */;

    utf8_t *pair = malloc(pair_len);
    err = js_get_value_string_utf8(env, value, pair, pair_len, NULL);
    assert(err == 0);

    pairs[i] = pair;
  }

  uint32_t stdio_len;
  err = js_get_array_length(env, argv[5], &stdio_len);
  assert(err == 0);

  uv_stdio_container_t *stdio = malloc(stdio_len * sizeof(uv_stdio_container_t));

  for (uint32_t i = 0; i < stdio_len; i++) {
    js_value_t *value;
    err = js_get_element(env, argv[5], i, &value);
    assert(err == 0);

    js_value_t *property;

    err = js_get_named_property(env, value, "flags", &property);
    assert(err == 0);

    uint32_t flags;
    err = js_get_value_uint32(env, property, &flags);
    assert(err == 0);

    stdio[i] = (uv_stdio_container_t) {
      .flags = flags,
    };

    if (flags & UV_INHERIT_FD) {
      err = js_get_named_property(env, value, "fd", &property);
      assert(err == 0);

      uint32_t fd;
      err = js_get_value_uint32(env, property, &fd);
      assert(err == 0);

      stdio[i].data.fd = fd;
    }

    if (flags & UV_CREATE_PIPE) {
      err = js_get_named_property(env, value, "pipe", &property);
      assert(err == 0);

      uv_stream_t *pipe;
      err = js_get_arraybuffer_info(env, property, (void **) &pipe, NULL);
      assert(err == 0);

      stdio[i].data.stream = pipe;
    }
  }

  bool detached;
  err = js_get_value_bool(env, argv[6], &detached);
  assert(err == 0);

  int32_t uid;
  err = js_get_value_int32(env, argv[7], &uid);
  assert(err == 0);

  int32_t gid;
  err = js_get_value_int32(env, argv[8], &gid);
  assert(err == 0);

  bool windows_hide;
  err = js_get_value_bool(env, argv[9], &windows_hide);
  assert(err == 0);

  bool windows_verbatim_arguments;
  err = js_get_value_bool(env, argv[10], &windows_verbatim_arguments);
  assert(err == 0);

  int flags = UV_PROCESS_WINDOWS_HIDE_CONSOLE;

  if (detached) flags |= UV_PROCESS_DETACHED;
  if (uid != -1) flags |= UV_PROCESS_SETUID;
  if (gid != -1) flags |= UV_PROCESS_SETGID;
  if (windows_hide) flags |= UV_PROCESS_WINDOWS_HIDE;
  if (windows_verbatim_arguments) flags |= UV_PROCESS_WINDOWS_VERBATIM_ARGUMENTS;

  uv_process_options_t opts = {
    .exit_cb = bare_subprocess__on_exit,
    .file = (char *) file,
    .args = (char **) args,
    .env = (char **) pairs,
    .cwd = (char *) cwd,
    .flags = flags,
    .stdio_count = stdio_len,
    .stdio = stdio,
    .uid = uid,
    .gid = gid,
  };

  err = uv_spawn(loop, &subprocess->handle, &opts);

  js_value_t *pid = NULL;

  if (err < 0) {
    err = js_throw_error(env, uv_err_name(err), uv_strerror(err));
    assert(err == 0);
  } else {
    err = js_create_uint32(env, subprocess->handle.pid, &pid);
    assert(err == 0);
  }

  for (uint32_t i = 0; i < args_len; i++) {
    free(args[i + 1]);
  }

  for (uint32_t i = 0; i < pairs_len; i++) {
    free(pairs[i]);
  }

  free(args);
  free(pairs);
  free(stdio);

  return pid;
}

static js_value_t *
bare_subprocess_spawn_sync(js_env_t *env, js_callback_info_t *info) {
  int err;

  size_t argc = 11;
  js_value_t *argv[11];

  err = js_get_callback_info(env, info, &argc, argv, NULL, NULL);
  assert(err == 0);

  assert(argc == 11);

  uv_loop_t loop;
  err = uv_loop_init(&loop);
  assert(err == 0);

  bare_subprocess_t *subprocess;
  err = js_get_arraybuffer_info(env, argv[0], (void **) &subprocess, NULL);
  assert(err == 0);

  bare_subprocess_path_t file;
  err = js_get_value_string_utf8(env, argv[1], file, sizeof(bare_subprocess_path_t), NULL);
  assert(err == 0);

  uint32_t args_len;
  err = js_get_array_length(env, argv[2], &args_len);
  assert(err == 0);

  utf8_t **args = calloc(1 /* file */ + args_len + 1 /* NULL */, sizeof(utf8_t *));

  args[0] = file;

  for (uint32_t i = 0; i < args_len; i++) {
    js_value_t *value;
    err = js_get_element(env, argv[2], i, &value);
    assert(err == 0);

    size_t arg_len;
    err = js_get_value_string_utf8(env, value, NULL, 0, &arg_len);
    assert(err == 0);

    arg_len += 1 /* NULL */;

    utf8_t *arg = malloc(arg_len);
    err = js_get_value_string_utf8(env, value, arg, arg_len, NULL);
    assert(err == 0);

    args[i + 1] = arg;
  }

  bare_subprocess_path_t cwd;
  err = js_get_value_string_utf8(env, argv[3], cwd, sizeof(bare_subprocess_path_t), NULL);
  assert(err == 0);

  uint32_t pairs_len;
  err = js_get_array_length(env, argv[4], &pairs_len);
  assert(err == 0);

  utf8_t **pairs = calloc(pairs_len + 1 /* NULL */, sizeof(utf8_t *));

  for (uint32_t i = 0; i < pairs_len; i++) {
    js_value_t *value;
    err = js_get_element(env, argv[4], i, &value);
    assert(err == 0);

    size_t pair_len;
    err = js_get_value_string_utf8(env, value, NULL, 0, &pair_len);
    assert(err == 0);

    pair_len += 1 /* NULL */;

    utf8_t *pair = malloc(pair_len);
    err = js_get_value_string_utf8(env, value, pair, pair_len, NULL);
    assert(err == 0);

    pairs[i] = pair;
  }

  uint32_t stdio_len;
  err = js_get_array_length(env, argv[5], &stdio_len);
  assert(err == 0);

  uv_stdio_container_t *stdio = malloc(stdio_len * sizeof(uv_stdio_container_t));

  for (uint32_t i = 0; i < stdio_len; i++) {
    js_value_t *value;
    err = js_get_element(env, argv[5], i, &value);
    assert(err == 0);

    js_value_t *property;

    err = js_get_named_property(env, value, "flags", &property);
    assert(err == 0);

    uint32_t flags;
    err = js_get_value_uint32(env, property, &flags);
    assert(err == 0);

    stdio[i] = (uv_stdio_container_t) {
      .flags = flags,
    };

    if (flags & UV_INHERIT_FD) {
      err = js_get_named_property(env, value, "fd", &property);
      assert(err == 0);

      uint32_t fd;
      err = js_get_value_uint32(env, property, &fd);
      assert(err == 0);

      stdio[i].data.fd = fd;
    }

    if (flags & UV_CREATE_PIPE) {
      err = js_get_named_property(env, value, "buffer", &property);
      assert(err == 0);

      bare_subprocess_buffered_pipe_t *pipe = malloc(sizeof(bare_subprocess_buffered_pipe_t));

      pipe->written = 0;

      err = uv_pipe_init(&loop, (uv_pipe_t *) pipe, false);
      assert(err == 0);

      err = js_get_typedarray_info(env, property, NULL, (void **) &pipe->read.base, (size_t *) &pipe->read.len, NULL, NULL);
      assert(err == 0);

      stdio[i].data.stream = (uv_stream_t *) pipe;
    }
  }

  bool detached;
  err = js_get_value_bool(env, argv[6], &detached);
  assert(err == 0);

  int32_t uid;
  err = js_get_value_int32(env, argv[7], &uid);
  assert(err == 0);

  int32_t gid;
  err = js_get_value_int32(env, argv[8], &gid);
  assert(err == 0);

  bool windows_hide;
  err = js_get_value_bool(env, argv[9], &windows_hide);
  assert(err == 0);

  bool windows_verbatim_arguments;
  err = js_get_value_bool(env, argv[10], &windows_verbatim_arguments);
  assert(err == 0);

  int flags = UV_PROCESS_WINDOWS_HIDE_CONSOLE;

  if (detached) flags |= UV_PROCESS_DETACHED;
  if (uid != -1) flags |= UV_PROCESS_SETUID;
  if (gid != -1) flags |= UV_PROCESS_SETGID;
  if (windows_hide) flags |= UV_PROCESS_WINDOWS_HIDE;
  if (windows_verbatim_arguments) flags |= UV_PROCESS_WINDOWS_VERBATIM_ARGUMENTS;

  uv_process_options_t opts = {
    .exit_cb = bare_subprocess__on_exit,
    .file = (char *) file,
    .args = (char **) args,
    .env = (char **) pairs,
    .cwd = (char *) cwd,
    .flags = flags,
    .stdio_count = stdio_len,
    .stdio = stdio,
    .uid = uid,
    .gid = gid,
  };

  err = uv_spawn(&loop, &subprocess->handle, &opts);

  js_value_t *pid = NULL;

  int throw = err;

  if (throw < 0) {
    uv_close((uv_handle_t *) &subprocess->handle, bare_subprocess__on_close);

    for (uint32_t i = 0; i < stdio_len; i++) {
      if (stdio[i].flags & UV_CREATE_PIPE) {
        bare_subprocess_buffered_pipe_t *pipe = (bare_subprocess_buffered_pipe_t *) stdio[i].data.stream;

        uv_close((uv_handle_t *) pipe, NULL);
      }
    }
  } else {
    for (uint32_t i = 0; i < stdio_len; i++) {
      if (stdio[i].flags & UV_CREATE_PIPE) {
        bare_subprocess_buffered_pipe_t *pipe = (bare_subprocess_buffered_pipe_t *) stdio[i].data.stream;

        err = uv_read_start((uv_stream_t *) pipe, bare_subprocess__on_alloc, bare_subprocess__on_read);
        assert(err == 0);
      }
    }

    err = js_create_uint32(env, subprocess->handle.pid, &pid);
    assert(err == 0);
  }

  err = uv_run(&loop, UV_RUN_DEFAULT);
  assert(err == 0);

  for (uint32_t i = 0; i < stdio_len; i++) {
    js_value_t *value;
    err = js_get_element(env, argv[5], i, &value);
    assert(err == 0);

    js_value_t *property;

    if (stdio[i].flags & UV_CREATE_PIPE) {
      bare_subprocess_buffered_pipe_t *pipe = (bare_subprocess_buffered_pipe_t *) stdio[i].data.stream;

      js_value_t *written;
      err = js_create_int64(env, pipe->written, &written);
      assert(err == 0);

      err = js_set_named_property(env, value, "written", written);
      assert(err == 0);

      free(pipe);
    }
  }

  err = uv_loop_close(&loop);
  assert(err == 0);

  for (uint32_t i = 0; i < args_len; i++) {
    free(args[i + 1]);
  }

  for (uint32_t i = 0; i < pairs_len; i++) {
    free(pairs[i]);
  }

  free(args);
  free(pairs);
  free(stdio);

  if (throw < 0) {
    err = js_throw_error(env, uv_err_name(throw), uv_strerror(throw));
    assert(err == 0);

    return NULL;
  }

  return pid;
}

static js_value_t *
bare_subprocess_kill(js_env_t *env, js_callback_info_t *info) {
  int err;

  size_t argc = 2;
  js_value_t *argv[2];

  err = js_get_callback_info(env, info, &argc, argv, NULL, NULL);
  assert(err == 0);

  assert(argc == 2);

  bare_subprocess_t *subprocess;
  err = js_get_arraybuffer_info(env, argv[0], (void **) &subprocess, NULL);
  assert(err == 0);

  if (subprocess->killed) return NULL;

  uint32_t signum;
  err = js_get_value_uint32(env, argv[1], &signum);
  assert(err == 0);

  subprocess->killed = true;

  err = uv_process_kill(&subprocess->handle, signum);

  if (err < 0) {
    err = js_throw_error(env, uv_err_name(err), uv_strerror(err));
    assert(err == 0);
  }

  return NULL;
}

static js_value_t *
bare_subprocess_ref(js_env_t *env, js_callback_info_t *info) {
  int err;

  size_t argc = 1;
  js_value_t *argv[1];

  err = js_get_callback_info(env, info, &argc, argv, NULL, NULL);
  assert(err == 0);

  assert(argc == 1);

  bare_subprocess_t *subprocess;
  err = js_get_arraybuffer_info(env, argv[0], (void **) &subprocess, NULL);
  assert(err == 0);

  uv_ref((uv_handle_t *) &subprocess->handle);

  return NULL;
}

static js_value_t *
bare_subprocess_unref(js_env_t *env, js_callback_info_t *info) {
  int err;

  size_t argc = 1;
  js_value_t *argv[1];

  err = js_get_callback_info(env, info, &argc, argv, NULL, NULL);
  assert(err == 0);

  assert(argc == 1);

  bare_subprocess_t *subprocess;
  err = js_get_arraybuffer_info(env, argv[0], (void **) &subprocess, NULL);
  assert(err == 0);

  uv_unref((uv_handle_t *) &subprocess->handle);

  return NULL;
}

static js_value_t *
bare_subprocess_exports(js_env_t *env, js_value_t *exports) {
  int err;

#define V(name, fn) \
  { \
    js_value_t *val; \
    err = js_create_function(env, name, -1, fn, NULL, &val); \
    assert(err == 0); \
    err = js_set_named_property(env, exports, name, val); \
    assert(err == 0); \
  }

  V("init", bare_subprocess_init)
  V("spawn", bare_subprocess_spawn)
  V("spawnSync", bare_subprocess_spawn_sync)
  V("kill", bare_subprocess_kill)
  V("ref", bare_subprocess_ref)
  V("unref", bare_subprocess_unref)
#undef V

#define V(name) \
  { \
    js_value_t *val; \
    err = js_create_uint32(env, name, &val); \
    assert(err == 0); \
    err = js_set_named_property(env, exports, #name, val); \
    assert(err == 0); \
  }

  V(UV_IGNORE)
  V(UV_CREATE_PIPE)
  V(UV_INHERIT_FD)
  V(UV_INHERIT_STREAM)
  V(UV_READABLE_PIPE)
  V(UV_WRITABLE_PIPE)
  V(UV_NONBLOCK_PIPE)
#undef V

  return exports;
}

BARE_MODULE(bare_subprocess, bare_subprocess_exports)
