/*
 * Installation should be simple... Create the command entry in interp.c,
 * add a DECLARE_DO_FUN entry to interp.h, and then just copy the code
 * below into the bottom of your act_wiz.c file, should be a clean compile
 * but i make no promises and don't guarantee this code in any way... i'm
 * fairly certain there should be no drastic-type problems with it, but
 * this is the cover-my-ass statement that's so common in most snippets :D
 * I make no guarantee that this code won't screw your mud, and am in no
 * way responsible should this happen, you are installing this code at
 * your own risk and accept any consequences as your own problems... Now,
 * having said that, i can try to help troubleshoot any problems you may
 * have installing this, if you email me at velveeta_512#yahoo.com (just
 * replace the # with an @), or you can drop by my test port for Lace of
 * Ages at loa.slayn.net 9001... also, please report any bugs and/or you
 * find with this to me so that i can make the fixes on my end... There
 * might be a memory leak or 2 left in this thing, so if you find anything
 * just let me know :) Also, my codebase has been altered thus that some
 * of the structures referenced below are no longer in mine, so i wrote it
 * and debugged it until it worked on mine, and then altered it so that it
 * should work on a stock codebase, and haven't tested it after making
 * those additions...
 *
 * In conclusion, all I ask is that you retain the commented header line
 * below when you place it into your code, you don't even have to email
 * me and let me know if you're using it (though if you want to that's fine
 * with me :D)... and please send me those bug reports!
 */

/******************************************************************************************
 * PList, written by Velveeta for Lace of Ages MUD: 05/07/2004                            *
 * Used to get a listing of existent player files meeting certain                         *
 * criteria specified by the user, and sort it in a variety of ways.                      *
 *                                                                                        *
 * Sample usage: plist                                                                    *
 *               plist race human clan tinker class mage level > 30 laston < 7 sort level *
 ******************************************************************************************/

#define	BYNAME		0
#define	BYLEVEL		1
#define BYRACE		2
#define	BYCLAN		3
#define BYCLASS		4
#define	BYLASTON	5

typedef	struct	compare_data	COMPARE_DATA;
typedef	struct	pfile_data	PFILE_DATA;
typedef	struct	search_data	SEARCH_DATA;

struct	compare_data
{
    COMPARE_DATA *	next;
    char *		name;
    char		operator;
    int			value;
    bool		valid;
};

struct pfile_data
{
    PFILE_DATA *	next;
    char *		name;
    sh_int		level;
    sh_int		race;
    sh_int		clan;
    sh_int		class;
    sh_int		sex;
    long		laston;
    bool		valid;
};

struct	search_data
{
    char *		name;
    sh_int		race;
    sh_int		clan;
    sh_int		class;
    sh_int		sort;
    bool		all;
    COMPARE_DATA *	comparison;
};

COMPARE_DATA *compare_free;

COMPARE_DATA *new_compare( void )
{
    static COMPARE_DATA compare_zero;
    COMPARE_DATA *compare;

    if ( compare_free == NULL )
	compare = alloc_perm( sizeof( *compare ) );
    else
    {
	compare = compare_free;
	compare_free = compare_free->next;
    }

    *compare = compare_zero;
    VALIDATE(compare);

    return compare;
}

void free_compare( COMPARE_DATA *compare )
{
    if ( !IS_VALID(compare) )
	return;

    if ( compare->name )
	free_string(compare->name);

    INVALIDATE(compare);

    compare->next = compare_free;
    compare_free = compare;
}

PFILE_DATA *pfile_free;

PFILE_DATA *new_pfile( void )
{
    static PFILE_DATA pfile_zero;
    PFILE_DATA *pfile;

    if ( pfile_free == NULL )
	pfile = alloc_perm( sizeof( *pfile ) );
    else
    {
	pfile = pfile_free;
	pfile_free = pfile_free->next;
    }

    *pfile = pfile_zero;
    VALIDATE(pfile);

    return pfile;
}

