/* -------------------------------------------------------------------------
 * Title: Hangman v1.0 (bilingual)
 * 
 * From: migf1 (mig_f1@hotmail.com)
 * 
 * Description:
 *	A rather simplistic implementation of the Hangman game, written in C (C99)
 *	This is NOT AI! The human player makes the guesses!
 * 
 * Notes:
 *	The program WILL NOT run without its accompanying world-list files:
 *	- 	en_scrabble.dic: 	SCRABBLE tournament Englih words (172804 words)
 *	-	el_ispell_small.dic:	small version of iSPELL Greek words (76496 words)
 *
 *	To play with Greek words you MUST USE Windows1253 8-BIT FONTS at your terminal
 *	AND compile the source code with GCC 4.x
 *
 * Implementation:
 *	When the user specifies the desired length for the secret word,
 *	the program loads from the dictionary all the words with the specified
 *	length into a singly linked-list. It then picks a word randomly and
 *	destroys the list.
 *
 *	At the guessing prompt, the user may enter either a single letter or
 *	a whole word. In the latter case, if the entered word MATCHES the
 *	secret word, we have a winner and the moves loop stops. Otherwse the
 *	1st letter of the entered word is checked against the secret letters.
 *
 *	The program keeps track of all the letters already typed in by the user
 *	and it doesn't let him re-enter them. It also keeps track of the number
 *	of letters remained to be found, using it as a condition to decide whether
 *	the user has found the secret word or not (missing == 0).
 *
 *	In general, all input is validated in real time, keep prompting the user
 *	until he enters valid input.
 *
 *	You can play an infinite number of rounds (new words of the same length)
 *	as long as you answer positively when you are asked for one more round.
 *	Each round consists of a user-defined number of moves (9 to 13)
 *
 * -------------------------------------------------------------------------
 */

#include <stdio.h>
#include <stdlib.h>				// for atoi(), exit()
#include <string.h>				// for strlen()
#include <time.h>				// for time()
#include <ctype.h>				// for toupper(), isblank()

#define MAXINPUT	255+1			// max # of chars read from user input

#define MINMOVES	9			// minimum allowed guesses
#define MAXMOVES	13			// maximum allowed guesses

#define DIC_FNAME_EN	"en_scrabble.dic"	// filename of english wordlist
#define DIC_FNAME_EL	"el_ispell_small.dic"	// filename of greek wordlist

#define MAX_WLEN	20+1			// maximum allowed lebgth of secret word
#define MIN_WLEN	4+1			// minimum allowed lebgth of secret word

						// our alphabet of allowed letters
#define ALPHABET_UPPER	"ABCDEFGHIJKLMNOPQRSTUVWXYZΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩ"

#define MIX(x,y)	(x) < (y) ? (x) : (y)

#define CREDITS()	printf("\n%78s", "H A N G M A N  1.0  (by migf1) \n")

#define PAUSE()						\
	do{						\
		char myinbufr[255]="";			\
		printf("\npress ENTER to continue...");	\
		fgets(myinbufr, 255, stdin);		\
	}while(0)

typedef enum { FALSE=0, TRUE } Bool;		// our custom boolean type

typedef struct node {				// node struct for the words linked-list
	char word[ MAX_WLEN ];
	struct node *next;
} Node;

typedef struct {				// struct for the words linked-list
	Node *head, *tail;
	long int len;
} List;

/* ------------------------------------------------------------------
 * Read up to len-1 chars from stdin into string s & null terminate it
 */
char *s_get(char *s, size_t len)
{
        char *cp;
        for (cp=s; (*cp=getc(stdin)) != '\n' && (cp-s) < len-1; cp++ )
                ;					// for-loop with empty body
        *cp = '\0';					// null-terminate

        return s;
}

/* ------------------------------------------------------------------
 * Convert char c to uppercase (explicit care is taken for Greek 8-bit chars)
 */
