#include "byebug.h"

#ifdef _WIN32
#include <ctype.h>
#endif

#if defined DOSISH
#define isdirsep(x) ((x) == '/' || (x) == '\\')
#else
#define isdirsep(x) ((x) == '/')
#endif

static VALUE cBreakpoint;
static int breakpoint_max;

static ID idEval;

static VALUE
eval_expression(VALUE args)
{
  return rb_funcall2(rb_mKernel, idEval, 2, RARRAY_PTR(args));
}

static void
mark_breakpoint(void *data)
{
  breakpoint_t *breakpoint = data;

  rb_gc_mark(breakpoint->source);
  rb_gc_mark(breakpoint->expr);
}

static const rb_data_type_t breakpoint_type = {
  "Byebug::Breakpoint",
  {mark_breakpoint, RUBY_TYPED_DEFAULT_FREE},
};

static breakpoint_t *
brkpt_ptr(VALUE self)
{
  breakpoint_t *breakpoint;

  TypedData_Get_Struct(self, breakpoint_t, &breakpoint_type, breakpoint);
  return breakpoint;
}

/*
 *  call-seq:
 *    breakpoint.enabled? -> bool
 *
 *  Returns +true+ if breakpoint is enabled, false otherwise.
 */
static VALUE
brkpt_enabled(VALUE self)
{
  breakpoint_t *breakpoint = brkpt_ptr(self);

  return breakpoint->enabled;
}

/*
 *  call-seq:
 *    breakpoint.enabled = true | false
 *
 *  Enables or disables breakpoint.
 */
static VALUE
brkpt_set_enabled(VALUE self, VALUE enabled)
{
  breakpoint_t *breakpoint = brkpt_ptr(self);

  return breakpoint->enabled = enabled;
}

/*
 *  call-seq:
 *    breakpoint.expr -> string
 *
 *  Returns a conditional expression which indicates when this breakpoint should
 *  be activated.
 */
static VALUE
brkpt_expr(VALUE self)
{
  breakpoint_t *breakpoint = brkpt_ptr(self);

  return breakpoint->expr;
}

/*
 *  call-seq:
 *    breakpoint.expr = string | nil
 *
 *  Sets or unsets the conditional expression which indicates when this
 *  breakpoint should be activated.
 */
static VALUE
brkpt_set_expr(VALUE self, VALUE expr)
{
  breakpoint_t *breakpoint = brkpt_ptr(self);

  breakpoint->expr = NIL_P(expr) ? expr : StringValue(expr);
  return expr;
}

/*
 *  call-seq:
 *    breakpoint.hit_condition -> symbol
 *
 *   Returns the hit condition of the breakpoint: +nil+ if it is an
 *   unconditional breakpoint, or :greater_or_equal, :equal or :modulo otherwise
 */
static VALUE
brkpt_hit_condition(VALUE self)
{
  breakpoint_t *breakpoint = brkpt_ptr(self);

  switch (breakpoint->hit_condition)
  {
    case HIT_COND_GE:
      return ID2SYM(rb_intern("greater_or_equal"));
    case HIT_COND_EQ:
      return ID2SYM(rb_intern("equal"));
    case HIT_COND_MOD:
      return ID2SYM(rb_intern("modulo"));
    case HIT_COND_NONE:
    default:
      return Qnil;
  }
}

/*
 *  call-seq:
 *    breakpoint.hit_condition = symbol
 *
 *  Sets the hit condition of the breakpoint which must be one of the following
 *  values:
 *
 *  +nil+ if it is an unconditional breakpoint, or
 *  :greater_or_equal(:ge), :equal(:eq), :modulo(:mod)
 */
static VALUE
brkpt_set_hit_condition(VALUE self, VALUE value)
{
  breakpoint_t *breakpoint = brkpt_ptr(self);
  ID id_value;

  if (NIL_P(value))
  {
    breakpoint->hit_condition = HIT_COND_NONE;
    return value;
  }

  id_value = rb_to_id(value);

  if (rb_intern("greater_or_equal") == id_value || rb_intern("ge") == id_value)
    breakpoint->hit_condition = HIT_COND_GE;
  else if (rb_intern("equal") == id_value || rb_intern("eq") == id_value)
    breakpoint->hit_condition = HIT_COND_EQ;
  else if (rb_intern("modulo") == id_value || rb_intern("mod") == id_value)
    breakpoint->hit_condition = HIT_COND_MOD;
  else
    rb_raise(rb_eArgError, "Invalid condition parameter");
  return value;
}

/*
 *  call-seq:
 *    breakpoint.hit_count -> int
 *
 *  Returns the number of times this breakpoint has been hit.
 */
static VALUE
brkpt_hit_count(VALUE self)
{
  breakpoint_t *breakpoint = brkpt_ptr(self);

  return INT2FIX(breakpoint->hit_count);
}

/*
 *  call-seq:
 *    breakpoint.hit_value -> int
 *
 *  Returns the hit value of the breakpoint, namely, a value to build a
 *  condition on the number of hits of the breakpoint.
 */
