/*
 * pg_type_map_in_ruby.c - PG::TypeMapInRuby class extension
 * $Id$
 *
 */

#include "pg.h"

VALUE rb_cTypeMapInRuby;
static VALUE s_id_fit_to_result;
static VALUE s_id_fit_to_query;
static VALUE s_id_fit_to_copy_get;
static VALUE s_id_typecast_result_value;
static VALUE s_id_typecast_query_param;
static VALUE s_id_typecast_copy_get;

typedef struct {
	t_typemap typemap;
	VALUE self;
} t_tmir;

static size_t
pg_tmir_memsize( const void *_this )
{
	const t_tmir *this = (const t_tmir *)_this;
	return sizeof(*this);
}

static void
pg_tmir_compact( void *_this )
{
	t_tmir *this = (t_tmir *)_this;
	pg_typemap_compact(&this->typemap);
	pg_gc_location(this->self);
}

static const rb_data_type_t pg_tmir_type = {
	"PG::TypeMapInRuby",
	{
		pg_typemap_mark,
		RUBY_TYPED_DEFAULT_FREE,
		pg_tmir_memsize,
		pg_compact_callback(pg_tmir_compact),
	},
	&pg_typemap_type,
	0,
	RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | PG_RUBY_TYPED_FROZEN_SHAREABLE,
};

/*
 * call-seq:
 *    typemap.fit_to_result( result )
 *
 * Check that the type map fits to the result.
 *
 * This method is called, when a type map is assigned to a result.
 * It must return a PG::TypeMap object or raise an Exception.
 * This can be +self+ or some other type map that fits to the result.
 *
 */
static VALUE
pg_tmir_fit_to_result( VALUE self, VALUE result )
{
	t_tmir *this = RTYPEDDATA_DATA( self );
	t_typemap *default_tm;
	t_typemap *p_new_typemap;
	VALUE sub_typemap;
	VALUE new_typemap;

	if( rb_respond_to(self, s_id_fit_to_result) ){
		t_typemap *tm;
		UNUSED(tm);
		new_typemap = rb_funcall( self, s_id_fit_to_result, 1, result );

		if ( !rb_obj_is_kind_of(new_typemap, rb_cTypeMap) ) {
			/* TypedData_Get_Struct() raises "wrong argument type", which is misleading,
			* so we better raise our own message */
			rb_raise( rb_eTypeError, "wrong return type from fit_to_result: %s expected kind of PG::TypeMap",
					rb_obj_classname( new_typemap ) );
		}
		TypedData_Get_Struct(new_typemap, t_typemap, &pg_typemap_type, tm);
	} else {
		new_typemap = self;
	}

	/* Ensure that the default type map fits equally. */
	default_tm = RTYPEDDATA_DATA( this->typemap.default_typemap );
	sub_typemap = default_tm->funcs.fit_to_result( this->typemap.default_typemap, result );

	if( sub_typemap != this->typemap.default_typemap ){
		new_typemap = rb_obj_dup( new_typemap );
	}

	p_new_typemap = RTYPEDDATA_DATA(new_typemap);
	p_new_typemap->default_typemap = sub_typemap;
	return new_typemap;
}

static VALUE
pg_tmir_result_value( t_typemap *p_typemap, VALUE result, int tuple, int field )
{
	t_tmir *this = (t_tmir *) p_typemap;

	return rb_funcall( this->self, s_id_typecast_result_value, 3, result, INT2NUM(tuple), INT2NUM(field) );
}

/*
 * call-seq:
 *    typemap.typecast_result_value( result, tuple, field )
 *
 * Retrieve and cast a field of the given result.
 *
 * This method implementation uses the #default_type_map to get the
 * field value. It can be derived to change this behaviour.
 *
 * Parameters:
 * * +result+ : The PG::Result received from the database.
 * * +tuple+ : The row number to retrieve.
 * * +field+ : The column number to retrieve.
 *
 * Note: Calling any value retrieving methods of +result+ will result
 * in an (endless) recursion. Instead super() can be used to retrieve
 * the value using the default_typemap.
 *
 */
static VALUE
pg_tmir_typecast_result_value( VALUE self, VALUE result, VALUE tuple, VALUE field )
{
	t_tmir *this = RTYPEDDATA_DATA( self );
	t_typemap *default_tm = RTYPEDDATA_DATA( this->typemap.default_typemap );
	return default_tm->funcs.typecast_result_value( default_tm, result, NUM2INT(tuple), NUM2INT(field) );
}