char to_upper( int c )
{
	switch( c )
	{
		case 'α':
		case 'ά':
			c = 'Α';
			break;
		case 'β':
			c = 'Β';
			break;
		case 'γ':
			c = 'Γ';
			break;
		case 'δ':
			c = 'Δ';
			break;
		case 'ε':
		case 'έ':
			c = 'Ε';
			break;
		case 'ζ':
			c = 'Ζ';
			break;
		case 'η':
		case 'ή':
			c = 'Η';
			break;
		case 'θ':
			c = 'Θ';
			break;
		case 'ι':
		case 'ί':
			c = 'Ι';
			break;
		case 'ϊ':
		case 'ΐ':
			c = 'Ϊ';
			break;
		case 'κ':
			c = 'Κ';
			break;
		case 'λ':
			c = 'Λ';
			break;
		case 'μ':
			c = 'Μ';
			break;
		case 'ν':
			c = 'Ν';
			break;
		case 'ξ':
			c = 'Ξ';
			break;
		case 'ο':
		case 'ό':
			c = 'Ο';
			break;
		case 'π':
			c = 'Π';
			break;
		case 'ρ':
			c = 'Ρ';
			break;
		case 'σ':
		case 'ς':
			c = 'Σ';
			break;
		case 'τ':
			c = 'Τ';
			break;
		case 'υ':
		case 'ύ':
			c = 'Υ';
			break;
		case 'ϋ':
		case 'ΰ':
			c = 'Ϋ';
			break;
		case 'φ':
			c = 'Φ';
			break;
		case 'χ':
			c = 'Χ';
			break;
		case 'ψ':
			c = 'Ψ';
			break;
		case 'ω':
		case 'ώ':
			c = 'Ω';
			break;

		default:
			c = toupper( c );
			break;
	}

	return c;
}

/* ------------------------------------------------------------------
 * Convert to uppercase all chars of the null-terminated string s
 */
char *s_toupper(char *s)
{
	char *ret;

	for ( ret=s; (*s=to_upper(*s)); s++ )
		;
	return ret;
}

/* ------------------------------------------------------------------
 * Copy up to n-1 chars of string src into string dst (both strings are null-terminated)
 */
char *s_ncopy( char *dst, const char *src, int n )
{
	char *ret = dst;
	while ( (dst-ret) < n-1 && (*dst=*src) != '\0' )
		dst++, src++;

	if ( *dst )
		*dst = 0;

	return ret;
}

/* ------------------------------------------------------------------
 * Trim leading & trailing spaces from a null-termintated string s
 */
char *s_trim( char *s )
{
	char *cp1;				// for parsing the whole s
	char *cp2;				// for shifting & padding

	// trim leading & shift left remaining
	for (cp1=s; isblank(*cp1); cp1++ )	// skip leading spaces, via cp1
		;
	for (cp2=s; *cp1; cp1++, cp2++)		// shift left remaining chars, via cp2
		*cp2 = *cp1;
	*cp2-- = '\0';				// mark end of left trimmed s

	// replace trailing spaces with '\0's
	while ( cp2 > s && isblank(*cp2) )
		*cp2-- = '\0';			// pad with '\0'

	return s;
}

/* ------------------------------------------------------------------
 * Append char c into null-terminated string s (if there's no room, s is returned intact)
 */
char *s_cappend( char *s, const char c, const int slen )
{
	if ( !s )
		return NULL;

	char *cp;
	for (cp=s; *cp && (cp-s) < slen-1; cp++)
		;
	if ( cp-s < slen-1 ) {
		*cp = c;
		*++cp = '\0';
	}

	return s;
}


/* ------------------------------------------------------------------
 * Insert data as LAST node of list
 */
Bool list_append( List *list, char *data )
{
	Node *new = calloc(1, sizeof(Node) );
	if ( !new )
		return FALSE;

	s_ncopy(new->word, data, MAX_WLEN);
	new->next = NULL;

	if ( list->head == NULL ) {
		list->head = list->tail = new;
		(list->len)++;
		return TRUE;
	}

	list->tail->next = new;
	list->tail = new;
	(list->len)++;

	return TRUE;
}

/* ------------------------------------------------------------------
 * Return a pointer to the ith node of list
 */
Node *list_getinode( List list, long int i )
{
	long int j;
	for (j=0; j < i && list.head; j++)
		list.head = list.head->next;

	return list.head;
}

