/*
	namazu.c - Search::Namazu - libnmz wrapper for Ruby
	
	Copyright (C) 2001 by Tietew, All Rights Reserved.
	This is free software with ABOSOLYTELY NO WARRANTY.
	
	Contact:
	<http://www.tietew.net/ruby/>
	<mailto:tietew@tietew.net>
*/

#include "ruby.h"
#include <namazu/libnamazu.h>
#include <namazu/search.h>
#include <namazu/codeconv.h>
#include <namazu/field.h>
#include <namazu/idxname.h>

/* string literal */
#define STRING_LITERAL(s)	rb_str_new(s, sizeof(s) - 1)

/* Ruby 1.7 feature */
#ifndef StringValue
#define StringValue(s)	Check_Type(s, T_STRING)
#endif
#ifndef SafeStringValue
#define SafeStringValue(s)	Check_SafeStr(s)
#endif
#ifndef StringValuePtr
#define StringValuePtr(s)	STR2CSTR(s)
#endif

/* global variables */
static VALUE rb_mNamazu;
static VALUE rb_cResult;
static VALUE rb_cResultData;
static VALUE rb_cHitnumData;
static VALUE rb_eNamazuError;
static ID fetch, split, uniq_bang, push;


/* class Search::Namazu::ResultData */
struct resultdata_t
{
	VALUE score;
	VALUE fields;
	VALUE date;
	VALUE rank;
};
typedef struct resultdata_t resultdata_t;

static void rdata_mark(resultdata_t *rdata)
{
	rb_gc_mark(rdata->score);
	rb_gc_mark(rdata->fields);
	rb_gc_mark(rdata->date);
	rb_gc_mark(rdata->rank);
}

static VALUE rdata_score(VALUE self)
{
	resultdata_t *rdata;
	Data_Get_Struct(self, resultdata_t, rdata);
	return rdata->score;
}

static VALUE rdata_fields(VALUE self)
{
	resultdata_t *rdata;
	Data_Get_Struct(self, resultdata_t, rdata);
	return rdata->fields;
}

static VALUE rdata_date(VALUE self)
{
	resultdata_t *rdata;
	Data_Get_Struct(self, resultdata_t, rdata);
	return rdata->date;
}

static VALUE rdata_rank(VALUE self)
{
	resultdata_t *rdata;
	Data_Get_Struct(self, resultdata_t, rdata);
	return rdata->rank;
}

static VALUE rdata_missing(VALUE self, VALUE id)
{
	VALUE key;
	resultdata_t *rdata;
	Data_Get_Struct(self, resultdata_t, rdata);
	
	return rb_funcall(rdata->fields, fetch, 1, rb_str_new2(rb_id2name(SYM2ID(id))));
}

static VALUE rdata_inspect(VALUE self)
{
	VALUE v[5];
	resultdata_t *rdata;
	Data_Get_Struct(self, resultdata_t, rdata);
	
	v[0] = STRING_LITERAL("#<Search::Namazu::ResultData score=%d, fields=%s, date=%s, rank=%d>");
	v[1] = rdata->score;
	v[2] = rb_inspect(rdata->fields);
	v[3] = rdata->date;
	v[4] = rdata->rank;
	return rb_f_sprintf(5, v);
}


/* class Search::Namazu::HitnumData */
struct hitnumdata_t
{
	VALUE word;
	VALUE hitnum;
	VALUE stat;
	VALUE phrase;
};
typedef struct hitnumdata_t hitnumdata_t;

static void hdata_mark(hitnumdata_t *hdata)
{
	rb_gc_mark(hdata->word);
	rb_gc_mark(hdata->hitnum);
	rb_gc_mark(hdata->stat);
	rb_gc_mark(hdata->phrase);
}

static VALUE hdata_word(VALUE self)
{
	hitnumdata_t *hdata;
	Data_Get_Struct(self, hitnumdata_t, hdata);
	
	return hdata->word;
}

static VALUE hdata_hitnum(VALUE self)
{
	hitnumdata_t *hdata;
	Data_Get_Struct(self, hitnumdata_t, hdata);
	
	return hdata->hitnum;
}

