#include <stdlib.h>
#include <string.h>
#include <jsapi.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include "js_util_functions.h"
#include "stream.h"
#include "filestream.h"

#define CHUNK_SIZE 1024

static void FileStream_finalize(JSContext *cx, JSObject *obj);
static JSBool FileStream_ctor(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval);
static JSBool FileStream_open(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval);
static JSBool FileStream_getBytesAvailable(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval);
static JSBool FileStream_read(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval);
static JSBool FileStream_write(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval);
static JSBool FileStream_close(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval);
static JSBool FileStream_seek(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval);

static JSBool filestream_open(FileStreamInformation *info);
static JSBool filestream_close(FileStreamInformation *info);
static JSBool filestream_fill_buffer(FileStreamInformation *info);

static char *errnostr(int err);

static JSObject *FileStreamPrototype = NULL;
static JSClass js_FileStream_class = {
    "FileStream",
    JSCLASS_HAS_PRIVATE,
    JS_PropertyStub,
    JS_PropertyStub,
    JS_PropertyStub,
    JS_PropertyStub,
    JS_EnumerateStub,
    JS_ResolveStub,
    JS_ConvertStub,
    FileStream_finalize,
    JSCLASS_NO_OPTIONAL_MEMBERS
};

static JSFunctionSpec FileStream_methods[] = {
    {"open", FileStream_open, 0, 0 ,0},
    {"getBytesAvailable", FileStream_getBytesAvailable, 0, 0 ,0},
    {"read", FileStream_read, 0, 0 ,0},
	{"write", FileStream_write, 0, 0, 0},
    {"close", FileStream_close, 0, 0 ,0},
	{"seek", FileStream_seek, 2, 0, 0},
	{NULL}
};

JSBool register_class_FileStream(JSContext *cx){
	JSObject *globalObj = JS_GetGlobalObject(cx);
	JSObject *proto = Stream_getPrototypeObject();

    /* Define the file object. */
    FileStreamPrototype = JS_InitClass(cx, globalObj, proto, &js_FileStream_class, FileStream_ctor, 1, NULL, FileStream_methods, NULL, NULL);
    if (!FileStreamPrototype){
        return JS_FALSE;
    }
	return JS_TRUE;
}

JSObject *FileStream_getPrototypeObject(){
	return FileStreamPrototype;
}

static char *errnostr(int e){
    char *msg = NULL;
    switch (e){
		case ENOENT:
			msg = strdup("File does not exist.");
			break;
        case EACCES:
            msg = strdup("Permission denied to check file.");
            break;
        case EBADF:
            msg = strdup("Bad file descriptor");
            break;
        case ELOOP:
            msg = strdup("Symbolic link loop encountered.");
            break;
        case ENAMETOOLONG:
            msg = strdup("File name too long, not supported.");
            break;
        case ENOMEM:
            msg = strdup("Out of memory.");
            break;
        case ENOTDIR:
            msg = strdup("Path component not a directory.");
            break;
        default:
            msg = strdup("Unknown error occured.");
    }
    return msg;
}


static void FileStream_finalize(JSContext *cx, JSObject *obj){
	FileStreamInformation *info = (FileStreamInformation *)JS_GetPrivate(cx, obj);

	if (info != NULL){
		filestream_close(info);
		free(info->path);
		if (info->streamInfo.buffer != NULL){
			free(info->streamInfo.buffer);
		}
		JS_free(cx, info);
	}
}


static JSBool FileStream_ctor(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval){
	if (argc < 1){
		JS_SetPendingException(cx, printf_exception(cx, "Incorrect number of parameters to FileStream constructor."));
		return JS_FALSE;
	}
	else {
		char *path=NULL;
		FileStreamInformation *info=NULL;

		info = JS_malloc(cx, sizeof(*info));
		info->streamInfo.handle = -1;
		info->streamInfo.buffer = NULL;
		info->streamInfo.bufferLength = 0;
		info->streamInfo.eofFlag = JS_FALSE;
		info->path = NULL;

		JSString_to_CString(JS_ValueToString(cx, argv[0]), &path);
		info->path = strdup(path);

		JS_SetPrivate(cx, obj, info);
		return JS_TRUE;
	}
}

static JSBool FileStream_open(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval){
	FileStreamInformation *info=(FileStreamInformation*)JS_GetPrivate(cx, obj);

	JSBool result = filestream_open(info);
	if (result == JS_FALSE){
		char *msg = errnostr(errno);
		jsval exception = printf_exception(cx, "Unable to open stream. (%d) %s", errno, msg);
		free(msg);
		JS_SetPendingException(cx, exception);
	}
	return result;
}

