#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <jsapi.h>
#include <sys/types.h>
#include <sys/stat.h>

/* Function Prototypes */
JSBool js_engine_execute_file(JSContext *ctx, const char *file);
static void js_error_handler(JSContext *ctx, const char *msg, JSErrorReport *er);

/* Console object functions */
JSBool Console_write(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval);
JSBool Console_writeln(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval);
JSBool Console_writeError(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval);
JSBool Console_writeErrorln(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval);
JSBool Console_read(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval);
JSBool Console_readInt(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval);
JSBool Console_readFloat(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval);
JSBool Console_readChar(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval);

JSClass js_global_object_class = {
  "System",
  0,
  JS_PropertyStub,
  JS_PropertyStub,
  JS_PropertyStub,
  JS_PropertyStub,
  JS_EnumerateStub,
  JS_ResolveStub,
  JS_ConvertStub,
  JS_FinalizeStub,
  JSCLASS_NO_OPTIONAL_MEMBERS
};

JSClass js_Console_class = {
  "Console",
  0,
  JS_PropertyStub,
  JS_PropertyStub,
  JS_PropertyStub,
  JS_PropertyStub,
  JS_EnumerateStub,
  JS_ResolveStub,
  JS_ConvertStub,
  JS_FinalizeStub,
  JSCLASS_NO_OPTIONAL_MEMBERS
};

JSFunctionSpec Console_methods[] = {
  {"write", Console_write, 0, 0, 0},
  {"writeln", Console_writeln, 0, 0, 0},
  {"writeError", Console_writeError, 0, 0, 0},
  {"writeErrorln", Console_writeErrorln, 0, 0, 0},
  {"read", Console_read, 0, 0, 0},
  {"readInt", Console_readInt, 0, 0, 0},
  {"readFloat", Console_readFloat, 0, 0, 0},
  {"readChar", Console_readChar, 0, 0, 0},
  {NULL},
};

int main(int argc, char **argv){
    char *fileToRun=NULL;
    JSRuntime *rt=NULL;
    JSContext *cx=NULL;
    JSObject *obj=NULL;

    if (argc > 1){
      fileToRun = argv[1];
    }
    
    rt = JS_NewRuntime(8L*1024L);
    if (!rt){
      printf("Failed to initialize JS Runtime.\n");
      return 1;
    }

    cx = JS_NewContext(rt, 8L*1024L*1024L);
    if (!cx){
      printf("Failed to initialize JS Context.\n");
      JS_DestroyRuntime(rt);
      return 1;
    }
    
    JS_SetErrorReporter(cx, js_error_handler);
    
    obj = JS_NewObject(cx, &js_global_object_class, NULL, NULL);
    if (!obj){
      printf("Failed to create global object.\n");
      JS_DestroyContext(cx);
      JS_DestroyRuntime(rt);
      return 1;
    }

    JS_InitStandardClasses(cx, obj);
    
    /* Define our new object and make it a property of the global object. */
    obj = JS_DefineObject(cx, obj, js_Console_class.name, &js_Console_class, NULL, JSPROP_PERMANENT|JSPROP_READONLY|JSPROP_ENUMERATE);
    if (!obj){
      printf("Failed to create Console object.\n");
      JS_DestroyContext(cx);
      JS_DestroyRuntime(rt);
      return 1;
    }
    
    /* Define the console object's methods. */
    JS_DefineFunctions(cx, obj, Console_methods);
    
    if (fileToRun){
      if (js_engine_execute_file(cx, fileToRun)){
        printf("File %s has been successfully executed.\n", fileToRun);
      }
      else {
        printf("Failed to executed %s.\n", fileToRun);
      }
    }
    else {
      printf("Usage: %s file\n", argv[0]);
    }


    JS_DestroyContext(cx);
    JS_DestroyRuntime(rt);
    return 0;
}


JSBool js_engine_execute_file(JSContext *ctx, const char *file){
    JSScript *script;
    jsval returnValue;
    JSBool returnVal;
    JSObject *global = JS_GetGlobalObject(ctx);
    struct stat statinfo;

    if (file == NULL){
        return JS_FALSE;
    }

    if (stat(file, &statinfo) == -1){
        return JS_FALSE;
    }

    if (!S_ISREG(statinfo.st_mode)){
        return JS_FALSE;
    }

    script = JS_CompileFile(ctx, global, file);

    if (script == NULL){
        return JS_FALSE;
    }

    returnVal = JS_ExecuteScript(ctx, global, script, &returnValue);
    JS_DestroyScript(ctx, script);
    return returnVal;
}