static VALUE hdata_stat(VALUE self)
{
	hitnumdata_t *hdata;
	Data_Get_Struct(self, hitnumdata_t, hdata);
	
	return hdata->stat;
}

static VALUE hdata_phrase(VALUE self)
{
	hitnumdata_t *hdata;
	Data_Get_Struct(self, hitnumdata_t, hdata);
	
	return hdata->phrase;
}

static VALUE hdata_inspect(VALUE self)
{
	VALUE v[5];
	hitnumdata_t *hdata;
	Data_Get_Struct(self, hitnumdata_t, hdata);
	
	v[0] = STRING_LITERAL("#<Search::Namazu::HitnumData word=%s, hitnum=%d, stat=%d, phrase=%s>");
	v[1] = rb_inspect(hdata->word);
	v[2] = hdata->hitnum;
	v[3] = hdata->stat;
	v[4] = rb_inspect(hdata->phrase);
	return rb_f_sprintf(5, v);
}


/* class Search::Namazu::Result */
struct result_t
{
	VALUE hlist;
	VALUE hitnumlist;
};
typedef struct result_t result_t;

static void result_mark(result_t *result)
{
	rb_gc_mark(result->hlist);
	rb_gc_mark(result->hitnumlist);
}

static VALUE result_hlist(VALUE self)
{
	result_t *result;
	Data_Get_Struct(self, result_t, result);
	
	return result->hlist;
}

static VALUE result_hitnumlist(VALUE self)
{
	result_t *result;
	Data_Get_Struct(self, result_t, result);
	
	return result->hitnumlist;
}

static VALUE result_inspect(VALUE self)
{
	VALUE v[3];
	result_t *result;
	Data_Get_Struct(self, result_t, result);
	
	v[0] = STRING_LITERAL("#<Search::Namazu::Result hlist=%s, hitnumlist=%s>");
	v[1] = rb_inspect(result->hlist);
	v[2] = rb_inspect(result->hitnumlist);
	return rb_f_sprintf(3, v);
}


/* class Search::Namazu::Error */
static VALUE nmzerr_stat(VALUE self)
{
	return rb_iv_get(self, "stat");
}


/* module Search::Namazu */
static void process_sort(VALUE sort)
{
	VALUE asort;
	char *str;
	
	if(NIL_P(sort))
	{
		nmz_set_sortmethod(SORT_BY_SCORE);
		nmz_set_sortorder(DESCENDING);
		return;
	}
	
	StringValue(sort);
	asort = rb_funcall(sort, split, 2, STRING_LITERAL(":"), INT2FIX(3));
	str = StringValuePtr(RARRAY(asort)->ptr[0]);
	if(strcmp(str, "score") == 0)
	{
		nmz_set_sortmethod(SORT_BY_SCORE);
		nmz_set_sortorder(DESCENDING);
	}
	else if(strcmp(str, "later") == 0)
	{
		nmz_set_sortmethod(SORT_BY_DATE);
		nmz_set_sortorder(DESCENDING);
	}
	else if(strcmp(str, "earlier") == 0)
	{
		nmz_set_sortmethod(SORT_BY_DATE);
		nmz_set_sortorder(ASCENDING);
	}
	else if(strcmp(str, "date") == 0)
	{
		nmz_set_sortmethod(SORT_BY_DATE);
		if(RARRAY(asort)->len <= 1)
			rb_raise(rb_eArgError, "Invalid sort type %s", RSTRING(sort)->ptr);
		str = StringValuePtr(RARRAY(asort)->ptr[1]);
		if(strcmp(str, "late") == 0)
			nmz_set_sortorder(DESCENDING);
		else if(strcmp(str, "early") == 0)
			nmz_set_sortorder(ASCENDING);
		else
			goto invalid;
	}
	else if(strcmp(str, "field") == 0)
	{
		if(RARRAY(asort)->len <= 1)
			rb_raise(rb_eArgError, "Invalid sort type %s", RSTRING(sort)->ptr);
		str = StringValuePtr(RARRAY(asort)->ptr[1]);
		nmz_set_sortmethod(SORT_BY_FIELD);
		nmz_set_sortfield(str);
		if(RARRAY(asort)->len == 2)
			nmz_set_sortorder(ASCENDING);
		else
		{
			str = StringValuePtr(RARRAY(asort)->ptr[2]);
			if(strcmp(str, "ascending") == 0)
				nmz_set_sortorder(ASCENDING);
			else if(strcmp(str, "descending") == 0)
				nmz_set_sortorder(DESCENDING);
			else
				goto invalid;
		}
	}
	else
	{
	invalid:
		rb_raise(rb_eArgError, "Invalid sort type %s", RSTRING(sort)->ptr);
	}
}

