/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

#pragma once

#ifdef HERMES_ENABLE_DEBUGGER

#include <condition_variable>
#include <mutex>
#include <optional>
#include <queue>
#include <thread>

#include <hermes/DebuggerAPI.h>
#include <hermes/Public/HermesExport.h>
#include <hermes/hermes.h>

#if defined(__clang__) && (!defined(SWIG)) && defined(_LIBCPP_VERSION) && \
    defined(_LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS)
#include <hermes/ThreadSafetyAnalysis.h>
#else
#ifndef TSA_GUARDED_BY
#define TSA_GUARDED_BY(x)
#endif
#ifndef TSA_NO_THREAD_SAFETY_ANALYSIS
#define TSA_NO_THREAD_SAFETY_ANALYSIS
#endif
#endif

namespace facebook {
namespace hermes {
namespace debugger {

class AsyncDebuggerAPI;

enum class DebuggerEventType {
  // Informational Events
  ScriptLoaded, /// A script file was loaded, and the debugger has requested
  /// pausing after script load.
  Exception, /// An Exception was thrown.
  Resumed, /// Script execution has resumed.

  // Events Requiring Next Command
  DebuggerStatement, /// A debugger; statement was hit.
  Breakpoint, /// A breakpoint was hit.
  StepFinish, /// A Step operation completed.
  ExplicitPause, /// A pause requested using Explicit AsyncBreak
};

/// This represents the list of possible commands that can be given to
/// \p resumeFromPaused. This is used instead of DebuggerAPI's Command class in
/// order to prevent callers from constructing an eval Command. The eval
/// functionality is implemented as a separate mechansim with
/// \p evalWhilePaused.
enum class AsyncDebugCommand {
  Continue, /// Continues execution
  StepInto, /// Perform a step into and then pause again
  StepOver, /// Steps over the current instruction and then pause again
  StepOut, /// Step out from the current scope and then pause again
};

using DebuggerEventCallback = std::function<void(
    HermesRuntime &runtime,
    AsyncDebuggerAPI &asyncDebugger,
    DebuggerEventType event)>;
using InterruptCallback = std::function<void(HermesRuntime &runtime)>;
using EvalCompleteCallback = std::function<
    void(HermesRuntime &runtime, const debugger::EvalResult &result)>;

/// This class wraps the DebuggerAPI to expose an asynchronous didPause
/// functionality as well as an interrupt API. This class must be constructed at
/// the same time as HermesRuntime.
///
/// Functions in this class with the suffix "_TS" (Thread-Safe) are the only
/// functions that are safe to call on any thread. All other functions must be
/// called on the runtime thread.
class HERMES_EXPORT AsyncDebuggerAPI : private debugger::EventObserver {
 public:
  /// Constructs an AsyncDebuggerAPI for use with the provided HermesRuntime.
  /// This should be called and created at the same time as creating
  /// HermesRuntime.
  explicit AsyncDebuggerAPI(HermesRuntime &runtime);

  /// Must be destroyed on the runtime thread or when you're sure nothing is
  /// interacting with the runtime. Must be destroyed before destroying
  /// HermesRuntime.
  ~AsyncDebuggerAPI() override;

  /// Set a callback function to invoke when the runtime pauses due to various
  /// conditions such as hitting a "debugger;" statement. Can be called from any
  /// thread. If there is no DebuggerEventCallback, then any reason that might
  /// trigger a pause, such as a "debugger;" statement or breakpoints, will not
  /// actually pause and will simply continue execution. Any caller that sets an
  /// event callback cannot just be observing events and never call
  /// \p resumeFromPaused in any of its code paths. The caller must either
  /// expose UI enabling human action for controlling the debugger, or it must
  /// have programmatic logic that controls the debugger via
  /// \p resumeFromPaused.
  ///
  /// The provided callback must be non-empty. Use \p
  /// clearDebuggerEventCallback_TS to clear the callback.
  void setDebuggerEventCallback_TS(DebuggerEventCallback callback);

  /// Clear the debugger event callback. Can be called from any thread.
  void clearDebuggerEventCallback_TS();

  /// Whether the runtime is currently paused waiting for the next action.
  /// Should only be called from the runtime thread.
  bool isWaitingForCommand();

  /// Whether the runtime is currently paused for any reason (e.g. script
  /// parsed, running interrupts, or waiting for a command).
  /// Should only be called from the runtime thread.
  bool isPaused() const;

  /// Provide the next action to perform. Should only be called from the runtime
  /// thread and only if the next command is expected to be set.
  bool resumeFromPaused(AsyncDebugCommand command);

  /// Evaluate JavaScript code \p expression in the frame at index
  /// \p frameIndex. Receives evaluation result in the \p callback. Should only
  /// be called from the runtime thread and only if debugger is paused waiting
  /// for the next action.
  bool evalWhilePaused(
      const std::string &expression,
      uint32_t frameIndex,
      EvalCompleteCallback callback);

