#include <ruby.h>
#include <intern.h>

static VALUE rb_mXTemplate;
static VALUE rb_mXPath;
static VALUE rb_cXNode;

static VALUE rb_mUtil;
static VALUE rb_cSanitizedString;
static VALUE PredefinedEntity;
static VALUE PredefinedStringsRegex;
static VALUE PredefinedEntitiesRegex;
static VALUE RevPredefinedEntity;
static VALUE EntityAmp;
static VALUE SanitizedAmp;
static VALUE empty_str;
static VALUE quot_regexp;

static ID i_to_s, i_gsub, i_new, i_dup, i_gtgt, i_collect, i_strip;
static ID ii_name, ii_attrs, ii_children, ii_data_path, ii_propagation;
static ID ii_exname, ii_exattr, ii_expand, ii_option;


static VALUE
rb_xt_sanitize_i(VALUE obj)
{
    return rb_funcall(obj, i_gsub, 1, PredefinedStringsRegex);
}

static VALUE
rb_xt_sanitize_ii(VALUE s, VALUE data)
{
    if( strcmp(RSTRING(s)->ptr, RSTRING(EntityAmp)->ptr) == 0 ){
	return s;
    }
    else{
	return rb_hash_aref(RevPredefinedEntity, s);
    }
}

static VALUE
rb_xt_sanitize(VALUE m, VALUE obj)
{
    VALUE str;
    if( rb_obj_is_kind_of(obj, rb_cSanitizedString) ){
	return obj;
    }
    else{
	obj = rb_funcall(obj, i_to_s, 0);
	obj = rb_funcall(obj, i_gsub, 2, EntityAmp, SanitizedAmp);
	obj = rb_iterate(rb_xt_sanitize_i, obj, rb_xt_sanitize_ii, Qnil);
	return rb_funcall(rb_cSanitizedString, i_new, 1, obj);
    }
}

static VALUE
rb_xt_unsanitize_i(VALUE obj)
{
    return rb_funcall(obj, i_gsub, 1, PredefinedEntitiesRegex);
}

static VALUE
rb_xt_unsanitize_ii(VALUE s, VALUE data)
{
    return rb_hash_aref(PredefinedEntity, s);
}

static VALUE
rb_xt_unsanitize(VALUE m, VALUE str)
{
    if( rb_obj_is_kind_of(str, rb_cSanitizedString) ){
	str = rb_iterate(rb_xt_unsanitize_i, str, rb_xt_unsanitize_ii, Qnil);
	return rb_funcall(rb_cString, i_new, 1, str);
    }
    else{
	return str;
    }
}

static VALUE
rb_xt_path_split(VALUE m, VALUE path)
{
    char *str;
    int len;
    int i;
    int l;
    int s;
    VALUE ids;

    Check_Type(path, T_STRING);
    len = RSTRING(path)->len;
    str = ALLOCA_N(char,len+1);
    memcpy(str,RSTRING(path)->ptr,len+1);
    ids = rb_ary_new();
    l = 0;
    s = 0;
    for( i=0; i < len; i++ ){
	switch( str[i] ){
	case '{':
	case '[':
	    l++;
	    break;
	case '}':
	case ']':
	    l--;
	    break;
	case '/':
	    if( l == 0 ){
		rb_ary_push(ids, rb_tainted_str_new(str + s, i - s));
		s = i + 1;
	    }
	    break;
	}
    }
    rb_ary_push(ids, rb_tainted_str_new(str + s, i - s));
    if( str[0] == '/' ){
	rb_ary_store(ids, 0, rb_const_get(rb_mXPath, rb_intern("RootNode")));
    }
    return ids;
}

static VALUE
rb_xt_args_split(VALUE m, VALUE path)
{
    char *str;
    int len;
    int i;
    int l;
    int s;
    VALUE ids;
    VALUE v;
    int escape, inref;

    Check_Type(path, T_STRING);
    path = rb_xt_unsanitize(m,path);
    len = RSTRING(path)->len;
    if( len == 0 ){
	return rb_ary_new();
    }
    str = ALLOCA_N(char,len+1);
    memcpy(str,RSTRING(path)->ptr,len+1);
    ids = rb_ary_new();
    l = 0;
    s = 0;
    escape = 0;
    inref  = 0;
    for( i=0; i < len; i++ ){
	switch( str[i] ){
	case '\'':
	case '"':
	    if( escape ){
		escape = 0;
	    }
	    else{
		if( l ){
		    l = 0;
		}
		else{
		    l = 1;
		}
	    }
	    break;
	case '\\':
	    escape = 1;
	    break;
	case ',':
	    if( !l ){
		v = rb_tainted_str_new(str + s, i - s);
		v = rb_funcall(v, i_strip, 0);
		v = rb_funcall(v, i_gsub, 2, quot_regexp, empty_str);
		if( RSTRING(v)->len > 0 )
		    rb_ary_push(ids, v);
		s = i + 1;
	    }
	    break;
	}
    }
    v = rb_tainted_str_new(str + s, i - s);
    v = rb_funcall(v, i_strip, 0);
    v = rb_funcall(v, i_gsub, 2, quot_regexp, empty_str);
    if( RSTRING(v)->len > 0 )
	rb_ary_push(ids, v);
    return ids;
}

