/* 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 "debugger/Source.h"

#include "mozilla/Assertions.h"  // for AssertionConditionType, MOZ_ASSERT
#include "mozilla/Maybe.h"       // for Some, Maybe, Nothing
#include "mozilla/Variant.h"     // for AsVariant, Variant

#include <stdint.h>  // for uint32_t
#include <string.h>  // for memcpy
#include <utility>   // for move

#include "debugger/Debugger.h"         // for DebuggerSourceReferent, Debugger
#include "debugger/Script.h"           // for DebuggerScript
#include "frontend/FrontendContext.h"  // for AutoReportFrontendContext
#include "gc/Tracer.h"        // for TraceManuallyBarrieredCrossCompartmentEdge
#include "js/ColumnNumber.h"  // JS::WasmFunctionIndex, JS::ColumnNumberOneOrigin
#include "js/CompilationAndEvaluation.h"  // for Compile
#include "js/ErrorReport.h"  // for JS_ReportErrorASCII,  JS_ReportErrorNumberASCII
#include "js/experimental/TypedData.h"  // for JS_NewUint8Array
#include "js/friend/ErrorMessages.h"    // for GetErrorMessage, JSMSG_*
#include "js/GCVariant.h"               // for GCVariant
#include "js/SourceText.h"              // for JS::SourceOwnership
#include "js/String.h"                  // for JS_CopyStringCharsZ
#include "vm/BytecodeUtil.h"            // for JSDVG_SEARCH_STACK
#include "vm/JSContext.h"               // for JSContext (ptr only)
#include "vm/JSObject.h"                // for JSObject, RequireObject
#include "vm/JSScript.h"                // for ScriptSource, ScriptSourceObject
#include "vm/StringType.h"        // for NewStringCopyZ, JSString (ptr only)
#include "vm/TypedArrayObject.h"  // for TypedArrayObject, JSObject::is
#include "wasm/WasmCode.h"        // for Metadata
#include "wasm/WasmDebug.h"       // for DebugState
#include "wasm/WasmInstance.h"    // for Instance
#include "wasm/WasmJS.h"          // for WasmInstanceObject
#include "wasm/WasmTypeDecls.h"   // for Bytes, Rooted<WasmInstanceObject*>

#include "debugger/Debugger-inl.h"  // for Debugger::fromJSObject
#include "vm/JSObject-inl.h"        // for InitClass
#include "vm/NativeObject-inl.h"
#include "wasm/WasmInstance-inl.h"

namespace js {
class GlobalObject;
}

using namespace js;

using mozilla::AsVariant;
using mozilla::Maybe;
using mozilla::Nothing;
using mozilla::Some;

const JSClassOps DebuggerSource::classOps_ = {
    .trace = CallTraceMethod<DebuggerSource>,
};

const JSClass DebuggerSource::class_ = {
    "Source",
    JSCLASS_HAS_RESERVED_SLOTS(RESERVED_SLOTS),
    &classOps_,
};

/* static */
NativeObject* DebuggerSource::initClass(JSContext* cx,
                                        Handle<GlobalObject*> global,
                                        HandleObject debugCtor) {
  return InitClass(cx, debugCtor, nullptr, nullptr, "Source", construct, 0,
                   properties_, methods_, nullptr, nullptr);
}

/* static */
DebuggerSource* DebuggerSource::create(JSContext* cx, HandleObject proto,
                                       Handle<DebuggerSourceReferent> referent,
                                       Handle<NativeObject*> debugger) {
  Rooted<DebuggerSource*> sourceObj(
      cx, NewObjectWithGivenProto<DebuggerSource>(cx, proto,
                                                  {.newKind = TenuredObject}));
  if (!sourceObj) {
    return nullptr;
  }
  sourceObj->setReservedSlot(OWNER_SLOT, ObjectValue(*debugger));
  referent.get().match([&](auto sourceHandle) {
    sourceObj->setReservedSlotGCThingAsPrivate(SOURCE_SLOT, sourceHandle);
  });

  return sourceObj;
}