static void process_lang(VALUE lang)
{
	char *slang = "C";
	
	if(!NIL_P(lang))
		slang = StringValuePtr(lang);
	nmz_set_lang(slang);
}

static VALUE process_fields(VALUE fields)
{
	VALUE result, str;
	int i;
	
	result = rb_ary_new3(1, STRING_LITERAL("uri"));
	if(NIL_P(fields))
		return result;
	Check_Type(fields, T_ARRAY);
	for(i = 0; i < RARRAY(fields)->len; ++i)
	{
		str = RARRAY(fields)->ptr[i];
		SafeStringValue(str);
		rb_ary_push(result, rb_obj_dup(str));
	}
	rb_funcall2(result, uniq_bang, 0, NULL);
	return result;
}

static VALUE process_indices(VALUE indices)
{
	VALUE index;
	int i;
	
	if(TYPE(indices) == T_ARRAY)
	{
		if(RARRAY(indices)->len >= INDEX_MAX)
			rb_raise(rb_eArgError, "Too many indices (max %d)", INDEX_MAX);
		for(i = 0; i < RARRAY(indices)->len; ++i)
		{
			index = RARRAY(indices)->ptr[i];
			SafeStringValue(index);
			if(nmz_add_index(RSTRING(index)->ptr) != SUCCESS)
				rb_raise(rb_eArgError, "nmz_add_index failed: %s", RSTRING(index)->ptr);
		}
	}
	else
	{
		SafeStringValue(indices);
		if(nmz_add_index(RSTRING(indices)->ptr) != SUCCESS)
			rb_raise(rb_eArgError, "nmz_add_index failed: %s", RSTRING(indices)->ptr);
		indices = rb_ary_new3(1, indices);
	}
	return indices;
}

static VALUE process_query(VALUE query, VALUE subquery)
{
	StringValue(query);
	if(!NIL_P(subquery))
	{
		query = rb_funcall(query, '+', 1, STRING_LITERAL(" "));
		rb_funcall(query, push, 1, subquery);
	}
	if(RSTRING(query)->len >= QUERY_MAX)
		rb_raise(rb_eArgError, "Too long query (max %d bytes)", QUERY_MAX);
	
	return query;
}

static VALUE result_make_fields(int idxid, int docid, VALUE field_list)
{
	char result[BUFSIZE];
	VALUE fields, field;
	int i;
	
	fields = rb_hash_new();
	for(i = 0; i < RARRAY(field_list)->len; ++i)
	{
		field = RARRAY(field_list)->ptr[i];
		nmz_get_field_data(idxid, docid, RSTRING(field)->ptr, result);
		rb_hash_aset(fields, field, rb_tainted_str_new2(result));
	}
	return fields;
}

static VALUE result_make_hlist(NmzResult *hlist, VALUE fields)
{
	VALUE resultdata, results;
	resultdata_t *rdata;
	int i;
	
	results = rb_ary_new2(hlist->num);
	for(i = 0; i < hlist->num; ++i)
	{
		resultdata = Data_Make_Struct(rb_cResultData, resultdata_t, rdata_mark, free, rdata);
		rdata->score = INT2NUM(hlist->data[i].score);
		rdata->fields = result_make_fields(hlist->data[i].idxid, hlist->data[i].docid, fields);
		rdata->date = rb_time_new(hlist->data[i].date, 0);
		rdata->rank = INT2NUM(hlist->data[i].rank);
		rb_ary_push(results, resultdata);
	}
	return results;
}

