/*
 * call-seq:
 *    conn.exec(sql [, params, result_format ] ) -> PGresult
 *    conn.exec(sql [, params, result_format ] ) {|pg_result| block }
 *
 * Sends SQL query request specified by _sql_ to PostgreSQL.
 * Returns a PGresult instance on success.
 * On failure, it raises a PGError exception.
 *
 * +params+ is an optional array of the bind parameters for the SQL query.
 * Each element of the +params+ array may be either:
 *   a hash of the form:
 *     {:value  => String (value of bind parameter)
 *      :type   => Fixnum (oid of type of bind parameter)
 *      :format => Fixnum (0 for text, 1 for binary)
 *     }
 *   or, it may be a String. If it is a string, that is equivalent to the hash:
 *     { :value => <string value>, :type => 0, :format => 0 }
 * 
 * PostgreSQL bind parameters are represented as $1, $1, $2, etc.,
 * inside the SQL query. The 0th element of the +params+ array is bound
 * to $1, the 1st element is bound to $2, etc. +nil+ is treated as +NULL+.
 * 
 * If the types are not specified, they will be inferred by PostgreSQL.
 * Instead of specifying type oids, it's recommended to simply add
 * explicit casts in the query to ensure that the right type is used.
 *
 * For example: "SELECT $1::int"
 *
 * The optional +result_format+ should be 0 for text results, 1
 * for binary.
 *
 * If the optional code block is given, it will be passed <i>result</i> as an argument, 
 * and the PGresult object will  automatically be cleared when the block terminates. 
 * In this instance, <code>conn.exec</code> returns the value of the block.
 */
static VALUE
pgconn_exec(int argc, VALUE *argv, VALUE self)
{
        PGconn *conn = get_pgconn(self);
        PGresult *result = NULL;
        VALUE rb_pgresult;
        VALUE command, params, in_res_fmt;
        VALUE param, param_type, param_value, param_format;
        VALUE param_value_tmp;
        VALUE sym_type, sym_value, sym_format;
        VALUE gc_array;
        int i=0;
        int nParams;
        Oid *paramTypes;
        char ** paramValues;
        int *paramLengths;
        int *paramFormats;
        int resultFormat;

        rb_scan_args(argc, argv, "12", &command, &params, &in_res_fmt);

        Check_Type(command, T_STRING);

        /* If called with no parameters, use PQexec */
        if(NIL_P(params)) {
                result = PQexec(conn, StringValuePtr(command));
                rb_pgresult = new_pgresult(result, conn);
                pgresult_check(self, rb_pgresult);
                if (rb_block_given_p()) {
                        return rb_ensure(rb_yield, rb_pgresult, 
                                pgresult_clear, rb_pgresult);
                }
                return rb_pgresult;
        }

        /* If called with parameters, and optionally result_format,
         * use PQexecParams
         */
        Check_Type(params, T_ARRAY);

        if(NIL_P(in_res_fmt)) {
                resultFormat = 0;
        }
        else {
                resultFormat = NUM2INT(in_res_fmt);
        }

        gc_array = rb_ary_new();
        rb_gc_register_address(&gc_array);
        sym_type = ID2SYM(rb_intern("type"));
        sym_value = ID2SYM(rb_intern("value"));
        sym_format = ID2SYM(rb_intern("format"));
        nParams = RARRAY_LEN(params);
        paramTypes = ALLOC_N(Oid, nParams); 
        paramValues = ALLOC_N(char *, nParams);
        paramLengths = ALLOC_N(int, nParams);
        paramFormats = ALLOC_N(int, nParams);
        for(i = 0; i < nParams; i++) {
                param = rb_ary_entry(params, i);
                if (TYPE(param) == T_HASH) {
                        param_type = rb_hash_aref(param, sym_type);
                        param_value_tmp = rb_hash_aref(param, sym_value);
                        if(param_value_tmp == Qnil)
                                param_value = param_value_tmp;
                        else
                                param_value = rb_obj_as_string(param_value_tmp);
                        param_format = rb_hash_aref(param, sym_format);
                }
                else {
                        param_type = Qnil;
                        if(param == Qnil)
                                param_value = param;
                        else
                                param_value = rb_obj_as_string(param);
                        param_format = Qnil;
                }

                if(param_type == Qnil)
                        paramTypes[i] = 0;
                else
                        paramTypes[i] = NUM2INT(param_type);

                if(param_value == Qnil) {
                        paramValues[i] = NULL;
                        paramLengths[i] = 0;
                }
                else {
                        Check_Type(param_value, T_STRING);
                        /* make sure param_value doesn't get freed by the GC */
                        rb_ary_push(gc_array, param_value);
                        paramValues[i] = StringValuePtr(param_value);
                        paramLengths[i] = RSTRING_LEN(param_value);
                }

                if(param_format == Qnil)
                        paramFormats[i] = 0;
                else
                        paramFormats[i] = NUM2INT(param_format);
        }

        result = PQexecParams(conn, StringValuePtr(command), nParams, paramTypes, 
                (const char * const *)paramValues, paramLengths, paramFormats, resultFormat);

        rb_gc_unregister_address(&gc_array);

        xfree(paramTypes);
        xfree(paramValues);
        xfree(paramLengths);
        xfree(paramFormats);

        rb_pgresult = new_pgresult(result, conn);
        pgresult_check(self, rb_pgresult);
        if (rb_block_given_p()) {
                return rb_ensure(rb_yield, rb_pgresult, 
                        pgresult_clear, rb_pgresult);
        }
        return rb_pgresult;
}