/** C99 **/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <inttypes.h>
#include <stdbool.h>

#define	APP_NAME	"mywc"
#define	APP_VERSION	"0.01"

#define MAX_FNAMES	100+1

#define MAXLEN_OPTSSTR	(2+1)			/* max len for an option's short string*/
#define MAXLEN_OPTLSTR	(20+1)			/* max len for an option's long string */
#define MAXLEN_OPTDESC	(128+1)			/* max len for an option's description */

#define INFO_VERSION									\
	"%s (just for fun) %s\n"							\
	"Copyright (C) 2012 migf1\n"							\
	"This is free software.  You may redistribute copies of it under the terms of\n"\
	"the GNU General Public License <http://w...content-available-to-author-only...u.org/licenses/gpl.html>.\n"	\
	"There is NO WARRANTY, to the extent permitted by law.\n"			\
	"\n"										\
	"Written by migf1 <mig_f1@hotmail.com>"

#define INFO_USAGE									\
	"Usage: %s [OPTION]... [FILE]...\n"						\
	"Print newline, word, and byte counts for each FILE, and a total line if\n"	\
	"more than one FILE is specified.  With no FILE, or when FILE is -,\n"		\
	"read standard input."

#define INFO_REPORT_BUGS	"Report bugs to <mig_f1@hotmail.com>."

typedef uint16_t Bitmap16;

enum ErrId {
	ERR_NOERROR = 0,
	ERR_INTERNAL,
	ERR_NOFILE,
	ERR_RDFILE,
	ERR_OPTINVALID,
	ERR_OPTUNRECOG,
	/* not an id, just their total count*/
	MAXERRORS
};

enum OptionBit {
	OBIT_NONE	= 0x00,
	OBIT_BCOUNT	= (1 << 1),
	OBIT_CCOUNT	= (1 << 2),
	OBIT_LCOUNT	= (1 << 3),
	OBIT_MAXLL	= (1 << 4),
	OBIT_WCOUNT	= (1 << 5),
	OBIT_HELP	= (1 << 6),
	OBIT_VERS	= (1 << 7)
};

enum OptionId {
	OID_INVALID 	= -1,
	OID_BCOUNT 	= 0,
	OID_CCOUNT,
	OID_LCOUNT,
	OID_MAXLL,
	OID_WCOUNT,
	OID_HELP,
	OID_VERS,
	/* not an id, just their total count*/
	MAXOPTIONS
};

typedef struct Option {
	Bitmap16 	obit;
	char 		sstr[ MAXLEN_OPTSSTR ];		/* short string */
	char 		lstr[ MAXLEN_OPTLSTR ];		/* long string */
	char 		desc[ MAXLEN_OPTDESC ];		/* description */
}Option;

typedef struct Count {
	uintmax_t nl;		/* newlines count */
	uintmax_t w;		/* words count    */
	uintmax_t c, b;		/* chars & bytes count    */
	uintmax_t maxlnlen;	/* length of maximum line */
}Count;

/*********************************************************//**
 * @brief	Print a different error, according to the value of errid.
 *************************************************************
 */
void print_error( enum ErrId errid, Option options[] )
{
	switch ( errid )
	{
		case ERR_INTERNAL:
			printf( "%s: Internal error!\n", APP_NAME );
			break;

		case ERR_NOFILE:
			printf( "%s: No such file or directory\n", APP_NAME );
			break;

		case ERR_RDFILE:
			printf( "%s: File could not be read!\n", APP_NAME );
			break;

		case ERR_OPTINVALID:
			printf( "%s: invalid option\n", APP_NAME );
			printf( "Try `%s %s' for more information.\n",
				APP_NAME, options[OID_HELP].lstr );
			break;

		case ERR_OPTUNRECOG:
			printf( "%s: unrecognized option\n", APP_NAME );
			printf( "Try `%s %s' for more information.\n",
				APP_NAME, options[OID_HELP].lstr );
			break;

		case ERR_NOERROR:
			printf( "%s: no error\n", APP_NAME );
			break;

		case MAXERRORS:
		default:
			break;
	}

	putchar('\n');

	return;
}


/*********************************************************//**
 * @brief	Print program-version information.
 *************************************************************
 */
void print_version( void )
{
	printf( INFO_VERSION, APP_NAME, APP_VERSION );
	puts("\n");

	return;
}

/*********************************************************//**
 * @brief	Print short & long aliases, and the description of all options.
 *************************************************************
 */
void print_options( Option options[] )
{
	for (int i=0; i < MAXOPTIONS; i++)
	{
		printf( "\t%s%s%s\t\t%s\n",
			options[i].sstr[0] ? options[i].sstr : "  ",
			options[i].sstr[0] ? ", " : "  ",
			options[i].lstr,
			options[i].desc );
	}

	return;
}