static VALUE result_make_hitnumlist(struct nmz_hitnumlist *hitnumlist)
{
	hitnumdata_t *hdata;
	VALUE hlist, hitnumdata;
	
	if(!hitnumlist)
		return Qnil;
	
	hlist = rb_ary_new();
	while(hitnumlist)
	{
		hitnumdata = Data_Make_Struct(rb_cHitnumData, hitnumdata_t, hdata_mark, free, hdata);
		hdata->word = rb_tainted_str_new2(hitnumlist->word);
		hdata->hitnum = INT2NUM(hitnumlist->hitnum);
		hdata->stat = INT2NUM(hitnumlist->stat);
		hdata->phrase = result_make_hitnumlist(hitnumlist->phrase);
		rb_ary_push(hlist, hitnumdata);
		hitnumlist = hitnumlist->next;
	}
	return hlist;
}

static VALUE result_make_hitnumlists(int len)
{
	VALUE results;
	int i;
	
	results = rb_ary_new2(len);
	for(i = 0; i < len; ++i)
		rb_ary_push(results, result_make_hitnumlist(nmz_get_idx_hitnumlist(i)));
	return results;
}

struct search_t
{
	int argc;
	VALUE *argv;
	NmzResult hlist;
};

static VALUE search_main(VALUE search_0)
{
	struct search_t *search = (struct search_t *)search_0;
	VALUE query, indices, option;
	VALUE sort, lang, fields, subquery;
	VALUE result, exc;
	char *squery;
	result_t *aresult;
	int i;
	
	sort = lang = subquery = fields = Qnil;
	if(rb_scan_args(search->argc, search->argv, "21", &query, &indices, &option) == 3)
	{
		Check_Type(option, T_HASH);
		sort = rb_hash_aref(option, STRING_LITERAL("sort"));
		lang = rb_hash_aref(option, STRING_LITERAL("lang"));
		fields = rb_hash_aref(option, STRING_LITERAL("fields"));
		subquery = rb_hash_aref(option, STRING_LITERAL("subquery"));
	}
	process_sort(sort);
	process_lang(lang);
	indices = process_indices(indices);
	fields = process_fields(fields);
	query = process_query(query, subquery);
	
	squery = ALLOCA_N(char, RSTRING(query)->len * 2 + 4);
	strcpy(squery, RSTRING(query)->ptr);
	nmz_codeconv_query(squery);
	
	search->hlist = nmz_search(squery);
	if(search->hlist.stat != SUCCESS)
	{
		exc = rb_exc_new2(rb_eNamazuError, nmz_get_dyingmsg());
		rb_iv_set(exc, "stat", INT2NUM(search->hlist.stat));
		rb_exc_raise(exc);
	}
	
	result = Data_Make_Struct(rb_cResult, result_t, result_mark, free, aresult);
	aresult->hlist = result_make_hlist(&search->hlist, fields);
	aresult->hitnumlist = result_make_hitnumlists(RARRAY(indices)->len);
	
	return result;
}

static VALUE search_ensure(VALUE search_0)
{
	struct search_t *search = (struct search_t *)search_0;
	if(search->hlist.data)
		nmz_free_hlist(search->hlist);
	nmz_free_internal();
	return Qnil;
}

static VALUE namazu_f_search(int argc, VALUE *argv, VALUE self)
{
	struct search_t search;
	
	search.argc = argc;
	search.argv = argv;
	search.hlist.data = NULL;
	return rb_ensure(search_main, (VALUE)&search, search_ensure, (VALUE)&search);
}


