/* (c) 2009 Karel Kulhavy
 * Under GPL */

#define _GNU_SOURCE
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <ctype.h>

#define DEBUG_MATCHES
#define PSIZE 65536
int result_count=64;
int print_input=0; /* Print only input halves of the pairs - the matches */

#define MAX(a,b) ((a)>(b)?(a):(b))

char *db_path;
static char buf[4096];
struct pair{
	char *question;
	char *answer;
	float rating; /* The higher the better match */
	float rating_words_input_in_db;
	float rating_words_db_in_input;
	unsigned rating_substring;
	unsigned rating_identicals;
}pairs[PSIZE];
unsigned n_pairs;
FILE *rnd_file;

/* 1=equal 0=not equal */
int equal_ci(unsigned char c1, unsigned char c2)
{
	if ((c1^c2)&~0x20) return 0;
	return toupper(c1)==toupper(c2);
} 

/* Returns length of the longest common prefix */
unsigned common_prefix(char *s1, char *s2)
{
	unsigned len=0;

	while (*s1&&equal_ci(*s1,*s2)){
		s1++;
		s2++;
		len++;
	}
	return len;
}

/* Advances to the first word character or terminator */
char *find_word_character_or_terminator(char *in)
{
	while ((*in>0&&*in<=32)||*in=='.'||*in=='\''||*in==',') in++;
	return in;
}

/* Advances to the first non-word character or terminator */
char *find_nonword_character_or_terminator(char *in)
{
	while ((*in<0||*in>32)&&*in!='.'&&*in!='\''&&*in!=',') in++;
	return in;
}

/* *needle must be pokable. Returns word length, not counting whitespace.
 * Or 0 if not found. Doesn't give extra points for repeated occurence.
 * Needles only one letter long are ignored. Whitespace are automatically
 * skipped at the beginning. Returns word length with exception for 1-letter
 * words, which return word length of 0. */
unsigned word_match(char *haystack, char **needle, unsigned *word_length)
{
	char *first_whitespace;
	char saved_char;
	unsigned rv;

	first_whitespace=find_nonword_character_or_terminator(*needle);
	saved_char=*first_whitespace;
	*first_whitespace=0;
	*word_length=first_whitespace-*needle;
	if (strlen(*needle)<=1) *word_length=0;
	rv=strcasestr(haystack, *needle)?*word_length:0;
	*first_whitespace=saved_char;
	*needle=first_whitespace; /* Advance the passed pointer on next whitespace
			     or terminator */
	return rv;
}

/* Sum of lengths of words that were found */
/* Returns 0-1 how many percent of word length from total word length from
 * needle were found in haystack. The length of individual found words
 * is summed and the total word length too and that is then divided.*/
float words_match(char *haystack, char *needle)
{
	unsigned word_length_total=0;
	unsigned hits=0;
	unsigned word_length;
	char *needle_copy;
	char *word_beginning;

	needle_copy=malloc(strlen(needle)+1);
	strcpy(needle_copy, needle);
	word_beginning=needle_copy;
	while (1){
		word_beginning=find_word_character_or_terminator(word_beginning);
		if (!*word_beginning) break; /* Terminator */
		hits+=word_match(haystack, &word_beginning, &word_length); /* Automatically advances on first
					whitespace character or terminator */
		word_length_total+=word_length;
	}
	free(needle_copy);
	return word_length_total?(float)hits/word_length_total:1;
}

/* Clobbers buf. Returns offset in array with first occurence of the question or
 * <0 if not found */