static JSBool FileStream_close(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval){
	FileStreamInformation *info=(FileStreamInformation*)JS_GetPrivate(cx, obj);

	JSBool result = filestream_close(info);
	if (result == JS_FALSE){
		char *msg = errnostr(errno);
		jsval exception = printf_exception(cx, "Unable to open stream. (%d) %s", errno, msg);
		free(msg);
		JS_SetPendingException(cx, exception);
	}
	return result;
}

static JSBool FileStream_getBytesAvailable(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval){
	FileStreamInformation *info=(FileStreamInformation*)JS_GetPrivate(cx, obj);
	off_t curPos = 0;
	off_t endPos = 0;
	unsigned int length = 0;

	curPos = lseek(info->streamInfo.handle, 0, SEEK_CUR);

	if (curPos == (off_t)-1){
		char *msg = errnostr(errno);
		jsval exception = printf_exception(cx, "Unable to determine available bytes. (%d) %s", errno, msg);
		free(msg);
		JS_SetPendingException(cx, exception);
		return JS_FALSE;
	}

	endPos = lseek(info->streamInfo.handle, 0, SEEK_END);
	if (endPos == (off_t)-1){
		char *msg = errnostr(errno);
		jsval exception = printf_exception(cx, "Unable to determine available bytes. (%d) %s", errno, msg);
		free(msg);
		JS_SetPendingException(cx, exception);
		return JS_FALSE;
	}

	length = endPos - curPos;
	curPos = lseek(info->streamInfo.handle, curPos, SEEK_SET);
	if (curPos == (off_t)-1){
		char *msg = errnostr(errno);
		jsval exception = printf_exception(cx, "Stream has become corrupt. (%d) %s", errno, msg);
		free(msg);
		JS_SetPendingException(cx, exception);
		return JS_FALSE;
	}
	
	length += info->streamInfo.bufferLength;

	*rval = INT_TO_JSVAL(length);
	return JS_TRUE;
}