static VALUE
brkpt_hit_value(VALUE self)
{
  breakpoint_t *breakpoint = brkpt_ptr(self);

  return INT2FIX(breakpoint->hit_value);
}

/*
 *  call-seq:
 *    breakpoint.hit_value = int
 *
 *  Sets the hit value of the breakpoint. This allows the user to set conditions
 *  on the number of hits to enable/disable the breakpoint.
 */
static VALUE
brkpt_set_hit_value(VALUE self, VALUE value)
{
  breakpoint_t *breakpoint = brkpt_ptr(self);

  breakpoint->hit_value = FIX2INT(value);
  return value;
}

/*
 *  call-seq:
 *    breakpoint.id -> int
 *
 *  Returns the id of the breakpoint.
 */
static VALUE
brkpt_id(VALUE self)
{
  breakpoint_t *breakpoint = brkpt_ptr(self);

  return INT2FIX(breakpoint->id);
}

/*
 *  call-seq:
 *    breakpoint.pos -> string or int
 *
 *   Returns the position of this breakpoint, either a method name or a line
 *   number.
 */
static VALUE
brkpt_pos(VALUE self)
{
  breakpoint_t *breakpoint = brkpt_ptr(self);

  if (breakpoint->type == BP_METHOD_TYPE)
    return rb_str_new2(rb_id2name(breakpoint->pos.mid));
  else
    return INT2FIX(breakpoint->pos.line);
}

/*
 *  call-seq:
 *    breakpoint.source -> string
 *
 *  Returns the source file of the breakpoint.
 */
static VALUE
brkpt_source(VALUE self)
{
  breakpoint_t *breakpoint = brkpt_ptr(self);

  return breakpoint->source;
}

static VALUE
brkpt_create(VALUE klass)
{
  breakpoint_t *breakpoint;

  return TypedData_Make_Struct(klass, breakpoint_t, &breakpoint_type, breakpoint);
}

static VALUE
brkpt_initialize(VALUE self, VALUE source, VALUE pos, VALUE expr)
{
  breakpoint_t *breakpoint = brkpt_ptr(self);

  breakpoint->type = FIXNUM_P(pos) ? BP_POS_TYPE : BP_METHOD_TYPE;
  if (breakpoint->type == BP_POS_TYPE)
    breakpoint->pos.line = FIX2INT(pos);
  else
    breakpoint->pos.mid = SYM2ID(pos);

  breakpoint->id = ++breakpoint_max;
  breakpoint->source = StringValue(source);
  breakpoint->enabled = Qtrue;
  breakpoint->expr = NIL_P(expr) ? expr : StringValue(expr);
  breakpoint->hit_count = 0;
  breakpoint->hit_value = 0;
  breakpoint->hit_condition = HIT_COND_NONE;

  return Qnil;
}

static int
filename_cmp_impl(VALUE source, char *file)
{
  char *source_ptr, *file_ptr;
  long s_len, f_len, min_len;
  long s, f;
  int dirsep_flag = 0;

  s_len = RSTRING_LEN(source);
  f_len = strlen(file);
  min_len = s_len < f_len ? s_len : f_len;

  source_ptr = RSTRING_PTR(source);
  file_ptr = file;

  for (s = s_len - 1, f = f_len - 1;
       s >= s_len - min_len && f >= f_len - min_len; s--, f--)
  {
    if ((source_ptr[s] == '.' || file_ptr[f] == '.') && dirsep_flag)
      return 1;
    if (isdirsep(source_ptr[s]) && isdirsep(file_ptr[f]))
      dirsep_flag = 1;
#ifdef DOSISH_DRIVE_LETTER
    else if (s == 0)
      return (toupper(source_ptr[s]) == toupper(file_ptr[f]));
#endif
    else if (source_ptr[s] != file_ptr[f])
      return 0;
  }
  return 1;
}

static int
filename_cmp(VALUE source, char *file)
{
#ifdef _WIN32
  return filename_cmp_impl(source, file);
#else
#ifdef PATH_MAX
  char path[PATH_MAX + 1];

  path[PATH_MAX] = 0;
  return filename_cmp_impl(source, realpath(file, path) != NULL ? path : file);
#else
  char *path;
  int result;

  path = realpath(file, NULL);
  result = filename_cmp_impl(source, path == NULL ? file : path);
  free(path);
  return result;
#endif
#endif
}

static int
classname_cmp(VALUE name, VALUE klass)
{
  VALUE mod_name;
  VALUE class_name = NIL_P(name) ? rb_str_new2("main") : name;

  if (NIL_P(klass))
    return 0;

  mod_name = rb_mod_name(klass);
  return (!NIL_P(mod_name) && rb_str_cmp(class_name, mod_name) == 0);
}

