#include "ruby.h"
#include "node.h"
#include "yarvcore.h"

VALUE yarv_new_iseqval(VALUE node, VALUE name, VALUE file,
		       VALUE parent, VALUE type, VALUE block_opt, VALUE opt);

static VALUE
yarv_iseq_special_block(yarv_iseq_t *iseq, void *builder)
{
#if OPT_BLOCKINLINING
    VALUE parent = Qfalse;
    VALUE iseqval;
    if (iseq->argc > 1 || iseq->arg_simple == 0) {
	/* argument check */
	return 0;
    }

    if (iseq->cached_special_block_builder) {
	if (iseq->cached_special_block_builder == builder) {
	    return iseq->cached_special_block;
	}
	else {
	    return 0;
	}
    }
    else {
	iseq->cached_special_block_builder = (void *)1;
    }
    if (iseq->parent_iseq) {
	parent = iseq->parent_iseq->self;
    }
    iseqval = yarv_new_iseqval((VALUE)iseq->node, iseq->name,
			       iseq->file_name, parent, iseq->type,
			       GC_GUARDED_PTR(builder), Qnil);
    if (0) {
	printf("%s\n", RSTRING(iseq_disasm(iseqval))->ptr);
    }
    iseq->cached_special_block = iseqval;
    iseq->cached_special_block_builder = builder;
    return iseqval;
#else
    return 0;
#endif
}

static NODE *
new_block(NODE * head, NODE * tail)
{
    head = NEW_BLOCK(head);
    tail = NEW_BLOCK(tail);
    head->nd_next = tail;
    return head;
}

static NODE *
new_ary(NODE * head, NODE * tail)
{
    head = NEW_ARRAY(head);
    head->nd_next = tail;
    return head;
}

static NODE *
new_assign(NODE * lnode, NODE * rhs)
{
    switch (nd_type(lnode)) {
      case NODE_LASGN:{
	  return NEW_NODE(NODE_LASGN, lnode->nd_vid, rhs, lnode->nd_cnt);
	  /* NEW_LASGN(lnode->nd_vid, rhs); */
      }
      case NODE_GASGN:{
	  return NEW_GASGN(lnode->nd_vid, rhs);
      }
      case NODE_DASGN:{
	  return NEW_DASGN(lnode->nd_vid, rhs);
      }
      case NODE_ATTRASGN:{
	  NODE *args = 0;
	  if (lnode->nd_args) {
	      args = NEW_ARRAY(lnode->nd_args->nd_head);
	      args->nd_next = NEW_ARRAY(rhs);
	      args->nd_alen = 2;
	  }
	  else {
	      args = NEW_ARRAY(rhs);
	  }

	  return NEW_ATTRASGN(lnode->nd_recv,
			      lnode->nd_mid,
			      args);
      }
      default:
	rb_bug("unimplemented (block inlining): %s", node_name(nd_type(lnode)));
    }
    return 0;
}

static NODE *
build_Integer_times_node(yarv_iseq_t *iseq, NODE * node, NODE * lnode,
			 VALUE param_vars, VALUE local_vars)
{
    /* Special Block for Integer#times
       {|e, _self|
       _e = e
       while(e < _self)
       e = _e
       redo_point:
       BODY
       next_point:
       _e = _e.succ
       end
       }

       {|e, _self|
       while(e < _self)
       BODY
       next_point:
       e = e.succ
       end
       }
     */
    ID _self = rb_intern("#_self");
    if (iseq->argc == 0) {
	ID e = rb_intern("#e");
	rb_ary_push(param_vars, ID2SYM(e));
	rb_ary_push(param_vars, ID2SYM(_self));
	iseq->argc += 2;

	node =
	    NEW_WHILE(NEW_CALL
		      (NEW_DVAR(e), idLT, new_ary(NEW_DVAR(_self), 0)),
		      new_block(NEW_OPTBLOCK(node),
				NEW_DASGN(e,
					  NEW_CALL(NEW_DVAR(e), idSucc, 0))),
		      Qundef);
    }
    else {
	ID _e = rb_intern("#_e");
	ID e = SYM2ID(rb_ary_entry(param_vars, 0));
	NODE *assign;

	rb_ary_push(param_vars, ID2SYM(_self));
	rb_ary_push(local_vars, ID2SYM(_e));
	iseq->argc++;

	if (nd_type(lnode) == NODE_DASGN_CURR) {
	    assign = NEW_DASGN(e, NEW_DVAR(_e));
	}
	else {
	    assign = new_assign(lnode, NEW_DVAR(_e));
	}

	node =
	    new_block(NEW_DASGN(_e, NEW_DVAR(e)),
		      NEW_WHILE(NEW_CALL
				(NEW_DVAR(_e), idLT,
				 new_ary(NEW_DVAR(_self), 0)),
				new_block(assign,
					  new_block(NEW_OPTBLOCK(node),
						    NEW_DASGN(_e,
							      NEW_CALL
							      (NEW_DVAR(_e),
							       idSucc, 0)))),
				Qundef));
    }
    return node;
}

