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

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},
  {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;
  }
}