static void js_error_handler(JSContext *ctx, const char *msg, JSErrorReport *er){
    char *pointer=NULL;
    char *line=NULL;
    int len;

    if (er->linebuf != NULL){
        len = er->tokenptr - er->linebuf + 1;
        pointer = malloc(len);
        memset(pointer, '-', len);
        pointer[len-1]='\0';
        pointer[len-2]='^';

        len = strlen(er->linebuf);
        line = malloc(len);
        strncpy(line, er->linebuf, len);
    }
    else {
        len=0;
        pointer = malloc(1);
        line = malloc(1);
        pointer[0]='\0';
        line[0] = '\0';
    }

    while (len > 0 && (line[len-1] == '\r' || line[len-1] == '\n')){ line[len-1]='\0'; len--; }

    printf("JS Error: %s\nFile: %s:%u\n", msg, er->filename, er->lineno);
    printf("%s\n%s\n", line, pointer);

    free(pointer);
    free(line);
}

/*****************************************/
/****** Begin Console object code. *******/
/*****************************************/

JSBool Console_write(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval){
  if (argc < 1){
    /* No arguments passed in, so do nothing. */
    /* We still want to return JS_TRUE though, other wise an exception will be thrown by the engine. */
    *rval = INT_TO_JSVAL(0); /* Send back a return value of 0. */
    return JS_TRUE;
  }
  else {
    /* "Hidden" feature.  The function will accept multiple arguments.  Each one is considered to be a string 
       and will be written to the console accordingly.  This makes it possible to avoid concatenation in the code 
       by using the Console.write function as so:
       Console.write("Welcome to my application", name, ". I hope you enjoy the ride!");
       */
    int i;
    size_t amountWritten=0;
    for (i=0; i<argc; i++){
      JSString *val = JS_ValueToString(cx, argv[i]); /* Convert the value to a javascript string. */
      char *str = JS_GetStringBytes(val); /* Then convert it to a C-style string. */
      size_t length = JS_GetStringLength(val); /* Get the length of the string, # of chars. */
      amountWritten = fwrite(str, sizeof(*str), length, stdout); /* write the string to stdout. */
    }
    *rval = INT_TO_JSVAL(amountWritten); /* Set the return value to be the number of bytes/chars written */
    return JS_TRUE;
  }
}

JSBool Console_writeln(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval){
  /* Just call the write function and then write out a \n */
  Console_write(cx, obj, argc, argv, rval);
  fwrite("\n", sizeof("\n"), 1, stdout);
  *rval = INT_TO_JSVAL(JSVAL_TO_INT(*rval)+1);
  return JS_TRUE;
}


JSBool Console_writeError(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval){
  if (argc < 1){
    *rval = INT_TO_JSVAL(0);
    return JS_TRUE;
  }
  else {
    int i;
    size_t amountWritten=0;
    for (i=0; i<argc; i++){
      JSString *val = JS_ValueToString(cx, argv[i]);
      char *str = JS_GetStringBytes(val); 
          size_t length = JS_GetStringLength(val); 
          amountWritten = fwrite(str, sizeof(*str), length, stderr); 
    }
    *rval = INT_TO_JSVAL(amountWritten);
    return JS_TRUE;
  }
}

JSBool Console_writeErrorln(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval){
  /* Just call the writeError function and then write out a \n */
  Console_writeError(cx, obj, argc, argv, rval);
  fwrite("\n", sizeof("\n"), 1, stderr);
  *rval = INT_TO_JSVAL(JSVAL_TO_INT(*rval)+1);
  return JS_TRUE;
}

