/* 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/. */

#include "mozilla/RefPtr.h"     // RefPtr
#include "mozilla/ScopeExit.h"  // MakeScopeExit
#include "mozilla/Utf8.h"       // mozilla::Utf8Unit

#include "frontend/CompilationStencil.h"  // JS::Stencil, frontend::CompilationStencil
#include "js/CompileOptions.h"  // JS::CompileOptions, JS::InstantiateOptions
#include "js/experimental/CompileScript.h"  // JS::NewFrontendContext
#include "js/SourceText.h"                  // JS::Source{Ownership,Text}
#include "jsapi-tests/tests.h"
#include "vm/ErrorReporting.h"
#include "vm/JSONPrinter.h"  // js::JSONPrinter

using namespace JS;

template <typename T>
static void dump(const T& data) {
  js::Fprinter printer(stderr);
  js::JSONPrinter json(printer, true);

  data->dump(json);
  printer.put("\n");
}

BEGIN_TEST(testCompileScript) {
  CHECK(testCompile());
  CHECK(testNonsyntacticCompile());
  CHECK(testCompileModule());
  CHECK(testPrepareForInstantiate());

  return true;
}

bool testCompile() {
  static constexpr std::string_view src = "42\n";
  static constexpr std::u16string_view src_16 = u"42\n";

  static_assert(src.length() == src_16.length(),
                "Source buffers must be same length");

  JS::CompileOptions options(cx);

  JS::FrontendContext* fc = JS::NewFrontendContext();
  CHECK(fc);
  auto destroyFc =
      mozilla::MakeScopeExit([fc] { JS::DestroyFrontendContext(fc); });

  {  // 16-bit characters
    JS::SourceText<char16_t> buf16;
    CHECK(buf16.init(cx, src_16.data(), src_16.length(),
                     JS::SourceOwnership::Borrowed));

    RefPtr<JS::Stencil> stencil =
        CompileGlobalScriptToStencil(fc, options, buf16);
    CHECK(stencil);
    RefPtr<const js::frontend::CompilationStencil> initial =
        stencil->getInitial();
    CHECK(initial->scriptExtra.size() == 1);
    CHECK(initial->scriptExtra[0].extent.sourceStart == 0);
    CHECK(initial->scriptExtra[0].extent.sourceEnd == 3);
    CHECK(initial->scriptData.size() == 1);
    CHECK(initial->scriptData[0].hasSharedData());  // has generated bytecode
    CHECK(initial->scriptData[0].gcThingsLength == 1);
  }

  {  // 8-bit characters
    JS::SourceText<mozilla::Utf8Unit> buf8;
    CHECK(
        buf8.init(cx, src.data(), src.length(), JS::SourceOwnership::Borrowed));

    RefPtr<JS::Stencil> stencil =
        CompileGlobalScriptToStencil(fc, options, buf8);
    CHECK(stencil);
    RefPtr<const js::frontend::CompilationStencil> initial =
        stencil->getInitial();
    CHECK(initial->scriptExtra.size() == 1);
    CHECK(initial->scriptExtra[0].extent.sourceStart == 0);
    CHECK(initial->scriptExtra[0].extent.sourceEnd == 3);
    CHECK(initial->scriptData.size() == 1);
    CHECK(initial->scriptData[0].hasSharedData());  // has generated bytecode
    CHECK(initial->scriptData[0].gcThingsLength == 1);
  }

  {  // propagates failures
    static constexpr std::string_view badSrc = "{ a: 1, b: 3\n";
    JS::SourceText<mozilla::Utf8Unit> srcBuf;
    CHECK(srcBuf.init(cx, badSrc.data(), badSrc.length(),
                      JS::SourceOwnership::Borrowed));

    RefPtr<JS::Stencil> stencil =
        CompileGlobalScriptToStencil(fc, options, srcBuf);
    CHECK(!stencil);
    CHECK(fc->maybeError().isSome());
    const js::CompileError& error = fc->maybeError().ref();
    CHECK(JSEXN_SYNTAXERR == error.exnType);
    CHECK(error.lineno == 1);
    CHECK(error.column.oneOriginValue() == 10);
  }

  return true;
}

bool testNonsyntacticCompile() {
  const char* chars =
      "function f() { return x; }"
      "f();";

  JS::SourceText<mozilla::Utf8Unit> srcBuf;
  CHECK(srcBuf.init(cx, chars, strlen(chars), JS::SourceOwnership::Borrowed));

  JS::CompileOptions options(cx);
  options.setNonSyntacticScope(true);

  JS::FrontendContext* fc = JS::NewFrontendContext();
  CHECK(fc);
  auto destroyFc =
      mozilla::MakeScopeExit([fc] { JS::DestroyFrontendContext(fc); });

  RefPtr<JS::Stencil> stencil =
      CompileGlobalScriptToStencil(fc, options, srcBuf);
  CHECK(stencil);

  JS::InstantiateOptions instantiateOptions(options);
  JS::RootedScript script(
      cx, JS::InstantiateGlobalStencil(cx, instantiateOptions, stencil));
  CHECK(script);
  CHECK(script->hasNonSyntacticScope());

  return true;
}

