#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <stdbool.h>
#include <setjmp.h>
#include <assert.h>
#include <signal.h>

#define INLINE static inline

/* === CSV-OPTIONS Interface === */

typedef struct {
	char quote_symbol;
	char sep_symbol;
	void *cb_arg;
	void (*cb_func)(const char *, void *);
} csv_options_t;

extern const csv_options_t csv_default_options;

const csv_options_t csv_default_options = {
	.quote_symbol = '"',
	.sep_symbol = ',',
	.cb_func = NULL
};

/* === CSV-ERROR Interface ===  */

INLINE void
csv_fatal_error(const char *msg, const csv_options_t * const csv_options)
{
	if ( csv_options->cb_func != NULL ) {
		(*csv_options->cb_func)(msg, csv_options->cb_arg);
	}

	fprintf(stderr, "fatal error: %s\n", msg);
	abort();
}

/* === CSV-STRING Interface ===  */

typedef struct {
	char *str;
	int cap, pos;
} csv_string_t;

INLINE void
csv_string_init(csv_string_t *csv_string)
{
	assert(csv_string != NULL);

	csv_string->str = NULL;
	csv_string->cap = 0;
	csv_string->pos = 0;
}

INLINE void
csv_string_reset(csv_string_t *csv_string)
{
	assert(csv_string != NULL);

	csv_string->pos = 0;
}

INLINE bool
csv_string_empty(csv_string_t *csv_string)
{
	assert(csv_string != NULL);

	return ( csv_string->pos == 0 ) ? true : false;
}

INLINE void
csv_string_append(csv_string_t *csv_string, char ch, const csv_options_t * const csv_options)
{
	assert(csv_string != NULL);
	assert(csv_options != NULL);

	static const int INITIAL_CAP = 16;

	if ( csv_string->pos == csv_string->cap ) {	/* grow if needed */
		if ( csv_string->str == NULL ) {		/* first call? */
			if ( (csv_string->str = malloc(INITIAL_CAP)) == NULL ) {
				csv_fatal_error("out of memory...", csv_options);
				return;
			}

			csv_string->cap = INITIAL_CAP;
		}
		else {	/* subsequent call */
			int cap = (csv_string->cap * 3) / 2;	/* *= 1.5 */
			char *str = realloc(csv_string->str, cap);
			if ( str == NULL ) {
				csv_fatal_error("out of memory...", csv_options);
				return;
			}
			csv_string->str = str;
			csv_string->cap = cap;
		}
	}

	csv_string->str[csv_string->pos++] = ch; /* append char */
}

INLINE void
csv_string_free(csv_string_t *csv_string)
{
	assert(csv_string != NULL);

	free(csv_string->str);

	/* call *_init() for sane default values; prevent possible double-free */
	csv_string_init(csv_string);
}

/* === CSV-FIELD Interface === */

typedef struct {
	int *fields;
	int cap, pos;
} csv_field_t;

INLINE void
csv_field_init(csv_field_t *csv_field)
{
	assert(csv_field != NULL);

	csv_field->fields = NULL;
	csv_field->cap = 0;
	csv_field->pos = 0;
}

INLINE void
csv_field_reset(csv_field_t *csv_field)
{
	assert(csv_field != NULL);

	csv_field->pos = 0;
}

INLINE void
csv_field_append(csv_field_t *csv_field, int idx, const csv_options_t * const csv_options)
{
	assert(csv_field != NULL);
	assert(csv_options != NULL);

	static const size_t INITIAL_CAP = 16;

	if ( csv_field->pos == csv_field->cap ) {	/* grow if needed */
		if ( csv_field->fields == NULL ) {
			if ( (csv_field->fields = calloc(INITIAL_CAP, sizeof(csv_field->fields[0]))) == NULL ) {
				csv_fatal_error("out of memory", csv_options);
				return;
			}

			csv_field->cap = INITIAL_CAP;
		}
		else {
			int cap = (csv_field->cap * 3) / 2;	/* *= 1.5 */
			int *fields = reallocarray(csv_field->fields, cap, sizeof(csv_field->fields[0]));
			if ( fields == NULL ) {
				csv_fatal_error("out of memory", csv_options);
				return;
			}
			csv_field->fields = fields;
			csv_field->cap = cap;
		}
	}

	csv_field->fields[csv_field->pos++] = idx; /* append idx */
}