//This function serves multiple purposes depending on the number of arguments passed in (argc)
static JSBool FileStream_read(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval){
	FileStreamInformation *info=(FileStreamInformation*)JS_GetPrivate(cx, obj);
	char *buffer=NULL;
	char *bufferPtr = NULL;
	char *bufferDst = NULL;
	char *bufferReallocPtr = NULL;
	int bufferFillLen=0;
	int bufferSize=0;
	JSString *returnValue=NULL;
	JSBool foundEndOfLine=JS_FALSE;

	if (argc == 0){
		while (foundEndOfLine == JS_FALSE){
			if (info->streamInfo.bufferLength == 0){
				if (filestream_fill_buffer(info) == JS_FALSE){
					JS_SetPendingException(cx, printf_exception(cx, "Unable to read from file."));
					JS_free(cx, buffer);
					return JS_FALSE;
				}
				else {
					if (info->streamInfo.bufferLength == 0){
						goto complete;
					}
				}
			}

			bufferPtr = info->streamInfo.buffer;
			bufferSize = 0;
			while (bufferSize < info->streamInfo.bufferLength && (*bufferPtr != '\n' && *bufferPtr != '\r')){
				bufferPtr++;
				bufferSize++;
			}

			if (bufferSize < info->streamInfo.bufferLength){
				if (*bufferPtr == '\r' && *(bufferPtr+1) == '\n'){
					bufferPtr++;
					bufferSize++;
				}
				if (*bufferPtr == '\r' || *bufferPtr == '\n'){
					foundEndOfLine = JS_TRUE;
					bufferSize++;
				}
			}
			
			bufferReallocPtr = JS_realloc(cx, buffer, sizeof(*buffer)*(bufferSize+bufferFillLen+1));
			if (bufferReallocPtr == NULL){
				JS_SetPendingException(cx, printf_exception(cx, "Unable to allocate memory."));
				if (buffer != NULL){
					JS_free(cx, buffer);
				}
				return JS_FALSE;
			}
			else {
				buffer = bufferReallocPtr;
			}

			memcpy(buffer+bufferFillLen, info->streamInfo.buffer, bufferSize);
			bufferFillLen += bufferSize;
			buffer[bufferFillLen] = '\0';

			if (bufferSize < info->streamInfo.bufferLength){
				info->streamInfo.bufferLength = info->streamInfo.bufferLength - bufferSize;
				bufferPtr = info->streamInfo.buffer+bufferSize;
				bufferDst = info->streamInfo.buffer;
				while (bufferDst < (info->streamInfo.buffer+info->streamInfo.bufferLength)){
					*bufferDst = *bufferPtr;
					bufferDst++;
					bufferPtr++;
				}

				bufferReallocPtr = realloc(info->streamInfo.buffer, sizeof(*(info->streamInfo.buffer))*info->streamInfo.bufferLength);
				if (bufferReallocPtr == NULL){
					JS_SetPendingException(cx, printf_exception(cx, "Unable to allocate memory for read buffer."));
					JS_free(cx, buffer);
					return JS_FALSE;
				}
				else {
					info->streamInfo.buffer = bufferReallocPtr;
				}
			}
			else {
				free(info->streamInfo.buffer);
				info->streamInfo.buffer = NULL;
				info->streamInfo.bufferLength = 0;
			}
		}
	}
	else {
		if (JS_ValueToInt32(cx, argv[0], &bufferSize) == JS_TRUE){
			buffer = JS_malloc(cx, bufferSize+1);
			memset(buffer, 0, bufferSize+1);


			while (bufferFillLen < bufferSize){
				if (info->streamInfo.bufferLength == 0){
					if (filestream_fill_buffer(info) == JS_FALSE){
						JS_SetPendingException(cx, printf_exception(cx, "Unable to read from file."));
						JS_free(cx, buffer);
						return JS_FALSE;
					}
					else {
						if (info->streamInfo.bufferLength == 0){
							goto complete;
						}
					}
				}

				if (bufferFillLen+info->streamInfo.bufferLength > bufferSize){
					int diff = bufferSize - bufferFillLen;
					memcpy(buffer+bufferFillLen, info->streamInfo.buffer, diff);
					bufferFillLen += diff;

					info->streamInfo.bufferLength = info->streamInfo.bufferLength - diff;
					bufferPtr = info->streamInfo.buffer+diff;
					bufferDst = info->streamInfo.buffer;
					while (bufferDst < (info->streamInfo.buffer+diff)){
						*bufferDst = *bufferPtr;
						bufferDst++;
						bufferPtr++;
					}

					bufferReallocPtr = realloc(info->streamInfo.buffer, sizeof(*(info->streamInfo.buffer))*info->streamInfo.bufferLength);
					if (bufferReallocPtr == NULL){
						JS_SetPendingException(cx, printf_exception(cx, "Unable to allocate memory for read buffer."));
						JS_free(cx, buffer);
						return JS_FALSE;
					}
					else {
						info->streamInfo.buffer = bufferReallocPtr;
					}
				}
				else {
					memcpy(buffer+bufferFillLen, info->streamInfo.buffer, info->streamInfo.bufferLength);
					bufferFillLen += info->streamInfo.bufferLength;

					free(info->streamInfo.buffer);
					info->streamInfo.buffer = NULL;
					info->streamInfo.bufferLength = 0;
				}
			}
		}
		else {
			JS_SetPendingException(cx, printf_exception(cx, "Bad parameter passed to FileStream.read()"));
			return JS_FALSE;
		}
	}

complete:
	if (buffer != NULL){
		returnValue = JS_NewString(cx, buffer, bufferFillLen);
		*rval = STRING_TO_JSVAL(returnValue);
	}
	else {
		buffer = JS_malloc(cx, 1);
		buffer[0] = '\0';
		returnValue = JS_NewString(cx, buffer, 0);
		*rval = STRING_TO_JSVAL(returnValue);
	}
	return JS_TRUE;
}



static JSBool filestream_open(FileStreamInformation *info){
	if (info->streamInfo.handle == -1 && info->path != NULL){
		info->streamInfo.handle = open(info->path, O_RDWR);
		info->streamInfo.eofFlag = JS_FALSE;
		if (info->streamInfo.handle == -1){
			return JS_FALSE;
		}
		return JS_TRUE;
	}
	else {
		return JS_FALSE;
	}
}

static JSBool filestream_close(FileStreamInformation *info){
	if (info->streamInfo.buffer != NULL){
		free(info->streamInfo.buffer);
		info->streamInfo.buffer = NULL;
	}

	info->streamInfo.bufferLength = 0;
	info->streamInfo.eofFlag = JS_FALSE;

	if (info->streamInfo.handle != -1){
		if (close(info->streamInfo.handle) == 0){
			info->streamInfo.handle = -1;
			return JS_TRUE;
		}
		return JS_FALSE;
	}
	else {
		return JS_TRUE;
	}
}