Debugger* DebuggerSource::owner() const {
  JSObject* dbgobj = &getReservedSlot(OWNER_SLOT).toObject();
  return Debugger::fromJSObject(dbgobj);
}

// For internal use only.
NativeObject* DebuggerSource::getReferentRawObject() const {
  return maybePtrFromReservedSlot<NativeObject>(SOURCE_SLOT);
}

DebuggerSourceReferent DebuggerSource::getReferent() const {
  if (NativeObject* referent = getReferentRawObject()) {
    if (referent->is<ScriptSourceObject>()) {
      return AsVariant(&referent->as<ScriptSourceObject>());
    }
    return AsVariant(&referent->as<WasmInstanceObject>());
  }
  return AsVariant(static_cast<ScriptSourceObject*>(nullptr));
}

void DebuggerSource::trace(JSTracer* trc) {
  // There is a barrier on private pointers, so the Unbarriered marking
  // is okay.
  if (JSObject* referent = getReferentRawObject()) {
    TraceManuallyBarrieredCrossCompartmentEdge(trc, this, &referent,
                                               "Debugger.Source referent");
    if (referent != getReferentRawObject()) {
      setReservedSlotGCThingAsPrivateUnbarriered(SOURCE_SLOT, referent);
    }
  }
}

/* static */
bool DebuggerSource::construct(JSContext* cx, unsigned argc, Value* vp) {
  JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NO_CONSTRUCTOR,
                            "Debugger.Source");
  return false;
}

/* static */
DebuggerSource* DebuggerSource::check(JSContext* cx, HandleValue thisv) {
  JSObject* thisobj = RequireObject(cx, thisv);
  if (!thisobj) {
    return nullptr;
  }
  if (!thisobj->is<DebuggerSource>()) {
    JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                              JSMSG_INCOMPATIBLE_PROTO, "Debugger.Source",
                              "method", thisobj->getClass()->name);
    return nullptr;
  }

  return &thisobj->as<DebuggerSource>();
}

struct MOZ_STACK_CLASS DebuggerSource::CallData {
  JSContext* cx;
  const CallArgs& args;

  Handle<DebuggerSource*> obj;
  Rooted<DebuggerSourceReferent> referent;

  CallData(JSContext* cx, const CallArgs& args, Handle<DebuggerSource*> obj)
      : cx(cx), args(args), obj(obj), referent(cx, obj->getReferent()) {}

  bool getText();
  bool getBinary();
  bool getURL();
  bool getStartLine();
  bool getStartColumn();
  bool getId();
  bool getDisplayURL();
  bool getElementProperty();
  bool getIntroductionScript();
  bool getIntroductionOffset();
  bool getIntroductionType();
  bool setSourceMapURL();
  bool getSourceMapURL();
  bool reparse();

  using Method = bool (CallData::*)();

  template <Method MyMethod>
  static bool ToNative(JSContext* cx, unsigned argc, Value* vp);
};

template <DebuggerSource::CallData::Method MyMethod>
/* static */
bool DebuggerSource::CallData::ToNative(JSContext* cx, unsigned argc,
                                        Value* vp) {
  CallArgs args = CallArgsFromVp(argc, vp);

  Rooted<DebuggerSource*> obj(cx, DebuggerSource::check(cx, args.thisv()));
  if (!obj) {
    return false;
  }

  CallData data(cx, args, obj);
  return (data.*MyMethod)();
}

class DebuggerSourceGetTextMatcher {
  JSContext* cx_;

 public:
  explicit DebuggerSourceGetTextMatcher(JSContext* cx) : cx_(cx) {}

  using ReturnType = JSString*;

  ReturnType match(Handle<ScriptSourceObject*> sourceObject) {
    ScriptSource* ss = sourceObject->source();
    bool hasSourceText;
    if (!ScriptSource::loadSource(cx_, ss, &hasSourceText)) {
      return nullptr;
    }
    if (!hasSourceText) {
      return NewStringCopyZ<CanGC>(cx_, "[no source]");
    }

    if (ss->shouldUnwrapEventHandlerBody()) {
      return ss->functionBodyString(cx_);
    }

    return ss->substring(cx_, 0, ss->length());
  }

