#include "HangDetails.h"
#include "nsIHangDetails.h"
#include "nsPrintfCString.h"
#include "mozilla/gfx/GPUParent.h"
#include "mozilla/dom/ContentChild.h"
#include "mozilla/Unused.h"
#include "mozilla/GfxMessageUtils.h" // For ParamTraits<GeckoProcessType>

namespace mozilla {

NS_IMETHODIMP
nsHangDetails::GetDuration(uint32_t* aDuration)
{
  *aDuration = mDetails.mDuration;
  return NS_OK;
}

NS_IMETHODIMP
nsHangDetails::GetThread(nsACString& aName)
{
  aName.Assign(mDetails.mThreadName);
  return NS_OK;
}

NS_IMETHODIMP
nsHangDetails::GetRunnableName(nsACString& aRunnableName)
{
  aRunnableName.Assign(mDetails.mRunnableName);
  return NS_OK;
}

NS_IMETHODIMP
nsHangDetails::GetProcess(nsACString& aName)
{
  aName.AssignASCII(XRE_ChildProcessTypeToString(mDetails.mProcess));
  return NS_OK;
}

NS_IMETHODIMP
nsHangDetails::GetRemoteType(nsAString& aName)
{
  aName.Assign(mDetails.mRemoteType);
  return NS_OK;
}

NS_IMETHODIMP
nsHangDetails::GetAnnotations(JSContext* aCx, JS::MutableHandleValue aVal)
{
  // We create an object with { "key" : "value" } string pairs for each item in
  // our annotations object.
  JS::RootedObject jsAnnotation(aCx, JS_NewPlainObject(aCx));
  if (!jsAnnotation) {
    return NS_ERROR_OUT_OF_MEMORY;
  }

  for (auto& annot : mDetails.mAnnotations) {
    JSString* jsString = JS_NewUCStringCopyN(aCx, annot.mValue.get(), annot.mValue.Length());
    if (!jsString) {
      return NS_ERROR_OUT_OF_MEMORY;
    }
    JS::RootedValue jsValue(aCx);
    jsValue.setString(jsString);
    if (!JS_DefineUCProperty(aCx, jsAnnotation, annot.mName.get(), annot.mName.Length(),
                             jsValue, JSPROP_ENUMERATE)) {
      return NS_ERROR_OUT_OF_MEMORY;
    }
  }

  aVal.setObject(*jsAnnotation);
  return NS_OK;
}

namespace  {

nsresult
StringFrame(JSContext* aCx,
            JS::RootedObject& aTarget,
            size_t aIndex,
            const char* aString)
{
  JSString* jsString = JS_NewStringCopyZ(aCx, aString);
  if (!jsString) {
    return NS_ERROR_OUT_OF_MEMORY;
  }
  JS::RootedString string(aCx, jsString);
  if (!string) {
    return NS_ERROR_OUT_OF_MEMORY;
  }
  if (!JS_DefineElement(aCx, aTarget, aIndex, string, JSPROP_ENUMERATE)) {
    return NS_ERROR_OUT_OF_MEMORY;
  }
  return NS_OK;
}

} // anonymous namespace

NS_IMETHODIMP
nsHangDetails::GetStack(JSContext* aCx, JS::MutableHandle<JS::Value> aVal)
{
  JS::RootedObject ret(aCx, JS_NewArrayObject(aCx, mDetails.mStack.length()));
  if (!ret) {
    return NS_ERROR_OUT_OF_MEMORY;
  }
  for (size_t i = 0; i < mDetails.mStack.length(); ++i) {
    const HangStack::Frame& frame = mDetails.mStack[i];
    switch (frame.GetKind()) {
      case HangStack::Frame::Kind::STRING: {
        nsresult rv = StringFrame(aCx, ret, i, frame.AsString());
        NS_ENSURE_SUCCESS(rv, rv);
        break;
      }

      case HangStack::Frame::Kind::MODOFFSET: {
        JS::RootedObject jsFrame(aCx, JS_NewArrayObject(aCx, 2));
        if (!jsFrame) {
          return NS_ERROR_OUT_OF_MEMORY;
        }

        if (!JS_DefineElement(aCx, jsFrame, 0, frame.AsModOffset().mModule, JSPROP_ENUMERATE)) {
          return NS_ERROR_OUT_OF_MEMORY;
        }

        nsPrintfCString hexString("%" PRIxPTR, (uintptr_t)frame.AsModOffset().mOffset);
        JS::RootedString hex(aCx, JS_NewStringCopyZ(aCx, hexString.get()));
        if (!hex || !JS_DefineElement(aCx, jsFrame, 1, hex, JSPROP_ENUMERATE)) {
          return NS_ERROR_OUT_OF_MEMORY;
        }

        if (!JS_DefineElement(aCx, ret, i, jsFrame, JSPROP_ENUMERATE)) {
          return NS_ERROR_OUT_OF_MEMORY;
        }
        break;
      }

      case HangStack::Frame::Kind::PC: {
        JS::RootedObject jsFrame(aCx, JS_NewArrayObject(aCx, 2));
        if (!jsFrame) {
          return NS_ERROR_OUT_OF_MEMORY;
        }

        if (!JS_DefineElement(aCx, jsFrame, 0, -1, JSPROP_ENUMERATE)) {
          return NS_ERROR_OUT_OF_MEMORY;
        }

        nsPrintfCString hexString("%" PRIxPTR, frame.AsPC());
        JS::RootedString hex(aCx, JS_NewStringCopyZ(aCx, hexString.get()));
        if (!hex || !JS_DefineElement(aCx, jsFrame, 1, hex, JSPROP_ENUMERATE)) {
          return NS_ERROR_OUT_OF_MEMORY;
        }

        if (!JS_DefineElement(aCx, ret, i, jsFrame, JSPROP_ENUMERATE)) {
          return NS_ERROR_OUT_OF_MEMORY;
        }
        break;
      }

      case HangStack::Frame::Kind::CONTENT: {
        nsresult rv = StringFrame(aCx, ret, i, "(content script)");
        NS_ENSURE_SUCCESS(rv, rv);
        break;
      }

      case HangStack::Frame::Kind::JIT: {
        nsresult rv = StringFrame(aCx, ret, i, "(jit frame)");
        NS_ENSURE_SUCCESS(rv, rv);
        break;
      }

      case HangStack::Frame::Kind::WASM: {
        nsresult rv = StringFrame(aCx, ret, i, "(wasm)");
        NS_ENSURE_SUCCESS(rv, rv);
        break;
      }

      case HangStack::Frame::Kind::SUPPRESSED: {
        nsresult rv = StringFrame(aCx, ret, i, "(profiling suppressed)");
        NS_ENSURE_SUCCESS(rv, rv);
        break;
      }

      default:
        MOZ_ASSERT_UNREACHABLE("Invalid variant");
        break;
    }
  }

  aVal.setObject(*ret);
  return NS_OK;
}

NS_IMETHODIMP
nsHangDetails::GetModules(JSContext* aCx, JS::MutableHandleValue aVal)
{
  auto& modules = mDetails.mStack.GetModules();
  size_t length = modules.Length();
  JS::RootedObject retObj(aCx, JS_NewArrayObject(aCx, length));
  if (!retObj) {
    return NS_ERROR_OUT_OF_MEMORY;
  }

  for (size_t i = 0; i < length; ++i) {
    const HangStack::Module& module = modules[i];
    JS::RootedObject jsModule(aCx, JS_NewArrayObject(aCx, 2));
    if (!jsModule) {
      return NS_ERROR_OUT_OF_MEMORY;
    }

    JS::RootedString name(aCx, JS_NewUCStringCopyN(aCx, module.mName.BeginReading(), module.mName.Length()));
    if (!JS_DefineElement(aCx, jsModule, 0, name, JSPROP_ENUMERATE)) {
      return NS_ERROR_OUT_OF_MEMORY;
    }

    JS::RootedString breakpadId(aCx, JS_NewStringCopyN(aCx, module.mBreakpadId.BeginReading(), module.mBreakpadId.Length()));
    if (!JS_DefineElement(aCx, jsModule, 1, breakpadId, JSPROP_ENUMERATE)) {
      return NS_ERROR_OUT_OF_MEMORY;
    }

    if (!JS_DefineElement(aCx, retObj, i, jsModule, JSPROP_ENUMERATE)) {
      return NS_ERROR_OUT_OF_MEMORY;
    }
  }

  aVal.setObject(*retObj);
  return NS_OK;
}

// Processing and submitting the stack as an observer notification.

void
nsHangDetails::Submit()
{
  if (NS_WARN_IF(!SystemGroup::Initialized())) {
    return;
  }

  RefPtr<nsHangDetails> hangDetails = this;
  nsCOMPtr<nsIRunnable> notifyObservers = NS_NewRunnableFunction("NotifyBHRHangObservers", [hangDetails] {
    // The place we need to report the hang to varies depending on process.
    //
    // In child processes, we report the hang to our parent process, while if
    // we're in the parent process, we report a bhr-thread-hang observer
    // notification.
    switch (XRE_GetProcessType()) {
    case GeckoProcessType_Content: {
      auto cc = dom::ContentChild::GetSingleton();
      if (cc) {
        hangDetails->mDetails.mRemoteType.Assign(cc->GetRemoteType());
        Unused << cc->SendBHRThreadHang(hangDetails->mDetails);
      }
      break;
    }
    case GeckoProcessType_GPU: {
      auto gp = gfx::GPUParent::GetSingleton();
      if (gp) {
        Unused << gp->SendBHRThreadHang(hangDetails->mDetails);
      }
      break;
    }
    case GeckoProcessType_Default: {
      nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
      if (os) {
        os->NotifyObservers(hangDetails, "bhr-thread-hang", nullptr);
      }
      break;
    }
    default:
      // XXX: Consider handling GeckoProcessType_GMPlugin and
      // GeckoProcessType_Plugin?
      NS_WARNING("Unsupported BHR process type - discarding hang.");
      break;
    }
  });

  nsresult rv = SystemGroup::Dispatch(TaskCategory::Other,
                                      notifyObservers.forget());
  MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
}

NS_IMPL_ISUPPORTS(nsHangDetails, nsIHangDetails)

NS_IMETHODIMP
ProcessHangStackRunnable::Run()
{
  // NOTE: Reading module information can take a long time, which is why we do
  // it off-main-thread.
  mHangDetails.mStack.ReadModuleInformation();

  RefPtr<nsHangDetails> hangDetails = new nsHangDetails(Move(mHangDetails));
  hangDetails->Submit();

  return NS_OK;
}

} // namespace mozilla


