/*
    StrongTyping - Method parameter checking for Ruby
    Copyright (C) 2003  Ryan Pavlik

    This library is free software; you can redistribute it and/or
    modify it under the terms of the GNU Lesser General Public
    License as published by the Free Software Foundation; either
    version 2.1 of the License, or (at your option) any later version.

    This library is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public
    License along with this library; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA
*/

#include "ruby.h"
#include "strongtyping.h"

static int check_args(int argc, VALUE *obj, VALUE *mod);

static VALUE
strongtyping_expect(int argc, VALUE *argv, VALUE self UNUSED) {
    int i = 0;
    VALUE obj[MAXARGS], mod[MAXARGS];
    VALUE typestr;

    if(!argc) return Qnil;
    if(argc % 2)
        rb_raise(rb_eSyntaxError, "expect() requires argument pairs");

#ifndef __GNUC__
    if(argc*2 > MAXARGS*2)
        rb_raise(rb_eSyntaxError, "too many arguments to expect()");
#endif

    for(i = 0; i < argc; i += 2) {
        obj[i/2]     = argv[i];
        mod[(i+1)/2] = argv[i+1];
    }

    if(rb_funcall(obj[0], id_isa, 1, cQueryParams)) {
        rb_funcall(obj[0], rb_intern("<<"), 1, rb_ary_new4(argc/2, mod));
        rb_raise(eArgList, "");
    }
    
    i = check_args(argc / 2, obj, mod);

    if(i < 0) return Qnil;

    typestr = rb_funcall(mod[i], id_inspect, 0);
    rb_raise(eArgumentTypeError, "Expecting %s as argument %d, got %s",
             RSTRING(typestr)->ptr, i + 1,
             rb_class2name(rb_funcall(obj[i], id_class, 0)));
}

static VALUE
strongtyping_overload(int argc, VALUE *argv, VALUE self UNUSED) {
    struct RArray *q;
    
    if(argc < 1)
        rb_raise(rb_eArgError, "At least one parameter required");

    Check_Type(argv[0], T_ARRAY);
    q = RARRAY(argv[0]);

    if(q->len && rb_funcall(q->ptr[0], id_isa, 1, cQueryParams)) {
        rb_funcall(q->ptr[0], rb_intern("<<"), 1,
                   rb_ary_new4(argc - 1, argv + 1));
        return Qnil;
    }

    if(q->len != (argc - 1))
        return Qnil;

    if(check_args(argc - 1, q->ptr, argv + 1) < 0) {
        if(argc == 2) rb_yield(*RARRAY(*argv)->ptr);
        else          rb_yield(*argv);
    }

    return Qnil;
}

static VALUE
strongtyping_overload_exception(int argc, VALUE *argv, VALUE self UNUSED) {
    struct RArray *q;
    
    if(argc < 1)
        rb_raise(rb_eArgError, "At least one parameters required");

    Check_Type(argv[0], T_ARRAY);
    q = RARRAY(argv[0]);

    if(q->len && (argc - 1) == 0)
        return Qnil;

    if(check_args(argc - 1, q->ptr, argv + 1) < 0)
        rb_yield(argv[0]);

    return Qnil;
}

static VALUE
strongtyping_overload_error(VALUE self UNUSED, VALUE args) {
    struct  RArray *q;
    VALUE           classlist;
    char           *name = 0;
    int             i    = 0;
    
    Check_Type(args, T_ARRAY);
    q = RARRAY(args);
    
    if(q->len && rb_funcall(q->ptr[0], id_isa, 1, cQueryParams))
        rb_raise(eArgList, "");

    classlist = rb_str_new2("");
    for(i = 0; i < q->len; i++) {
        if(i > 0) rb_str_cat(classlist, ", ", 2);
        name = rb_class2name(rb_funcall(q->ptr[i], id_class, 0));
        rb_str_cat(classlist, name, strlen(name));
    }
    rb_raise(eOverloadError, "No matching template for arguments: [%s]",
             RSTRING(classlist)->ptr);
}