VALUE
yarv_invoke_Integer_times_special_block(VALUE num)
{
    yarv_thread_t *th = GET_THREAD();
    yarv_block_t *orig_block = GC_GUARDED_PTR_REF(th->cfp->lfp[0]);

    if (orig_block && BUILTIN_TYPE(orig_block->iseq) != T_NODE) {
	VALUE tsiseqval = yarv_iseq_special_block(orig_block->iseq,
						  build_Integer_times_node);
	yarv_iseq_t *tsiseq;
	VALUE argv[2], val;

	if (tsiseqval) {
	    yarv_block_t block = *orig_block;
	    GetISeqPtr(tsiseqval, tsiseq);
	    block.iseq = tsiseq;
	    th->cfp->lfp[0] = GC_GUARDED_PTR(&block);
	    argv[0] = INT2FIX(0);
	    argv[1] = num;
	    val = th_invoke_yield(th, 2, argv);
	    if (val == Qundef) {
		return num;
	    }
	    else {
		return val;
	    }
	}
    }
    return Qundef;
}

static NODE *
build_Range_each_node(yarv_iseq_t *iseq, NODE * node, NODE * lnode,
		      VALUE param_vars, VALUE local_vars, ID mid)
{
    /* Special Block for Range#each
       {|e, _last|
       _e = e
       while _e < _last
       e = _e
       next_point:
       BODY
       redo_point:
       _e = _e.succ
       end
       }
       {|e, _last|
       while e < _last
       BODY
       redo_point:
       e = e.succ
       end
       }
     */
    ID _last = rb_intern("#_last");
    if (iseq->argc == 0) {
	ID e = rb_intern("#e");
	rb_ary_push(param_vars, ID2SYM(e));
	rb_ary_push(param_vars, ID2SYM(_last));
	iseq->argc += 2;

	node =
	    NEW_WHILE(NEW_CALL(NEW_DVAR(e), mid, new_ary(NEW_DVAR(_last), 0)),
		      new_block(NEW_OPTBLOCK(node),
				NEW_DASGN(e,
					  NEW_CALL(NEW_DVAR(e), idSucc, 0))),
		      Qundef);
    }
    else {
	ID _e = rb_intern("#_e");
	ID e = SYM2ID(rb_ary_entry(param_vars, 0));
	NODE *assign;

	rb_ary_push(param_vars, ID2SYM(_last));
	rb_ary_push(local_vars, ID2SYM(_e));
	iseq->argc++;

	if (nd_type(lnode) == NODE_DASGN_CURR) {
	    assign = NEW_DASGN(e, NEW_DVAR(_e));
	}
	else {
	    assign = new_assign(lnode, NEW_DVAR(_e));
	}

	node =
	    new_block(NEW_DASGN(_e, NEW_DVAR(e)),
		      NEW_WHILE(NEW_CALL
				(NEW_DVAR(_e), mid,
				 new_ary(NEW_DVAR(_last), 0)),
				new_block(assign,
					  new_block(NEW_OPTBLOCK(node),
						    NEW_DASGN(_e,
							      NEW_CALL
							      (NEW_DVAR(_e),
							       idSucc, 0)))),
				Qundef));
    }
    return node;
}

static NODE *
build_Range_each_node_LE(yarv_iseq_t *iseq, NODE * node, NODE * lnode,
			 VALUE param_vars, VALUE local_vars)
{
    return build_Range_each_node(iseq, node, lnode,
				 param_vars, local_vars, idLE);
}

static NODE *
build_Range_each_node_LT(yarv_iseq_t *iseq, NODE * node, NODE * lnode,
			 VALUE param_vars, VALUE local_vars)
{
    return build_Range_each_node(iseq, node, lnode,
				 param_vars, local_vars, idLT);
}

