#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 "directorystream.h"

static void DirectoryStream_finalize(JSContext *cx, JSObject *obj);
static JSBool DirectoryStream_ctor(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval);
static JSBool DirectoryStream_open(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval);
static JSBool DirectoryStream_getBytesAvailable(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval);
static JSBool DirectoryStream_read(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval);
static JSBool DirectoryStream_close(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval);

static JSBool directorystream_open(DirectoryStreamInformation *info);
static JSBool directorystream_close(DirectoryStreamInformation *info);

static char *errnostr(int err);

static JSObject *DirectoryStreamPrototype = NULL;
static JSClass js_DirectoryStream_class = {
    "DirectoryStream",
    JSCLASS_HAS_PRIVATE,
    JS_PropertyStub,
    JS_PropertyStub,
    JS_PropertyStub,
    JS_PropertyStub,
    JS_EnumerateStub,
    JS_ResolveStub,
    JS_ConvertStub,
    DirectoryStream_finalize,
    JSCLASS_NO_OPTIONAL_MEMBERS
};

static JSFunctionSpec DirectoryStream_methods[] = {
    {"open", DirectoryStream_open, 0, 0 ,0},
    {"getBytesAvailable", DirectoryStream_getBytesAvailable, 0, 0 ,0},
    {"read", DirectoryStream_read, 0, 0 ,0},
    {"close", DirectoryStream_close, 0, 0 ,0},
	{NULL}
};

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

    /* Define the file object. */
    DirectoryStreamPrototype = JS_InitClass(cx, globalObj, proto, &js_DirectoryStream_class, DirectoryStream_ctor, 1, NULL, DirectoryStream_methods, NULL, NULL);
    if (!DirectoryStreamPrototype){
        return JS_FALSE;
    }
	return JS_TRUE;
}

JSObject *DirectoryStream_getPrototypeObject(){
	return DirectoryStreamPrototype;
}

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 DirectoryStream_finalize(JSContext *cx, JSObject *obj){
	DirectoryStreamInformation *info = (DirectoryStreamInformation *)JS_GetPrivate(cx, obj);

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


static JSBool DirectoryStream_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 DirectoryStream constructor."));
		return JS_FALSE;
	}
	else {
		char *path=NULL;
		DirectoryStreamInformation *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;
		info->handle = NULL;

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

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

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

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

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

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

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

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

	errno = 0;
	while (readdir(info->handle) != NULL){
		length++;
	}
	if (errno != 0){
		char *msg = errnostr(errno);
		jsval exception = printf_exception(cx, "Unable to determine number of files available. (%d) %s", errno, msg);
		free(msg);
		seekdir(info->handle, curPos);
		JS_SetPendingException(cx, exception);
		return JS_FALSE;
	}
	else {
		seekdir(info->handle, curPos);
		*rval = INT_TO_JSVAL(length);
		return JS_TRUE;
	}
}

/*This function serves multiple purposes depending on the number of arguments passed in (argc)*/
static JSBool DirectoryStream_read(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval){
	DirectoryStreamInformation *info=(DirectoryStreamInformation*)JS_GetPrivate(cx, obj);
	struct dirent *entry;

	if (argc == 0){
		errno = 0;
		entry = readdir(info->handle);
		if (entry != NULL){
			JSString *fileName = NULL;
			int len = strlen(entry->d_name);
			char *copy = JS_malloc(cx, len+1);
			if (copy == NULL){
				JS_SetPendingException(cx, printf_exception(cx, "Unable to read directory entry."));
				return JS_FALSE;
			}

			memset(copy, 0, len+1);
			strncpy(copy, entry->d_name, len);
			fileName = JS_NewString(cx, copy, len);
			*rval = STRING_TO_JSVAL(fileName);
			return JS_TRUE;
		}
		else if (errno == 0){
			*rval = BOOLEAN_TO_JSVAL(JS_FALSE);
			info->streamInfo.eofFlag=JS_TRUE;
			return JS_TRUE;
		}
		else {
			char *msg = errnostr(errno);
			jsval exception = printf_exception(cx, "Unable to read directory stream.  (%d) %s", errno, msg);
			free(msg);
			JS_SetPendingException(cx, exception);
			return JS_FALSE;
		}
	}
	else {
		int numberToRead = 0;

		if (JS_ValueToInt32(cx, argv[0], &numberToRead) == JS_TRUE){
			jsval *fileNameArray = NULL;
			int totalNamesRead = 0;

			errno=0;
			while ((totalNamesRead < numberToRead) && (entry = readdir(info->handle)) != NULL){
				JSString *fileName = NULL;
				int len = strlen(entry->d_name);
				char *copy = JS_malloc(cx, len+1);
				jsval *reallocPtr = NULL;
				if (copy == NULL){
					JS_SetPendingException(cx, printf_exception(cx, "Unable to read directory entry."));
					return JS_FALSE;
				}

				memset(copy, 0, len+1);
				strncpy(copy, entry->d_name, len);
				fileName = JS_NewString(cx, copy, len);
				totalNamesRead++;


				reallocPtr = JS_realloc(cx, fileNameArray, sizeof(*fileNameArray)*totalNamesRead);
				if (reallocPtr == NULL){
					JS_SetPendingException(cx, printf_exception(cx, "Unable to read directory entry."));
					return JS_FALSE;
				}
				else {
					fileNameArray = reallocPtr;
					fileNameArray[totalNamesRead-1] = STRING_TO_JSVAL(fileName);
				}
				errno = 0;
			}

			if (totalNamesRead < numberToRead){
				if (errno == 0){
					if (totalNamesRead == 0){
						*rval = BOOLEAN_TO_JSVAL(JS_FALSE);
						info->streamInfo.eofFlag=JS_TRUE;
						return JS_TRUE;
					}
					else {
						*rval = OBJECT_TO_JSVAL(JS_NewArrayObject(cx, totalNamesRead, fileNameArray));
						return JS_TRUE;
					}
				}
				else {
					char *msg = errnostr(errno);
					jsval exception = printf_exception(cx, "Unable to read directory stream.  (%d) %s", errno, msg);
					free(msg);
					JS_SetPendingException(cx, exception);
					return JS_FALSE;
				}
			}
			else {
				*rval = OBJECT_TO_JSVAL(JS_NewArrayObject(cx, totalNamesRead, fileNameArray));
				return JS_TRUE;				
			}
		}
		else {
			JS_SetPendingException(cx, printf_exception(cx, "Unable to understand length parameter."));
			return JS_FALSE;
		}
	}
}



static JSBool directorystream_open(DirectoryStreamInformation *info){
	if (info->handle == NULL && info->path != NULL){
		info->handle = opendir(info->path);
		info->streamInfo.eofFlag = JS_FALSE;
		if (info->handle == NULL){
			return JS_FALSE;
		}
		info->streamInfo.handle = dirfd(info->handle);
		return JS_TRUE;
	}
	else {
		return JS_FALSE;
	}
}

static JSBool directorystream_close(DirectoryStreamInformation *info){
	info->streamInfo.eofFlag = JS_FALSE;
	if (info->handle != NULL){
		if (closedir(info->handle) == 0){
			info->streamInfo.handle = -1;
			info->handle = NULL;
			return JS_TRUE;
		}
		return JS_FALSE;
	}
	else {
		return JS_TRUE;
	}
}