  ReturnType match(Handle<WasmInstanceObject*> instanceObj) {
    wasm::Instance& instance = instanceObj->instance();
    const char* msg;
    if (!instance.debugEnabled()) {
      msg = "Restart with developer tools open to view WebAssembly source.";
    } else {
      msg = "[debugger missing wasm binary-to-text conversion]";
    }
    return NewStringCopyZ<CanGC>(cx_, msg);
  }
};

bool DebuggerSource::CallData::getText() {
  Value textv = obj->getReservedSlot(TEXT_SLOT);
  if (!textv.isUndefined()) {
    MOZ_ASSERT(textv.isString());
    args.rval().set(textv);
    return true;
  }

  DebuggerSourceGetTextMatcher matcher(cx);
  JSString* str = referent.match(matcher);
  if (!str) {
    return false;
  }

  args.rval().setString(str);
  obj->setReservedSlot(TEXT_SLOT, args.rval());
  return true;
}

bool DebuggerSource::CallData::getBinary() {
  if (!referent.is<WasmInstanceObject*>()) {
    ReportValueError(cx, JSMSG_DEBUG_BAD_REFERENT, JSDVG_SEARCH_STACK,
                     args.thisv(), nullptr, "a wasm source");
    return false;
  }

  Rooted<WasmInstanceObject*> instanceObj(cx,
                                          referent.as<WasmInstanceObject*>());
  wasm::Instance& instance = instanceObj->instance();

  if (!instance.debugEnabled()) {
    JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                              JSMSG_DEBUG_NO_BINARY_SOURCE);
    return false;
  }

  const wasm::BytecodeSource& bytecode = instance.debug().bytecode();
  RootedObject arr(cx, JS_NewUint8Array(cx, bytecode.length()));
  if (!arr) {
    return false;
  }

  bytecode.copyTo((uint8_t*)arr->as<TypedArrayObject>().dataPointerUnshared());

  args.rval().setObject(*arr);
  return true;
}

class DebuggerSourceGetURLMatcher {
  JSContext* cx_;

 public:
  explicit DebuggerSourceGetURLMatcher(JSContext* cx) : cx_(cx) {}

  using ReturnType = Maybe<JSString*>;

  ReturnType match(Handle<ScriptSourceObject*> sourceObject) {
    ScriptSource* ss = sourceObject->source();
    MOZ_ASSERT(ss);
    if (const char* filename = ss->filename()) {
      JS::UTF8Chars utf8chars(filename, strlen(filename));
      JSString* str = NewStringCopyUTF8N(cx_, utf8chars);
      return Some(str);
    }
    return Nothing();
  }
  ReturnType match(Handle<WasmInstanceObject*> instanceObj) {
    return Some(instanceObj->instance().createDisplayURL(cx_));
  }
};

bool DebuggerSource::CallData::getURL() {
  DebuggerSourceGetURLMatcher matcher(cx);
  Maybe<JSString*> str = referent.match(matcher);
  if (str.isSome()) {
    if (!*str) {
      return false;
    }
    args.rval().setString(*str);
  } else {
    args.rval().setNull();
  }
  return true;
}

class DebuggerSourceGetStartLineMatcher {
 public:
  using ReturnType = uint32_t;

  ReturnType match(Handle<ScriptSourceObject*> sourceObject) {
    ScriptSource* ss = sourceObject->source();
    return ss->startLine();
  }
  ReturnType match(Handle<WasmInstanceObject*> instanceObj) { return 0; }
};

bool DebuggerSource::CallData::getStartLine() {
  DebuggerSourceGetStartLineMatcher matcher;
  uint32_t line = referent.match(matcher);
  args.rval().setNumber(line);
  return true;
}

class DebuggerSourceGetStartColumnMatcher {
 public:
  using ReturnType = JS::LimitedColumnNumberOneOrigin;