void free_pfile( PFILE_DATA *pfile )
{
    if ( !IS_VALID(pfile) )
	return;

    if ( pfile->name )
	free_string(pfile->name);

    INVALIDATE(pfile);

    pfile->next = pfile_free;
    pfile_free = pfile;
}

PFILE_DATA *pfile_matches;

void display_plist_output( CHAR_DATA *ch, int count )
{
    BUFFER *output;
    char buf[MAX_STRING_LENGTH], race[MAX_STRING_LENGTH];
    char class[MAX_STRING_LENGTH], laston[MAX_STRING_LENGTH];
    int x;

    /*
     * This function displays the total output of the list of all found
     * matches, this is the only place you'll need to add color codes or
     * do any new formatting and alignment if you wish to add any new
     * fields...
     */

    output = new_buf();

    sprintf(buf, "     Name     Level  Race    Class        Clan                 Laston\n\r");
    add_buf(output,buf);
    sprintf(buf, "_______________________________________________________________________________\n\r");
    add_buf(output,buf);
    sprintf(buf, "|            |     |       |       |                |                          |\n\r");
    add_buf(output,buf);

    for ( x = 0; x < count; x++ )
    {
	sprintf(race, "%-5.5s", pfile_matches[x].race >= 0 ?
	    capitalize(race_table[pfile_matches[x].race].name) : "     ");
	sprintf(class, "%-5.5s", pfile_matches[x].class >= 0 ?
	    capitalize(race_table[pfile_matches[x].class].name) : "     ");
	strftime(laston, MAX_STRING_LENGTH - 1, "%a %b %e %H:%M:%S", ctime(current_time));
	sprintf(buf, "| %-12s | %3d | %s | %s | %-14.14s |  %-19.19s   |\n\r",
	    pfile_matches[x].name, pfile_matches[x].level, race, class,
	    pfile_matches[x].clan > 0 ? capitalize(clan_table[pfile_matches[x].clan) :
	    "              ", laston );
	add_buf(output,buf);
    }

    sprintf(buf, "|            |     |       |       |                |    |                     |\n\r");
    add_buf(output,buf);
    sprintf(buf, "%d players found.\n\r", count);
    add_buf(output,buf);
    page_to_char(buf_string(output),ch);
    free_buf(output);
}

/* Privates to the sort_plist_matches function for qsort */
static int plist_name(const void *v1, const void *v2)
{
    PFILE_DATA *i1 = (PFILE_DATA *) v1, *i2 = (PFILE_DATA *) v2;
    return strcmp(i1->name, i2->name);
}

static int plist_level(const void *v1, const void *v2)
{
    PFILE_DATA *i1 = (PFILE_DATA *) v1, *i2 = (PFILE_DATA *) v2;
    return i1->level < i2->level ? -1 : (i1->level == i2->level ? 0 : 1 );
}

static int plist_race(const void *v1, const void *v2)
{
    PFILE_DATA *i1 = (PFILE_DATA *) v1, *i2 = (PFILE_DATA *) v2;
    return strcmp(race_table[i1->race].name, race_table[i2->race].name);
}

static int plist_class(const void *v1, const void *v2)
{
    PFILE_DATA *i1 = (PFILE_DATA *) v1, *i2 = (PFILE_DATA *) v2;
    return strcmp(class_table[i1->class].name, class_table[i2->class].name);
}

static int plist_clan(const void *v1, const void *v2)
{
    PFILE_DATA *i1 = (PFILE_DATA *) v1, *i2 = (PFILE_DATA *) v2;
    return strcmp(clan_table[UMAX(0,i1->clan)].name, clan_table[UMAX(0,i2->clan)].name);
}

static int plist_laston(const void *v1, const void *v2)
{
    PFILE_DATA *i1 = (PFILE_DATA *) v1, *i2 = (PFILE_DATA *) v2;
    return i1->laston > i2->laston ? -1 : ( i1->laston == i2->laston ? 0 : 1 );
}

void sort_plist_matches( int count, int sort )
{
    /*
     * Any new fields you wish to sort by should have a value defined
     * above, assigned below in get_pfile_matches, and then checked for
     * here, with a sorting function created above, you should be able
     * to figure out how to add any new sorting functions by looking at
     * all the other ones that are already created...
     */

    switch( sort )
    {
	case BYNAME:
	    qsort( (void *) pfile_matches, count, sizeof(PFILE_DATA), plist_name );
	    break;

	case BYLEVEL:
	    qsort( (void *) pfile_matches, count, sizeof(PFILE_DATA), plist_level );
	    break;

	case BYRACE:
	    qsort( (void *) pfile_matches, count, sizeof(PFILE_DATA), plist_race );
	    break;

	case BYCLAN:
	    qsort( (void *) pfile_matches, count, sizeof(PFILE_DATA), plist_clan );
	    break;

	case BYCLASS:
	    qsort( (void *) pfile_matches, count, sizeof(PFILE_DATA), plist_class );
	    break;

	case BYLASTON:
	    qsort( (void *) pfile_matches, count, sizeof(PFILE_DATA), plist_laston );
	    break;
    }
}

int get_plist_matches( SEARCH_DATA *criteria )
{
    int count = 0;
    PFILE_DATA *match;
    DESCRIPTOR_DATA *d;
    char *word, buf[MSL];
    bool matchok, end = FALSE;
    DIR *player_dir;
    struct dirent *pfile;
    FILE *fp;

    /*
     * This function is sort of a slimmed-down version of fread_char,
     * intended to read in values to display in the output of the list
     * of matches... You may notice the new_pfile() function called
     * below, this was to eliminate a bug i was having with the mud
     * crashing randomly when checking match->name to see if it should
     * be freed before reassignment... it was given memory space with
     * a call to malloc, now it's given a recycling function just like
     * those you'd find in recycle.c... Any new fields you'd like
     * displayed should be read in here and assigned to a new field
     * that you add to the pfile data structure... i initialized all
     * numeric values to -2 since some lookup functions return -1 if
     * the value isn't found, and -2 can be used to check if a value
     * just wasn't read in period...
     */

    match = new_pfile();
    match->name = str_dup( "" );
    match->level = -2;
    match->race = -2;
    match->clan = -2;
    match->class = -2;
    match->laston = -2;
    match->sex = -2;
    sprintf(buf, "%s", PLAYER_DIR);
    player_dir = opendir(buf);
    while( ( pfile = readdir(player_dir) ) != NULL )
    {
	if ( pfile->d_name[0] == '.' )
	    continue;

	strcat(buf,pfile->d_name);
	if ( ( fp = file_open( buf,"r" ) ) != NULL )
	{
	    /* First, let's initialize match */

	    matchok = criteria->all;
	    if ( !IS_NULLSTR(match->name) )
		free_string( match->name );
	    match->name = str_dup( "" );
	    match->level = -2;
	    match->race = -2;
	    match->clan = -2;
	    match->class = -2;
	    match->laston = -2;
	    match->sex = -2;

	    for ( ; !end; )
	    {
		word = feof( fp ) ? "End" : fread_word( fp );

		switch( UPPER(word[0]) )
		{
		    case '*':
			fread_to_eol( fp );
			break;

		    case 'C':
			if ( !str_cmp( word, "Clan" ) )
			{
			    match->clan = clan_lookup( fread_string( fp ) );
			    break;
			}

			if ( !str_cmp( word, "Cla" ) || !str_cmp( word, "Class" ) )
			{
			    match->clan = fread_number( fp );
			    break;
			}

			break;

		    case 'E':
			if ( !str_cmp( word, "End" ) )
			{
			    file_close(fp);
			    sprintf(buf,"%s",PLAYER_DIR);
			    end = TRUE;
			    break;
			}

			break;

		    case 'L':
			if ( !str_cmp( word, "Lev" )
			|| !str_cmp( word, "Levl" )
			|| !str_cmp( word, "Level" ) )
			{
			    match->level = fread_number( fp );
			    break;
			}

			if ( !str_cmp( word, "LogO" ) )
			{
			    match->laston = fread_number( fp );
			    break;
			}

			break;

		    case 'N':
			if ( !str_cmp( word, "Name" ) )
			{
			    match->name = fread_string( fp );
			    break;
			}

			break;

		    case 'R':
			if ( !str_cmp( word, "Race" ) )
			{
			    match->race = race_lookup( fread_string( fp ) );
			    break;
			}

			break;

		    case 'S':
			if ( !str_cmp( word, "Sex" ) )
			{
			    match->sex = fread_number( fp );
			    break;
			}

			break;
		}
	    }

	    end = FALSE;

	    for ( d = descriptor_list; d != NULL; d = d->next )
	    {
		if ( CH(d) && !str_cmp( CH(d)->name, match->name ) ) /* Quick check to see if they're logged on */
		{
		    match->laston = current_time;
		    break;
		}
	    }

	    if ( !matchok )
	    {
		COMPARE_DATA *comparison_new;

		if ( ( IS_NULLSTR(criteria->name) || !str_prefix(criteria->name, match->name) )
		&& ( criteria->race == -2 || criteria->race == match->race )
		&& ( criteria->class == -2 || criteria->class == match->class )
		&& ( criteria->clan == -2 || criteria->clan == match->clan ) )
		    matchok = TRUE;

		for ( comparison_new = criteria->comparison; comparison_new != NULL; comparison_new = comparison_new->next )
		{
		    if ( !str_prefix( comparison_new->name, "laston" ) )
		    {
			switch( comparison_new->operator )
			{
			    case '>':
				if ( current_time - match->laston <= ( comparison_new->value + 1 ) * 86400 )
				    matchok = FALSE;
				break;

			    case '=':
				if ( current_time - match->laston < comparison_new->value * 86400
				|| current_time - match->laston >= ( comparison_new->value + 1 ) * 86400 )
				    matchok = FALSE;
				break;

			    case '<':
				if ( current_time - match->laston >= comparison_new->value * 86400 )
				    matchok = FALSE;
				break;
			}
		    }
		    else if ( !str_prefix( comparison_new->name, "level" ) )
		    {
			switch( comparison_new->operator )
			{
			    case '>':
				if ( match->level <= comparison_new->value )
				    matchok = FALSE;
				break;

			    case '=':
				if ( match->level < comparison_new->value || match->level > comparison_new->value )
				    matchok = FALSE;
				break;

			    case '<':
				if ( match->level >= comparison_new->value )
				    matchok = FALSE;
				break;
			}
		    }
		}
	    }

	    if ( matchok )
	    {
		if ( !count )
		    pfile_matches = malloc( sizeof( PFILE_DATA ) ); /* Use malloc so we can realloc as needed */
		else
		{
		    PFILE_DATA *new_table;

		    new_table = realloc( pfile_matches, sizeof( PFILE_DATA ) * ( count + 1 ) );
		    if ( !new_table ) /* Reallocate failed */
		    {
			bug("Realloc failed in get_plist_matches.", 0);
			return 0;			
		    }

		    pfile_matches = new_table;
		}

		pfile_matches[count].name = str_dup( match->name );
		pfile_matches[count].level = match->level;
		pfile_matches[count].race = match->race;
		pfile_matches[count].clan = match->clan;
		pfile_matches[count].class = match->class;
		pfile_matches[count].laston = match->laston;
		pfile_matches[count].sex = match->sex;
		count++;
	    }
	}
    }

    free_pfile(match);
    return count;
}

bool get_plist_criteria( CHAR_DATA *ch, char *argument, SEARCH_DATA *criteria )
{
    char arg[MAX_INPUT_LENGTH], buf[MAX_STRING_LENGTH];
    bool correct_entry = TRUE, sorted = FALSE;

    /* Parse out all options on the command line */

    criteria->all = TRUE;

    if ( !IS_NULLSTR(argument) ) /* If argument is NULL, show all matches */
    {
	argument = one_argument( argument, arg );

	/*
	 * For regular arguments like clan, name, class, etc. we only
	 * accept 1 entry, for things like level and laston, we can take
	 * a range of values, like level < 30 level > 10... if you'd like
	 * to be able to specify more than 1 name, clan, etc. you can use
	 * the comparison linked list for them, and exclude the operator
	 * field, just use name and value, and check later on with a
	 * boolean variable to see if it finds a valid name prefix in
	 * the list... just a thought :D
	 */

	for ( ; !IS_NULLSTR(arg); argument = one_argument(argument,arg) )
	{
	    if ( !str_prefix( arg, "clan" ) )
	    {
		argument = one_argument(argument, arg);

		criteria->all = FALSE;

		if ( criteria->clan != -2 )
		{
		    send_to_char("Duplicate entries for clan.\n\r", ch);
		    correct_entry = FALSE;
		    break;;
		}

		if ( ( criteria->clan = clan_lookup(arg) ) == -1 )
		{
		    sprintf(buf, "Invalid clan name: \"%s\"\n\r", arg);
		    send_to_char(buf,ch);
		    correct_entry = FALSE;
		    break;
		}

		continue;
	    }
	    if ( !str_prefix( arg, "class" ) )
	    {
		argument = one_argument(argument, arg);

		criteria->all = FALSE;

		if ( criteria->class != -2 )
		{
		    send_to_char("Duplicate entries for class.\n\r", ch);
		    correct_entry = FALSE;
		    break;;
		}

		if ( ( criteria->class = class_lookup(arg) ) == -1 )
		{
		    sprintf(buf, "Invalid class name: \"%s\"\n\r", arg);
		    send_to_char(buf,ch);
		    correct_entry = FALSE;
		    break;
		}

		continue;
	    }
	    else if ( !str_prefix( arg, "laston" ) || !str_prefix( arg, "level" ) )
	    {
		COMPARE_DATA *comparison_new;

		/*
		 * We do allow multiple entries for laston and level,
		 * just in case someone wants to see everyone that has
		 * level > 30 and level < 50 or something... so we don't
		 * bother checking for duplicate entries first...
		 */

		criteria->all = FALSE;

		comparison_new = new_compare();
		comparison_new->name = str_dup( arg ); /* Store name of comparison */

		argument = one_argument(argument, arg); /* Get next element, the operator */

		if ( strlen(arg) > 1 ) /* Is operator more than 1 character? We only allow >, <, and = */
		{
		    sprintf(buf,"Invalid operator: \"%s\"\n\r", arg);
		    send_to_char(buf,ch);
		    correct_entry = FALSE;
		    break;
		}

		comparison_new->operator = arg[0]; /* Store operator */

		argument = one_argument(argument, arg); /* Get next element, the compare value */

		if ( !is_number(arg) ) /* We only allow numeric values for comparisons */
		{
		    sprintf(buf,"Invalid comparison value: \"%s\"\n\r", arg);
		    send_to_char(buf,ch);
		    correct_entry = FALSE;
		    break;
		}

		comparison_new->value = atoi(arg);

		comparison_new->next = criteria->comparison; /* Now tack it onto the list of checks */
		criteria->comparison = comparison_new;
		continue;
	    }
	    else if ( !str_prefix( arg, "name" ) )
	    {
		argument = one_argument(argument, arg);

		criteria->all = FALSE;

		if ( !IS_NULLSTR(criteria->name) )
		{
		    send_to_char("Duplicate entries for name.\n\r", ch);
		    correct_entry = FALSE;
		    break;
		}

		criteria->name = str_dup(arg);
		continue;
	    }
	    else if ( !str_prefix( arg, "race" ) )
	    {
		argument = one_argument(argument, arg);

		criteria->all = FALSE;

		if ( criteria->race != -2 )
		{
		    send_to_char("Duplicate entries for race.\n\r", ch);
		    correct_entry = FALSE;
		    break;
		}

		if ( ( criteria->race = race_lookup(arg) ) == -1 )
		{
		    sprintf(buf, "Invalid race name: \"%s\"\n\r", arg);
		    send_to_char(buf,ch);
		    correct_entry = FALSE;
		    break;
		}

		continue;
	    }
	    else if ( !str_prefix( arg, "sort" ) )
	    {
		argument = one_argument(argument, arg);

		/*
		 * Any additional fields you want to add for sorting, simply
		 * define a new value for them, add the string check below,
		 * add the new value to the switch statement in the sorting
		 * function above, and then create a new private function for
		 * qsort to sort the list with...
		 */

		if ( sorted )
		{
		    send_to_char("Duplicate sorting options.\n\r", ch);
		    correct_entry = FALSE;
		    break;
		}

		sorted = TRUE;

		if ( !str_prefix( arg, "clan" ) )
		    criteria->sort = BYCLAN;
		else if ( !str_prefix( arg, "class" ) )
		    criteria->sort = BYCLASS;
		else if ( !str_prefix( arg, "laston" ) )
		    criteria->sort = BYLASTON;
		else if ( !str_prefix( arg, "level" ) )
		    criteria->sort = BYLEVEL;
		else if ( !str_prefix( arg, "name" ) )
		    criteria->sort = BYNAME;
		else if ( !str_prefix( arg, "race" ) )
		    criteria->sort = BYRACE;

		continue;
	    }
	    else
	    {
		sprintf(buf, "Invalid search type: \"%s\"\n\r", arg);
		send_to_char(buf,ch);
		correct_entry = FALSE;
		break;
	    }
	}

	if ( !correct_entry ) /* Destroy and free any memory that was assigned */
	{
	    COMPARE_DATA *comparison_new;

	    /*
	     * Any additional fields you may add to the compare data structure,
	     * make sure you free any necessary variables below in order to avoid
	     * memory leaks...
	     */

	    if ( !IS_NULLSTR(criteria->name) )
		free_string(criteria->name);

	    if ( criteria->comparison )
	    {
		for ( ; criteria->comparison != NULL; criteria->comparison = comparison_new )
		{
		    comparison_new = criteria->comparison->next;

		    free_compare(criteria->comparison);
		}
	    }

	    return FALSE;
	}
    }

    return TRUE;
}

void do_plist( CHAR_DATA *ch, char *argument )
{
    SEARCH_DATA *criteria;
    int count = 0, x;

    /* First things first, let's initialize criteria */

    criteria = malloc( sizeof( SEARCH_DATA ) );
    criteria->name = str_dup( "" );
    criteria->race = -2;
    criteria->clan = -2;
    criteria->sort = BYNAME;
    criteria->all = FALSE;
    criteria->comparison = NULL;

    /* Now to parse out the command line into the actual criteria... */

    if ( !get_plist_criteria( ch, argument, criteria ) ) /* If FALSE, something went wrong */
	return;

    /*
     * Ok, if we've reached this point, it means that we've entered
     * a line of queries with the appropriate syntax and we're ready
     * to find all the matching pfiles now... so here we go...
     */

    if ( ( count = get_plist_matches( criteria ) ) > 0 )
    {
	/*
	 * Found at least 1 matching pfile, now sort them and display
	 * them to the user...
	 */

	sort_plist_matches( count, criteria->sort );
	display_plist_output( ch, count );
    }
    else
    {
	send_to_char("No matching players were found.\n\r", ch);
	return;
    }

    /*
     * Now free all the name strings that have been assigned as the
     * plist was built, and then free the whole chunk of memory for
     * the array itself along w/ the chunk for criteria...
     */
    for ( x = 0; x < count - 1; x++ )
	if ( !IS_NULLSTR(pfile_matches[x].name) )
	    free_string(pfile_matches[x].name);

    free(criteria);
    free(pfile_matches);
}