#ifndef __serialization_translator_H__
#define __serialization_translator_H__

#include "binaryserialization/BinarySerializationContext.h"
#include "binaryserialization/BinarySerializationShared.h"
#include "jsonserialization/JSONSerializationContext.h"
#include "jsonserialization/JSONSerializationShared.h"
#include "serialization/DeserializationContext.h"
#include "serialization/DeserializationTypeDetector.h"
#include "serialization/SerializationContext.h"
#include "utilities/IOUtilities.h"
#include "utilities/printfFormats.h"
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

static void printUsage(void) {
	fprintf(stderr, "Usage: serialization_translator [--format <json|binary>] [-o <output file>] <input file>\n"
	                "       JSON format options: [--json-format <compact|singleLine|multiLine>]\n"
	                "       Binary format options: [--binary-endian <big|little>]\n");
}

enum outputFormat {
	FORMAT_BINARY,
	FORMAT_JSON
};

static int serialization_translator_main(int argc, const char ** argv, void * (* deserializeCallback)(DeserializationContext * context), void (* serializeCallback)(void * object, SerializationContext * context)) {
	DeserializationTypeDetector * typeDetector = DeserializationTypeDetector_create();
	DeserializationTypeDetector_registerDetectionCallback(typeDetector, JSONSerialization_typeDetector);
	DeserializationTypeDetector_registerDetectionCallback(typeDetector, BinarySerialization_typeDetector);
	const char * inputPath = NULL, * outputPath = NULL;
	enum outputFormat format = FORMAT_JSON;
	enum JSONEmitterFormat jsonFormat = JSONEmitterFormat_multiLine;
	bool bigEndian = false;
	for (int argIndex = 1; argIndex < argc; argIndex++) {
		if (!strcmp(argv[argIndex], "--help")) {
			printUsage();
			return EXIT_SUCCESS;
		}
		if (!strcmp(argv[argIndex], "--format")) {
			if (argIndex >= argc - 1) {
				fprintf(stderr, "Error: --format specified at the end of argv\n");
				printUsage();
				return EXIT_FAILURE;
			}
			++argIndex;
			if (!strcmp(argv[argIndex], "binary")) {
				format = FORMAT_BINARY;
			} else if (!strcmp(argv[argIndex], "json")) {
				format = FORMAT_JSON;
			} else {
				fprintf(stderr, "Error: Couldn't understand format \"%s\"\n", argv[argIndex]);
				printUsage();
				return EXIT_FAILURE;
			}
			
		} else if (!strcmp(argv[argIndex], "-o")) {
			if (argIndex >= argc - 1) {
				fprintf(stderr, "Error: -o specified at the end of argv\n");
				printUsage();
				return EXIT_FAILURE;
			}
			outputPath = argv[++argIndex];
			
		} else if (!strcmp(argv[argIndex], "--json-format")) {
			if (argIndex >= argc - 1) {
				fprintf(stderr, "Error: --json-format specified at the end of argv\n");
				printUsage();
				return EXIT_FAILURE;
			}
			++argIndex;
			if (!strcmp(argv[argIndex], "compact")) {
				jsonFormat = JSONEmitterFormat_compact;
			} else if (!strcmp(argv[argIndex], "singleLine")) {
				jsonFormat = JSONEmitterFormat_singleLine;
			} else if (!strcmp(argv[argIndex], "multiLine")) {
				jsonFormat = JSONEmitterFormat_multiLine;
			} else {
				fprintf(stderr, "Error: Couldn't understand JSON format \"%s\"\n", argv[argIndex]);
				printUsage();
				return EXIT_FAILURE;
			}
			
		} else if (!strcmp(argv[argIndex], "--binary-endian")) {
			if (argIndex >= argc - 1) {
				fprintf(stderr, "Error: --binary-endian specified at the end of argv\n");
				printUsage();
				return EXIT_FAILURE;
			}
			++argIndex;
			if (!strcmp(argv[argIndex], "big")) {
				bigEndian = true;
			} else if (!strcmp(argv[argIndex], "little")) {
				bigEndian = false;
			} else {
				fprintf(stderr, "Error: Couldn't understand binary endianness \"%s\"\n", argv[argIndex]);
				printUsage();
				return EXIT_FAILURE;
			}
			
		} else {
			if (inputPath != NULL) {
				fprintf(stderr, "Couldn't understand argument \"%s\" (already took \"%s\" as input file path)\n", argv[argIndex], inputPath);
				printUsage();
				return EXIT_FAILURE;
			}
			inputPath = argv[argIndex];
		}
	}
	
	DeserializationContext * deserializationContext = NULL;
	if (inputPath == NULL || !strcmp(inputPath, "-")) {
		size_t inputSize;
		void * input = readStdinSimple(&inputSize);
		deserializationContext = DeserializationTypeDetector_createDeserializationContextWithData(typeDetector, input, inputSize);
		free(input);
	} else {
		deserializationContext = DeserializationTypeDetector_createDeserializationContextWithFile(typeDetector, inputPath);
	}
	if (deserializationContext == NULL) {
		fprintf(stderr, "Couldn't create deserialization context for \"%s\"\n", inputPath);
	}
	
	void * object = deserializeCallback(deserializationContext);
	if (object == NULL) {
		fprintf(stderr, "Failed to deserialize file \"%s\"\n", inputPath);
		return EXIT_FAILURE;
	}
	call_virtual(dispose, deserializationContext);
	
	SerializationContext * serializationContext = NULL;
	switch (format) {
		case FORMAT_BINARY:
			serializationContext = (SerializationContext *) BinarySerializationContext_create(bigEndian);
			break;
			
		case FORMAT_JSON:
			serializationContext = (SerializationContext *) JSONSerializationContext_create();
			break;
	}
	serializeCallback(object, serializationContext);
	
	void * outputData = NULL;
	size_t outputLength = 0;
	switch (format) {
		case FORMAT_BINARY:
			outputData = BinarySerializationContext_writeToBytes((BinarySerializationContext *) serializationContext, &outputLength);
			break;
			
		case FORMAT_JSON:
			outputData = JSONSerializationContext_writeToString((JSONSerializationContext *) serializationContext, jsonFormat, &outputLength, NULL);
			break;
	}
	if (outputData == NULL) {
		fprintf(stderr, "Error: Couldn't write to \"%s\" (errno = %d)\n", outputPath, errno);
		return EXIT_FAILURE;
	}
	
	if (outputPath == NULL) {
		switch (format) {
			case FORMAT_BINARY:
				outputPath = "a.bin";
				break;
				
			case FORMAT_JSON:
				outputPath = "a.json";
				break;
		}
	}
	if (!strcmp(outputPath, "-")) {
		size_t result = write(STDOUT_FILENO, outputData, outputLength);
		if (result < outputLength) {
			fprintf(stderr, "Warning: Only " SIZE_T_FORMAT " bytes out of " SIZE_T_FORMAT" could be written\n", result, outputLength);
		}
	} else {
		writeFileSimple(outputPath, outputData, outputLength);
	}
	
	call_virtual(dispose, serializationContext);
	return EXIT_SUCCESS;
}

#endif