/*********************************************************//**
 * @brief	Display the help screen.
 *************************************************************
 */
void print_help( Option options[] )
{
	printf( INFO_USAGE, APP_NAME );
	putchar('\n');
	print_options( options );
	putchar('\n');
	puts( INFO_REPORT_BUGS );
	putchar('\n');

	return;
}

/*********************************************************//**
 * @brief	Print (one file's) counted elements.
 *************************************************************
 */
bool print_counted(
	const Count 	*count,
	Bitmap16 	obitmap,
	Option 		options[],
	FILE		*fp,
	const char 	*fname
)
{
	if ( !count || !fp)
		return false;

	if ( obitmap & OBIT_HELP ) {
		print_help( options );
		goto ret_ok;
	}
	if ( obitmap & OBIT_VERS ) {
		print_version( );
		goto ret_ok;
	}

	if ( obitmap & OBIT_LCOUNT )
		printf( "%-"PRIuMAX"\t", count->nl );
	if ( obitmap & OBIT_WCOUNT )
		printf( "%-"PRIuMAX"\t", count->w );
	if ( obitmap & OBIT_CCOUNT )
		printf( "%-"PRIuMAX"\t", count->c );

	if ( obitmap & OBIT_BCOUNT )
		printf( "%-"PRIuMAX"\t", count->b );
	if ( obitmap & OBIT_MAXLL )
		printf( "%-"PRIuMAX"\t", count->maxlnlen );

	if ( stdin != fp && fname )
		puts( fname );
	else
		putchar('\n');

ret_ok:
	return true;
}

/*********************************************************//**
 * @brief	Print totals (of all files).
 *************************************************************
 */
bool print_totals( const Count *totals, Bitmap16 obitmap )
{
	if ( !totals )
		goto ret_fail;

	if ( obitmap & OBIT_HELP || obitmap & OBIT_VERS )
		goto ret_ok;

	if ( obitmap & OBIT_LCOUNT )
		printf( "%-"PRIuMAX"\t", totals->nl );
	if ( obitmap & OBIT_WCOUNT )
		printf( "%-"PRIuMAX"\t", totals->w );
	if ( obitmap & OBIT_CCOUNT )
		printf( "%-"PRIuMAX"\t", totals->c );

	if ( obitmap & OBIT_BCOUNT )
		printf( "%-"PRIuMAX"\t", totals->b );
	if ( obitmap & OBIT_MAXLL )
		printf( "%-"PRIuMAX"\t", totals->maxlnlen );

	puts("total");

	putchar('\n');

ret_ok:
	return true;
ret_fail:
	return false;
}

/*********************************************************//**
 * @brief	True if filename exists, false otherwise.
 *************************************************************
 */
bool f_exists(const char *fname)
{
	FILE *fp = fopen(fname, "rb");
	if ( !fp )
		return false;

	fclose(fp);
	return true;
}

/*********************************************************//**
 * @brief	Convert an option alias (short or long string) into an option id.
 *************************************************************
 */
enum OptionId ostr2oid( const char *ostr, Option options[] )
{
	enum OptionId i;

	if ( !ostr || !*ostr )
		return OID_INVALID;

	for (i=0; i < MAXOPTIONS; i++)
		if ( 0 == strncmp(ostr, options[i].sstr, MAXLEN_OPTSSTR)
		|| 0 == strncmp(ostr, options[i].lstr, MAXLEN_OPTLSTR) )
			return i;

	return OID_INVALID;
}

/*********************************************************//**
 * @brief	Update totals (after a file's elements have been counted).
 *************************************************************
 */
bool refresh_totals( Count *totals, const Count *count )
{
	if ( !totals || !count )
		return false;

	totals->nl 	+= count->nl;
	totals->w  	+= count->w;
	totals->b  	+= count->b;
	totals->maxlnlen += count->maxlnlen;

	return true;
}

/*********************************************************//**
 * @brief	Count and store a file's elements.
 *************************************************************
 */
bool fcount( FILE *fp, Count *count )
{
	int c;
	bool onword = false;
	uintmax_t lnchars = 0;

	if ( !fp || !count )
		return false;

	/* ensure everyting starts zeroed */
	memset( count, 0, sizeof(Count) );

	onword = (c=isspace( getc(fp) )) ? false : true;
	ungetc(c, fp);
	while ( (c=getc(fp)) != EOF )
	{
		if ( isspace(c) )
		{
			if ( '\n' == c ) {
				count->nl++;
				if (count->maxlnlen < --lnchars)
					count->maxlnlen = lnchars;
				lnchars = 0;
				if ( onword ) {
					count->w++;
					onword = false;
				}
			}
			else if ( onword ) {
				count->w++;
				onword = false;
			}
		}
		else
			onword = true;

		if ( c != '\n')
			lnchars++;
		count->c++;
	}

	count->b = count->c / sizeof(char);

	return true;
}