static JSBool filestream_fill_buffer(FileStreamInformation *info){
	int newLen = info->streamInfo.bufferLength + CHUNK_SIZE;
	char *buff = realloc(info->streamInfo.buffer, newLen);
	if (buff == NULL){
		return JS_FALSE;
	}
	else {
		info->streamInfo.buffer = buff;
		info->streamInfo.bufferLength += read(info->streamInfo.handle, (info->streamInfo.buffer+info->streamInfo.bufferLength), CHUNK_SIZE);
		if (info->streamInfo.bufferLength == 0){
			free(buff);
			info->streamInfo.buffer=NULL;
			info->streamInfo.eofFlag=JS_TRUE;
		}
		return JS_TRUE;
	}
}

static JSBool FileStream_write(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval){
	FileStreamInformation *info=(FileStreamInformation*)JS_GetPrivate(cx, obj);
	if (argc == 1){
		char *stringToWrite = NULL;
		int stringLength=0;
		size_t amountWritten=0;
		stringLength = JSString_to_CString(JS_ValueToString(cx, argv[0]), &stringToWrite);
		
		amountWritten = write(info->streamInfo.handle, stringToWrite, stringLength);
		if (amountWritten == -1){
			char *msg = errnostr(errno);
			jsval exception = printf_exception(cx, "Unable to write data to file. (%d) %s", errno, msg);
			free(msg);
			JS_SetPendingException(cx, exception);
			return JS_FALSE;
		}
		else {
			*rval = INT_TO_JSVAL(amountWritten);
			return JS_TRUE;
		}
	}
	else if (argc == 2){
		char *stringToWrite = NULL;
		int stringLength = 0;
		size_t amountWritten=0;

		JSString_to_CString(JS_ValueToString(cx, argv[0]), &stringToWrite);
		if (JS_ValueToInt32(cx, argv[1], &stringLength) == JS_FALSE){
			JS_SetPendingException(cx, printf_exception(cx, "Cannot understand length argument."));
			return JS_FALSE;
		}
		
		amountWritten = write(info->streamInfo.handle, stringToWrite, stringLength);
		if (amountWritten == -1){
			char *msg = errnostr(errno);
			jsval exception = printf_exception(cx, "Unable to write data to file. (%d) %s", errno, msg);
			free(msg);
			JS_SetPendingException(cx, exception);
			return JS_FALSE;
		}
		else {
			*rval = INT_TO_JSVAL(amountWritten);
			return JS_TRUE;
		}
	}
	else {
		JS_SetPendingException(cx, printf_exception(cx, "Incorrect number of arguments."));
		return JS_FALSE;
	}
}

static JSBool FileStream_seek(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval){
	FileStreamInformation *info=(FileStreamInformation*)JS_GetPrivate(cx, obj);
	int offset=-1;
	int base_point=-1;
	off_t newoffset=0;

	if (JS_ValueToInt32(cx, argv[0], &offset) == JS_FALSE){
		JS_SetPendingException(cx, printf_exception(cx, "Cannot understand offset number."));
		return JS_FALSE;
	}
	
	if (JS_ValueToInt32(cx, argv[1], &base_point) == JS_FALSE){
		JS_SetPendingException(cx, printf_exception(cx, "base_point argument must be one of Stream.SEEK_SET, Stream.SEEK_CURRENT, Stream.SEEK_END"));
		return JS_FALSE;
	}

	if (base_point != SEEK_SET && base_point != SEEK_CUR && base_point != SEEK_END){
		JS_SetPendingException(cx, printf_exception(cx, "base_point argument must be one of Stream.SEEK_SET, Stream.SEEK_CURRENT, Stream.SEEK_END"));
		return JS_FALSE;
	}

	newoffset = lseek(info->streamInfo.handle, offset, base_point);
	if (newoffset == (off_t)-1){
		char *msg = errnostr(errno);
		jsval exception = printf_exception(cx, "Seek failed.  (%d) %s", errno, msg);
		JS_SetPendingException(cx, exception);
		return JS_FALSE;
	}
	else {
		*rval = INT_TO_JSVAL(newoffset);
		return JS_TRUE;
	}
}