/**
 * IPC Serialization / Deserialization logic
 */
namespace IPC {

void
ParamTraits<mozilla::HangDetails>::Write(Message* aMsg, const mozilla::HangDetails& aParam)
{
  WriteParam(aMsg, aParam.mDuration);
  WriteParam(aMsg, aParam.mProcess);
  WriteParam(aMsg, aParam.mRemoteType);
  WriteParam(aMsg, aParam.mThreadName);
  WriteParam(aMsg, aParam.mRunnableName);
  WriteParam(aMsg, aParam.mStack);
  WriteParam(aMsg, aParam.mAnnotations);
}

bool
ParamTraits<mozilla::HangDetails>::Read(const Message* aMsg,
                                        PickleIterator* aIter,
                                        mozilla::HangDetails* aResult)
{
  if (!ReadParam(aMsg, aIter, &aResult->mDuration)) {
    return false;
  }
  if (!ReadParam(aMsg, aIter, &aResult->mProcess)) {
    return false;
  }
  if (!ReadParam(aMsg, aIter, &aResult->mRemoteType)) {
    return false;
  }
  if (!ReadParam(aMsg, aIter, &aResult->mThreadName)) {
    return false;
  }
  if (!ReadParam(aMsg, aIter, &aResult->mRunnableName)) {
    return false;
  }
  if (!ReadParam(aMsg, aIter, &aResult->mStack)) {
    return false;
  }
  if (!ReadParam(aMsg, aIter, &aResult->mAnnotations)) {
    return false;
  }

  return true;
}

} // namespace IPC
