/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80:
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#ifdef HAVE_IO_H
#  include <io.h> /* for isatty() */
#endif
#ifdef HAVE_UNISTD_H
#  include <unistd.h> /* for isatty() */
#endif

#include "jsapi.h"
#include "js/CharacterEncoding.h"
#include "js/CompilationAndEvaluation.h"  // JS::Compile{,Utf8File}
#include "js/PropertyAndElement.h"  // JS_DefineFunctions, JS_DefineProperty, JS_GetProperty
#include "js/PropertySpec.h"
#include "js/RealmOptions.h"
#include "js/SourceText.h"  // JS::Source{Ownership,Text}

#include "xpcpublic.h"

#include "XPCShellEnvironment.h"

#include "mozilla/Utf8.h"  // mozilla::Utf8Unit
#include "mozilla/dom/AutoEntryScript.h"
#include "mozilla/dom/ScriptSettings.h"

#include "nsIPrincipal.h"
#include "nsIScriptSecurityManager.h"
#include "nsIXPConnect.h"
#include "nsServiceManagerUtils.h"

#include "nsJSUtils.h"

#include "SystemGlobal.h"

#include "TestShellChild.h"

using mozilla::dom::AutoEntryScript;
using mozilla::dom::AutoJSAPI;
using mozilla::ipc::XPCShellEnvironment;
using namespace JS;

