#include <rlang.h>
#include "utils.h"


#define BUFSIZE 8192

#define INTERP(BUF, FMT, DOTS)                  \
  {                                             \
    va_list dots;                               \
    va_start(dots, FMT);                        \
    vsnprintf(BUF, BUFSIZE, FMT, dots);         \
    va_end(dots);                               \
                                                \
    BUF[BUFSIZE - 1] = '\0';                    \
  }

#include "decl/cnd-decl.h"


r_obj* ffi_format_error_arg(r_obj* arg) {
  switch (r_typeof(arg)) {
  case R_TYPE_symbol: arg = r_sym_as_utf8_character(arg); break;
  case R_TYPE_string: arg = r_str_as_character(arg); break;
  case R_TYPE_call: arg = r_as_label(arg); break;
  case R_TYPE_character: if (r_length(arg) == 1) break; else goto error;
  default: error: r_abort("`arg` must be a string or an expression.");
  }
  KEEP(arg);

  r_obj* out = r_eval_with_x(format_arg_call, arg, rlang_ns_env);

  FREE(1);
  return out;
}

const char* rlang_format_error_arg(r_obj* arg) {
  arg = KEEP(ffi_format_error_arg(arg));

  const char* arg_str = r_chr_get_c_string(arg, 0);
  int n = strlen(arg_str) + 1;

  // Uses the vmax protection stack.
  char* out = R_alloc(n, sizeof(char));
  r_memcpy(out, arg_str, n);

  FREE(1);
  return out;
}


struct without_winch_data {
  r_obj* old_on_error;
  r_obj* old_use_winch;
};
struct stop_internal_data {
  const char* file;
  int line;
  r_obj* call;
  const char* msg;
};

r_no_return
void rlang_stop_internal2(const char* file,
                          int line,
                          r_obj* call,
                          const char* fmt,
                          ...) {
  R_CheckStack2(BUFSIZE);
  char msg[BUFSIZE];
  INTERP(msg, fmt, ...);

  struct stop_internal_data stop_internal_data = {
    .file = file,
    .line = line,
    .call = call,
    .msg = msg
  };

  struct r_pair_callback with_winch_data = {
    .fn = &stop_internal_cb,
    .data = &stop_internal_data
  };
  struct without_winch_data without_winch_data = {
    .old_on_error = KEEP(r_peek_option("rlang_backtrace_on_error")),
    .old_use_winch = KEEP(r_peek_option("rlang_trace_use_winch"))
  };

  R_ExecWithCleanup(&with_winch, &with_winch_data,
                    &without_winch, &without_winch_data);

  r_abort("unreachable");
}

// For compatibility, exported as C callable `rlang_stop_internal`
r_no_return
void rlang_stop_internal(const char* fn,
                         const char* fmt,
                         ...) {
  R_CheckStack2(BUFSIZE);
  char msg[BUFSIZE];
  INTERP(msg, fmt, ...);

  r_obj* call = KEEP(r_call(r_sym(fn)));

  rlang_stop_internal2("", -1, call, msg);
  r_abort("unreachable");
}

static
r_no_return
r_obj* stop_internal_cb(void* payload) {
  struct stop_internal_data* data = (struct stop_internal_data*) payload;

  struct r_pair args[] = {
    { r_sym("file"), KEEP(r_chr(data->file)) },
    { r_sym("line"), KEEP(r_int(data->line)) },
    { r_sym("call"), data->call },
    { r_sym("message"), KEEP(r_chr(data->msg)) },
    { r_sym("frame"), KEEP(r_peek_frame()) }
  };

  r_exec_mask_n(r_null, r_sym("stop_internal_c_lib"),
                args, R_ARR_SIZEOF(args),
                rlang_ns_env);
  r_abort("unreachable");
}

static
r_obj* with_winch(void* payload) {
  r_poke_option("rlang_backtrace_on_error", r_chrs.full);
  r_poke_option("rlang_trace_use_winch", r_true);

  struct r_pair_callback* data = (struct r_pair_callback*) payload;
  return data->fn(data->data);
}
static
void without_winch(void* payload) {
  struct without_winch_data* data = (struct without_winch_data*) payload;

  r_poke_option("rlang_backtrace_on_error", data->old_on_error);
  r_poke_option("rlang_trace_use_winch", data->old_use_winch);
}

r_obj* ffi_test_stop_internal(void) {
  r_stop_internal("foo");
  return r_null;
}


// Probably should be implemented at R level
r_obj* ffi_new_condition(r_obj* class,
                         r_obj* msg,
                         r_obj* data) {
  if (msg == r_null) {
    msg = r_chrs.empty_string;
  } else if (r_typeof(msg) != R_TYPE_character) {
    const char* arg = r_format_error_arg(r_sym("message"));
    const char* what = r_obj_type_friendly(msg);
    r_abort("%s must be a character vector, not %s.", arg, what);
  }

  if (r_typeof(class) != R_TYPE_character) {
    const char* arg = r_format_error_arg(r_sym("class"));
    const char* what = r_obj_type_friendly(class);
    r_abort("%s must be a character vector, not %s.", arg, what);
  }

  r_ssize n_data = r_length(data);
  r_obj* cnd = KEEP(r_alloc_list(n_data + 1));

  r_list_poke(cnd, 0, msg);
  r_vec_poke_n(cnd, 1, data, 0, r_length(cnd) - 1);

  r_attrib_poke_names(cnd, KEEP(new_condition_names(data)));
  r_attrib_poke_class(cnd, KEEP(chr_append(class, KEEP(r_str("condition")))));

  r_obj* cnd_nms = KEEP(r_names(cnd));
  if (Rf_any_duplicated(cnd_nms, FALSE)) {
    r_abort("Condition fields can't have the same name.");
  }

  FREE(5);
  return cnd;
}
static
r_obj* new_condition_names(r_obj* data) {
  if (!r_is_named(data)) {
    r_abort("Conditions must have named data fields");
  }

  r_obj* data_nms = KEEP(r_names(data));

  if (r_chr_has_any(data_nms, (const char* []) { "message", NULL })) {
    r_abort("Conditions can't have a `message` data field");
  }

  r_obj* nms = KEEP(r_alloc_character(r_length(data) + 1));
  r_chr_poke(nms, 0, r_str("message"));
  r_vec_poke_n(nms, 1, data_nms, 0, r_length(nms) - 1);

  FREE(2);
  return nms;
}

// `length` is no longer a valid argument
const char* rlang_obj_type_friendly_full(r_obj* x, bool value, bool _length) {
  r_obj* out_obj = KEEP(r_eval_with_xy(obj_type_friendly_call,
                                       x,
                                       r_lgl(value),
                                       rlang_ns_env));

  if (!r_is_string(out_obj)) {
    r_stop_unexpected_type(r_typeof(out_obj));
  }
  const char* out_str = r_chr_get_c_string(out_obj, 0);

  // Uses the vmax protection stack.
  int n = strlen(out_str) + 1;
  char* out = R_alloc(n, sizeof(char));
  r_memcpy(out, out_str, n);

  FREE(1);
  return out;
}

void rlang_init_cnd(r_obj* ns) {
  format_arg_call = r_parse("format_arg(x)");
  r_preserve(format_arg_call);

  obj_type_friendly_call = r_parse("obj_type_friendly(x, y)");
  r_preserve(obj_type_friendly_call);
}

static
r_obj* format_arg_call = NULL;

static
r_obj* obj_type_friendly_call = NULL;