void rate_by_question(char *question)
{
	char *question_in_ary;
	unsigned ary_index;

	/* Symmetrized. */
	for (ary_index=0;ary_index<n_pairs;ary_index++){
		question_in_ary=pairs[ary_index].question;

		/* ---------------- identicals -------------------- */
		/* Input is identical with database */
		pairs[ary_index].rating_identicals=strcmp(question_in_ary,question)?0:2;
		/* This implies all 6 */

		/* Input is case insensitive identical with database */
		if (!strcasecmp(question_in_ary,question))
			pairs[ary_index].rating_identicals+=12; /* case insensitive
			match is more important than case sensitive prefix */

		pairs[ary_index].rating+=pairs[ary_index].rating_identicals;

		/* ---------------- prefixes -------------------- */

		/* Input is prefix of database */
		if (!strncmp(question_in_ary,question,strlen(question)))
			pairs[ary_index].rating+=2; /* Implies all 4 */

		/* Input is case insensitive prefix of database */
		if (!strncasecmp(question_in_ary,question,strlen(question)))
			pairs[ary_index].rating+=4; /* case insensitive prefix
			is more important than case sensitive substring. */
		
		/* ---------------- substrings -------------------- */

		/* Input is substring of database */
		pairs[ary_index].rating_substring=strstr(question_in_ary,question)?1:0;

		/* Input is case insensitive substring of database */
		pairs[ary_index].rating_substring+=strcasestr(question_in_ary,question)?1:0;

		/* Removed database is (case sensitive) substring of input,
		 * because database entries with one space matched almost
		 * everything */

		pairs[ary_index].rating+=pairs[ary_index].rating_substring;

		/* Words from input match words from database */
		pairs[ary_index].rating_words_input_in_db=words_match
					(question_in_ary,question);

		pairs[ary_index].rating_words_db_in_input=words_match
					(question, question_in_ary);
		/* Multiply together */
		pairs[ary_index].rating
			+=pairs[ary_index].rating_words_db_in_input
			*pairs[ary_index].rating_words_input_in_db;
			/* If product is replaced by sum, the result is
			 * horrible */

	}
}

/* in points to: number of newlines in the string, space, string, newline.
 * clobbers buf. Can be extra whitespace before the initial number of newlines.
 * */
char *read_string(FILE *f, char *in)
{
	char * allocated;
	unsigned newlines=strtoul(in, &in, 10);
	if (*in) in++; /* Skip space after number */
	allocated=malloc(strlen(in)+1);
	strcpy(allocated, in);
	for (;newlines;newlines--){
		fgets(buf, sizeof(buf), f);
		allocated=realloc(allocated,strlen(allocated)+strlen(buf)+1);
		strcat(allocated, buf);
	}
	if (*allocated&&allocated[strlen(allocated)-1]=='\n')
		allocated[strlen(allocated)-1]=0;
	/* Remove terminating newline. It's not part of the string. */
	return allocated;
}

/* Fills in rating to zero too */
void add_pair(char *question, char *answer)
{
	if (n_pairs>=PSIZE){
		fprintf(stderr,"Programs fixed internal memory was exheusted!\n");
		exit(1);
	}
	if (!question){
		fprintf(stderr,"Answer without a question!\n");
		exit(1);
	}
	pairs[n_pairs].question=question;
	pairs[n_pairs].answer=answer;
	pairs[n_pairs].rating=0;
	n_pairs++;
}

unsigned count_questions(unsigned idx)
{
	unsigned count=1;
	while(idx+count<n_pairs&&!strcmp(pairs[idx+count].question,
				pairs[idx].question)) count++;
	return count;
}

/* 0<=rnd_num()<range.
 * rnd_file must be open /dev/urandom */
unsigned rnd_num(unsigned range)
{
	unsigned long target;

	fread(&target, sizeof(target), 1, rnd_file);
	target%=range;
	return target;
}

unsigned count_newlines(char *text)
{
	unsigned newlines=0;
	while (*text){
		if (*text=='\n') newlines++;
		text++;
	}
	return newlines;
}

void write_string(FILE *g, char *string)
{
	fprintf(g, "%u %s\n", count_newlines(string), string);
}