static int
check_args(int argc, VALUE *obj, VALUE *mod) {
    int i = 0;
    VALUE ret;
    
    for(i = 0; i < argc; i++) {
        if(TYPE(mod[i]) == T_ARRAY) {
            int j = 0, ok = 0;
            for(j = 0; j < RARRAY(mod[i])->len; j++)
                if(rb_funcall(obj[i], id_isa, 1, RARRAY(mod[i])->ptr[j]) == Qtrue)
                    ok = 1;

            if(ok) continue;
            else   return i;
        } else {
            ret = rb_funcall(obj[i], id_isa, 1, mod[i]);
            if(ret == Qfalse) return i;
        }
    }

    return -1;
}

static VALUE
call_method(VALUE ary) {
    VALUE  method = RARRAY(ary)->ptr[0],
           query  = RARRAY(ary)->ptr[1];
    VALUE *argv   =  NULL;
    VALUE  ret;
    int    argc   =  0,
           i      =  0;

    argc = FIX2INT(rb_funcall(method, rb_intern("arity"), 0));
    if(argc == 0) {
        rb_funcall(query, rb_intern("<<"), 1, rb_ary_new());
        rb_raise(eArgList, "");
    }  else if(argc < 0) argc = -argc;

    argv    = malloc(sizeof(VALUE) * argc);
    argv[0] = query;
    for(i = 1; i < argc - 1; i++) argv[i] = Qnil;

    ret = rb_funcall2(method, rb_intern("call"), argc, argv);
    free(argv);

    return ret;
}

static VALUE
grab_types(VALUE query) {
    return query;
}

static VALUE
strongtyping_get_arg_types(VALUE obj UNUSED, VALUE method) {
    VALUE query, ary;
    query = rb_funcall(cQueryParams, rb_intern("new"), 0);
    ary   = rb_ary_new3(2, method, query);
    
    return rb_rescue2(call_method, ary, grab_types, query, eArgList, 0);
}


static VALUE
strongtyping_verify_args_for(VALUE self, VALUE method, VALUE args) {
    struct RArray *list     = NULL,
                  *t        = NULL,
                  *a        = NULL;
    int            i        = 0;
    VALUE          template = strongtyping_get_arg_types(self, method);

    list = RARRAY(template);
    a    = RARRAY(args);

    for(i = 0; i < list->len; i++) {
        t = RARRAY(list->ptr[i]);

        if(a->len != t->len)                        continue;
        if(check_args(a->len, a->ptr, t->ptr) < 0)  return Qtrue;
    }
    
    return Qfalse;
}

void Init_strongtyping() {
    mStrongTyping = rb_define_module("StrongTyping");
    id_isa        = rb_intern("is_a?");
    id_class      = rb_intern("class");
    id_inspect    = rb_intern("inspect");

    cQueryParams       = rb_define_class_under(mStrongTyping,
                                               "%QueryParams",
                                               rb_cArray);

    eArgumentTypeError = rb_define_class_under(mStrongTyping,
                                               "ArgumentTypeError",
                                               rb_eArgError);
    eOverloadError     = rb_define_class_under(mStrongTyping,
                                               "OverloadError",
                                               eArgumentTypeError);
    eArgList           = rb_define_class_under(mStrongTyping,
                                               "%ArgList",
                                               rb_eException);

    rb_define_module_function(mStrongTyping, "expect",
                              strongtyping_expect, -1);
    rb_define_module_function(mStrongTyping, "overload",
                              strongtyping_overload, -1);
    rb_define_module_function(mStrongTyping, "overload_exception",
                              strongtyping_overload_exception, -1);
    rb_define_module_function(mStrongTyping, "overload_default",
                              strongtyping_overload_error, 1);
    rb_define_module_function(mStrongTyping, "overload_error",
                              strongtyping_overload_error, 1);
    rb_define_module_function(mStrongTyping, "get_arg_types",
                              strongtyping_get_arg_types, 1);
    rb_define_module_function(mStrongTyping, "verify_args_for",
                              strongtyping_verify_args_for, 2);
}
