/* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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 "nsCocoaDebugUtils.h"

#include <pthread.h>
#include <libproc.h>
#include <stdarg.h>
#include <time.h>
#include <execinfo.h>
#include <asl.h>

static char gProcPath[PROC_PIDPATHINFO_MAXSIZE] = {0};
static char gBundleID[MAXPATHLEN] = {0};

static void MaybeGetPathAndID() {
  if (!gProcPath[0]) {
    proc_pidpath(getpid(), gProcPath, sizeof(gProcPath));
  }
  if (!gBundleID[0]) {
    // Apple's CFLog() uses "com.apple.console" (in its call to asl_open()) if
    // it can't find the bundle id.
    CFStringRef bundleID = NULL;
    CFBundleRef mainBundle = CFBundleGetMainBundle();
    if (mainBundle) {
      bundleID = CFBundleGetIdentifier(mainBundle);
    }
    if (!bundleID) {
      strcpy(gBundleID, "com.apple.console");
    } else {
      CFStringGetCString(bundleID, gBundleID, sizeof(gBundleID), kCFStringEncodingUTF8);
    }
  }
}

static void GetThreadName(char* aName, size_t aSize) {
  pthread_getname_np(pthread_self(), aName, aSize);
}

void nsCocoaDebugUtils::DebugLog(const char* aFormat, ...) {
  va_list args;
  va_start(args, aFormat);
  CFStringRef formatCFSTR =
      CFStringCreateWithCString(kCFAllocatorDefault, aFormat, kCFStringEncodingUTF8);
  DebugLogV(true, formatCFSTR, args);
  CFRelease(formatCFSTR);
  va_end(args);
}

void nsCocoaDebugUtils::DebugLogInt(bool aDecorate, const char* aFormat, ...) {
  va_list args;
  va_start(args, aFormat);
  CFStringRef formatCFSTR =
      CFStringCreateWithCString(kCFAllocatorDefault, aFormat, kCFStringEncodingUTF8);
  DebugLogV(aDecorate, formatCFSTR, args);
  CFRelease(formatCFSTR);
  va_end(args);
}

void nsCocoaDebugUtils::DebugLogV(bool aDecorate, CFStringRef aFormat, va_list aArgs) {
  MaybeGetPathAndID();

  CFStringRef message =
      CFStringCreateWithFormatAndArguments(kCFAllocatorDefault, NULL, aFormat, aArgs);

  int msgLength =
      CFStringGetMaximumSizeForEncoding(CFStringGetLength(message), kCFStringEncodingUTF8);
  char* msgUTF8 = (char*)calloc(msgLength, 1);
  CFStringGetCString(message, msgUTF8, msgLength, kCFStringEncodingUTF8);
  CFRelease(message);

  int finishedLength = msgLength + PROC_PIDPATHINFO_MAXSIZE;
  char* finished = (char*)calloc(finishedLength, 1);
  const time_t currentTime = time(NULL);
  char timestamp[30] = {0};
  ctime_r(&currentTime, timestamp);
  if (aDecorate) {
    char threadName[MAXPATHLEN] = {0};
    GetThreadName(threadName, sizeof(threadName));
    snprintf(finished, finishedLength, "(%s) %s[%u] %s[%p] %s\n", timestamp, gProcPath, getpid(),
             threadName, pthread_self(), msgUTF8);
  } else {
    snprintf(finished, finishedLength, "%s\n", msgUTF8);
  }
  free(msgUTF8);

  fputs(finished, stdout);

  // Use the Apple System Log facility, as NSLog and CFLog do.
  aslclient asl = asl_open(NULL, gBundleID, ASL_OPT_NO_DELAY);
  aslmsg msg = asl_new(ASL_TYPE_MSG);
  asl_set(msg, ASL_KEY_LEVEL, "4");  // kCFLogLevelWarning, used by NSLog()
  asl_set(msg, ASL_KEY_MSG, finished);
  asl_send(asl, msg);
  asl_free(msg);
  asl_close(asl);

  free(finished);
}

CSTypeRef nsCocoaDebugUtils::sInitializer = {0};

CSSymbolicatorRef nsCocoaDebugUtils::sSymbolicator = {0};

#define STACK_MAX 256

void nsCocoaDebugUtils::PrintStackTrace() {
  void** addresses = (void**)calloc(STACK_MAX, sizeof(void*));
  if (!addresses) {
    return;
  }

  CSSymbolicatorRef symbolicator = GetSymbolicatorRef();
  if (CSIsNull(symbolicator)) {
    free(addresses);
    return;
  }

  uint32_t count = backtrace(addresses, STACK_MAX);
  for (uint32_t i = 0; i < count; ++i) {
    PrintAddress(addresses[i]);
  }

  ReleaseSymbolicator();
  free(addresses);
}

void nsCocoaDebugUtils::PrintAddress(void* aAddress) {
  const char* ownerName = "unknown";
  const char* addressString = "unknown + 0";

  char* allocatedOwnerName = nullptr;
  char* allocatedAddressString = nullptr;

  CSSymbolOwnerRef owner = {0};
  CSSymbolicatorRef symbolicator = GetSymbolicatorRef();

  if (!CSIsNull(symbolicator)) {
    owner = CSSymbolicatorGetSymbolOwnerWithAddressAtTime(symbolicator,
                                                          (unsigned long long)aAddress, kCSNow);
  }
  if (!CSIsNull(owner)) {
    ownerName = allocatedOwnerName = GetOwnerNameInt(aAddress, owner);
    addressString = allocatedAddressString = GetAddressStringInt(aAddress, owner);
  }
  DebugLogInt(false, "    (%s) %s", ownerName, addressString);

  free(allocatedOwnerName);
  free(allocatedAddressString);

  ReleaseSymbolicator();
}

char* nsCocoaDebugUtils::GetOwnerName(void* aAddress) { return GetOwnerNameInt(aAddress); }

char* nsCocoaDebugUtils::GetOwnerNameInt(void* aAddress, CSTypeRef aOwner) {
  char* retval = (char*)calloc(MAXPATHLEN, 1);

  const char* ownerName = "unknown";

  CSSymbolicatorRef symbolicator = GetSymbolicatorRef();
  CSTypeRef owner = aOwner;

  if (CSIsNull(owner) && !CSIsNull(symbolicator)) {
    owner = CSSymbolicatorGetSymbolOwnerWithAddressAtTime(symbolicator,
                                                          (unsigned long long)aAddress, kCSNow);
  }

  if (!CSIsNull(owner)) {
    ownerName = CSSymbolOwnerGetName(owner);
  }

  snprintf(retval, MAXPATHLEN, "%s", ownerName);
  ReleaseSymbolicator();

  return retval;
}

char* nsCocoaDebugUtils::GetAddressString(void* aAddress) { return GetAddressStringInt(aAddress); }

char* nsCocoaDebugUtils::GetAddressStringInt(void* aAddress, CSTypeRef aOwner) {
  char* retval = (char*)calloc(MAXPATHLEN, 1);

  const char* addressName = "unknown";
  unsigned long long addressOffset = 0;

  CSSymbolicatorRef symbolicator = GetSymbolicatorRef();
  CSTypeRef owner = aOwner;

  if (CSIsNull(owner) && !CSIsNull(symbolicator)) {
    owner = CSSymbolicatorGetSymbolOwnerWithAddressAtTime(symbolicator,
                                                          (unsigned long long)aAddress, kCSNow);
  }

  if (!CSIsNull(owner)) {
    CSSymbolRef symbol = CSSymbolOwnerGetSymbolWithAddress(owner, (unsigned long long)aAddress);
    if (!CSIsNull(symbol)) {
      addressName = CSSymbolGetName(symbol);
      CSRange range = CSSymbolGetRange(symbol);
      addressOffset = (unsigned long long)aAddress - range.location;
    } else {
      addressOffset = (unsigned long long)aAddress - CSSymbolOwnerGetBaseAddress(owner);
    }
  }

  snprintf(retval, MAXPATHLEN, "%s + 0x%llx", addressName, addressOffset);
  ReleaseSymbolicator();

  return retval;
}

CSSymbolicatorRef nsCocoaDebugUtils::GetSymbolicatorRef() {
  if (CSIsNull(sSymbolicator)) {
    // 0x40e0000 is the value returned by
    // uint32_t CSSymbolicatorGetFlagsForNListOnlyData(void).  We don't use
    // this method directly because it doesn't exist on OS X 10.6.  Unless
    // we limit ourselves to NList data, it will take too long to get a
    // stack trace where Dwarf debugging info is available (about 15 seconds
    // with Firefox).  This means we won't be able to get a CSSourceInfoRef,
    // or line number information.  Oh well.
    sSymbolicator = CSSymbolicatorCreateWithPidFlagsAndNotification(getpid(), 0x40e0000, 0);
  }
  // Retaining just after creation prevents crashes when calling symbolicator
  // code (for example from PrintStackTrace()) as Firefox is quitting.  Not
  // sure why.  Doing this may mean that we leak sSymbolicator on quitting
  // (if we ever created it).  No particular harm in that, though.
  return CSRetain(sSymbolicator);
}

void nsCocoaDebugUtils::ReleaseSymbolicator() {
  if (!CSIsNull(sSymbolicator)) {
    CSRelease(sSymbolicator);
  }
}