INLINE void
csv_field_free(csv_field_t *csv_field)
{
	assert(csv_field != NULL);

	free(csv_field->fields);

	csv_field_init(csv_field);
}

/* === CSV Interface === */

typedef struct {
	const csv_options_t *csv_options;
	csv_string_t csv_string;
	csv_field_t csv_field;
} csv_t;

void csv_init(csv_t *csv);
void csv_init_opt(csv_t *csv, const csv_options_t * const csv_options);
void csv_free(csv_t *csv);
int csv_nfields(csv_t *csv);
const char * csv_field(csv_t *csv, int idx);
int csv_read(csv_t *csv, FILE *in);
void csv_write(const csv_options_t * const csv_options, int n, const char *fields[], FILE *out);

void
csv_init(csv_t *csv)
{
	assert(csv != NULL);

	csv_init_opt(csv, NULL);
}

void
csv_init_opt(csv_t *csv, const csv_options_t * const csv_options)
{
	assert(csv != NULL);

	if ( csv_options != NULL ) {
		csv->csv_options = csv_options;
	}
	else {
		csv->csv_options = &csv_default_options;
	}

	csv_string_init(&csv->csv_string);
	csv_field_init(&csv->csv_field);
}

void
csv_free(csv_t *csv)
{
	assert(csv != NULL);

	csv_string_free(&csv->csv_string);
	csv_field_free(&csv->csv_field);
}

int
csv_nfields(csv_t *csv)
{
	assert(csv != NULL);

	if ( csv->csv_string.pos != 0 ) {
		return csv->csv_field.pos;
	}
	return 0;
}

const char *
csv_field(csv_t *csv, int idx)
{
	assert(csv != NULL);
	assert(idx >= 0 && idx < csv->csv_field.pos);

	if ( idx < 0 || idx >= csv->csv_field.pos ) {
		csv_fatal_error("range error", csv->csv_options);
	}

	return &csv->csv_string.str[csv->csv_field.fields[idx]];
}

int
csv_read(csv_t *csv, FILE *in)
{
	assert(csv != NULL);
	assert(in != NULL);

	enum {
		STATE_START_FIELD,
		STATE_QUOTED_FIELD,
		STATE_SIMPLE_FIELD,
		STATE_END_FIELD,
		STATE_END_LINE,
		STATE_END_FILE,
	};

	register const int QUOTE = csv->csv_options->quote_symbol;
	register const int SEP = csv->csv_options->sep_symbol;

	csv_string_reset(&csv->csv_string);
	csv_field_reset(&csv->csv_field);

	for ( int state = STATE_START_FIELD; ; ) {
		int ch;

		switch ( state ) {
		case STATE_START_FIELD:
			csv_field_append(&csv->csv_field, csv->csv_string.pos, csv->csv_options);

			ch = getc(in);
			if ( ch == EOF ) {
				state = STATE_END_FILE;
			}
			else if ( ch == '\r' ) {	/* Teste auf CR.. */
				ch = getc(in);
				if ( ch != '\n' ) {		/* ... LF */
					ungetc(ch, in);
				}
				state = STATE_END_LINE;
			}
			else if ( ch == '\n' ) {
				state = STATE_END_LINE;
			}
			else if ( ch == SEP ) {
				state = STATE_END_FIELD;
			}
			else if ( ch == QUOTE ) {
				state = STATE_QUOTED_FIELD;
			}
			else {
				csv_string_append(&csv->csv_string, ch, csv->csv_options);
				state = STATE_SIMPLE_FIELD;
			}
			break;

		case STATE_QUOTED_FIELD:
			do {
				ch = getc(in);
				if ( ch == EOF ) {
					state = STATE_END_FILE;
				}
				else if ( ch == QUOTE ) {
					ch = getc(in);
					if ( ch == EOF ) {
						state = STATE_END_FILE;
					}
					else if ( ch == QUOTE ) {
						csv_string_append(&csv->csv_string, QUOTE, csv->csv_options);
					}
					else if ( ch == SEP ) {
						state = STATE_END_FIELD;
					}
					else if ( ch == '\r' ) {
						ch = getc(in);
						if ( ch != '\n' ) {
							ungetc(ch, in);
						}
						state = STATE_END_LINE;
					}
					else if ( ch == '\n' ) {
						state = STATE_END_LINE;
					}
					else {
						csv_string_append(&csv->csv_string, QUOTE, csv->csv_options);
						ungetc(ch, in);	/* zuviel gelesenes Zeichen zurückstellen */
					}
				}
				else {
					csv_string_append(&csv->csv_string, ch, csv->csv_options);
				}
			} while ( state == STATE_QUOTED_FIELD );
			break;

		case STATE_SIMPLE_FIELD:
			do {
				ch = getc(in);
				if ( ch == EOF ) {
					state = STATE_END_FILE;
				}
				else if ( ch == SEP ) {
					state = STATE_END_FIELD;
				}
				else if ( ch == '\r' ) {
					ch = getc(in);
					if ( ch != '\n' ) {
						ungetc(ch, in);
					}
					state = STATE_END_LINE;
				}
				else if ( ch == '\n' ) {
					state = STATE_END_LINE;
				}
				else {
					csv_string_append(&csv->csv_string, ch, csv->csv_options);
				}
			} while ( state == STATE_SIMPLE_FIELD );
			break;

		case STATE_END_FIELD:
			csv_string_append(&csv->csv_string, '\0', csv->csv_options);
			state = STATE_START_FIELD;
			break;

		case STATE_END_LINE:
			csv_string_append(&csv->csv_string, '\0', csv->csv_options);
			return csv->csv_field.pos;

		case STATE_END_FILE:
			if ( csv_string_empty(&csv->csv_string) ) {
				return -1;	/* EOF reached */
			}

			/* EOF wurde gelesen, allerdings sind Daten zur Vearbeitung vorhanden! */
			/* => aktuelles Feld abschließen... */ 
			csv_string_append(&csv->csv_string, '\0', csv->csv_options);

			/* ... und die Anzahl der Felder zurückliefern! */
			return csv->csv_field.pos;

		default:
			assert(!"this should never be happen...");
			break;
		}
	}
	/* NOT REACHED */
}