JSBool Console_read(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval){
  if (argc < 1){
    /* Read a full line off the console.  */
    char *finalBuff = NULL;
    char *newBuff = NULL;
    char tmpBuff[100];
    size_t totalLen = 0;
    int len = 0;
    do {
      memset(tmpBuff, 0, sizeof(tmpBuff));
      fgets(tmpBuff, sizeof(tmpBuff), stdin);
      len = strlen(tmpBuff);
      

      if (len > 0){
        char lastChar = tmpBuff[len-1];
        
        newBuff = JS_realloc(cx, finalBuff, totalLen+len+1);      
        if (newBuff == NULL){
          JS_free(cx, finalBuff);
          *rval = BOOLEAN_TO_JSVAL(JS_FALSE);
          return JS_TRUE;
        }
        else {
          finalBuff = newBuff;
          memset(finalBuff+totalLen, 0, len);
        }
        
        strncat(finalBuff+totalLen, tmpBuff, len);
        totalLen += len;
        if (lastChar == '\n' || lastChar == '\r' || feof(stdin)){
          JSString *str = JS_NewString(cx, finalBuff, totalLen);
          *rval = STRING_TO_JSVAL(str);
          return JS_TRUE;
        }
      }
      else if (feof(stdin) && totalLen == 0){
        *rval = BOOLEAN_TO_JSVAL(JS_FALSE);
        return JS_TRUE;
      }
      else if (totalLen > 0){
        JSString *str = JS_NewString(cx, finalBuff, totalLen);
        *rval = STRING_TO_JSVAL(str);
        return JS_TRUE;
      }
    } while (1);
  }
  else {
    int32 maxlen=0;
    if (JS_ValueToInt32(cx, argv[0], &maxlen) == JS_TRUE){
      JSString *ret=NULL;
      size_t amountRead = 0;
      char *newPointer = NULL;
      char *cstring = JS_malloc(cx, sizeof(*cstring)*(maxlen+1));
      if (cstring == NULL){
        *rval = BOOLEAN_TO_JSVAL(JS_FALSE);
        return JS_TRUE;
      }
      memset(cstring, 0, sizeof(*cstring)*(maxlen+1));
      amountRead = fread(cstring, sizeof(*cstring), maxlen, stdin);
      newPointer = JS_realloc(cx, cstring, amountRead);
      if (newPointer == NULL){
        JS_free(cx, cstring);
        *rval = BOOLEAN_TO_JSVAL(JS_FALSE);
        return JS_TRUE;
      }
      else {
        cstring = newPointer;
      }
      
      ret = JS_NewString(cx, cstring, sizeof(*cstring)*amountRead);
      *rval = STRING_TO_JSVAL(ret);
      return JS_TRUE;
    }
    else {
      *rval = BOOLEAN_TO_JSVAL(JS_FALSE);
      return JS_TRUE;
    }
  }
}

JSBool Console_readInt(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval){
  int32 readinteger = 0;
  if (fscanf(stdin, "%d", &readinteger) == 1){
    if (JS_NewNumberValue(cx, readinteger, rval) == JS_TRUE){
      return JS_TRUE;
    }
  }
  *rval = BOOLEAN_TO_JSVAL(JS_FALSE);
  return JS_TRUE;
}

JSBool Console_readFloat(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval){
  jsdouble readfloat = 0;
  if (fscanf(stdin, "%lf", &readfloat) == 1){
    if (JS_NewDoubleValue(cx, readfloat, rval) == JS_TRUE){
      return JS_TRUE;
    }
  }
 *rval = BOOLEAN_TO_JSVAL(JS_FALSE);
  return JS_TRUE;
}


JSBool Console_readChar(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval){
  JSString *str=NULL;
  char ch, *ptr=NULL;
  if (feof(stdin)){
    *rval = BOOLEAN_TO_JSVAL(JS_FALSE);
    return JS_TRUE;
  }
  
  ch=fgetc(stdin);
  if (ch == EOF){
    *rval = BOOLEAN_TO_JSVAL(JS_FALSE);
    return JS_TRUE;
  }
  
  ptr = JS_malloc(cx, sizeof(char)*1);
  if (ptr == NULL){
    *rval = BOOLEAN_TO_JSVAL(JS_FALSE);
    return JS_TRUE;
  }
  
  *ptr = ch;
  str = JS_NewString(cx, ptr, sizeof(char)*1);
  *rval = STRING_TO_JSVAL(str);
  return JS_TRUE;
}