/* ------------------------------------------------------------------
 * Print list contents
 */
void list_print( List list )
{
	while ( list.head ) {
		printf("\t%s\n", list.head->word );
		list.head = list.head->next;
	}
	putchar('\n');				// αλλαγή γραμμής

	return;
}

/* ------------------------------------------------------------------
 * Free memory reserved for list
 */
void list_destroy( List *list )
{
	if ( list->head == NULL )
		return;

	Node *dummy = NULL;
	while ( list->head ) {
		dummy = list->head->next;
		free( list->head );
		list->head = dummy;
	}

	return;
}

/* ------------------------------------------------------------------
 * Helper function, for reading the input buffer, trimming leading & trailing blanks
 * and convert it to uppercase
 */
char *read_input( char *input, const int maxinput )
{
	if ( !input )
		return NULL;

	s_get(input, maxinput);
	s_trim( s_toupper(input) );

	return input;
}

/* ------------------------------------------------------------------
 * Print the secret word.
 *
 * Already found letters must be contained inside the null-terminated
 * string: found. If so, they get printed at their correct positions
 * inside the secret word, otherwise a dash is printed instead of the letter. 
 *
 * IMPORTANT:
 *	The function returns the number of letters that have NOT been found
 *	yet! We use this information to control the main loop of the game.
 *	Specidically, we stop the loop when this number gets equal to 0
 *	because that means ALL the letters of the secret word have been found!
 */
int print_secret( char *secret, char *found )
{
	register int i, ret=0;
	for (i=0; secret[i] != '\0'; i++)
	{
		if ( strchr(found, secret[i]) ) {
			putchar( to_upper(secret[i]) );
			ret++;
		}
		else
			putchar('-');
	}

	return i-ret;
}

/* ------------------------------------------------------------------
 * Ask the user to choose which dictionary will the program use for pickicng
 * later on the secret word.
 */
void get_dic( char *dic_fname, const char *en_fname, const char *el_fname )
{
	char input[MAXINPUT] = "";

	CREDITS();
	printf("%76s\n", "------------------------------");
	printf("%70s\n", "en\tEnglish dictionary");
	printf("%70s\n", "el\tΕλληνικό λεξιλόγιο");
	printf("%76s\n", "------------------------------");
	do {
		printf("%62s","Select (en/el): ");
		read_input(input, MAXINPUT);
	}while ( strncmp("EN", input, MAXINPUT) && strncmp("EL", input, MAXINPUT) );
	if ( strncmp(input, "EN", MAXINPUT) == 0 )
		s_ncopy(dic_fname, en_fname, MAXINPUT);
	else
		s_ncopy(dic_fname, el_fname, MAXINPUT);
	printf(	"\n%76s\n",
		strncmp(input, "EN", MAXINPUT) == 0 ?
		"OFFICIAL  SCRABBLE  WORD  LIST" :
		"με χρήση  ΕΛΛΗΝΙΚΟΥ λεξιλογίου"
	);
	if ( strncmp(input, "EL", MAXINPUT) == 0 ) {
		printf("%76s\n\n", "γυρίστε  το  πληκτρολόγιό  σας");
		printf("%76s\n\n", "Win1253 (8-BIT) FONTS REQUIRED");
	}
	else
		putchar('\n');

	return;
}

/* ------------------------------------------------------------------
 * Ask user for the desired length of the secret word
 */
int get_wlen( const int minwlen, const int maxwlen )
{
	int wlen = 0;
	char input[MAXINPUT] = "";

	do {
		printf("How many letters (%d - %d)? ", minwlen-1, maxwlen-1);
		read_input(input, MAXINPUT);
		wlen = atoi( input );
	} while (wlen < minwlen-1 || wlen > maxwlen-1 );

	return wlen;
}

/* ------------------------------------------------------------------
 * Ask user for the number of moves he''l be allowed to use for guessing the secret word
 */