namespace {

static const char kDefaultRuntimeScriptFilename[] = "xpcshell.js";

inline XPCShellEnvironment* Environment(JS::Handle<JSObject*> global) {
  AutoJSAPI jsapi;
  if (!jsapi.Init(global)) {
    return nullptr;
  }
  JSContext* cx = jsapi.cx();
  Rooted<Value> v(cx);
  if (!JS_GetProperty(cx, global, "__XPCShellEnvironment", &v) ||
      !v.get().isDouble()) {
    return nullptr;
  }
  return static_cast<XPCShellEnvironment*>(v.get().toPrivate());
}

static bool Print(JSContext* cx, unsigned argc, JS::Value* vp) {
  JS::CallArgs args = JS::CallArgsFromVp(argc, vp);

  for (unsigned i = 0; i < args.length(); i++) {
    JS::Rooted<JSString*> str(cx, JS::ToString(cx, args[i]));
    if (!str) return false;
    JS::UniqueChars bytes = JS_EncodeStringToUTF8(cx, str);
    if (!bytes) return false;
    fprintf(stdout, "%s%s", i ? " " : "", bytes.get());
    fflush(stdout);
  }
  fputc('\n', stdout);
  args.rval().setUndefined();
  return true;
}

static bool GetLine(char* bufp, FILE* file, const char* prompt) {
  char line[256];
  fputs(prompt, stdout);
  fflush(stdout);
  if (!fgets(line, sizeof line, file)) return false;
  strcpy(bufp, line);
  return true;
}

static bool Dump(JSContext* cx, unsigned argc, JS::Value* vp) {
  JS::CallArgs args = JS::CallArgsFromVp(argc, vp);

  if (!args.length()) return true;

  JS::Rooted<JSString*> str(cx, JS::ToString(cx, args[0]));
  if (!str) return false;
  JS::UniqueChars bytes = JS_EncodeStringToUTF8(cx, str);
  if (!bytes) return false;

  fputs(bytes.get(), stdout);
  fflush(stdout);
  return true;
}

static bool Load(JSContext* cx, unsigned argc, JS::Value* vp) {
  JS::CallArgs args = JS::CallArgsFromVp(argc, vp);

  JS::RootedObject thisObject(cx);
  if (!args.computeThis(cx, &thisObject)) return false;
  if (!JS_IsGlobalObject(thisObject)) {
    JS_ReportErrorASCII(cx, "Trying to load() into a non-global object");
    return false;
  }

  for (unsigned i = 0; i < args.length(); i++) {
    JS::Rooted<JSString*> str(cx, JS::ToString(cx, args[i]));
    if (!str) {
      return false;
    }
    JS::UniqueChars filename = JS_EncodeStringToUTF8(cx, str);
    if (!filename) {
      return false;
    }

    JS::CompileOptions options(cx);
    JS::Rooted<JSScript*> script(
        cx, JS::CompileUtf8Path(cx, options, filename.get()));
    if (!script) {
      return false;
    }

    if (!JS_ExecuteScript(cx, script)) {
      return false;
    }
  }
  args.rval().setUndefined();
  return true;
}

static bool Quit(JSContext* cx, unsigned argc, JS::Value* vp) {
  Rooted<JSObject*> global(cx, JS::CurrentGlobalOrNull(cx));
  XPCShellEnvironment* env = Environment(global);
  env->SetIsQuitting();

  return false;
}

static bool DumpXPC(JSContext* cx, unsigned argc, JS::Value* vp) {
  JS::CallArgs args = CallArgsFromVp(argc, vp);

  uint16_t depth = 2;
  if (args.length() > 0) {
    if (!JS::ToUint16(cx, args[0], &depth)) return false;
  }

  nsCOMPtr<nsIXPConnect> xpc = nsIXPConnect::XPConnect();
  if (xpc) xpc->DebugDump(int16_t(depth));
  args.rval().setUndefined();
  return true;
}

static bool GC(JSContext* cx, unsigned argc, JS::Value* vp) {
  JS::CallArgs args = JS::CallArgsFromVp(argc, vp);

  JS_GC(cx);

  args.rval().setUndefined();
  return true;
}

#ifdef JS_GC_ZEAL
static bool GCZeal(JSContext* cx, unsigned argc, JS::Value* vp) {
  CallArgs args = CallArgsFromVp(argc, vp);

  uint32_t zeal;
  if (!ToUint32(cx, args.get(0), &zeal)) return false;

  JS::SetGCZeal(cx, uint8_t(zeal), JS::ShellDefaultGCZealFrequency);
  return true;
}
#endif

#ifdef ANDROID
static bool ChangeTestShellDir(JSContext* cx, unsigned argc, Value* vp) {
  // This method should only be used by testing/xpcshell/head.js to change to
  // the correct directory on Android Remote XPCShell tests.
  //
  // TODO: Bug 1801725 - Find a more ergonomic way to do this than exposing
  // identical methods in XPCShellEnvironment and XPCShellImpl to chdir on
  // android for Remote XPCShell tests on Android.
  CallArgs args = CallArgsFromVp(argc, vp);

  if (args.length() != 1) {
    JS_ReportErrorASCII(cx, "changeTestShellDir() takes one argument");
    return false;
  }

  nsAutoJSCString path;
  if (!path.init(cx, args[0])) {
    JS_ReportErrorASCII(
        cx, "changeTestShellDir(): could not convert argument 1 to string");
    return false;
  }

  if (chdir(path.get())) {
    JS_ReportErrorASCII(cx, "changeTestShellDir(): could not change directory");
    return false;
  }

  return true;
}
#endif

const JSFunctionSpec gGlobalFunctions[] = {
    JS_FN("print", Print, 0, 0),
    JS_FN("load", Load, 1, 0),
    JS_FN("quit", Quit, 0, 0),
    JS_FN("dumpXPC", DumpXPC, 1, 0),
    JS_FN("dump", Dump, 1, 0),
    JS_FN("gc", GC, 0, 0),
#ifdef JS_GC_ZEAL
    JS_FN("gczeal", GCZeal, 1, 0),
#endif
#ifdef ANDROID
    JS_FN("changeTestShellDir", ChangeTestShellDir, 1, 0),
#endif
    JS_FS_END};

typedef enum JSShellErrNum {
#define MSG_DEF(name, number, count, exception, format) name = number,
#include "jsshell.msg"
#undef MSG_DEF
  JSShellErr_Limit
#undef MSGDEF
} JSShellErrNum;

} /* anonymous namespace */

