// Copyright 2004-present Facebook. All Rights Reserved.

#include <algorithm>
#include <JavaScriptCore/JavaScript.h>
#include <JavaScriptCore/API/JSProfilerPrivate.h>
#include <fbsystrace.h>
#include <sys/types.h>
#include <unistd.h>
#include "JSCHelpers.h"

using std::min;

static const char *ENABLED_FBSYSTRACE_PROFILE_NAME = "__fbsystrace__";

static uint64_t tagFromJSValue(
    JSContextRef ctx,
    JSValueRef value,
    JSValueRef* exception) {
  // XXX validate that this is a lossless conversion.
  // XXX should we just have separate functions for bridge, infra, and apps,
  // then drop this argument to save time?
  (void)exception;
  uint64_t tag = (uint64_t) JSValueToNumber(ctx, value, NULL);
  return tag;
}

static int64_t int64FromJSValue(
    JSContextRef ctx,
    JSValueRef value,
    JSValueRef* exception) {
  (void)exception;
  int64_t num = (int64_t) JSValueToNumber(ctx, value, NULL);
  return num;
}

static size_t copyTruncatedAsciiChars(
    char* buf,
    size_t bufLen,
    JSContextRef ctx,
    JSValueRef value,
    size_t maxLen) {
  JSStringRef jsString = JSValueToStringCopy(ctx, value, NULL);
  size_t stringLen = JSStringGetLength(jsString);
  // Unlike the Java version, we truncate from the end of the string,
  // rather than the beginning.
  size_t toWrite = min(stringLen, min(bufLen, maxLen));

  const char *startBuf = buf;
  const JSChar* chars = JSStringGetCharactersPtr(jsString);
  while (toWrite-- > 0) {
    *(buf++) = (char)*(chars++);
  }

  JSStringRelease(jsString);

  // Return the number of bytes written
  return buf - startBuf;
}

static size_t copyArgsToBuffer(
    char* buf,
    size_t bufLen,
    size_t pos,
    JSContextRef ctx,
    size_t argumentCount,
    const JSValueRef arguments[]) {
  char separator = '|';
  for (
      size_t idx = 0;
      idx + 1 < argumentCount;  // Make sure key and value are present.
      idx += 2) {
    JSValueRef key = arguments[idx];
    JSValueRef value = arguments[idx+1];

    buf[pos++] = separator;
    separator = ';';
    if (FBSYSTRACE_UNLIKELY(pos >= bufLen)) { break; }
    pos += copyTruncatedAsciiChars(
        buf + pos, bufLen - pos, ctx, key, FBSYSTRACE_MAX_MESSAGE_LENGTH);
    if (FBSYSTRACE_UNLIKELY(pos >= bufLen)) { break; }
    buf[pos++] = '=';
    if (FBSYSTRACE_UNLIKELY(pos >= bufLen)) { break; }
    pos += copyTruncatedAsciiChars(
        buf + pos, bufLen - pos, ctx, value, FBSYSTRACE_MAX_MESSAGE_LENGTH);
    if (FBSYSTRACE_UNLIKELY(pos >= bufLen)) { break; }
  }
  return pos;
}

static JSValueRef nativeTraceBeginSection(
    JSContextRef ctx,
    JSObjectRef function,
    JSObjectRef thisObject,
    size_t argumentCount,
    const JSValueRef arguments[],
    JSValueRef* exception) {
  if (FBSYSTRACE_UNLIKELY(argumentCount < 2)) {
    if (exception) {
      *exception = facebook::react::makeJSCException(
        ctx,
        "nativeTraceBeginSection: requires at least 2 arguments");
    }
    return JSValueMakeUndefined(ctx);
  }

  uint64_t tag = tagFromJSValue(ctx, arguments[0], exception);
  if (!fbsystrace_is_tracing(tag)) {
    return JSValueMakeUndefined(ctx);
  }

  char buf[FBSYSTRACE_MAX_MESSAGE_LENGTH];
  size_t pos = 0;

  pos += snprintf(buf + pos, sizeof(buf) - pos, "B|%d|", getpid());
  // Skip the overflow check here because the int will be small.
  pos += copyTruncatedAsciiChars(buf + pos, sizeof(buf) - pos, ctx, arguments[1], FBSYSTRACE_MAX_SECTION_NAME_LENGTH);
  // Skip the overflow check here because the section name will be small-ish.

  pos = copyArgsToBuffer(buf, sizeof(buf), pos, ctx, argumentCount - 2, arguments + 2);
  if (FBSYSTRACE_UNLIKELY(pos >= sizeof(buf))) {
    goto flush;
  }

flush:
  fbsystrace_trace_raw(buf, min(pos, sizeof(buf)-1));

  return JSValueMakeUndefined(ctx);
}