static VALUE
rb_xt_cond_split(VALUE m, VALUE path)
{
    char *str;
    int len;
    int i;
    int l;
    int s;
    VALUE xs;

    Check_Type(path, T_STRING);
    len = RSTRING(path)->len;
    str = ALLOCA_N(char,len+1);
    memcpy(str,RSTRING(path)->ptr,len+1);
    xs = rb_ary_new();
    l = 0;
    s = 0;
    for( i=0; i < len; i++ ){
	switch( str[i] ){
	case '{':
	case '[':
	    if( l == 0 ){
		if( i == 0 ){
		    rb_ary_push(xs, rb_str_new2(""));
		}
		else if( i != s ){
		    VALUE tmp = rb_tainted_str_new(str + s, i - s);
		    if( OBJ_TAINTED(str) ){ OBJ_TAINT(tmp); };
		    rb_ary_push(xs, tmp);
		}
		s = i;
	    }
	    l ++;
	    break;
	case '}':
	case ']':
	    l--;
	    if( l == 0 ){
		VALUE tmp = rb_tainted_str_new(str + s, i - s + 1);
		if( OBJ_TAINTED(str) ){ OBJ_TAINT(tmp); };
		rb_ary_push(xs, tmp);
		s = i + 1;
	    }
	    break;
	}
    }
    if( s != i ){
	VALUE tmp = rb_tainted_str_new(str + s, i - s);
	if( OBJ_TAINTED(str) ){ OBJ_TAINT(tmp); };
	rb_ary_push(xs, tmp);
    }
    return xs;
}

static VALUE
rb_xt_xnode_deep_dup_i(VALUE children)
{
    return rb_funcall(children, i_collect, 0);
}

static VALUE
rb_xt_xnode_deep_dup_ii(VALUE child, VALUE data)
{
    VALUE node = data;
    static VALUE rb_xt_xnode_deep_dup(int, VALUE[], VALUE);

    if( rb_obj_is_kind_of(child, rb_cXNode) ){
	VALUE argv[1] = {node};
	return rb_xt_xnode_deep_dup(1, argv, child);
    }
    else{
	return child;
    }
}

static VALUE
rb_xt_xnode_deep_dup(int argc, VALUE argv[], VALUE xnode)
{
    VALUE newnode;
    VALUE name, attrs, children, parent, data_path;
    VALUE propagation, exname, exattr, expand, option;

    rb_scan_args(argc, argv, "01", &parent);

    name = rb_ivar_get(xnode, ii_name);
    if( ! NIL_P(name) )
	name = rb_funcall(name, i_dup, 0);

    attrs = rb_ivar_get(xnode, ii_attrs);
    if( ! NIL_P(name) )
	attrs = rb_funcall(attrs, i_dup, 0);

    children = Qnil;

    data_path = rb_ivar_get(xnode, ii_data_path);
    if( ! NIL_P(data_path) )
	data_path = rb_funcall(data_path, i_dup, 0);

    propagation = rb_ivar_get(xnode, ii_propagation);

    exname = rb_ivar_get(xnode, ii_exname);
    if( ! NIL_P(exname) )
	exname = rb_funcall(exname, i_dup, 0);

    exattr = rb_ivar_get(xnode, ii_exattr);
    if( ! NIL_P(exattr) )
	exattr = rb_funcall(exattr, i_dup, 0);

    expand = rb_ivar_get(xnode, ii_expand);

    option = rb_funcall(rb_ivar_get(xnode, ii_option), i_dup, 0);

    newnode = rb_funcall(rb_cXNode, i_new, 10,
			 name, attrs, Qnil, parent, data_path, propagation,
			 exname, exattr, expand, option);

    children = rb_iterate(rb_xt_xnode_deep_dup_i, rb_ivar_get(xnode, ii_children),
			  rb_xt_xnode_deep_dup_ii, newnode);

    rb_ivar_set(newnode, ii_children, children);

    return newnode;
}

static VALUE
rb_xt_xnode_dump_i(VALUE attrs)
{
    return rb_funcall(attrs, i_collect, 0);
}

static VALUE
rb_xt_xnode_dump_ii(VALUE ary, VALUE data)
{
    VALUE attr, val, str;
    Check_Type(ary, T_ARRAY);
    attr = RARRAY(ary)->ptr[0];
    val  = RARRAY(ary)->ptr[1];
    str  = rb_str_new2("");
    rb_str_concat(str, attr);
    rb_str_cat2(str, "=\"");
    rb_str_concat(str, val);
    rb_str_cat2(str, "\"");
    return str;
}