/*
 * call-seq:
 *    typemap.fit_to_query( params )
 *
 * Check that the type map fits to the given user values.
 *
 * This method is called, when a type map is used for sending a query
 * and for encoding of copy data, before the value is casted.
 *
 */
static VALUE
pg_tmir_fit_to_query( VALUE self, VALUE params )
{
	t_tmir *this = RTYPEDDATA_DATA( self );
	t_typemap *default_tm;

	if( rb_respond_to(self, s_id_fit_to_query) ){
		rb_funcall( self, s_id_fit_to_query, 1, params );
	}

	/* Ensure that the default type map fits equally. */
	default_tm = RTYPEDDATA_DATA( this->typemap.default_typemap );
	default_tm->funcs.fit_to_query( this->typemap.default_typemap, params );

	return self;
}

static t_pg_coder *
pg_tmir_query_param( t_typemap *p_typemap, VALUE param_value, int field )
{
	t_tmir *this = (t_tmir *) p_typemap;

	VALUE coder = rb_funcall( this->self, s_id_typecast_query_param, 2, param_value, INT2NUM(field) );

	if ( NIL_P(coder) ){
		return NULL;
	} else if( rb_obj_is_kind_of(coder, rb_cPG_Coder) ) {
		return RTYPEDDATA_DATA(coder);
	} else {
		rb_raise( rb_eTypeError, "wrong return type from typecast_query_param: %s expected nil or kind of PG::Coder",
				rb_obj_classname( coder ) );
	}
}

/*
 * call-seq:
 *    typemap.typecast_query_param( param_value, field )
 *
 * Cast a field string for transmission to the server.
 *
 * This method implementation uses the #default_type_map to cast param_value.
 * It can be derived to change this behaviour.
 *
 * Parameters:
 * * +param_value+ : The value from the user.
 * * +field+ : The field number from left to right.
 *
 */
static VALUE
pg_tmir_typecast_query_param( VALUE self, VALUE param_value, VALUE field )
{
	t_tmir *this = RTYPEDDATA_DATA( self );
	t_typemap *default_tm = RTYPEDDATA_DATA( this->typemap.default_typemap );
	t_pg_coder *p_coder = default_tm->funcs.typecast_query_param( default_tm, param_value, NUM2INT(field) );

	return p_coder ? p_coder->coder_obj : Qnil;
}

/* This is to fool rdoc's C parser */
#if 0
/*
 * call-seq:
 *    typemap.fit_to_copy_get()
 *
 * Check that the type map can be used for PG::Connection#get_copy_data.
 *
 * This method is called, when a type map is used for decoding copy data,
 * before the value is casted.
 *
 * Should return the expected number of columns or 0 if the number of columns is unknown.
 * This number is only used for memory pre-allocation.
 *
 */
static VALUE pg_tmir_fit_to_copy_get_dummy( VALUE self ){}
#endif

static int
pg_tmir_fit_to_copy_get( VALUE self )
{
	t_tmir *this = RTYPEDDATA_DATA( self );
	t_typemap *default_tm;
	VALUE num_columns = INT2NUM(0);

	if( rb_respond_to(self, s_id_fit_to_copy_get) ){
		num_columns = rb_funcall( self, s_id_fit_to_copy_get, 0 );
	}

	if ( !rb_obj_is_kind_of(num_columns, rb_cInteger) ) {
		rb_raise( rb_eTypeError, "wrong return type from fit_to_copy_get: %s expected kind of Integer",
				rb_obj_classname( num_columns ) );
	}
	/* Ensure that the default type map fits equally. */
	default_tm = RTYPEDDATA_DATA( this->typemap.default_typemap );
	default_tm->funcs.fit_to_copy_get( this->typemap.default_typemap );

	return NUM2INT(num_columns);;
}

static VALUE
pg_tmir_copy_get( t_typemap *p_typemap, VALUE field_str, int fieldno, int format, int enc_idx )
{
	t_tmir *this = (t_tmir *) p_typemap;
	rb_encoding *p_encoding = rb_enc_from_index(enc_idx);
	VALUE enc = rb_enc_from_encoding(p_encoding);
	/* field_str is reused in-place by pg_text_dec_copy_row(), so we need to make
	 * a copy of the string buffer for use in ruby space. */
	VALUE field_str_copy = rb_str_dup(field_str);
	rb_str_modify(field_str_copy);

	return rb_funcall( this->self, s_id_typecast_copy_get, 4, field_str_copy, INT2NUM(fieldno), INT2NUM(format), enc );
}