  ReturnType match(Handle<ScriptSourceObject*> sourceObject) {
    ScriptSource* ss = sourceObject->source();
    return ss->startColumn();
  }
  ReturnType match(Handle<WasmInstanceObject*> instanceObj) {
    return JS::LimitedColumnNumberOneOrigin(
        JS::WasmFunctionIndex::DefaultBinarySourceColumnNumberOneOrigin);
  }
};

bool DebuggerSource::CallData::getStartColumn() {
  DebuggerSourceGetStartColumnMatcher matcher;
  JS::LimitedColumnNumberOneOrigin column = referent.match(matcher);
  args.rval().setNumber(column.oneOriginValue());
  return true;
}

class DebuggerSourceGetIdMatcher {
 public:
  using ReturnType = uint32_t;

  ReturnType match(Handle<ScriptSourceObject*> sourceObject) {
    ScriptSource* ss = sourceObject->source();
    return ss->id();
  }
  ReturnType match(Handle<WasmInstanceObject*> instanceObj) { return 0; }
};

bool DebuggerSource::CallData::getId() {
  DebuggerSourceGetIdMatcher matcher;
  uint32_t id = referent.match(matcher);
  args.rval().setNumber(id);
  return true;
}

struct DebuggerSourceGetDisplayURLMatcher {
  using ReturnType = const char16_t*;
  ReturnType match(Handle<ScriptSourceObject*> sourceObject) {
    ScriptSource* ss = sourceObject->source();
    MOZ_ASSERT(ss);
    return ss->hasDisplayURL() ? ss->displayURL() : nullptr;
  }
  ReturnType match(Handle<WasmInstanceObject*> wasmInstance) {
    // Wasm sources don't have display URLs
    return nullptr;
  }
};

bool DebuggerSource::CallData::getDisplayURL() {
  DebuggerSourceGetDisplayURLMatcher matcher;
  if (const char16_t* displayURL = referent.match(matcher)) {
    JSString* str = JS_NewUCStringCopyZ(cx, displayURL);
    if (!str) {
      return false;
    }
    args.rval().setString(str);
  } else {
    args.rval().setNull();
  }
  return true;
}

struct DebuggerSourceGetElementPropertyMatcher {
  using ReturnType = Value;
  ReturnType match(Handle<ScriptSourceObject*> sourceObject) {
    return sourceObject->unwrappedElementAttributeName();
  }
  ReturnType match(Handle<WasmInstanceObject*> wasmInstance) {
    return UndefinedValue();
  }
};

bool DebuggerSource::CallData::getElementProperty() {
  DebuggerSourceGetElementPropertyMatcher matcher;
  args.rval().set(referent.match(matcher));
  return obj->owner()->wrapDebuggeeValue(cx, args.rval());
}

class DebuggerSourceGetIntroductionScriptMatcher {
  JSContext* cx_;
  Debugger* dbg_;
  MutableHandleValue rval_;

 public:
  DebuggerSourceGetIntroductionScriptMatcher(JSContext* cx, Debugger* dbg,
                                             MutableHandleValue rval)
      : cx_(cx), dbg_(dbg), rval_(rval) {}

  using ReturnType = bool;

  ReturnType match(Handle<ScriptSourceObject*> sourceObject) {
    Rooted<BaseScript*> script(cx_,
                               sourceObject->unwrappedIntroductionScript());
    if (script) {
      RootedObject scriptDO(cx_, dbg_->wrapScript(cx_, script));
      if (!scriptDO) {
        return false;
      }
      rval_.setObject(*scriptDO);
    } else {
      rval_.setUndefined();
    }
    return true;
  }

  ReturnType match(Handle<WasmInstanceObject*> wasmInstance) {
    RootedObject ds(cx_, dbg_->wrapWasmScript(cx_, wasmInstance));
    if (!ds) {
      return false;
    }
    rval_.setObject(*ds);
    return true;
  }
};

bool DebuggerSource::CallData::getIntroductionScript() {
  Debugger* dbg = obj->owner();
  DebuggerSourceGetIntroductionScriptMatcher matcher(cx, dbg, args.rval());
  return referent.match(matcher);
}