/* q question a answer. q=NULL to finalize. */
void compress(FILE *g, char *q, char *a)
{
	static char *last_q, *last_a, count;

	if (!q){
		/* Finalize */
		if (last_a){
			fprintf(g,"%u ", count);
			write_string(g, last_a);
		}
		count=0;
		last_a=NULL;
		last_q=NULL;
		return;

	}
	/* No finalization here */
	if (!last_q||strcmp(q,last_q)){
		/* First invocation or Question doesn't match */
		if (last_a){
			/* Question doesn't match */
			fprintf(g,"%u ", count);
			write_string(g, last_a);
			putc('\n', g); /* Empty line before the heading */
		}
		putc('=',g);
		write_string(g, q);
		count=1;
	}else{
		/* Question matches */
		if (strcmp(a,last_a)){
			/* Only question matches */
			fprintf(g,"%u ", count);
			write_string(g, last_a);
			count=1;

		}else{
			/* both match */
			count++;
		}
	}


	last_q=q;
	last_a=a;
}

void write_db(void)
{
	unsigned idx;
	char *temp_path;
	FILE *g;
	
	temp_path=malloc(strlen(db_path)+128);
	sprintf(temp_path,"%s_%u.tmp",db_path,getpid());
	g=fopen(temp_path,"w");
	if (!g){
		fprintf(stderr,"cx: cannot open %s: ", temp_path);
		perror("");
		exit(1);
	}
	for (idx=0;idx<n_pairs;idx++)
		compress(g, pairs[idx].question, pairs[idx].answer);
	compress(g, NULL, NULL); /* Finalize */
	fclose(g);
	rename(temp_path,db_path);
}

int pair_comp_alphabetically(const void *in1, const void *in2)
{
	const struct pair *p1=in1;
	const struct pair *p2=in2;
	int rv;

	rv=strcmp(p1->question, p2->question);
	if (rv) return rv;
	return strcmp(p1->answer, p2->answer);
}

int pair_comp_by_rating(const void *in1, const void *in2)
{
	const struct pair *p1=in1;
	const struct pair *p2=in2;

	if (p1->rating>p2->rating) return -1;
	else if (p1->rating<p2->rating) return 1;
	else return 0;
}

void sort_db_alphabetically(void)
{
	qsort(pairs, n_pairs, sizeof(*pairs), &pair_comp_alphabetically);
}

void sort_db_by_rating(void)
{
	qsort(pairs, n_pairs, sizeof(*pairs), &pair_comp_by_rating);
}

void print_help(void)
{
	printf(
		"\ncx is Twibright Casanova, a free software to "
		"semiautomate conversation on dating services.\n"
		"Casanova uses the file %s for database, which "
		"has just been created empty.\n\n"
		"How it works:\n"
		"=============\n"
		"Casanova stores conversation in pairs, like "
		"B was replied on A. Then you give it what the other "
		"party says and Casanova suggests what you should "
		"reply.\n\n"
		"Special messages:\n"
		"=================\n"
		"\" \" (1 space) - no preceeding "
		"message, start of conversation.\n"
		"\"  \" (2 spaces) - topic change without "
		"the conversation being interrupted\n"
		"\"   \" (3 spaces) - interruption in the conversation "
		"(days)\n"
		"@vote - favourable vote on your picture or "
		"profile\n"
		"@visit - visit of your profile\n"
		"@pic - picture(s) sent\n\n"
		"Usage:\n"
		"======\n"
		"cx (without parameters) - Casanova tells you how to "
		"start conversation\n"
		"cx <other_said> - Casanova tells you what to answer, "
		"doesn't learn anything.\n"
		"cx \" \" <other_said> - Casanova "
		"learns what the other said as a start of "
		"conversation and tells you "
		"what to answer. Similar with other special messages "
		"than \" \".\n"
		"cx <i_said> <other_said> - Casanova "
		"learns the pair i_said, other_said and tells you "
		"what to answer\n"
		"cx <i_said> <other_said> # - The extra parameter (#)"
		"disables learning and otherwise works the same. "
		"Good when the answer doesn't fit and you want a "
		"different one.\n"
		"How to handle newlines and spaces\n"
		"=================================\n"
		"In shell you can use \"\" or '' to enclose messages "
		"containing spaces and newlines. Do not use \"\" on "
		"messages with ! (exclamation mark) and '' on messages "
		"with ' (apostrophe). If a message contains both, use "
		"\"\" on one part and '' on another.\n\n"
		,db_path);
}

