#include <mruby.h>
#include <mruby/array.h>
#include <mruby/class.h>
#include <mruby/error.h>
#include <mruby/hash.h>
#include <mruby/numeric.h>
#include <mruby/string.h>
#include <mruby/variable.h>
#include <mruby/internal.h>
#include <errno.h>
#include <string.h>

static const struct {
#define itsdefined(name, sym)   { sym, name },
  mrb_sym sym;
  int eno;
} e2c[] = {
#define itsnotdefined(name, sym)
#include "known_errors_def.cstub"
#undef itsdefined
#undef itsnotdefined
};

static const struct {
#define itsnotdefined(name, sym)   { sym },
  mrb_sym sym;
} noe2c[] = {
#define itsdefined(name, sym)
#include "known_errors_def.cstub"
#undef itsdefined
#undef itsnotdefined
};

#define ENTRY_SYM(e)    (e).sym

#define E2C_LEN         (sizeof(e2c) / sizeof(e2c[0]))
#define NOE2C_LEN       (sizeof(noe2c) / sizeof(noe2c[0]))

static struct RClass*
mrb_errno_define_exxx(mrb_state *mrb, mrb_sym name, int eno)
{
  struct RClass *errno_module = mrb_module_get_id(mrb, MRB_SYM(Errno));

  if (mrb_const_defined_at(mrb, mrb_obj_value(errno_module), name)) {
    mrb_value v = mrb_const_get(mrb, mrb_obj_value(errno_module), name);

    if (mrb_class_p(v)) {
      return mrb_class_ptr(v);
    }
  }

  struct RClass *sce_class = mrb_exc_get_id(mrb, MRB_SYM(SystemCallError));
  struct RClass *e = mrb_define_class_under_id(mrb, errno_module, name, sce_class);
  mrb_define_const_id(mrb, e, MRB_SYM(Errno), mrb_fixnum_value(eno));

  return e;
}

typedef mrb_sym sym_ref;
#define sym_ref_init(mrb, id) (id)
#define errno_name_matched_p(errentry, ref) ((errentry).sym == *(ref))

static mrb_bool
ary_included_in_head(mrb_state *mrb, mrb_value ary, mrb_value obj, mrb_ssize head)
{
  const mrb_value *p = RARRAY_PTR(ary);

  for (; head > 0; head--, p++) {
    if (mrb_obj_eq(mrb, obj, *p)) {
      return TRUE;
    }
  }

  return FALSE;
}

/*
 * Internal method used by the Errno module to check if a specific
 * error constant exists on this platform.
 */
static mrb_value
mrb_errno_defined_p(mrb_state *mrb, mrb_value self)
{
  mrb_sym name;
  mrb_get_args(mrb, "n", &name);
  const sym_ref ref = sym_ref_init(mrb, name);

  for (size_t i = 0; i < E2C_LEN; i++) {
    if (errno_name_matched_p(e2c[i], &ref)) {
      return mrb_true_value();
    }
  }

  for (size_t i = 0; i < NOE2C_LEN; i++) {
    if (errno_name_matched_p(noe2c[i], &ref)) {
      return mrb_true_value();
    }
  }

  return mrb_false_value();
}

/*
 * Internal method used by the Errno module to define errno classes
 * for error constants that exist on this platform.
 */
static mrb_value
mrb_errno_define(mrb_state *mrb, mrb_value self)
{
  mrb_sym name;
  mrb_get_args(mrb, "n", &name);
  const sym_ref ref = sym_ref_init(mrb, name);

  for (size_t i = 0; i < E2C_LEN; i++) {
    if (errno_name_matched_p(e2c[i], &ref)) {
      return mrb_obj_value(mrb_errno_define_exxx(mrb, name, e2c[i].eno));
    }
  }

  for (size_t i = 0; i < NOE2C_LEN; i++) {
    if (errno_name_matched_p(noe2c[i], &ref)) {
      struct RClass *errno_module = mrb_module_get_id(mrb, MRB_SYM(Errno));
      return mrb_obj_value(mrb_class_get_under_id(mrb, errno_module, MRB_SYM(NOERROR)));
    }
  }

  return mrb_nil_value();
}