static JSValueRef nativeTraceEndSection(
    JSContextRef ctx,
    JSObjectRef function,
    JSObjectRef thisObject,
    size_t argumentCount,
    const JSValueRef arguments[],
    JSValueRef* exception) {
  if (FBSYSTRACE_UNLIKELY(argumentCount < 1)) {
    if (exception) {
      *exception = facebook::react::makeJSCException(
        ctx,
        "nativeTraceEndSection: requires at least 1 argument");
    }
    return JSValueMakeUndefined(ctx);
  }

  uint64_t tag = tagFromJSValue(ctx, arguments[0], exception);
  if (!fbsystrace_is_tracing(tag)) {
    return JSValueMakeUndefined(ctx);
  }

  if (FBSYSTRACE_LIKELY(argumentCount == 1)) {
    fbsystrace_end_section(tag);
  } else {
    char buf[FBSYSTRACE_MAX_MESSAGE_LENGTH];
    size_t pos = 0;

    buf[pos++] = 'E';
    buf[pos++] = '|';
    buf[pos++] = '|';
    pos = copyArgsToBuffer(buf, sizeof(buf), pos, ctx, argumentCount - 1, arguments + 1);
    if (FBSYSTRACE_UNLIKELY(pos >= sizeof(buf))) {
      goto flush;
    }

flush:
    fbsystrace_trace_raw(buf, min(pos, sizeof(buf)-1));
  }

  return JSValueMakeUndefined(ctx);
}

static JSValueRef beginOrEndAsync(
    bool isEnd,
    bool isFlow,
    JSContextRef ctx,
    JSObjectRef function,
    JSObjectRef thisObject,
    size_t argumentCount,
    const JSValueRef arguments[],
    JSValueRef* exception) {
  if (FBSYSTRACE_UNLIKELY(argumentCount < 3)) {
    if (exception) {
      *exception = facebook::react::makeJSCException(
        ctx,
        "beginOrEndAsync: requires at least 3 arguments");
    }
    return JSValueMakeUndefined(ctx);
  }

  uint64_t tag = tagFromJSValue(ctx, arguments[0], exception);
  if (!fbsystrace_is_tracing(tag)) {
    return JSValueMakeUndefined(ctx);
  }

  char buf[FBSYSTRACE_MAX_MESSAGE_LENGTH];
  size_t pos = 0;

  // This uses an if-then-else instruction in ARMv7, which should be cheaper
  // than a full branch.
  buf[pos++] = ((isFlow) ? (isEnd ? 'f' : 's') : (isEnd ? 'F' : 'S'));
  pos += snprintf(buf + pos, sizeof(buf) - pos, "|%d|", getpid());
  // Skip the overflow check here because the int will be small.
  pos += copyTruncatedAsciiChars(buf + pos, sizeof(buf) - pos, ctx, arguments[1], FBSYSTRACE_MAX_SECTION_NAME_LENGTH);
  // Skip the overflow check here because the section name will be small-ish.

  // I tried some trickery to avoid a branch here, but gcc did not cooperate.
  // We could consider changing the implementation to be lest branchy in the
  // future.
  // This is not required for flow use an or to avoid introducing another branch
  if (!(isEnd | isFlow)) {
    buf[pos++] = '<';
    buf[pos++] = '0';
    buf[pos++] = '>';
  }
  buf[pos++] = '|';

  // Append the cookie.  It should be an integer, but copyTruncatedAsciiChars
  // will automatically convert it to a string.  We might be able to get more
  // performance by just getting the number and doing to string
  // conversion ourselves.  We truncate to FBSYSTRACE_MAX_SECTION_NAME_LENGTH
  // just to make sure we can avoid the overflow check even if the caller
  // passes in something bad.
  pos += copyTruncatedAsciiChars(buf + pos, sizeof(buf) - pos, ctx, arguments[2], FBSYSTRACE_MAX_SECTION_NAME_LENGTH);

  pos = copyArgsToBuffer(buf, sizeof(buf), pos, ctx, argumentCount - 3, arguments + 3);
  if (FBSYSTRACE_UNLIKELY(pos >= sizeof(buf))) {
    goto flush;
  }

flush:
  fbsystrace_trace_raw(buf, min(pos, sizeof(buf)-1));

  return JSValueMakeUndefined(ctx);
}