/*
 * call-seq:
 *    typemap.typecast_copy_get( field_str, fieldno, format, encoding )
 *
 * Cast a field string received by PG::Connection#get_copy_data.
 *
 * This method implementation uses the #default_type_map to cast field_str.
 * It can be derived to change this behaviour.
 *
 * Parameters:
 * * +field_str+ : The String received from the server.
 * * +fieldno+ : The field number from left to right.
 * * +format+ : The format code (0 = text, 1 = binary)
 * * +encoding+ : The encoding of the connection and encoding the returned
 *   value should get.
 *
 */
static VALUE
pg_tmir_typecast_copy_get( VALUE self, VALUE field_str, VALUE fieldno, VALUE format, VALUE enc )
{
	t_tmir *this = RTYPEDDATA_DATA( self );
	t_typemap *default_tm = RTYPEDDATA_DATA( this->typemap.default_typemap );
	int enc_idx = rb_to_encoding_index( enc );

	return default_tm->funcs.typecast_copy_get( default_tm, field_str, NUM2INT(fieldno), NUM2INT(format), enc_idx );
}

static VALUE
pg_tmir_s_allocate( VALUE klass )
{
	t_tmir *this;
	VALUE self;

	self = TypedData_Make_Struct( klass, t_tmir, &pg_tmir_type, this );

	this->typemap.funcs.fit_to_result = pg_tmir_fit_to_result;
	this->typemap.funcs.fit_to_query = pg_tmir_fit_to_query;
	this->typemap.funcs.fit_to_copy_get = pg_tmir_fit_to_copy_get;
	this->typemap.funcs.typecast_result_value = pg_tmir_result_value;
	this->typemap.funcs.typecast_query_param = pg_tmir_query_param;
	this->typemap.funcs.typecast_copy_get = pg_tmir_copy_get;
	RB_OBJ_WRITE(self, &this->typemap.default_typemap, pg_typemap_all_strings);
	this->self = self;

	return self;
}


void
init_pg_type_map_in_ruby(void)
{
	s_id_fit_to_result = rb_intern("fit_to_result");
	s_id_fit_to_query = rb_intern("fit_to_query");
	s_id_fit_to_copy_get = rb_intern("fit_to_copy_get");
	s_id_typecast_result_value = rb_intern("typecast_result_value");
	s_id_typecast_query_param = rb_intern("typecast_query_param");
	s_id_typecast_copy_get = rb_intern("typecast_copy_get");

	/*
	 * Document-class: PG::TypeMapInRuby < PG::TypeMap
	 *
	 * This class can be used to implement a type map in ruby, typically as a
	 * #default_type_map in a type map chain.
	 *
	 * This API is EXPERIMENTAL and could change in the future.
	 *
	 */
	rb_cTypeMapInRuby = rb_define_class_under( rb_mPG, "TypeMapInRuby", rb_cTypeMap );
	rb_define_alloc_func( rb_cTypeMapInRuby, pg_tmir_s_allocate );
	/* rb_define_method( rb_cTypeMapInRuby, "fit_to_result", pg_tmir_fit_to_result, 1 ); */
	/* rb_define_method( rb_cTypeMapInRuby, "fit_to_query", pg_tmir_fit_to_query, 1 ); */
	/* rb_define_method( rb_cTypeMapInRuby, "fit_to_copy_get", pg_tmir_fit_to_copy_get_dummy, 0 ); */
	rb_define_method( rb_cTypeMapInRuby, "typecast_result_value", pg_tmir_typecast_result_value, 3 );
	rb_define_method( rb_cTypeMapInRuby, "typecast_query_param", pg_tmir_typecast_query_param, 2 );
	rb_define_method( rb_cTypeMapInRuby, "typecast_copy_get", pg_tmir_typecast_copy_get, 4 );
	/* rb_mDefaultTypeMappable = rb_define_module_under( rb_cTypeMap, "DefaultTypeMappable"); */
	rb_include_module( rb_cTypeMapInRuby, rb_mDefaultTypeMappable );
}