void XPCShellEnvironment::ProcessFile(JSContext* cx, const char* filename,
                                      FILE* file, bool forceTTY) {
  XPCShellEnvironment* env = this;

  JS::Rooted<JS::Value> result(cx);
  int lineno, startline;
  bool ok, hitEOF;
  char *bufp, buffer[4096];
  JSString* str;

  JS::Rooted<JSObject*> global(cx, JS::CurrentGlobalOrNull(cx));
  MOZ_ASSERT(global);

  if (forceTTY) {
    file = stdin;
  } else if (!isatty(fileno(file))) {
    /*
     * It's not interactive - just execute it.
     *
     * Support the UNIX #! shell hack; gobble the first line if it starts
     * with '#'.
     */
    int ch = fgetc(file);
    if (ch == '#') {
      while ((ch = fgetc(file)) != EOF) {
        if (ch == '\n' || ch == '\r') break;
      }
    }
    ungetc(ch, file);

    JS::CompileOptions options(cx);
    options.setFileAndLine(filename, 1);

    JS::Rooted<JSScript*> script(cx, JS::CompileUtf8File(cx, options, file));
    if (script) {
      (void)JS_ExecuteScript(cx, script, &result);
    }

    return;
  }

  /* It's an interactive filehandle; drop into read-eval-print loop. */
  lineno = 1;
  hitEOF = false;
  do {
    bufp = buffer;
    *bufp = '\0';

    /*
     * Accumulate lines until we get a 'compilable unit' - one that either
     * generates an error (before running out of source) or that compiles
     * cleanly.  This should be whenever we get a complete statement that
     * coincides with the end of a line.
     */
    startline = lineno;
    do {
      if (!GetLine(bufp, file, startline == lineno ? "js> " : "")) {
        hitEOF = true;
        break;
      }
      bufp += strlen(bufp);
      lineno++;
    } while (
        !JS_Utf8BufferIsCompilableUnit(cx, global, buffer, strlen(buffer)));

    /* Clear any pending exception from previous failed compiles.  */
    JS_ClearPendingException(cx);

    JS::CompileOptions options(cx);
    options.setFileAndLine("typein", startline);

    JS::SourceText<mozilla::Utf8Unit> srcBuf;
    JS::Rooted<JSScript*> script(cx);

    if (srcBuf.init(cx, buffer, strlen(buffer),
                    JS::SourceOwnership::Borrowed) &&
        (script = JS::Compile(cx, options, srcBuf))) {
      ok = JS_ExecuteScript(cx, script, &result);
      if (ok && !result.isUndefined()) {
        /* Suppress warnings from JS::ToString(). */
        JS::AutoSuppressWarningReporter suppressWarnings(cx);
        str = JS::ToString(cx, result);
        JS::UniqueChars bytes;
        if (str) bytes = JS_EncodeStringToLatin1(cx, str);

        if (!!bytes)
          fprintf(stdout, "%s\n", bytes.get());
        else
          ok = false;
      }
    }
  } while (!hitEOF && !env->IsQuitting());

  fprintf(stdout, "\n");
}

// static
XPCShellEnvironment* XPCShellEnvironment::CreateEnvironment() {
  auto* env = new XPCShellEnvironment();
  if (env && !env->Init()) {
    delete env;
    env = nullptr;
  }
  return env;
}

XPCShellEnvironment::XPCShellEnvironment() : mQuitting(false) {}

XPCShellEnvironment::~XPCShellEnvironment() {
  if (GetGlobalObject()) {
    AutoJSAPI jsapi;
    if (!jsapi.Init(GetGlobalObject())) {
      return;
    }
    JS_SetAllNonReservedSlotsToUndefined(mGlobalHolder);
    mGlobalHolder.reset();

    JS_GC(jsapi.cx());
  }
}