static JSValueRef stageAsync(
    bool isFlow,
    JSContextRef ctx,
    JSObjectRef function,
    JSObjectRef thisObject,
    size_t argumentCount,
    const JSValueRef arguments[],
    JSValueRef* exception) {
  if (FBSYSTRACE_UNLIKELY(argumentCount < 4)) {
    if (exception) {
      *exception = facebook::react::makeJSCException(
        ctx,
        "stageAsync: requires at least 4 arguments");
    }
    return JSValueMakeUndefined(ctx);
  }

  uint64_t tag = tagFromJSValue(ctx, arguments[0], exception);
  if (!fbsystrace_is_tracing(tag)) {
    return JSValueMakeUndefined(ctx);
  }

  char buf[FBSYSTRACE_MAX_MESSAGE_LENGTH];
  size_t pos = 0;

  buf[pos++] = (isFlow ? 't' : 'T');
  pos += snprintf(buf + pos, sizeof(buf) - pos, "|%d", getpid());
  // Skip the overflow check here because the int will be small.

  // Arguments are section name, cookie, and stage name.
  // All added together, they still cannot cause an overflow.
  for (int i = 1; i < 4; i++) {
    buf[pos++] = '|';
    pos += copyTruncatedAsciiChars(buf + pos, sizeof(buf) - pos, ctx, arguments[i], FBSYSTRACE_MAX_SECTION_NAME_LENGTH);
  }

  fbsystrace_trace_raw(buf, min(pos, sizeof(buf)-1));

  return JSValueMakeUndefined(ctx);
}

static JSValueRef nativeTraceBeginAsyncSection(
    JSContextRef ctx,
    JSObjectRef function,
    JSObjectRef thisObject,
    size_t argumentCount,
    const JSValueRef arguments[],
    JSValueRef* exception) {
  return beginOrEndAsync(
      false /* isEnd */,
      false /* isFlow */,
      ctx,
      function,
      thisObject,
      argumentCount,
      arguments,
      exception);
}

static JSValueRef nativeTraceEndAsyncSection(
    JSContextRef ctx,
    JSObjectRef function,
    JSObjectRef thisObject,
    size_t argumentCount,
    const JSValueRef arguments[],
    JSValueRef* exception) {
  return beginOrEndAsync(
      true /* isEnd */,
      false /* isFlow */,
      ctx,
      function,
      thisObject,
      argumentCount,
      arguments,
      exception);
}

static JSValueRef nativeTraceAsyncSectionStage(
    JSContextRef ctx,
    JSObjectRef function,
    JSObjectRef thisObject,
    size_t argumentCount,
    const JSValueRef arguments[],
    JSValueRef* exception) {
  return stageAsync(
      false /* isFlow */,
      ctx,
      function,
      thisObject,
      argumentCount,
      arguments,
      exception);
}

static JSValueRef nativeTraceBeginAsyncFlow(
    JSContextRef ctx,
    JSObjectRef function,
    JSObjectRef thisObject,
    size_t argumentCount,
    const JSValueRef arguments[],
    JSValueRef* exception) {
  return beginOrEndAsync(
      false /* isEnd */,
      true /* isFlow */,
      ctx,
      function,
      thisObject,
      argumentCount,
      arguments,
      exception);
}

static JSValueRef nativeTraceEndAsyncFlow(
    JSContextRef ctx,
    JSObjectRef function,
    JSObjectRef thisObject,
    size_t argumentCount,
    const JSValueRef arguments[],
    JSValueRef* exception) {
  return beginOrEndAsync(
      true /* isEnd */,
      true /* isFlow */,
      ctx,
      function,
      thisObject,
      argumentCount,
      arguments,
      exception);
}

static JSValueRef nativeTraceAsyncFlowStage(
    JSContextRef ctx,
    JSObjectRef function,
    JSObjectRef thisObject,
    size_t argumentCount,
    const JSValueRef arguments[],
    JSValueRef* exception) {
  return stageAsync(
      true /* isFlow */,
      ctx,
      function,
      thisObject,
      argumentCount,
      arguments,
      exception);
}