void Init_namazu()
{
	VALUE mSearch;
	
	mSearch = rb_define_module("Search");
	rb_mNamazu = rb_define_module_under(mSearch, "Namazu");
	rb_cResult = rb_define_class_under(rb_mNamazu, "Result", rb_cData);
	rb_cResultData = rb_define_class_under(rb_mNamazu, "ResultData", rb_cData);
	rb_cHitnumData = rb_define_class_under(rb_mNamazu, "HitnumData", rb_cData);
	rb_eNamazuError = rb_define_class_under(rb_mNamazu, "Error", rb_eRuntimeError);
	
	rb_define_module_function(rb_mNamazu, "search", namazu_f_search, -1);
	rb_define_module_function(rb_mNamazu, "Search", namazu_f_search, -1);
	
	rb_define_method(rb_cResult, "hlist", result_hlist, 0);
	rb_define_method(rb_cResult, "result", result_hlist, 0);
	rb_define_method(rb_cResult, "hitnumlist", result_hitnumlist, 0);
	rb_define_method(rb_cResult, "inspect", result_inspect, 0);
	
	rb_define_method(rb_cResultData, "score", rdata_score, 0);
	rb_define_method(rb_cResultData, "fields", rdata_fields, 0);
	rb_define_method(rb_cResultData, "date", rdata_date, 0);
	rb_define_method(rb_cResultData, "rank", rdata_rank, 0);
	rb_define_method(rb_cResultData, "method_missing", rdata_missing, 1);
	rb_define_method(rb_cResultData, "inspect", rdata_inspect, 0);
	
	rb_define_method(rb_cHitnumData, "word", hdata_word, 0);
	rb_define_method(rb_cHitnumData, "hitnum", hdata_hitnum, 0);
	rb_define_method(rb_cHitnumData, "stat", hdata_stat, 0);
	rb_define_method(rb_cHitnumData, "phrase", hdata_phrase, 0);
	rb_define_method(rb_cHitnumData, "inspect", hdata_inspect, 0);
	
	rb_define_method(rb_eNamazuError, "stat", nmzerr_stat, 0);
	
	rb_define_const(rb_mNamazu, "SUCCESS", INT2NUM(SUCCESS));
	rb_define_const(rb_mNamazu, "ERR_FATAL", INT2NUM(ERR_FATAL));
	rb_define_const(rb_mNamazu, "ERR_TOO_LONG_QUERY", INT2NUM(ERR_TOO_LONG_QUERY));
	rb_define_const(rb_mNamazu, "ERR_INVALID_QUERY", INT2NUM(ERR_INVALID_QUERY));
	rb_define_const(rb_mNamazu, "ERR_TOO_MANY_TOKENS", INT2NUM(ERR_TOO_MANY_TOKENS));
	rb_define_const(rb_mNamazu, "ERR_TOO_MUCH_MATCH", INT2NUM(ERR_TOO_MUCH_MATCH));
	rb_define_const(rb_mNamazu, "ERR_TOO_MUCH_HIT", INT2NUM(ERR_TOO_MUCH_HIT));
	rb_define_const(rb_mNamazu, "ERR_REGEX_SEARCH_FAILED", INT2NUM(ERR_REGEX_SEARCH_FAILED));
	rb_define_const(rb_mNamazu, "ERR_PHRASE_SEARCH_FAILED", INT2NUM(ERR_PHRASE_SEARCH_FAILED));
	rb_define_const(rb_mNamazu, "ERR_FIELD_SEARCH_FAILED", INT2NUM(ERR_FIELD_SEARCH_FAILED));
	rb_define_const(rb_mNamazu, "ERR_CANNOT_OPEN_INDEX", INT2NUM(ERR_CANNOT_OPEN_INDEX));
	rb_define_const(rb_mNamazu, "ERR_NO_PERMISSION", INT2NUM(ERR_NO_PERMISSION));
	rb_define_const(rb_mNamazu, "ERR_CANNOT_OPEN_RESULT_FORMAT_FILE", INT2NUM(ERR_CANNOT_OPEN_RESULT_FORMAT_FILE));
	rb_define_const(rb_mNamazu, "ERR_INDEX_IS_LOCKED", INT2NUM(ERR_INDEX_IS_LOCKED));
	rb_define_const(rb_mNamazu, "ERR_OLD_INDEX_FORMAT", INT2NUM(ERR_OLD_INDEX_FORMAT));
	
	fetch = rb_intern("fetch");
	split = rb_intern("split");
	uniq_bang = rb_intern("uniq!");
	push = rb_intern("<<");
}

