/*
 * 23:34:07 .-- blaster/2937287 --- -- -
 * 23:34:07 | trudno byłoby napisać taki skrypt w bashu, który by wypisywał 
 * 23:34:07 | powiedzmy 5 najczęściej powtarzających się słów z danego pliku?
 * 23:34:07 `----- ---- --- -- -
 * 23:34:30 .-- gophi/1234 --- -- -
 * 23:34:30 | w bashu... bez sensu
 * 23:34:30 `----- ---- --- -- -
 * 23:34:31 ::: Wiadomość do blaster/2937287 została dostarczona
 * 23:34:33 .-- gophi/1234 --- -- -
 * 23:34:33 | w c bez problemu
 * 23:34:33 `----- ---- --- -- -
 * 23:34:34 ::: Wiadomość do blaster/2937287 została dostarczona
 * 23:34:58 .-- blaster/2937287 --- -- -
 * 23:34:58 | a miałbyś czas i chęci napisać dla mnie coś takiego?
 * 23:34:58 `----- ---- --- -- -
 * 23:35:04 .-- gophi/1234 --- -- -
 * 23:35:04 | luz
 * 23:35:04 `----- ---- --- -- -
 * 23:35:05 ::: Wiadomość do blaster/2937287 została dostarczona
 *
 * Changelog:
 *   17.08.2005: v1.0: Wersja pierwsza, dziewicza.
 */

#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <malloc.h>
#include <ctype.h>
#include <stdio.h>
#include <errno.h>
#include <regex.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>

#define VERSION "1.0"

#define print_version() printf ("Wordstat, v%s, (C) 2005 Adam Wysocki\n", VERSION)
#define prepare_delims() if (!opt.delims) opt.delims = xstrdup(DELIMS)
#define free_regex() regfree (&regex)
#define free_delims() free (opt.delims)
#define free_stats() if (words) free (words);
#define DELIMS " !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"
#define hex_to_int(x) ((x >= '0' && x <= '9') ? x - '0' : \
	(x >= 'a' && x <= 'f') ? x - 'a' + 10 : \
	(x >= 'A' && x <= 'F') ? x - 'A' + 10 : 0)

extern char *optarg;
extern int optind, opterr, optopt;
extern int errno;

struct opt_t {
	char *infile;
	int num_words;
	int sort_key;
	char *delims;	/* pamięć jest alokowana, trzeba zwolnić */
	char *regex;
	char sep_char;
	int sep_field;
	char append_flag;
	char case_flag;
} opt;

struct words_t {
	char *word;
	int count;
} *words = (struct words_t *) NULL;

int num_words;
int infd;
regex_t regex;

char *xstrdup (char *str)
{
	str = strdup(str);

	if (!str) {
		fprintf (stderr, "Oups, strdup zabrakło pamięci.\n");
		exit (1);
	}

	return str;
}

void *xrealloc (void *ptr, size_t size)
{
	ptr = realloc(ptr, size);

	if (!ptr) {
		fprintf (stderr, "Oups, realloc zabrakło pamięci.\n");
		exit (1);
	}

	return ptr;
}

void add_to_delim (unsigned char ch)
{
	int len;

	if (opt.delims) {
		if (!ch) {
			free (opt.delims);
			opt.delims = NULL;
			return;
		} else if (strchr(opt.delims, ch))
			return;
	}

	len = opt.delims ? strlen(opt.delims) : 0;
	opt.delims = xrealloc(opt.delims, len + 2);
	opt.delims[len++] = ch;
	opt.delims[len] = (char) NULL;
}

void del_from_delim (unsigned char ch)
{
	char *new_delims = NULL;
	int i = 0;
	int new_len = 0;

	if (!opt.delims) {
		opt.delims = xstrdup(DELIMS);
		if (!strchr(opt.delims, ch)) {
			free (opt.delims);
			opt.delims = NULL;
			return;
		}
	}

	if (!strchr(opt.delims, ch))
		return;

	while (opt.delims[i]) {
		if (opt.delims[i] != ch) {
			new_delims = (char *) xrealloc(new_delims, new_len + 1);
			new_delims[new_len++] = opt.delims[i];
		}
		i++;
	}

	new_delims = (char *) xrealloc(new_delims, new_len + 1);
	new_delims[new_len] = (char) NULL;

	free (opt.delims);
	opt.delims = new_delims;
}

