#include <string.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

#include "edl.h"
#include "str.h"
#include "datatypes.h"
#include "util.h"

/* CRC32 hashes of EDL headers */
#define EDL_HEADER_ID				0x1541c503
#define EDL_HEADER_TRACK			0x4b211812
#define EDL_HEADER_STARTTIME		0xbb46516f
#define EDL_HEADER_LENGTH			0xaeac5df7
#define EDL_HEADER_PLAYRATE			0x03834606
#define EDL_HEADER_LOCKED			0x0c2083d3
#define EDL_HEADER_NORMALIZED		0xe60c8b1d
#define EDL_HEADER_STRETCHMETHOD	0xbe5802c9
#define EDL_HEADER_LOOPED			0x4ec8be4c
#define EDL_HEADER_ONRULER			0xe6cb84d1
#define EDL_HEADER_MEDIATYPE		0x035f84c2
#define EDL_HEADER_FILENAME			0x379d32c5
#define EDL_HEADER_STREAM			0x9f334738
#define EDL_HEADER_STREAMSTART		0x07b4e0e7
#define EDL_HEADER_STREAMLENGTH		0x8f16c7b8
#define EDL_HEADER_FADETIMEIN		0xd2edd7e4
#define EDL_HEADER_FADETIMEOUT		0x792e8c40
#define EDL_HEADER_SUSTAINGAIN		0x98374657
#define EDL_HEADER_CURVEIN			0x3e998b1f
#define EDL_HEADER_GAININ			0x82fb09c4
#define EDL_HEADER_CURVEOUT			0x53add388
#define EDL_HEADER_GAINOUT			0x4210ba56
#define EDL_HEADER_LAYER			0x89c4df6c
#define EDL_HEADER_COLOR			0xadf2f1a3
#define EDL_HEADER_CURVEINR			0xa56b43e1
#define EDL_HEADER_CURVEOUTR		0xcb6d715e
#define EDL_HEADER_PLAYPITCH		0x9da1b9ed
#define EDL_HEADER_LOCKPITCH		0x2bda6ed4
#define EDL_HEADER_FIRSTCHANNEL		0x3071a4c6
#define EDL_HEADER_CHANNELS			0xe94981a4

/* use a CRC32 hash function to process the headers */
static uint32_t EDL_internal_crcn32b(const unsigned char* message, size_t length) {
	uint32_t crc = 0xffffffff;
	size_t i;

	for (i = 0; i < length && message[i]; i++) {
		size_t j;

		crc = crc ^ message[i];
		for (j = 0; j < 8; j++)
			crc = (crc >> 1) ^ (0xedb88320 & -(crc & 1));
	}

	return ~crc;
}

static size_t EDL_internal_size_t_min(size_t a, size_t b) {
	return (a < b) ? a : b;
}

/* This parses the header of the EDL and creates an array with CRC32 hashes
 * in the order of the strings.
*/
static uint32_t* EDL_internal_parse_header(const char* data, size_t* length) {
	/* */
	const size_t newline = EDL_internal_size_t_min(strchr(data, '\n') - data, strchr(data, '\r') - data);

	size_t current_hash = 0, hashes_size = 32;
	uint32_t* hashes = malloc(hashes_size * sizeof(uint32_t));

	/* overall iterates over the entire line,
	 * column stores the length of the current column */
	size_t overall, column;

	for (overall = 0, column = 0; overall <= newline; overall++) {
		if (data[overall] == ';' || data[overall] == ':') {
			/* process the current column */
			if (hashes_size < current_hash)
				hashes = realloc(hashes, (hashes_size *= 2) * sizeof(uint32_t));

			hashes[current_hash++] = EDL_internal_crcn32b((const unsigned char*)&data[overall - column], column);
			column = 0; /* reset */
		} else column++;
	}

	*length = current_hash;

	return hashes;
}

int EDL_reallocate(EDL* edl, size_t new_capacity) {
	edl->arr = realloc(edl->arr, new_capacity * sizeof(EDL_line));
	if (!edl->arr)
		return 0;

	if (new_capacity > edl->capacity)
		memset(&edl->arr[edl->capacity], 0, (new_capacity - edl->capacity) * sizeof(EDL_line));

	edl->capacity = new_capacity;

	return 1;
}