VALUE
yarv_invoke_Range_each_special_block(VALUE range,
				     VALUE beg, VALUE end, int excl)
{
    yarv_thread_t *th = GET_THREAD();
    yarv_block_t *orig_block = GC_GUARDED_PTR_REF(th->cfp->lfp[0]);

    if (BUILTIN_TYPE(orig_block->iseq) != T_NODE) {
	void *builder =
	    excl ? build_Range_each_node_LT : build_Range_each_node_LE;
	VALUE tsiseqval = yarv_iseq_special_block(orig_block->iseq, builder);
	yarv_iseq_t *tsiseq;
	VALUE argv[2];

	if (tsiseqval) {
	    VALUE val;
	    yarv_block_t block = *orig_block;
	    GetISeqPtr(tsiseqval, tsiseq);
	    block.iseq = tsiseq;
	    th->cfp->lfp[0] = GC_GUARDED_PTR(&block);
	    argv[0] = beg;
	    argv[1] = end;
	    val = th_invoke_yield(th, 2, argv);
	    if (val == Qundef) {
		return range;
	    }
	    else {
		return val;
	    }
	}
    }
    return Qundef;
}


static NODE *
build_Array_each_node(yarv_iseq_t *iseq, NODE * node, NODE * lnode,
		      VALUE param_vars, VALUE local_vars)
{
    /* Special block for Array#each
       ary.each{|e|
       BODY
       }
       =>
       {|e, _self|
       _i = 0
       while _i < _self.length
       e = _self[_i]
       redo_point:
       BODY
       next_point:
       _i = _i.succ
       end
       }

       ary.each{
       BODY
       }
       =>
       {|_i, _self|
       _i = 0
       while _i < _self.length
       redo_point:
       BODY
       next_point:
       _i = _i.succ
       end
       }
     */

    ID _self = rb_intern("#_self");
    ID _i = rb_intern("#_i");

    if (iseq->argc == 0) {
	ID _e = rb_intern("#_e");
	rb_ary_push(param_vars, ID2SYM(_e));
	rb_ary_push(param_vars, ID2SYM(_self));
	iseq->argc += 2;
	rb_ary_push(local_vars, ID2SYM(_i));

	node =
	    new_block(NEW_DASGN(_i, NEW_LIT(INT2FIX(0))),
		      NEW_WHILE(NEW_CALL(NEW_DVAR(_i), idLT,
					 new_ary(NEW_CALL
						 (NEW_DVAR(_self), idLength,
						  0), 0)),
				new_block(NEW_OPTBLOCK(node),
					  NEW_DASGN(_i,
						    NEW_CALL(NEW_DVAR(_i),
							     idSucc, 0))),
				Qundef));
    }
    else {
	ID e = SYM2ID(rb_ary_entry(param_vars, 0));
	NODE *assign;

	rb_ary_push(param_vars, ID2SYM(_self));
	iseq->argc++;
	rb_ary_push(local_vars, ID2SYM(_i));

	if (nd_type(lnode) == NODE_DASGN_CURR) {
	    assign = NEW_DASGN(e,
			       NEW_CALL(NEW_DVAR(_self), idAREF,
					new_ary(NEW_DVAR(_i), 0)));
	}
	else {
	    assign = new_assign(lnode,
				NEW_CALL(NEW_DVAR(_self), idAREF,
					 new_ary(NEW_DVAR(_i), 0)));
	}

	node =
	    new_block(NEW_DASGN(_i, NEW_LIT(INT2FIX(0))),
		      NEW_WHILE(NEW_CALL(NEW_DVAR(_i), idLT,
					 new_ary(NEW_CALL
						 (NEW_DVAR(_self), idLength,
						  0), 0)), new_block(assign,
								     new_block
								     (NEW_OPTBLOCK
								      (node),
								      NEW_DASGN
								      (_i,
								       NEW_CALL
								       (NEW_DVAR
									(_i),
									idSucc,
									0)))),
				Qundef));
    }
    return node;
}

VALUE
yarv_invoke_Array_each_special_block(VALUE ary)
{
    yarv_thread_t *th = GET_THREAD();
    yarv_block_t *orig_block = GC_GUARDED_PTR_REF(th->cfp->lfp[0]);

    if (BUILTIN_TYPE(orig_block->iseq) != T_NODE) {
	VALUE tsiseqval = yarv_iseq_special_block(orig_block->iseq,
						  build_Array_each_node);
	yarv_iseq_t *tsiseq;
	VALUE argv[2];

	if (tsiseqval) {
	    VALUE val;
	    yarv_block_t block = *orig_block;
	    GetISeqPtr(tsiseqval, tsiseq);
	    block.iseq = tsiseq;
	    th->cfp->lfp[0] = GC_GUARDED_PTR(&block);
	    argv[0] = 0;
	    argv[1] = ary;
	    val = th_invoke_yield(th, 2, argv);
	    if (val == Qundef) {
		return ary;
	    }
	    else {
		return val;
	    }
	}
    }
    return Qundef;
}