/* parsuje podany ciąg delimiterów. jeśli napotka błąd, 
 * wypisuje informację i ustawia *done na 3. uwaga: 
 * niszczy string pod przekazanym wskaźnikiem. */

void parse_delim (char *string, char *done)
{
	char *one_char;
	char *sp;

	/* parsujemy po jednym. one_char może zawierać znak albo 0xXX. */

	one_char = strtok_r(string, ",", &sp);
	while (one_char) {
		if (strlen(one_char) == 1)
			add_to_delim (*one_char);
		else if (strlen(one_char) == 2 && *one_char == '-')
			del_from_delim (one_char[1]);
		else if (strlen(one_char) == 4 && !strncasecmp(one_char, "0x", 2) 
			&& isxdigit(one_char[2]) && isxdigit(one_char[3]))
			add_to_delim ((hex_to_int(one_char[2]) << 4) | 
				hex_to_int(one_char[3]));
		else if (strlen(one_char) == 5 && *one_char == '-' && 
			!strncasecmp(one_char + 1, "0x", 2) && isxdigit(one_char[3]) 
			&& isxdigit(one_char[4]))
			del_from_delim ((hex_to_int(one_char[3]) << 4) |
				hex_to_int(one_char[4]));
		else {
			fprintf (stderr, "Nieprawidłowy argument dla opcji -d: %s.\n", 
				one_char);
			*done = 3;
			break;
		}

		one_char = strtok_r(NULL, ",", &sp);
	}
}

void fasthelp (void)
{
	print_version();
	printf ("\
Składnia: ws [opcje]\n\
\n\
Opcje:\n\
  -i plik: czyta dane z podanego pliku (- oznacza stdin).\n\
  -n liczba: wypisuje podaną liczbę najczęściej używanych słów. Jeśli \n\
     parametr przyjmie wartość 0, wypisywane są wszystkie słowa.\n\
  -s liczba: sortuje wynik:\n\
     0: brak sortowania (według kolejności pojawiania się wyrazów).\n\
     1: alfabetycznie rosnąco (zgodnie z ascii).\n\
     2: od najrzadziej występującego słowa.\n\
     3: od najkrótszego słowa.\n\
     4: alfabetycznie malejąco.\n\
     5: od najczęściej występującego słowa.\n\
     6: od najdłuższego słowa.\n\
  -d znaki: dodaje podany znak lub znaki do listy delimiterów słów. \n\
     Jeśli zostanie podane choć raz, domyślna lista delimiterów słów \n\
     nie jest brana pod uwagę. Znak można podać normalnie lub jako kod \n\
     szesnastkowy, poprzedzając go 0x (np. 0x20 to spacja). Możliwe jest \n\
     podanie kilku znaków w jednej opcji przez oddzielenie ich przecinkami \n\
     (ale chcąc w ten sposób podać przecinek, trzeba go wyeskejpować pisząc \n\
     0x2C), np. 0x20,0x2C. Jeśli zostanie podane 0x00, to wszystkie znaki \n\
     inne niż litery i cyfry będą uznawane za delimitery natomiast jeśli \n\
     znak zostanie poprzedzony myślnikiem, to zamiast dodania, zostanie \n\
     usunięty z listy (przykładowo 0x00,-- sprawi, że delimiterami będą \n\
     wszystkie znaki oprócz liter, cyfr i myślinika, tak że dupa-rysia \n\
     zostanie uznane za cały wyraz). Delimitery są to znaki rozdzielające \n\
     poszczególne wyrazy.\n\
  -m regex: bierze pod uwagę tylko linijki pasujące do podanego regexa \n\
     (man 7 regex). Program używa rozszerzonego standardu regex.\n\
  -f separatorliczba: przed podliczeniem linijki dzieli ją na pola oddzielone \n\
     separatorem i bierze pod uwagę tylko pole o numerze liczba (licząc od 0). \n\
     Między separatorem i liczbą nie powinno być spacji. Notacja 0xXX nie jest \n\
     akceptowana. Jeśli w wejściowej linijce będzie więcej niż jeden separator \n\
     obok siebie, to traktowane są jako jeden. Początkowe i końcowe separatory \n\
     w linijce są ignorowane.\n\
  -a: jeśli plik podany przy -o istnieje, to dzięki tej opcji program będzie \n\
      dopisywał do niego swoje statystyki, w przeciwnym wypadku nadpisze go.\n\
  -c: ignoruje rozmiar znaków w słowach (ANiA liczy razem z ania).\n\
  -h: wypisuje ten ekran pomocy.\n\
  -v: wypisuje informacje o wersji.\n\
\n\
Przykład: ws -i dupa.txt -o stats -n 10 -s 0 -c -d \'\", -m ^re -f ,3\n\
\n\
Domyślne wartości opcji:\n\
  -i -\n\
  -o -\n\
  -n 10\n\
  -s 1\n\
  -d 0x00\n\
\n\
Wejście może mieć zakończenia linii w formacie Unix (LF) lub DOS (CRLF).\n\
Wyjście ma zakończenia linii w formacie Unix.\n\
\n\
Program objęty jest licencją GPL. Inne projekty: http://www.gophi.rotfl.pl/.\n\
Skargi, piwo i bugreporty: gophi at chmurka.net.\n");
}