/*********************************************************//**
 * @brief	Parse the command line arguments, and update accordingly
 *		a) the specified options (obitmap)
 *		b) the total numeber of specified filenames (nfiles)
 *		c) the names of the files (fnames: just pointers to proper argv[i]'s)
 *************************************************************
 */
enum ErrId parse_cmdln_args(
	char 	*argv[],
	Option 	options[],
	char 	*fnames[MAX_FNAMES],
	int	*nfiles,
	Bitmap16 *obitmap
)
{
	int iarg = 1, ifname = 0;
	enum OptionId oid = OID_INVALID;

	if ( !obitmap || !fnames || !nfiles )
		return ERR_INTERNAL;

	/* ensure everything starts with default values */
	memset( fnames, 0, MAX_FNAMES * sizeof(char *) );
	*obitmap = OBIT_NONE;

	for (iarg = 1; argv[iarg]; iarg++ )
	{
		oid = ostr2oid( argv[iarg], options );
		if ( OID_INVALID == oid )
		{
			if ( 0 == strcmp( "-", argv[iarg])
			|| 0 == strcmp( "--", argv[iarg]) )
				continue;

			if ( '-' == argv[iarg][0] ) {
				if ( '-' == argv[iarg][1] )
					return ERR_OPTUNRECOG;
				return ERR_OPTINVALID;
			}
			if ( !f_exists(argv[iarg]) )
				return ERR_NOFILE;

			if ( ifname < MAX_FNAMES-1 )
				fnames[ ifname++ ] = argv[iarg];
		}
		else /* if ( OID_INVALID != oid ) */
			*obitmap |= options[oid].obit;
	}

	*nfiles = ifname;
	if ( *obitmap & OBIT_HELP || *obitmap & OBIT_VERS )
		return ERR_NOERROR;

	if ( OBIT_NONE == *obitmap || 1 == iarg || 0 == *nfiles )
		*obitmap = OBIT_LCOUNT + OBIT_WCOUNT + OBIT_BCOUNT;

	return ERR_NOERROR;
}

/*********************************************************//**
 *
 *************************************************************
 */
int main( int argc, char *argv[] )
{
	Option options[ MAXOPTIONS ] = {
		/* bitflag    sstr  lstr		desc */
		{OBIT_BCOUNT, "-c", "--bytes",		"print the byte counts"},
		{OBIT_CCOUNT, "-m", "--chars", 		"print the character counts"},
		{OBIT_LCOUNT, "-l", "--lines", 		"print the newline counts"},
		{OBIT_MAXLL,  "-L", "--max-line-length","print the length of the longest line"},
		{OBIT_WCOUNT,  "-w", "--words", 	"print the word counts"},
		{OBIT_HELP,    "\0", "--help", 		"display this help and exit"},
		{OBIT_VERS,    "\0", "--version", 	"output version information and exit"}
	};

	FILE 		*fp = NULL;
	int		nfiles = 0;
	char		*fnames[MAX_FNAMES] = {NULL};
	Bitmap16 	obitmap = OBIT_LCOUNT + OBIT_WCOUNT + OBIT_BCOUNT;
	Count 		count, totals;
	enum ErrId	errid = ERR_NOERROR;

	errid = parse_cmdln_args( argv, options, fnames, &nfiles, &obitmap );
	if ( ERR_NOERROR != errid ) {
		print_error( errid, options );
		goto ret_failure;
	}

	memset( &totals, 0, sizeof(Count) );
	for (int i=0; ; i++ )
	{
		if ( NULL == fnames[i] )
		{
			if (i == 0)
				fp = stdin;
			else
				break;
		}
		else if ( NULL == (fp = fopen(fnames[i], "rb")) ) {
			print_error( ERR_RDFILE, options );
			goto ret_failure;
		}

		if ( !(obitmap & OBIT_HELP) && !(obitmap & OBIT_VERS) )
			fcount( fp, &count);
		print_counted( &count, obitmap, options, fp, fnames[i] );

		refresh_totals( &totals, &count);

		if ( fp && fp != stdin )
			fclose(fp);
	}

	if ( nfiles > 1 )
		print_totals( &totals, obitmap );

	exit( EXIT_SUCCESS );

ret_failure:
	if ( fp && fp != stdin )
		fclose(fp);
	exit( EXIT_FAILURE );
}