struct DebuggerGetIntroductionOffsetMatcher {
  using ReturnType = Value;
  ReturnType match(Handle<ScriptSourceObject*> sourceObject) {
    // Regardless of what's recorded in the ScriptSourceObject and
    // ScriptSource, only hand out the introduction offset if we also have
    // the script within which it applies.
    ScriptSource* ss = sourceObject->source();
    if (ss->hasIntroductionOffset() &&
        sourceObject->unwrappedIntroductionScript()) {
      return Int32Value(ss->introductionOffset());
    }
    return UndefinedValue();
  }
  ReturnType match(Handle<WasmInstanceObject*> wasmInstance) {
    return UndefinedValue();
  }
};

bool DebuggerSource::CallData::getIntroductionOffset() {
  DebuggerGetIntroductionOffsetMatcher matcher;
  args.rval().set(referent.match(matcher));
  return true;
}

struct DebuggerSourceGetIntroductionTypeMatcher {
  using ReturnType = const char*;
  ReturnType match(Handle<ScriptSourceObject*> sourceObject) {
    ScriptSource* ss = sourceObject->source();
    MOZ_ASSERT(ss);
    return ss->hasIntroductionType() ? ss->introductionType() : nullptr;
  }
  ReturnType match(Handle<WasmInstanceObject*> wasmInstance) { return "wasm"; }
};

bool DebuggerSource::CallData::getIntroductionType() {
  DebuggerSourceGetIntroductionTypeMatcher matcher;
  if (const char* introductionType = referent.match(matcher)) {
    JSString* str = NewStringCopyZ<CanGC>(cx, introductionType);
    if (!str) {
      return false;
    }
    args.rval().setString(str);
  } else {
    args.rval().setUndefined();
  }

  return true;
}

ScriptSourceObject* EnsureSourceObject(JSContext* cx,
                                       Handle<DebuggerSource*> obj) {
  if (!obj->getReferent().is<ScriptSourceObject*>()) {
    RootedValue v(cx, ObjectValue(*obj));
    ReportValueError(cx, JSMSG_DEBUG_BAD_REFERENT, JSDVG_SEARCH_STACK, v,
                     nullptr, "a JS source");
    return nullptr;
  }
  return obj->getReferent().as<ScriptSourceObject*>();
}