void parse_options (int ac, char **av)
{
	const char optstr[] = "i:n:s:d:m:f:achv";
	char done = 0;

	opt.infile = NULL;	/* stdin */
	opt.num_words = 10;	/* liczba słów do wypisania */
	opt.sort_key = 1;	/* alfabetycznie rosnąco */
	opt.delims = NULL;	/* wszystkie znaki spoza liter i cyfr */
	opt.regex = NULL;	/* brak regexa (.*) */
	opt.sep_char = 0;
	opt.sep_field = 0;
	opt.append_flag = 0;
	opt.case_flag = 0;

	/* wartości done:
	 *   0: nie skończyło się przeglądanie opcji.
	 *   1: przeglądanie się skończyło, jest ok.
	 *   2: przeglądanie się skończyło, jest ok, wyjdź.
	 *   3: przeglądanie się skończyło, nie jest ok, wyjdź.
	 */

	while (!done) {
		switch (getopt(ac, av, optstr)) {
			default:
				/* obsługa ':' i '?' */
				fprintf (stderr, "Wpisz ws -h żeby uzyskać pomoc.\n");
				done = 3;
				break;
			case -1:
				done = 1;
				break;
			case 'h':
				fasthelp();
				done = 2;
				break;
			case 'v':
				print_version();
				done = 2;
				break;
			case 'i':
				if (*optarg != '-')
					opt.infile = optarg;
				break;
			case 'n':
				opt.num_words = atoi(optarg);
				break;
			case 's':
				opt.sort_key = atoi(optarg);
				if (opt.sort_key < 0 || opt.sort_key > 6) {
					fprintf (stderr, "Błędny argument dla opcji -s.\n");
					done = 3;
				}
				break;
			case 'd':
				parse_delim (optarg, &done);
				break;
			case 'm':
				opt.regex = optarg;
				break;
			case 'f':
				if (strlen(optarg) < 2) {
					fprintf (stderr, "Błędny argument dla opcji -f.\n");
					done = 3;
				}
				opt.sep_char = *optarg++;
				opt.sep_field = atoi(optarg);
				break;
			case 'a':
				opt.append_flag = 1;
				break;
			case 'c':
				opt.case_flag = 1;
				break;
		}
	}

	if (done >= 2)
		exit (done - 2);	/* done=2 -> exit(0), done=3 -> exit(1) */
}

void prepare_streams (void)
{
	if (opt.infile) {
		infd = open(opt.infile, O_RDONLY);
		if (infd < 0) {
			fprintf (stderr, "%s: %m.\n", opt.infile);
			exit (1);
		}
	} else
		infd = 0;	/* stdin */
}

void close_streams (void)
{
	if (infd != 0)
		close (infd);
}

void compile_regex (void)
{
	int rs;
	char err[256];

	if (!opt.regex)
		return;

	rs = regcomp(&regex, opt.regex, REG_EXTENDED | REG_NOSUB);
	if (!rs)
		return;

	regerror (rs, &regex, err, sizeof(err));
	fprintf (stderr, "Błąd kompilacji wyrażenia regex: %s.\n", err);
	exit (3);
}

char *get_line (void)
{
	char *line = NULL;
	char ch;
	int len = 0;
	int rs;

	while (!0) {
		rs = read(infd, &ch, sizeof(ch));
		if (rs == -1) {
			fprintf (stderr, "read(): %m.\n");
			exit (1);
		} else if (!rs) {
			if (line)
				free (line);

			return (char *) NULL;
		}

		if (ch == 0x0D)
			continue;
		else if (ch == 0x0A)
			break;

		line = (char *) xrealloc(line, len + 1);
		line[len++] = ch;
	}

	line = (char *) xrealloc(line, len + 1);
	line[len] = (char) NULL;

	return line;
}