static JSValueRef nativeTraceCounter(
    JSContextRef ctx,
    JSObjectRef function,
    JSObjectRef thisObject,
    size_t argumentCount,
    const JSValueRef arguments[],
    JSValueRef* exception) {
  if (FBSYSTRACE_UNLIKELY(argumentCount < 3)) {
    if (exception) {
      *exception = facebook::react::makeJSCException(
        ctx,
        "nativeTraceCounter: requires at least 3 arguments");
    }
    return JSValueMakeUndefined(ctx);
  }

  uint64_t tag = tagFromJSValue(ctx, arguments[0], exception);
  if (!fbsystrace_is_tracing(tag)) {
    return JSValueMakeUndefined(ctx);
  }

  char buf[FBSYSTRACE_MAX_MESSAGE_LENGTH];
  size_t len = copyTruncatedAsciiChars(buf, sizeof(buf), ctx,
    arguments[1], FBSYSTRACE_MAX_SECTION_NAME_LENGTH);
  buf[min(len,(FBSYSTRACE_MAX_MESSAGE_LENGTH-1))] = 0;
  int64_t value = int64FromJSValue(ctx, arguments[2], exception);

  fbsystrace_counter(tag, buf, value);

  return JSValueMakeUndefined(ctx);
}

static JSValueRef nativeTraceBeginLegacy(
    JSContextRef ctx,
    JSObjectRef function,
    JSObjectRef thisObject,
    size_t argumentCount,
    const JSValueRef arguments[],
    JSValueRef* exception) {
  if (FBSYSTRACE_LIKELY(argumentCount >= 1)) {
    uint64_t tag = tagFromJSValue(ctx, arguments[0], exception);
    if (!fbsystrace_is_tracing(tag)) {
      return JSValueMakeUndefined(ctx);
    }
  }

  JSStringRef title = JSStringCreateWithUTF8CString(ENABLED_FBSYSTRACE_PROFILE_NAME);
  #if WITH_REACT_INTERNAL_SETTINGS
  JSStartProfiling(ctx, title, true);
  #else
  JSStartProfiling(ctx, title);
  #endif
  JSStringRelease(title);

  return JSValueMakeUndefined(ctx);
}

static JSValueRef nativeTraceEndLegacy(
    JSContextRef ctx,
    JSObjectRef function,
    JSObjectRef thisObject,
    size_t argumentCount,
    const JSValueRef arguments[],
    JSValueRef* exception) {
  if (FBSYSTRACE_LIKELY(argumentCount >= 1)) {
    uint64_t tag = tagFromJSValue(ctx, arguments[0], exception);
    if (!fbsystrace_is_tracing(tag)) {
      return JSValueMakeUndefined(ctx);
    }
  }

  JSStringRef title = JSStringCreateWithUTF8CString(ENABLED_FBSYSTRACE_PROFILE_NAME);
  JSEndProfiling(ctx, title);
  JSStringRelease(title);

  return JSValueMakeUndefined(ctx);
}

namespace facebook {
namespace react {

void addNativeTracingHooks(JSGlobalContextRef ctx) {
  installGlobalFunction(ctx, "nativeTraceBeginSection", nativeTraceBeginSection);
  installGlobalFunction(ctx, "nativeTraceEndSection", nativeTraceEndSection);
  installGlobalFunction(ctx, "nativeTraceBeginLegacy", nativeTraceBeginLegacy);
  installGlobalFunction(ctx, "nativeTraceEndLegacy", nativeTraceEndLegacy);
  installGlobalFunction(ctx, "nativeTraceBeginAsyncSection", nativeTraceBeginAsyncSection);
  installGlobalFunction(ctx, "nativeTraceEndAsyncSection", nativeTraceEndAsyncSection);
  installGlobalFunction(ctx, "nativeTraceAsyncSectionStage", nativeTraceAsyncSectionStage);
  installGlobalFunction(ctx, "nativeTraceBeginAsyncFlow", nativeTraceBeginAsyncFlow);
  installGlobalFunction(ctx, "nativeTraceEndAsyncFlow", nativeTraceEndAsyncFlow);
  installGlobalFunction(ctx, "nativeTraceAsyncFlowStage", nativeTraceAsyncFlowStage);
  installGlobalFunction(ctx, "nativeTraceCounter", nativeTraceCounter);
}

} }
