#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

typedef struct {
	uint32_t* order;
	size_t size;
	size_t capacity;
} EDL_header;

/* 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;
}

static int EDL_internal_header_reallocate(EDL_header* header, size_t new_capacity) {
	header->order = realloc(header->order, new_capacity * sizeof(*header->order));
	if (!header->order)
		return -1;

	if (new_capacity > header->capacity)
		memset(&header->order[header->capacity], 0, (new_capacity - header->capacity) * sizeof(*header->order));

	header->capacity = new_capacity;

	return header->capacity;
}

static int EDL_internal_parse_header_item(EDL_header* header, const char* data, size_t offset, size_t length) {
	if (header->capacity <= header->size)
		if (EDL_internal_header_reallocate(header, header->capacity * 2) < 0)
			return -1;

	header->order[header->size++] = EDL_internal_crcn32b((const unsigned char*)&data[offset], length);

	return header->size;
}

/* EDL_header MUST be zero-initialized. */
static int EDL_internal_parse_header(EDL_header* header, const char* data, size_t offset, size_t length) {
	size_t newline = 0, overall = 0, column = 0;
	{
		const char* ln = memchr(&data[offset], '\n', length - offset);
		if (!ln)
			return -1;

		newline = ln - &data[offset];
	}

	EDL_internal_header_reallocate(header, 32);

	for (; overall <= newline; overall++) {
		if (data[offset + overall] == ';' || data[offset + overall] == ':') {
			if (EDL_internal_parse_header_item(header, data, overall - column, column) < 0)
				return -1;

			column = 0; /* reset */
		} else column++;
	}

	return overall;
}

static int EDL_internal_free_header(EDL_header* header) {
	free(header->order);
}

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

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

	edl->capacity = new_capacity;

	return edl->capacity;
}

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 offset = 0;
	EDL_header header = {0};

	if (EDL_reallocate(edl, 16) < 0)
		return EDL_PARSE_ERROR_OUT_OF_MEMORY;

	if (EDL_internal_parse_header(&header, data, offset, length) < 0)
		return EDL_PARSE_ERROR_HEADER;

	while ((offset = (const char*)memchr(&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 < header.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 (header.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)
			if (EDL_reallocate(edl, edl->capacity * 2) < 0)
				return EDL_PARSE_ERROR_OUT_OF_MEMORY;
	}

	EDL_internal_free_header(&header);

	/* put on the shrinkwrap */
	if (EDL_reallocate(edl, edl->size) < 0)
		return EDL_PARSE_ERROR_OUT_OF_MEMORY;

	return EDL_PARSE_ERROR_SUCCESS;
}

static void EDL_dump_line(EDL_internal_string* str, const EDL_line* line, const EDL_header* header) {
	size_t i;

	for (i = 0; i < header->size; i++) {
		switch (header->order[i]) {
			case EDL_HEADER_ID:
				EDL_internal_append_integer_to_string(str, line->id);
				break;
			case EDL_HEADER_TRACK:
				EDL_internal_append_integer_to_string(str, line->track);
				break;
			case EDL_HEADER_STARTTIME:
				EDL_internal_append_double_to_string(str, line->start_time);
				break;
			case EDL_HEADER_LENGTH:
				EDL_internal_append_double_to_string(str, line->length);
				break;
			case EDL_HEADER_PLAYRATE:
				EDL_internal_append_double_to_string(str, line->play_rate);
				break;
			case EDL_HEADER_LOCKED:
				EDL_internal_append_bool_to_string(str, line->locked);
				break;
			case EDL_HEADER_NORMALIZED:
				EDL_internal_append_bool_to_string(str, line->normalized);
				break;
			case EDL_HEADER_STRETCHMETHOD:
				EDL_internal_append_integer_to_string(str, line->stretch_method);
				break;
			case EDL_HEADER_LOOPED:
				EDL_internal_append_bool_to_string(str, line->looped);
				break;
			case EDL_HEADER_ONRULER:
				EDL_internal_append_bool_to_string(str, line->on_ruler);
				break;
			case EDL_HEADER_MEDIATYPE:
				EDL_internal_append_media_type_to_string(str, line->media_type);
				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:
				EDL_internal_append_integer_to_string(str, line->stream);
				break;
			case EDL_HEADER_STREAMSTART:
				EDL_internal_append_double_to_string(str, line->stream_start);
				break;
			case EDL_HEADER_STREAMLENGTH:
				EDL_internal_append_double_to_string(str, line->stream_length);
				break;
			case EDL_HEADER_FADETIMEIN:
				EDL_internal_append_double_to_string(str, line->fade_time_in);
				break;
			case EDL_HEADER_FADETIMEOUT:
				EDL_internal_append_double_to_string(str, line->fade_time_out);
				break;
			case EDL_HEADER_SUSTAINGAIN:
				EDL_internal_append_double_to_string(str, line->sustain_gain);
				break;
			case EDL_HEADER_CURVEIN:
				EDL_internal_append_integer_to_string(str, line->curve_in);
				break;
			case EDL_HEADER_GAININ:
				EDL_internal_append_double_to_string(str, line->gain_in);
				break;
			case EDL_HEADER_CURVEOUT:
				EDL_internal_append_integer_to_string(str, line->curve_out);
				break;
			case EDL_HEADER_GAINOUT:
				EDL_internal_append_double_to_string(str, line->gain_out);
				break;
			case EDL_HEADER_LAYER:
				EDL_internal_append_integer_to_string(str, line->layer);
				break;
			case EDL_HEADER_COLOR:
				EDL_internal_append_integer_to_string(str, line->color);
				break;
			case EDL_HEADER_CURVEINR:
				EDL_internal_append_integer_to_string(str, line->curve_in_r);
				break;
			case EDL_HEADER_CURVEOUTR:
				EDL_internal_append_integer_to_string(str, line->curve_out_r);
				break;
			case EDL_HEADER_PLAYPITCH:
				EDL_internal_append_double_to_string(str, line->play_pitch);
				break;
			case EDL_HEADER_LOCKPITCH:
				EDL_internal_append_bool_to_string(str, line->lock_pitch);
				break;
			case EDL_HEADER_FIRSTCHANNEL:
				EDL_internal_append_integer_to_string(str, line->first_channel);
				break;
			case EDL_HEADER_CHANNELS:
				EDL_internal_append_integer_to_string(str, line->channels);
				break;
			default:
				/* ... what */
				break;
		}

		if (i < header->size - 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));

	{
		/* how? */
		EDL_header header = {0};
		if (EDL_internal_parse_header(&header, order_str, 0, strlen(order_str)) < 0)
			return NULL;

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

		EDL_internal_free_header(&header);
	}

	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);
}
