/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the LICENSE
 * file in the root directory of this source tree.
 */
#include <lyra/lyra.h>

#include <atomic>
#include <ios>
#include <ostream>
#include <iomanip>
#include <memory>
#include <vector>

#include <dlfcn.h>
#include <unwind.h>

#include <fbjni/detail/Log.h>

using namespace std;

namespace facebook {
namespace lyra {

namespace {

class IosFlagsSaver {
  ios_base& ios_;
  ios_base::fmtflags flags_;

 public:
  IosFlagsSaver(ios_base& ios)
  : ios_(ios),
    flags_(ios.flags())
  {}

  ~IosFlagsSaver() {
    ios_.flags(flags_);
  }
};

struct BacktraceState {
  size_t skip;
  vector<InstructionPointer>& stackTrace;
};

_Unwind_Reason_Code unwindCallback(struct _Unwind_Context* context, void* arg) {
  BacktraceState* state = reinterpret_cast<BacktraceState*>(arg);
  auto absoluteProgramCounter =
      reinterpret_cast<InstructionPointer>(_Unwind_GetIP(context));

  if (state->skip > 0) {
    --state->skip;
    return _URC_NO_REASON;
  }

  if (state->stackTrace.size() == state->stackTrace.capacity()) {
    return _URC_END_OF_STACK;
  }

  state->stackTrace.push_back(absoluteProgramCounter);

  return _URC_NO_REASON;
}

void captureBacktrace(size_t skip, vector<InstructionPointer>& stackTrace) {
  // Beware of a bug on some platforms, which makes the trace loop until the
  // buffer is full when it reaches a noexcept function. It seems to be fixed in
  // newer versions of gcc. https://gcc.gnu.org/bugzilla/show_bug.cgi?id=56846
  // TODO(t10738439): Investigate workaround for the stack trace bug
  BacktraceState state = {skip, stackTrace};
  _Unwind_Backtrace(unwindCallback, &state);
}

// this is a pointer to a function
std::atomic<LibraryIdentifierFunctionType> gLibraryIdentifierFunction{nullptr};

}

void setLibraryIdentifierFunction(LibraryIdentifierFunctionType func) {
  gLibraryIdentifierFunction.store(func, std::memory_order_relaxed);
}

std::string StackTraceElement::buildId() const {
  if (!hasBuildId_) {
    auto getBuildId = gLibraryIdentifierFunction.load(std::memory_order_relaxed);
    if (getBuildId) {
      buildId_ = getBuildId(libraryName());
    } else {
      buildId_ = "<unimplemented>";
    }
    hasBuildId_ = true;
  }
  return buildId_;
}

void getStackTrace(vector<InstructionPointer>& stackTrace, size_t skip) {
  stackTrace.clear();
  captureBacktrace(skip + 1, stackTrace);
}

// TODO(t10737622): Improve on-device symbolification
void getStackTraceSymbols(vector<StackTraceElement>& symbols,
                          const vector<InstructionPointer>& trace) {
  symbols.clear();
  symbols.reserve(trace.size());

  for (size_t i = 0; i < trace.size(); ++i) {
    Dl_info info;
    if (dladdr(trace[i], &info)) {
      symbols.emplace_back(trace[i], info.dli_fbase, info.dli_saddr,
                           info.dli_fname ? info.dli_fname : "",
                           info.dli_sname ? info.dli_sname : "");
    }
  }
}

ostream& operator<<(ostream& out, const StackTraceElement& elm) {
  IosFlagsSaver flags{out};

  out << "{dso=" << elm.libraryName() << " offset=" << hex
      << showbase << elm.libraryOffset();

  if (!elm.functionName().empty()) {
    out << " func=" << elm.functionName() << "()+" << elm.functionOffset();
  }

  out << " build-id=" << hex << setw(8) << elm.buildId()
      << "}";

  return out;
}

// TODO(t10737667): The implement a tool that parse the stack trace and
// symbolicate it
ostream& operator<<(ostream& out, const vector<StackTraceElement>& trace) {
  IosFlagsSaver flags{out};

  auto i = 0;
  out << "Backtrace:\n";
  for (auto& elm : trace) {
    out << "    #" << dec << setfill('0') << setw(2) << i++ << " " << elm << '\n';
  }

  return out;
}

void logStackTrace(const vector<StackTraceElement>& trace) {
  auto i = 0;
  FBJNI_LOGE("Backtrace:");
  for (auto& elm : trace) {
    if (!elm.functionName().empty()) {
      FBJNI_LOGE("    #%02d |lyra|{dso=%s offset=%#x func=%s+%#x build-id=%s}",
          i++,
          elm.libraryName().c_str(),
          elm.libraryOffset(),
          elm.functionName().c_str(),
          elm.functionOffset(),
          elm.buildId().c_str());
    } else {
      FBJNI_LOGE("    #%02d |lyra|{dso=%s offset=%#x build-id=%s}",
          i++,
          elm.libraryName().c_str(),
          elm.libraryOffset(),
          elm.buildId().c_str());
    }
  }
}

}
}