/*
 * Internal method used by the Errno module to populate an array
 * with all errno symbols available on this platform.
 */
static mrb_value
mrb_errno_list(mrb_state *mrb, mrb_value self)
{
  mrb_value list;
  mrb_get_args(mrb, "A", &list);

  mrb_ary_modify(mrb, mrb_ary_ptr(list));
  mrb_ssize head = RARRAY_LEN(list);

  for (size_t i = 0; i < E2C_LEN; i++) {
    mrb_value id = mrb_symbol_value(ENTRY_SYM(e2c[i]));
    if (!ary_included_in_head(mrb, list, id, head)) {
      mrb_ary_push(mrb, list, id);
    }
  }

  for (size_t i = 0; i < NOE2C_LEN; i++) {
    mrb_value id = mrb_symbol_value(ENTRY_SYM(noe2c[i]));
    if (!ary_included_in_head(mrb, list, id, head)) {
      mrb_ary_push(mrb, list, id);
    }
  }

  return list;
}

static void
mrb_sce_init(mrb_state *mrb, mrb_value self, mrb_value m, mrb_value no)
{
  mrb_value str;
  char buf[20];

  if (!mrb_nil_p(no)) {
    size_t i;
    int n = (int)mrb_as_int(mrb, no);

    for (i=0; i < E2C_LEN; i++) {
      if (e2c[i].eno == n) {
        mrb_basic_ptr(self)->c = mrb_errno_define_exxx(mrb, ENTRY_SYM(e2c[i]), e2c[i].eno);
        str = mrb_str_new_cstr(mrb, strerror(n));
        break;
      }
    }
    if (i == E2C_LEN) {
      mrb_iv_set(mrb, self, MRB_SYM(errno), mrb_fixnum_value(n));
      str = mrb_str_new_cstr(mrb, "Unknown error: ");
      char *bp = mrb_int_to_cstr(buf, sizeof(buf), n, 10);
      mrb_str_cat2(mrb, str, bp);
    }
  }
  else {
    str = mrb_str_new_cstr(mrb, "unknown error");
  }
  if (!mrb_nil_p(m)) {
    mrb_str_cat2(mrb, str, " - ");
    mrb_str_append(mrb, str, m);
  }
  mrb_exc_mesg_set(mrb, mrb_exc_ptr(self), str);
}

/*
 * call-seq:
 *   errno_class.new(message = nil) -> errno_exception
 *
 * Creates a new instance of a specific errno exception class.
 * The optional message parameter provides additional context.
 *
 *   Errno::ENOENT.new                    #=> #<Errno::ENOENT: No such file or directory>
 *   Errno::ENOENT.new("custom message")  #=> #<Errno::ENOENT: No such file or directory - custom message>
 */
static mrb_value
mrb_exxx_init(mrb_state *mrb, mrb_value self)
{
  mrb_value m = mrb_nil_value();

  mrb_get_args(mrb, "|S", &m);
  mrb_sce_init(mrb, self, m, mrb_nil_value());
  return self;
}

/*
 * call-seq:
 *   SystemCallError.new(message)           -> system_call_error
 *   SystemCallError.new(errno)             -> system_call_error
 *   SystemCallError.new(message, errno)    -> system_call_error
 *
 * Creates a new SystemCallError exception. Can be called with:
 * - A message string only
 * - An errno number only
 * - Both a message string and errno number
 *
 *   SystemCallError.new("custom error")     #=> #<SystemCallError: custom error>
 *   SystemCallError.new(2)                  #=> #<SystemCallError: No such file or directory>
 *   SystemCallError.new("failed", 2)        #=> #<SystemCallError: No such file or directory - failed>
 */
static mrb_value
mrb_sce_init_m(mrb_state *mrb, mrb_value self)
{
  if (mrb_class(mrb, self) != mrb_exc_get_id(mrb, MRB_SYM(SystemCallError))) {
    return mrb_exxx_init(mrb, self);
  }

  mrb_value m, n;

  if (mrb_get_args(mrb, "o|o", &m, &n) == 1) {
    if (mrb_fixnum_p(m)) {
      n = m;
      m = mrb_nil_value();
    }
    else {
      n = mrb_nil_value();
    }
  }
  mrb_sce_init(mrb, self, m, n);
  return self;
}