static VALUE
rb_xt_xnode_dump(VALUE xnode, VALUE io)
{
    VALUE name = rb_ivar_get(xnode,ii_name);
    VALUE children = rb_ivar_get(xnode, ii_children);
    int i, len;
    VALUE child;
    VALUE tag;

    if( ! NIL_P(name) ){
	VALUE attrs = rb_ivar_get(xnode, ii_attrs);
	attrs = rb_iterate(rb_xt_xnode_dump_i, attrs, rb_xt_xnode_dump_ii, Qnil);
	if( RARRAY(attrs)->len > 0 ){
	    attrs = rb_str_concat(rb_str_new2(" "),
				  rb_ary_join(attrs, rb_str_new2(" ")));
	}
	else{
	    attrs = rb_str_new2("");
	}
	if( rb_funcall(children, rb_intern("empty?"), 0) ){
	    tag = rb_str_new2("<");
	    rb_str_concat(tag, name);
	    rb_str_concat(tag, attrs);
	    rb_str_cat2(tag, " />");
	    rb_funcall(io, i_gtgt, 1, tag);
	    return io;
	}
	else{
	    tag = rb_str_new2("<");
	    rb_str_concat(tag, name);
	    rb_str_concat(tag, attrs);
	    rb_str_cat2(tag, ">");
	    rb_funcall(io, i_gtgt, 1, tag);
	}
    }
    Check_Type(children,T_ARRAY);
    len = RARRAY(children)->len;
    for( i=0; i < len; i++ ){
	child = RARRAY(children)->ptr[i];
	if( rb_obj_is_kind_of(child, rb_cXNode) ){
	    rb_xt_xnode_dump(child, io);
	}
	else{
	    rb_funcall(io, i_gtgt, 1, child);
	}
    }
    if( ! NIL_P(name) ){
	tag = rb_tainted_str_new2("</");
	rb_str_concat(tag, name);
	rb_str_cat2(tag, ">");
	rb_funcall(io, i_gtgt, 1, tag);
    }
    return io;
}

void
Init_xtemplate_ext()
{
    i_to_s = rb_intern("to_s");
    i_gsub = rb_intern("gsub");
    i_new  = rb_intern("new");
    i_dup  = rb_intern("dup");
    i_gtgt = rb_intern("<<");
    i_collect = rb_intern("collect");
    i_strip = rb_intern("strip");
    ii_name = rb_intern("@name");
    ii_attrs = rb_intern("@attrs");
    ii_children = rb_intern("@children");
    ii_data_path = rb_intern("@data_path");
    ii_propagation = rb_intern("@propagation");
    ii_exname = rb_intern("@exname");
    ii_exattr = rb_intern("@exattr");
    ii_expand = rb_intern("@expand");
    ii_option = rb_intern("@option");

    rb_mXTemplate = rb_eval_string("::XTemplate");
    rb_mXPath     = rb_eval_string("::XTemplate::XPath");
    rb_cXNode     = rb_eval_string("::XTemplate::XNode");
    rb_mUtil      = rb_eval_string("::XTemplate::Util");
    rb_cSanitizedString = rb_eval_string("::XTemplate::SanitizedString");

    PredefinedStringsRegex = rb_eval_string("::XTemplate::Util::PredefinedStringsRegex");
    PredefinedEntitiesRegex = rb_eval_string("::XTemplate::Util::PredefinedEntitiesRegex");
    PredefinedEntity = rb_eval_string("::XTemplate::Util::PredefinedEntity");
    RevPredefinedEntity = rb_eval_string("::XTemplate::Util::RevPredefinedEntity");
    EntityAmp = rb_eval_string("::XTemplate::Util::EntityAmp");
    SanitizedAmp = rb_eval_string("::XTemplate::Util::SanitizedAmp");
    empty_str = rb_tainted_str_new2("");
    rb_gc_register_address(&empty_str);
    quot_regexp = rb_eval_string("/(\\A['\"])|([\"']\\z)/");
    rb_gc_register_address(&quot_regexp);

    rb_define_module_function(rb_mXPath, "path_split", rb_xt_path_split, 1);
    rb_define_module_function(rb_mXPath, "cond_split", rb_xt_cond_split, 1);
    rb_define_module_function(rb_mXPath, "args_split", rb_xt_args_split, 1);

    rb_define_module_function(rb_mUtil, "sanitize", rb_xt_sanitize, 1);
    rb_define_module_function(rb_mUtil, "unsanitize", rb_xt_unsanitize, 1);

    rb_define_method(rb_cXNode, "dump", rb_xt_xnode_dump, 1);
    rb_define_method(rb_cXNode, "deep_dup", rb_xt_xnode_deep_dup, -1);
}