static int
check_breakpoint_by_hit_condition(VALUE rb_breakpoint)
{
  breakpoint_t *breakpoint;

  if (NIL_P(rb_breakpoint))
    return 0;

  breakpoint = brkpt_ptr(rb_breakpoint);
  breakpoint->hit_count++;

  if (Qtrue != breakpoint->enabled)
    return 0;

  switch (breakpoint->hit_condition)
  {
    case HIT_COND_NONE:
      return 1;
    case HIT_COND_GE: {
      if (breakpoint->hit_count >= breakpoint->hit_value)
        return 1;
      break;
    }
    case HIT_COND_EQ: {
      if (breakpoint->hit_count == breakpoint->hit_value)
        return 1;
      break;
    }
    case HIT_COND_MOD: {
      if (breakpoint->hit_count % breakpoint->hit_value == 0)
        return 1;
      break;
    }
  }
  return 0;
}

static int
check_breakpoint_by_pos(VALUE rb_breakpoint, char *file, int line)
{
  breakpoint_t *breakpoint;

  if (NIL_P(rb_breakpoint))
    return 0;

  breakpoint = brkpt_ptr(rb_breakpoint);

  if (Qfalse == breakpoint->enabled || breakpoint->type != BP_POS_TYPE
      || breakpoint->pos.line != line)
    return 0;

  return filename_cmp(breakpoint->source, file);
}

static int
check_breakpoint_by_method(VALUE rb_breakpoint, VALUE klass, ID mid, VALUE self)
{
  breakpoint_t *breakpoint;

  if (NIL_P(rb_breakpoint))
    return 0;

  breakpoint = brkpt_ptr(rb_breakpoint);

  if (Qfalse == breakpoint->enabled || breakpoint->type != BP_METHOD_TYPE
      || breakpoint->pos.mid != mid)
    return 0;

  if (classname_cmp(breakpoint->source, klass)
      || ((rb_type(self) == T_CLASS || rb_type(self) == T_MODULE)
          && classname_cmp(breakpoint->source, self)))
    return 1;

  return 0;
}

static int
check_breakpoint_by_expr(VALUE rb_breakpoint, VALUE bind)
{
  breakpoint_t *breakpoint;
  VALUE args, expr_result;

  if (NIL_P(rb_breakpoint))
    return 0;

  breakpoint = brkpt_ptr(rb_breakpoint);

  if (Qfalse == breakpoint->enabled)
    return 0;

  if (NIL_P(breakpoint->expr))
    return 1;

  args = rb_ary_new3(2, breakpoint->expr, bind);
  expr_result = rb_protect(eval_expression, args, 0);

  return RTEST(expr_result);
}

extern VALUE
find_breakpoint_by_pos(VALUE breakpoints, VALUE source, VALUE pos, VALUE bind)
{
  VALUE breakpoint;
  char *file;
  int line;
  int i;

  file = RSTRING_PTR(source);
  line = FIX2INT(pos);
  for (i = 0; i < RARRAY_LENINT(breakpoints); i++)
  {
    breakpoint = rb_ary_entry(breakpoints, i);
    if (check_breakpoint_by_pos(breakpoint, file, line)
        && check_breakpoint_by_expr(breakpoint, bind)
        && check_breakpoint_by_hit_condition(breakpoint))
    {
      return breakpoint;
    }
  }
  return Qnil;
}

extern VALUE
find_breakpoint_by_method(VALUE breakpoints, VALUE klass, ID mid, VALUE bind,
                          VALUE self)
{
  VALUE breakpoint;
  int i;

  for (i = 0; i < RARRAY_LENINT(breakpoints); i++)
  {
    breakpoint = rb_ary_entry(breakpoints, i);
    if (check_breakpoint_by_method(breakpoint, klass, mid, self)
        && check_breakpoint_by_expr(breakpoint, bind)
        && check_breakpoint_by_hit_condition(breakpoint))
    {
      return breakpoint;
    }
  }
  return Qnil;
}

void
Init_byebug_breakpoint(VALUE mByebug)
{
  breakpoint_max = 0;

  cBreakpoint = rb_define_class_under(mByebug, "Breakpoint", rb_cObject);

  rb_define_alloc_func(cBreakpoint, brkpt_create);
  rb_define_method(cBreakpoint, "initialize", brkpt_initialize, 3);

  rb_define_method(cBreakpoint, "enabled?", brkpt_enabled, 0);
  rb_define_method(cBreakpoint, "enabled=", brkpt_set_enabled, 1);
  rb_define_method(cBreakpoint, "expr", brkpt_expr, 0);
  rb_define_method(cBreakpoint, "expr=", brkpt_set_expr, 1);
  rb_define_method(cBreakpoint, "hit_count", brkpt_hit_count, 0);
  rb_define_method(cBreakpoint, "hit_condition", brkpt_hit_condition, 0);
  rb_define_method(cBreakpoint, "hit_condition=", brkpt_set_hit_condition, 1);
  rb_define_method(cBreakpoint, "hit_value", brkpt_hit_value, 0);
  rb_define_method(cBreakpoint, "hit_value=", brkpt_set_hit_value, 1);
  rb_define_method(cBreakpoint, "id", brkpt_id, 0);
  rb_define_method(cBreakpoint, "pos", brkpt_pos, 0);
  rb_define_method(cBreakpoint, "source", brkpt_source, 0);

  idEval = rb_intern("eval");
}