int EDL_append(EDL* edl, EDL_line line) {
	if (edl->size + 1 < edl->capacity && !EDL_reallocate(edl, edl->capacity))
		return 0;

	edl->arr[++edl->size] = line;
	return 1;
}

/* the big important function */
EDL_parse_error_t EDL_parse(EDL* edl, const char* data, size_t length) {
	size_t order_size = 0;
	uint32_t* order = NULL;
	size_t offset = 0;

	if (!EDL_reallocate(edl, 16))
		return EDL_PARSE_ERROR_OUT_OF_MEMORY;

	order = EDL_internal_parse_header(data, &order_size);

	while ((offset = EDL_internal_strnchr(&data[offset], '\n', length - offset) - data + 1) < length) {
		size_t local_offset = offset; /* original offset stays intact */
		bool ok = true;
		int i;

		for (i = 0; i < order_size; i++) {
#define ADD_TO_OFFSET(x, a) \
{ \
	size_t o = a(data, local_offset, length, &edl->arr[edl->size].x); \
	if (!o) \
		ok = false; \
	local_offset += o; \
}
			switch (order[i]) {
				case EDL_HEADER_ID:
					ADD_TO_OFFSET(id, EDL_internal_parse_int);
					break;
				case EDL_HEADER_TRACK:
					ADD_TO_OFFSET(track, EDL_internal_parse_int);
					break;
				case EDL_HEADER_STARTTIME:
					ADD_TO_OFFSET(start_time, EDL_internal_parse_double);
					break;
				case EDL_HEADER_LENGTH:
					ADD_TO_OFFSET(length, EDL_internal_parse_double);
					break;
				case EDL_HEADER_PLAYRATE:
					ADD_TO_OFFSET(play_rate, EDL_internal_parse_double);
					break;
				case EDL_HEADER_LOCKED:
					ADD_TO_OFFSET(locked, EDL_internal_parse_bool);
					break;
				case EDL_HEADER_NORMALIZED:
					ADD_TO_OFFSET(normalized, EDL_internal_parse_bool);
					break;
				case EDL_HEADER_STRETCHMETHOD:
					ADD_TO_OFFSET(stretch_method, EDL_internal_parse_int);
					break;
				case EDL_HEADER_LOOPED:
					ADD_TO_OFFSET(looped, EDL_internal_parse_bool);
					break;
				case EDL_HEADER_ONRULER:
					ADD_TO_OFFSET(on_ruler, EDL_internal_parse_bool);
					break;
				case EDL_HEADER_MEDIATYPE:
					ADD_TO_OFFSET(media_type, EDL_internal_parse_media_type);
					break;
				case EDL_HEADER_FILENAME:
					ADD_TO_OFFSET(filename, EDL_internal_parse_string);
					break;
				case EDL_HEADER_STREAM:
					ADD_TO_OFFSET(stream, EDL_internal_parse_int);
					break;
				case EDL_HEADER_STREAMSTART:
					ADD_TO_OFFSET(stream_start, EDL_internal_parse_double);
					break;
				case EDL_HEADER_STREAMLENGTH:
					ADD_TO_OFFSET(stream_length, EDL_internal_parse_double);
					break;
				case EDL_HEADER_FADETIMEIN:
					ADD_TO_OFFSET(fade_time_in, EDL_internal_parse_double);
					break;
				case EDL_HEADER_FADETIMEOUT:
					ADD_TO_OFFSET(fade_time_out, EDL_internal_parse_double);
					break;
				case EDL_HEADER_SUSTAINGAIN:
					ADD_TO_OFFSET(sustain_gain, EDL_internal_parse_double);
					break;
				case EDL_HEADER_CURVEIN:
					ADD_TO_OFFSET(curve_in, EDL_internal_parse_int);
					break;
				case EDL_HEADER_GAININ:
					ADD_TO_OFFSET(gain_in, EDL_internal_parse_double);
					break;
				case EDL_HEADER_CURVEOUT:
					ADD_TO_OFFSET(curve_out, EDL_internal_parse_int);
					break;
				case EDL_HEADER_GAINOUT:
					ADD_TO_OFFSET(gain_out, EDL_internal_parse_double);
					break;
				case EDL_HEADER_LAYER:
					ADD_TO_OFFSET(layer, EDL_internal_parse_int);
					break;
				case EDL_HEADER_COLOR:
					ADD_TO_OFFSET(color, EDL_internal_parse_int);
					break;
				case EDL_HEADER_CURVEINR:
					ADD_TO_OFFSET(curve_in_r, EDL_internal_parse_int);
					break;
				case EDL_HEADER_CURVEOUTR:
					ADD_TO_OFFSET(curve_out_r, EDL_internal_parse_int);
					break;
				case EDL_HEADER_PLAYPITCH:
					ADD_TO_OFFSET(play_pitch, EDL_internal_parse_double);
					break;
				case EDL_HEADER_LOCKPITCH:
					ADD_TO_OFFSET(lock_pitch, EDL_internal_parse_bool);
					break;
				case EDL_HEADER_FIRSTCHANNEL:
					ADD_TO_OFFSET(first_channel, EDL_internal_parse_int);
					break;
				case EDL_HEADER_CHANNELS:
					ADD_TO_OFFSET(channels, EDL_internal_parse_int);
					break;
				default:
					/* ... what */
					break;
#undef ADD_TO_OFFSET
			}
		}

		if (!ok)
			break;

		if (++edl->size >= edl->capacity)
			EDL_reallocate(edl, edl->capacity * 2);
	}

	/* put on the shrinkwrap */
	EDL_reallocate(edl, edl->size);

	free(order);

	return EDL_PARSE_ERROR_SUCCESS;
}