static void
csv_write_field(const csv_options_t * const csv_options, const char *field, FILE *out)
{
	assert(csv_options != NULL);
	assert(field != NULL);
	assert(out != NULL);

	const char QUOTE = csv_options->quote_symbol;

	if ( *field != '\0' ) {
		putc(QUOTE, out);

		for ( ; *field != '\0'; ++field ) {
			if ( *field == QUOTE ) {
				putc(QUOTE, out);
				putc(QUOTE, out);
			}
			else {
				putc(*field, out);
			}
		}

		putc(QUOTE, out);
	}
}

void
csv_write(const csv_options_t * const csv_options, int n, const char *fields[], FILE *out)
{
	assert(csv_options != NULL);
	assert(fields != NULL);
	assert(out != NULL);

	const char SEP = csv_options->quote_symbol;

	/* process first field */
	if ( 0 != n && fields[0] != NULL ) {
		csv_write_field(csv_options, fields[0], out);

		/* process next fields */
		for ( int i = 1; i != n && fields[i] != NULL; ++i ) {
			putc(SEP, out);
			csv_write_field(csv_options, fields[i], out);
		}
	}
	fprintf(out, "\r\n");
}

jmp_buf env;

static void sig_handler(int sig)
{
	(void) sig;
	longjmp(env, 1);
}

int
main(void)
{
	csv_t csv[1];
	int n, line = 0;

	signal(SIGABRT, sig_handler);

	FILE *in = fopen("500000 Records.csv", "r");
	if ( in == NULL ) {
		fprintf(stderr, "failed to open testfile...\n");
		return EXIT_FAILURE;
	}

	clock_t start = clock();

	csv_init(csv);
	if ( setjmp(env) == 0 ) {
		while ( (n = csv_read(csv, in)) != -1 ) {
			//if ( line == 500000 ) csv_field(csv, -1);
			++line;
		}
	}
	else {
		printf("Fehler beim Verarbeiten der Datei...\n");
	}
	csv_free(csv);

	clock_t end = clock();

	fclose(in);

	printf("%d lines processed, duration: %.3lf sec\n", line, ((double)(end - start)) / CLOCKS_PER_SEC);

	return EXIT_SUCCESS;
}