/*
 * call-seq:
 *   system_call_error.errno -> integer or nil
 *
 * Returns the errno number associated with this SystemCallError.
 * Returns nil if no errno was set.
 *
 *   begin
 *     File.open("/nonexistent")
 *   rescue SystemCallError => e
 *     e.errno  #=> 2 (ENOENT)
 *   end
 */
static mrb_value
mrb_sce_errno(mrb_state *mrb, mrb_value self)
{
  struct RClass *c;
  mrb_sym sym;

  c = mrb_class(mrb, self);
  sym = MRB_SYM(Errno);
  if (mrb_const_defined_at(mrb, mrb_obj_value(c), sym)) {
    return mrb_const_get(mrb, mrb_obj_value(c), sym);
  }
  else {
    sym = MRB_SYM(errno);
    return mrb_attr_get(mrb, self, sym);
  }
}

/*
 * call-seq:
 *   SystemCallError._sys_fail(errno, message = nil)
 *
 * Internal method that raises a SystemCallError with the given errno
 * and optional message. This method does not return as it raises an exception.
 *
 *   SystemCallError._sys_fail(2)              # raises Errno::ENOENT
 *   SystemCallError._sys_fail(2, "failed")    # raises Errno::ENOENT with message
 */
static mrb_value
mrb_sce_sys_fail(mrb_state *mrb, mrb_value cls)
{
  mrb_value msg, no;

  mrb->c->ci->mid = 0;
  struct RClass *sce = mrb_class_ptr(cls);
  mrb_int argc = mrb_get_args(mrb, "o|S", &no, &msg);

  struct RBasic* e = mrb_obj_alloc(mrb, MRB_TT_EXCEPTION, sce);
  mrb_value exc = mrb_obj_value(e);
  if (argc == 1) {
    msg = mrb_nil_value();
  }
  mrb_sce_init(mrb, exc, msg, no);
  mrb_exc_raise(mrb, exc);
  return mrb_nil_value();  /* NOTREACHED */
}

void
mrb_mruby_errno_gem_init(mrb_state *mrb)
{
  struct RClass *sce = mrb_define_class_id(mrb, MRB_SYM(SystemCallError), E_STANDARD_ERROR);
  mrb_define_class_method_id(mrb, sce, MRB_SYM(_sys_fail), mrb_sce_sys_fail, MRB_ARGS_REQ(1));
  mrb_define_method_id(mrb, sce, MRB_SYM(errno), mrb_sce_errno, MRB_ARGS_NONE());
  mrb_define_method_id(mrb, sce, MRB_SYM(initialize), mrb_sce_init_m, MRB_ARGS_OPT(2));

  struct RClass *eno = mrb_define_module_id(mrb, MRB_SYM(Errno));
  mrb_define_class_method_id(mrb, eno, MRB_SYM_Q(__errno_defined), mrb_errno_defined_p, MRB_ARGS_REQ(1));
  mrb_define_class_method_id(mrb, eno, MRB_SYM(__errno_define), mrb_errno_define, MRB_ARGS_REQ(1));
  mrb_define_class_method_id(mrb, eno, MRB_SYM(__errno_list), mrb_errno_list, MRB_ARGS_REQ(1));

  struct RClass *e = mrb_define_class_under_id(mrb, eno, MRB_SYM(NOERROR), sce);
  mrb_define_const_id(mrb, e, MRB_SYM(Errno), mrb_fixnum_value(0));
  //mrb_define_method_id(mrb, e, MRB_SYM(eqq), mrb_exxx_cmp, MRB_ARGS_REQ(1));

  // Pre-allocation for Errno::ENOMEM only
  mrb_errno_define_exxx(mrb, MRB_SYM(ENOMEM), ENOMEM);
}

void
mrb_mruby_errno_gem_final(mrb_state *mrb)
{
}