static void EDL_dump_line(EDL_internal_string* str, const EDL_line* line, const uint32_t* order, const size_t order_len) {
	size_t i;

	for (i = 0; i < order_len; i++) {
		switch (order[i]) {
#define APPEND_ITEM(a, x) \
{ \
	char* tstr = x(line->a); \
	EDL_internal_string_append(str, tstr, strlen(tstr)); \
	free(tstr); \
}
			case EDL_HEADER_ID:
				APPEND_ITEM(id, EDL_internal_integer_to_string);
				break;
			case EDL_HEADER_TRACK:
				APPEND_ITEM(track, EDL_internal_integer_to_string);
				break;
			case EDL_HEADER_STARTTIME:
				APPEND_ITEM(start_time, EDL_internal_double_to_string);
				break;
			case EDL_HEADER_LENGTH:
				APPEND_ITEM(length, EDL_internal_double_to_string);
				break;
			case EDL_HEADER_PLAYRATE:
				APPEND_ITEM(play_rate, EDL_internal_double_to_string);
				break;
			case EDL_HEADER_LOCKED:
				APPEND_ITEM(locked, EDL_internal_bool_to_string);
				break;
			case EDL_HEADER_NORMALIZED:
				APPEND_ITEM(normalized, EDL_internal_bool_to_string);
				break;
			case EDL_HEADER_STRETCHMETHOD:
				APPEND_ITEM(stretch_method, EDL_internal_integer_to_string);
				break;
			case EDL_HEADER_LOOPED:
				APPEND_ITEM(looped, EDL_internal_bool_to_string);
				break;
			case EDL_HEADER_ONRULER:
				APPEND_ITEM(on_ruler, EDL_internal_bool_to_string);
				break;
			case EDL_HEADER_MEDIATYPE:
				APPEND_ITEM(media_type, EDL_internal_media_type_to_string);
				break;
			case EDL_HEADER_FILENAME:
				EDL_internal_string_append(str, "\"", 1);
				EDL_internal_string_append(str, line->filename, strlen(line->filename));
				EDL_internal_string_append(str, "\"", 1);
				break;
			case EDL_HEADER_STREAM:
				APPEND_ITEM(stream, EDL_internal_integer_to_string);
				break;
			case EDL_HEADER_STREAMSTART:
				APPEND_ITEM(stream_start, EDL_internal_double_to_string);
				break;
			case EDL_HEADER_STREAMLENGTH:
				APPEND_ITEM(stream_length, EDL_internal_double_to_string);
				break;
			case EDL_HEADER_FADETIMEIN:
				APPEND_ITEM(fade_time_in, EDL_internal_double_to_string);
				break;
			case EDL_HEADER_FADETIMEOUT:
				APPEND_ITEM(fade_time_out, EDL_internal_double_to_string);
				break;
			case EDL_HEADER_SUSTAINGAIN:
				APPEND_ITEM(sustain_gain, EDL_internal_double_to_string);
				break;
			case EDL_HEADER_CURVEIN:
				APPEND_ITEM(curve_in, EDL_internal_integer_to_string);
				break;
			case EDL_HEADER_GAININ:
				APPEND_ITEM(gain_in, EDL_internal_double_to_string);
				break;
			case EDL_HEADER_CURVEOUT:
				APPEND_ITEM(curve_out, EDL_internal_integer_to_string);
				break;
			case EDL_HEADER_GAINOUT:
				APPEND_ITEM(gain_out, EDL_internal_double_to_string);
				break;
			case EDL_HEADER_LAYER:
				APPEND_ITEM(layer, EDL_internal_integer_to_string);
				break;
			case EDL_HEADER_COLOR:
				APPEND_ITEM(color, EDL_internal_integer_to_string);
				break;
			case EDL_HEADER_CURVEINR:
				APPEND_ITEM(curve_in_r, EDL_internal_integer_to_string);
				break;
			case EDL_HEADER_CURVEOUTR:
				APPEND_ITEM(curve_out_r, EDL_internal_integer_to_string);
				break;
			case EDL_HEADER_PLAYPITCH:
				APPEND_ITEM(play_pitch, EDL_internal_double_to_string);
				break;
			case EDL_HEADER_LOCKPITCH:
				APPEND_ITEM(lock_pitch, EDL_internal_bool_to_string);
				break;
			case EDL_HEADER_FIRSTCHANNEL:
				APPEND_ITEM(first_channel, EDL_internal_integer_to_string);
				break;
			case EDL_HEADER_CHANNELS:
				APPEND_ITEM(channels, EDL_internal_integer_to_string);
				break;
			default:
				/* ... what */
				break;
#undef APPEND_ITEM
		}

		if (i < order_len - 1)
			EDL_internal_string_append(str, ";\t", 2);
		else
			EDL_internal_string_append(str, ";", 1);
	}

	EDL_internal_string_append(str, "\n", 1);
}