/* uwaga: niszczy string pod przekazanym wskaźnikiem, zwracając 
 * wartość z jego środka i nadpisując koniec fragmentu nullem. 
 * przekazany string można zwolnić dopiero jak zwrócona wartość 
 * nie będzie potrzebna. */

char *separate_line (char *line)
{
	char *new_line, *sp;
	char sep[2];
	int i;

	sep[0] = opt.sep_char;
	sep[1] = (char) NULL;

	new_line = strtok_r(line, sep, &sp);
	if (!new_line)
		return (char *) NULL;

	for (i = 0; i < opt.sep_field; i++) {
		new_line = strtok_r(NULL, sep, &sp);
		if (!new_line)
			return (char *) NULL;
	}

	return new_line;
}

void add_word (char *word)
{
	int i;

	if (opt.case_flag) {
		char *p;

		for (p = word; (*p = tolower(*p)); p++);
	}

	/* najpierw sprawdzamy, czy słowo jest już w tablicy */

	for (i = 0; i < num_words; i++) {
		if (!strcmp(words[i].word, word)) {
			words[i].count++;
			break;
		}
	}

	/* jeśli słowa nie ma, to dodajemy. */

	if (i == num_words) {
		words = (struct words_t *) xrealloc(words, sizeof(struct words_t) * 
			(num_words + 1));
		words[num_words].word = xstrdup(word);
		words[num_words].count = 1;
		num_words++;
	}
}

void parse_line (char *line)
{
	char *word, *sp;

	word = strtok_r(line, opt.delims, &sp);
	while (word) {
		add_word (word);
		word = strtok_r(NULL, opt.delims, &sp);
	}
}

void make_stats (void)
{
	while (!0) {
		char *line;

		line = get_line();
		if (!line)
			break;

		if (opt.regex && regexec(&regex, line, 0, (regmatch_t *) NULL, 0) == REG_NOMATCH) {
			free (line);
			continue;
		}

		if (opt.sep_char) {
			char *frag;

			/* nie mogę się powstrzymać. frag cholernie kojarzy 
			 * mi się z dukiem... jestem zboczony?
			 */

			frag = separate_line(line);
			if (frag)
				parse_line (frag);
		} else
			parse_line (line);

		free (line);
	}
}

int sort_ascend_name (void *p1, void *p2)
{
	return strcmp(((struct words_t *) p1)->word, ((struct words_t *) p2)->word);
}

int sort_ascend_count (void *p1, void *p2)
{
	return ((struct words_t *) p1)->count - ((struct words_t *) p2)->count;
}

int sort_ascend_length (void *p1, void *p2)
{
	return strlen(((struct words_t *) p1)->word) - strlen(((struct words_t *) p2)->word);
}

int sort_descend_name (void *p1, void *p2)
{
	return strcmp(((struct words_t *) p2)->word, ((struct words_t *) p1)->word);
}

int sort_descend_count (void *p1, void *p2)
{
	return ((struct words_t *) p2)->count - ((struct words_t *) p1)->count;
}

int sort_descend_length (void *p1, void *p2)
{
	return strlen(((struct words_t *) p2)->word) - strlen(((struct words_t *) p1)->word);
}

void print_stats (void)
{
	int i, max, width = 0;
	char fmt_string[10];
	const void *comp[] = {
		sort_ascend_name,
		sort_ascend_count,
		sort_ascend_length,
		sort_descend_name,
		sort_descend_count,
		sort_descend_length
	};

	if (!words) {
		printf ("Nie bardzo jest co wypisać...\n");
		return;
	}

	if (opt.sort_key)
		qsort (words, num_words, sizeof(struct words_t), comp[opt.sort_key - 1]);

	max = (num_words > opt.num_words) ? opt.num_words : num_words;
	if (!max)
		max = num_words;

	for (i = 0; i < max; i++)
		if (strlen(words[i].word) > width)
			width = strlen(words[i].word);

	snprintf (fmt_string, sizeof(fmt_string), "%%%ds %%d\n", width);

	for (i = 0; i < max; i++)
		printf (fmt_string, words[i].word, words[i].count);
}

int main (int ac, char **av)
{
	parse_options (ac, av);
	compile_regex();
	prepare_streams();
	prepare_delims();
	make_stats();
	print_stats();
	free_stats();
	free_delims();
	close_streams();
	free_regex();

	return 0;
}