bool DebuggerSource::CallData::setSourceMapURL() {
  Rooted<ScriptSourceObject*> sourceObject(cx, EnsureSourceObject(cx, obj));
  if (!sourceObject) {
    return false;
  }
  ScriptSource* ss = sourceObject->source();
  MOZ_ASSERT(ss);

  if (!args.requireAtLeast(cx, "set sourceMapURL", 1)) {
    return false;
  }

  JSString* str = ToString<CanGC>(cx, args[0]);
  if (!str) {
    return false;
  }

  UniqueTwoByteChars chars = JS_CopyStringCharsZ(cx, str);
  if (!chars) {
    return false;
  }

  AutoReportFrontendContext fc(cx);
  if (!ss->setSourceMapURL(&fc, std::move(chars))) {
    return false;
  }

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

class DebuggerSourceGetSourceMapURLMatcher {
  JSContext* cx_;
  MutableHandleString result_;

 public:
  explicit DebuggerSourceGetSourceMapURLMatcher(JSContext* cx,
                                                MutableHandleString result)
      : cx_(cx), result_(result) {}

  using ReturnType = bool;
  ReturnType match(Handle<ScriptSourceObject*> sourceObject) {
    ScriptSource* ss = sourceObject->source();
    MOZ_ASSERT(ss);
    if (!ss->hasSourceMapURL()) {
      result_.set(nullptr);
      return true;
    }
    JSString* str = JS_NewUCStringCopyZ(cx_, ss->sourceMapURL());
    if (!str) {
      return false;
    }
    result_.set(str);
    return true;
  }
  ReturnType match(Handle<WasmInstanceObject*> instanceObj) {
    wasm::Instance& instance = instanceObj->instance();
    if (!instance.debugEnabled()) {
      result_.set(nullptr);
      return true;
    }

    RootedString str(cx_);
    if (!instance.debug().getSourceMappingURL(cx_, &str)) {
      return false;
    }

    result_.set(str);
    return true;
  }
};

bool DebuggerSource::CallData::getSourceMapURL() {
  RootedString result(cx);
  DebuggerSourceGetSourceMapURLMatcher matcher(cx, &result);
  if (!referent.match(matcher)) {
    return false;
  }
  if (result) {
    args.rval().setString(result);
  } else {
    args.rval().setNull();
  }
  return true;
}

template <typename Unit>
static JSScript* ReparseSource(JSContext* cx, Handle<ScriptSourceObject*> sso,
                               bool asModule) {
  AutoRealm ar(cx, sso);
  ScriptSource* ss = sso->source();

  JS::CompileOptions options(cx);
  options.setHideScriptFromDebugger(true);
  options.setFileAndLine(ss->filename(), ss->startLine());
  options.setColumn(JS::ColumnNumberOneOrigin(ss->startColumn()));

  UncompressedSourceCache::AutoHoldEntry holder;

  ScriptSource::PinnedUnits<Unit> units(cx, ss, holder, 0, ss->length());
  if (!units.get()) {
    return nullptr;
  }

  JS::SourceText<Unit> srcBuf;
  if (!srcBuf.init(cx, units.get(), ss->length(),
                   JS::SourceOwnership::Borrowed)) {
    return nullptr;
  }

  if (asModule) {
    if (options.lineno == 0) {
      JS_ReportErrorASCII(cx, "Module cannot be reparsed with lineNumber == 0");
      return nullptr;
    }
    if (!options.filename()) {
      JS_ReportErrorASCII(cx, "Module cannot be reparsed without filename");
      return nullptr;
    }
    options.setModule();

    JSObject* module = JS::CompileModule(cx, options, srcBuf);
    if (!module) {
      return nullptr;
    }

    return module->as<ModuleObject>().script();
  }

  return JS::Compile(cx, options, srcBuf);
}

bool DebuggerSource::CallData::reparse() {
  Rooted<ScriptSourceObject*> sourceObject(cx, EnsureSourceObject(cx, obj));
  if (!sourceObject) {
    return false;
  }

  if (!sourceObject->source()->hasSourceText()) {
    JS_ReportErrorASCII(cx, "Source object missing text");
    return false;
  }

  bool asModule = ToBoolean(args.get(0));

  RootedScript script(cx);
  if (sourceObject->source()->hasSourceType<mozilla::Utf8Unit>()) {
    script = ReparseSource<mozilla::Utf8Unit>(cx, sourceObject, asModule);
  } else {
    script = ReparseSource<char16_t>(cx, sourceObject, asModule);
  }

  if (!script) {
    return false;
  }

  Debugger* dbg = obj->owner();
  RootedObject scriptDO(cx, dbg->wrapScript(cx, script));
  if (!scriptDO) {
    return false;
  }

  args.rval().setObject(*scriptDO);
  return true;
}

const JSPropertySpec DebuggerSource::properties_[] = {
    JS_DEBUG_PSG("text", getText),
    JS_DEBUG_PSG("binary", getBinary),
    JS_DEBUG_PSG("url", getURL),
    JS_DEBUG_PSG("startLine", getStartLine),
    JS_DEBUG_PSG("startColumn", getStartColumn),
    JS_DEBUG_PSG("id", getId),
    JS_DEBUG_PSG("displayURL", getDisplayURL),
    JS_DEBUG_PSG("introductionScript", getIntroductionScript),
    JS_DEBUG_PSG("introductionOffset", getIntroductionOffset),
    JS_DEBUG_PSG("introductionType", getIntroductionType),
    JS_DEBUG_PSG("elementAttributeName", getElementProperty),
    JS_DEBUG_PSGS("sourceMapURL", getSourceMapURL, setSourceMapURL),
    JS_PS_END,
};

const JSFunctionSpec DebuggerSource::methods_[] = {
    JS_DEBUG_FN("reparse", reparse, 0),
    JS_FS_END,
};