/* this gets progressively slower and slower... oops */
char* EDL_dump(const EDL* edl) {
	static const char order_str[] =
		"\"ID\";\"Track\";\"StartTime\";\"Length\";\"PlayRate\";\"Locked\";\"Normalized\";\"StretchMethod\";"
		"\"Looped\";\"OnRuler\";\"MediaType\";\"FileName\";\"Stream\";\"StreamStart\";\"StreamLength\";"
		"\"FadeTimeIn\";\"FadeTimeOut\";\"SustainGain\";\"CurveIn\";\"GainIn\";\"CurveOut\";\"GainOut\";"
		"\"Layer\";\"Color\";\"CurveInR\";\"CurveOutR\":\"PlayPitch\";\"LockPitch\";\"FirstChannel\";\"Channels\"\n";

	EDL_internal_string ret;
	EDL_internal_string_init(&ret);

	EDL_internal_string_append(&ret, order_str, strlen(order_str));

	{
		size_t order_len;
		uint32_t* order = EDL_internal_parse_header(order_str, &order_len);

		size_t i;
		for (i = 0; i < edl->size; i++)
			EDL_dump_line(&ret, &edl->arr[i], order, order_len);

		free(order);
	}

	return ret.data;
}

void EDL_free(EDL* edl) {
	size_t i;
	for (i = 0; i < edl->size; i++) {
		if (edl->arr[i].filename)
			free(edl->arr[i].filename);
	}
	free(edl->arr);
}