void read_db(void)
{
	FILE *f;
	char *question=NULL;

	f=fopen(db_path,"r");
	if (!f){
		print_help();
		return; /* Zero entries */
	}
	while(fgets(buf, sizeof(buf), f)){
		if (*buf=='='){
			/* Heading */
			question=read_string(f, buf+1);
			//puts(question);
		}else if (*buf<='9'&&*buf>='0'){
			/* Answer: rep string */
			char *after_rep, *answer;
			unsigned rep;
			rep=strtoul(buf, &after_rep, 10);
			answer=read_string(f, after_rep);
			//printf("->%ux %s\n",rep, answer);
			for (;rep;rep--){
				add_pair(question, answer);
			}
		}
	}
	fclose(f);
}

void set_db_path(void)
{
	char *fname="/.casanova_db";
	char *home=getenv("HOME");
	db_path=malloc(strlen(fname)+strlen(home)+1);
	strcpy(db_path, home);
	strcat(db_path,fname);

}

/* Works also for n1==n2 */
void shuffle_pair(int n1, int n2)
{
	struct pair xchg;

	xchg=pairs[n1];
	pairs[n1]=pairs[n2];
	pairs[n2]=xchg;
}

/* Fisher-Yates shuffle. Random permutation. */
void shuffle_db(void)
{
	int n;

	rnd_file=fopen("/dev/urandom", "r");
	if (!rnd_file){
		fprintf(stderr,"cx: cannot open /dev/urandom\n");
		exit(1);
	}

	for (n=n_pairs-1;n>0;n--)
		shuffle_pair(n,rnd_num(n+1));

	fclose(rnd_file);
}

int main(int argc, char **argv)
{
	char *question, *answer;
	char *envptr;

	print_help();

	envptr=getenv("N");
	if (envptr) result_count=atoi(envptr);
	
	envptr=getenv("IN");
	if (envptr) print_input=1;
	
	set_db_path();
	read_db();
	if (argc<2){
		/* No args. question unknown answer start symbol (" ") */
		question="";
		answer=" ";
	}else if (argc==2){
		/* 1 arg. Question unknown answer arg */
		question="";
		answer=argv[1];
	}else if (argc==3){
		/* 2 params. question and answer */
		question=argv[1];
		if (!strcmp(question,"")) question=" ";
		/* "" on input from user also means start of conversation
		 * for easiness */
		answer=argv[2];
	}else{
		/* 3 params in out -, - means in should be ignored and out
		 * as 1-param call */
		question="";
		answer=argv[2];
	}
	/* If the question is known add the pair */
	if (strcmp(question,"")) add_pair(question, answer);


	question=answer; /* Now let the answer be a question */
	rate_by_question(question); /* Assign to each pair how well the
		question of the pair matches the requested question */
	shuffle_db(); /* Random shuffle */
	sort_db_by_rating();

	if (!n_pairs){
		printf("No messages in database yet.\n");
	}else{
		int n;

		for (n=result_count-1;n>=0;n--){
			if (print_input){
				printf("========== total %f,"
					" identicals %d,"
					" substring %d,"
					" words input in db %f,"
					" words db in input %f, "
					" words product %f "
					" ===========\n"
					,pairs[n].rating,
					pairs[n].rating_identicals,
					pairs[n].rating_substring,
					pairs[n].rating_words_input_in_db,
					pairs[n].rating_words_db_in_input,
					pairs[n].rating_words_input_in_db
						*pairs[n].rating_words_db_in_input
					);
				printf(">>>%s<<<\n",pairs[n].question);
			}else{
				printf(
"____________________________________%08.5f____________________________________\n"
					,pairs[n].rating);
				puts(pairs[n].answer);
			}	
		}
		printf("~~~~~~~~~~~~~~~~~~~\n");
	}

	sort_db_alphabetically();
	write_db();

	return 0;
}