  /// Request to interrupt the runtime at a convenient time and get a callback
  /// on the runtime thread. Guaranteed to run "exactly once". This function can
  /// be called from any thread, but cannot be called while inside a
  /// DebuggerEventCallback.
  void triggerInterrupt_TS(InterruptCallback callback);

  /// EventObserver implementation
  debugger::Command didPause(debugger::Debugger &debugger) override;

 private:
  /// This function infinite loops and uses \p signal_ to block the runtime
  /// thread. It gets woken up if new InterruptCallback is queued or if
  /// DebuggerEventCallback changes.
  void processInterruptWhilePaused() TSA_NO_THREAD_SAFETY_ANALYSIS;

  /// Dequeues the next InterruptCallback if any.
  std::optional<InterruptCallback> takeNextInterruptCallback();

  /// If \p ignoreNextCommand is true, then runs every InterruptCallback that
  /// has been queued up so far. If \p ignoreNextCommand is false, then attempt
  /// to run all interrupts, but will stop if any interrupt sets a next command.
  void runInterrupts(bool ignoreNextCommand = true);

  /// Runs the DebuggerEventCallback that has been registered (if any).
  void runEventCallback(DebuggerEventType event);

  HermesRuntime &runtime_;

  /// Whether the runtime thread is currently paused in \p didPause and needs to
  /// be told what action to take next.
  bool isWaitingForCommand_;

  /// Stores the command to return from \p didPause.
  debugger::Command nextCommand_;

  /// Callback function to invoke after getting EvalResult from EvalComplete in
  /// didPause. Used once and then cleared out.
  EvalCompleteCallback oneTimeEvalCompleteCallback_{};

  /// Tracks whether we are already in a didPause callback to detect recursive
  /// calls to didPause.
  bool inDidPause_ = false;

  /// The debugger event callback to invoke when the runtime pauses.
  DebuggerEventCallback eventCallback_ TSA_GUARDED_BY(mutex_){};

  /// Queue of interrupt callback functions to invoke.
  std::queue<InterruptCallback> interruptCallbacks_ TSA_GUARDED_BY(mutex_){};

  /// Used as a mechanism to block the runtime thread in \p didPause and for
  /// protecting variables used across threads.
  std::mutex mutex_{};
  /// Used to implement \p triggerInterrupt while \p didPause is holding onto
  /// the runtime thread.
  std::condition_variable signal_{};
};

} // namespace debugger
} // namespace hermes
} // namespace facebook

#else // !HERMES_ENABLE_DEBUGGER

#include <hermes/DebuggerAPI.h>
#include <hermes/Public/HermesExport.h>
#include <hermes/hermes.h>

namespace facebook {
namespace hermes {
namespace debugger {

class AsyncDebuggerAPI;

enum class DebuggerEventType {
  // Informational Events
  ScriptLoaded, /// A script file was loaded, and the debugger has requested
  /// pausing after script load.
  Exception, /// An Exception was thrown.
  Resumed, /// Script execution has resumed.

  // Events Requiring Next Command
  DebuggerStatement, /// A debugger; statement was hit.
  Breakpoint, /// A breakpoint was hit.
  StepFinish, /// A Step operation completed.
  ExplicitPause, /// A pause requested using Explicit AsyncBreak
};

/// This represents the list of possible commands that can be given to
/// \p resumeFromPaused. This is used instead of DebuggerAPI's Command class in
/// order to prevent callers from constructing an eval Command. The eval
/// functionality is implemented as a separate mechansim with
/// \p evalWhilePaused.
enum class AsyncDebugCommand {
  Continue, /// Continues execution
  StepInto, /// Perform a step into and then pause again
  StepOver, /// Steps over the current instruction and then pause again
  StepOut, /// Step out from the current scope and then pause again
};

using DebuggerEventCallback = std::function<void(
    HermesRuntime &runtime,
    AsyncDebuggerAPI &asyncDebugger,
    DebuggerEventType event)>;
using InterruptCallback = std::function<void(HermesRuntime &runtime)>;
using EvalCompleteCallback = std::function<
    void(HermesRuntime &runtime, const debugger::EvalResult &result)>;

class HERMES_EXPORT AsyncDebuggerAPI {
 public:
  explicit AsyncDebuggerAPI(HermesRuntime &) {}

  ~AsyncDebuggerAPI() {}

  void setDebuggerEventCallback_TS(DebuggerEventCallback callback) {}

  void clearDebuggerEventCallback_TS() {}

  bool isWaitingForCommand() {
    return false;
  }

  bool isPaused() const {
    return false;
  }

  bool resumeFromPaused(AsyncDebugCommand command) {
    return false;
  }

  bool evalWhilePaused(
      const std::string &expression,
      uint32_t frameIndex,
      EvalCompleteCallback callback) {
    return false;
  }

  void triggerInterrupt_TS(InterruptCallback callback) {}
};

} // namespace debugger
} // namespace hermes
} // namespace facebook

#endif // !HERMES_ENABLE_DEBUGGER