bool XPCShellEnvironment::Init() {
  nsresult rv;

  // unbuffer stdout so that output is in the correct order; note that stderr
  // is unbuffered by default
  setbuf(stdout, 0);

  AutoSafeJSContext cx;

  mGlobalHolder.init(cx);

  nsCOMPtr<nsIPrincipal> principal;
  nsCOMPtr<nsIScriptSecurityManager> securityManager =
      do_GetService(NS_SCRIPTSECURITYMANAGER_CONTRACTID, &rv);
  if (NS_SUCCEEDED(rv) && securityManager) {
    rv = securityManager->GetSystemPrincipal(getter_AddRefs(principal));
    if (NS_FAILED(rv)) {
      fprintf(stderr,
              "+++ Failed to obtain SystemPrincipal from ScriptSecurityManager "
              "service.\n");
    }
  } else {
    fprintf(stderr,
            "+++ Failed to get ScriptSecurityManager service, running without "
            "principals");
  }

  auto systemGlobal = MakeRefPtr<SystemGlobal>();

  JS::RealmOptions options;
  options.creationOptions().setNewCompartmentInSystemZone();
  xpc::SetPrefableRealmOptions(options);

  JS::Rooted<JSObject*> globalObj(cx);
  rv = xpc::InitClassesWithNewWrappedGlobal(
      cx, static_cast<nsIGlobalObject*>(systemGlobal), principal, 0, options,
      &globalObj);
  if (NS_FAILED(rv)) {
    NS_ERROR("InitClassesWithNewWrappedGlobal failed!");
    return false;
  }

  if (!globalObj) {
    NS_ERROR("Failed to get global JSObject!");
    return false;
  }
  JSAutoRealm ar(cx, globalObj);

  systemGlobal->SetGlobalObject(globalObj);

  JS::Rooted<Value> privateVal(cx, PrivateValue(this));
  if (!JS_DefineProperty(cx, globalObj, "__XPCShellEnvironment", privateVal,
                         JSPROP_READONLY | JSPROP_PERMANENT) ||
      !JS_DefineFunctions(cx, globalObj, gGlobalFunctions)) {
    NS_ERROR("JS_DefineFunctions failed!");
    return false;
  }

  mGlobalHolder = globalObj;

  FILE* runtimeScriptFile = fopen(kDefaultRuntimeScriptFilename, "r");
  if (runtimeScriptFile) {
    fprintf(stdout, "[loading '%s'...]\n", kDefaultRuntimeScriptFilename);
    ProcessFile(cx, kDefaultRuntimeScriptFilename, runtimeScriptFile, false);
    fclose(runtimeScriptFile);
  }

  return true;
}

bool XPCShellEnvironment::EvaluateString(const nsAString& aString,
                                         nsString* aResult) {
  AutoEntryScript aes(GetGlobalObject(),
                      "ipc XPCShellEnvironment::EvaluateString");
  JSContext* cx = aes.cx();

  JS::CompileOptions options(cx);
  options.setFileAndLine("typein", 0);

  JS::SourceText<char16_t> srcBuf;
  if (!srcBuf.init(cx, aString.BeginReading(), aString.Length(),
                   JS::SourceOwnership::Borrowed)) {
    return false;
  }

  JS::Rooted<JSScript*> script(cx, JS::Compile(cx, options, srcBuf));
  if (!script) {
    return false;
  }

  if (aResult) {
    aResult->Truncate();
  }

  JS::Rooted<JS::Value> result(cx);
  bool ok = JS_ExecuteScript(cx, script, &result);
  if (ok && !result.isUndefined()) {
    /* Suppress warnings from JS::ToString(). */
    JS::AutoSuppressWarningReporter suppressWarnings(cx);
    JSString* str = JS::ToString(cx, result);
    nsAutoJSString autoStr;
    if (str) autoStr.init(cx, str);

    if (!autoStr.IsEmpty() && aResult) {
      aResult->Assign(autoStr);
    }
  }

  return true;
}