int get_maxmoves( const int minturns, int maxturns )
{
	int maxmoves = 0;
	char input[MAXINPUT] = "";

	do {
		printf("How many moves (%d-%d)? ", minturns, maxturns);
		read_input(input, MAXINPUT);
		maxmoves = atoi(input);
	} while ( maxmoves < minturns || maxmoves > maxturns  );

	return maxmoves;
}

/* ------------------------------------------------------------------
 * Open dictionary: dic_fname, load all words of length: lensecret into a
 * linked list, pick randomly a word from that list and save it into the
 * null-terminated string: secret
 */
void pick_word(
	char *secret, const int lensecret, const char *dic_fname, const int maxwlen )
{
	FILE *fp;
	List wlist = { .head=NULL, .tail=NULL, .len=0L };
	long int pick = 0L;
	char dummy[MAXINPUT] = "";

	// open the dictionary file
	fp = fopen(dic_fname, "rb");
	if ( !fp ) {
		puts("\t*** error: could not read dictionary");
		exit( EXIT_FAILURE );
	}

	// create a list of all words of length lensecret found in the dictionary 
	while ( !feof(fp) ) {
		fscanf(fp, "%s", dummy);
		s_toupper(dummy);
		if ( strlen(dummy) == lensecret )
			list_append( &wlist, dummy );
	}
	fclose(fp);

	// a random word fron the list
	pick = rand() % wlist.len;
	Node *p = list_getinode( wlist, pick );
	if ( p )
		s_ncopy(secret, p->word, maxwlen);

	// destroy the list
	list_destroy( &wlist );

	return;
}

/* ------------------------------------------------------------------
 * Our main function...
 * 	secret:	null-terminated string for the secret word
 *	found:	null-terminated string holding letters found correctly by the user
 *	played:	null-terminated string holding all the letters typed by the user
 *	lensecret:	length of the secret word (provided by the user)
 *	missing:	number of letters which have not been found yet
 */
int main( void )
{
	char input[ MAXINPUT ] = "";				// our input buffer
	char dic_fname[ MAXINPUT ] = "";			// dictionary filename
	char secret[ MAX_WLEN ] = "", found[ MAX_WLEN ] = "", played[ MAX_WLEN ] = "";
	int round=1, lensecret = 0, move=0, maxmoves=0, missing=0;

	srand( time(NULL) );					// init random generator

	get_dic( dic_fname, DIC_FNAME_EN, DIC_FNAME_EL );	// ask for dictionary
	lensecret = get_wlen( MIN_WLEN, MAX_WLEN );		// ask for word length
	maxmoves = get_maxmoves( MINMOVES, MAXMOVES );		// ask for max moves

	do {							// rounds loop
		printf("\n____________________________________________________________________ ROUND %d\n\n", round++);
								// pick the secret word
		pick_word( secret, lensecret, dic_fname, MAX_WLEN );
		move = 0;
		do {						// moves loop
			printf("\n\t");
			missing = print_secret(secret, found);	// # of missing letters
			if ( missing == 0)			// no missinng letters
				break;				// word found

			printf("\t(%d missing) played: %s\n\n", missing, played);

			do					// ask user for a guess
			{					// (allow letter or word)
				printf("Guess #%d/%d: ", move+1, maxmoves);
				read_input(input, MAXINPUT);
								// correct word entered
				if ( strncmp(input, secret, MAX_WLEN) == 0 )
					break;
								// check letter validity
			} while ( *input == '\0' || !strchr(ALPHABET_UPPER, *input) || strchr(played, *input) );

			// valid letter was entered
			s_cappend( played, *input, MAX_WLEN );	// append it to played
			if ( strchr(secret, *input) )		// correct letter...
				s_cappend( found, *input, MAX_WLEN );// append to found

			move++;					// increase moves counter

		} while( strncmp(input,secret,MAX_WLEN) != 0 && move < maxmoves && missing > 0 );

		printf("\n\tthe word was: %s\n", secret);	// reveal secret word

		printf("\nplay again with another word (/n)? ");// ask for another round
		read_input(input, MAXINPUT);
		*secret = *found = *played = '\0';		// initializations

	} while ( *input != 'N' && *input != 'Ο' );

	exit( EXIT_SUCCESS );
}