bool testCompileModule() {
  static constexpr std::string_view src = "import 'a'; 42\n";
  static constexpr std::u16string_view src_16 = u"import 'a'; 42\n";

  static_assert(src.length() == src_16.length(),
                "Source buffers must be same length");

  JS::CompileOptions options(cx);

  JS::FrontendContext* fc = JS::NewFrontendContext();
  CHECK(fc);
  auto destroyFc =
      mozilla::MakeScopeExit([fc] { JS::DestroyFrontendContext(fc); });

  {  // 16-bit characters
    JS::SourceText<char16_t> buf16;
    CHECK(buf16.init(cx, src_16.data(), src_16.length(),
                     JS::SourceOwnership::Borrowed));

    RefPtr<JS::Stencil> stencil =
        CompileModuleScriptToStencil(fc, options, buf16);
    CHECK(stencil);
    RefPtr<const js::frontend::CompilationStencil> initial =
        stencil->getInitial();
    CHECK(initial->isModule());
    CHECK(initial->scriptExtra.size() == 1);
    CHECK(initial->scriptExtra[0].extent.sourceStart == 0);
    CHECK(initial->scriptExtra[0].extent.sourceEnd == 15);
    CHECK(initial->scriptData.size() == 1);
    CHECK(initial->scriptData[0].hasSharedData());  // has generated bytecode
    CHECK(initial->scriptData[0].gcThingsLength == 1);
  }

  {  // 8-bit characters
    JS::SourceText<mozilla::Utf8Unit> buf8;
    CHECK(
        buf8.init(cx, src.data(), src.length(), JS::SourceOwnership::Borrowed));

    RefPtr<JS::Stencil> stencil =
        CompileModuleScriptToStencil(fc, options, buf8);
    CHECK(stencil);
    RefPtr<const js::frontend::CompilationStencil> initial =
        stencil->getInitial();
    CHECK(initial->scriptExtra.size() == 1);
    CHECK(initial->scriptExtra[0].extent.sourceStart == 0);
    CHECK(initial->scriptExtra[0].extent.sourceEnd == 15);
    CHECK(initial->scriptData.size() == 1);
    CHECK(initial->scriptData[0].hasSharedData());  // has generated bytecode
    CHECK(initial->scriptData[0].gcThingsLength == 1);
  }

  {  // propagates failures
    static constexpr std::string_view badSrc = "{ a: 1, b: 3\n";
    JS::SourceText<mozilla::Utf8Unit> srcBuf;
    CHECK(srcBuf.init(cx, badSrc.data(), badSrc.length(),
                      JS::SourceOwnership::Borrowed));

    RefPtr<JS::Stencil> stencil =
        CompileModuleScriptToStencil(fc, options, srcBuf);
    CHECK(!stencil);
    CHECK(fc->maybeError().isSome());
    const js::CompileError& error = fc->maybeError().ref();
    CHECK(JSEXN_SYNTAXERR == error.exnType);
    CHECK(error.lineno == 1);
    CHECK(error.column.oneOriginValue() == 10);
  }

  return true;
}

bool testPrepareForInstantiate() {
  static constexpr std::u16string_view src_16 =
      u"function f() { return {'field': 42};}; f()['field']\n";

  JS::CompileOptions options(cx);

  JS::SourceText<char16_t> buf16;
  CHECK(buf16.init(cx, src_16.data(), src_16.length(),
                   JS::SourceOwnership::Borrowed));

  JS::FrontendContext* fc = JS::NewFrontendContext();
  CHECK(fc);
  auto destroyFc =
      mozilla::MakeScopeExit([fc] { JS::DestroyFrontendContext(fc); });

  RefPtr<JS::Stencil> stencil =
      CompileGlobalScriptToStencil(fc, options, buf16);
  CHECK(stencil);
  RefPtr<const js::frontend::CompilationStencil> initial =
      stencil->getInitial();
  CHECK(initial->scriptData.size() == 2);
  CHECK(initial->scopeData.size() == 1);       // function f
  CHECK(initial->parserAtomData.size() == 1);  // 'field'

  JS::InstantiationStorage storage;
  CHECK(JS::PrepareForInstantiate(fc, *stencil, storage));
  CHECK(storage.isValid());
  // TODO storage.gcOutput_ is private, so there isn't a good way to check the
  // scriptData and scopeData capacities

  return true;
}
END_TEST(testCompileScript);
