#ifndef GENMC_MIRI_INTERFACE_HPP
#define GENMC_MIRI_INTERFACE_HPP

// CXX.rs generated headers:
#include "rust/cxx.h"

// GenMC generated headers:
#include "genmc/config.h"

// Miri `genmc-sys/src_cpp` headers:
#include "ResultHandling.hpp"

// GenMC headers:
#include "genmc/Execution/EventLabel.hpp"
#include "genmc/Support/MemOrdering.hpp"
#include "genmc/Support/RMWOps.hpp"
#include "genmc/Verification/Config.hpp"
#include "genmc/Verification/GenMCDriver.hpp"

// C++ headers:
#include <cstdint>
#include <format>
#include <iomanip>
#include <memory>

/**** Types available to both Rust and C++ ****/

struct GenmcParams;
enum class LogLevel : std::uint8_t;

struct GenmcScalar;
struct SchedulingResult;
struct EstimationResult;
struct LoadResult;
struct StoreResult;
struct ReadModifyWriteResult;
struct CompareExchangeResult;
struct MutexLockResult;
struct MallocResult;

// GenMC uses `int` for its thread IDs.
using ThreadId = int;

/// Set the log level for GenMC.
///
/// # Safety
///
/// This function is not thread safe, since it writes to the global, mutable, non-atomic `logLevel`
/// variable. Any GenMC function may read from `logLevel` unsynchronized.
/// The safest way to use this function is to set the log level exactly once before first calling
/// `create_handle`.
/// Never calling this function is safe, GenMC will fall back to its default log level.
/* unsafe */ void set_log_level_raw(LogLevel log_level);

struct MiriGenmcShim : private GenMCDriver {

  public:
    MiriGenmcShim(std::shared_ptr<const Config> conf, Mode mode /* = VerificationMode{} */)
        : GenMCDriver(std::move(conf), nullptr, mode) {}

    /// Create a new `MiriGenmcShim`, which wraps a `GenMCDriver`.
    ///
    /// # Safety
    ///
    /// This function is marked as unsafe since the `logLevel` global variable is non-atomic.
    /// This function should not be called in an unsynchronized way with `set_log_level_raw`,
    /// since this function and any methods on the returned `MiriGenmcShim` may read the
    /// `logLevel`, causing a data race. The safest way to use these functions is to call
    /// `set_log_level_raw` once, and only then start creating handles. There should not be any
    /// other (safe) way to create a `MiriGenmcShim`.
    /* unsafe */ static auto create_handle(const GenmcParams& params, bool estimation_mode)
        -> std::unique_ptr<MiriGenmcShim>;

    virtual ~MiriGenmcShim() {}

    /**** Execution start/end handling ****/

    // This function must be called at the start of any execution, before any events are
    // reported to GenMC.
    void handle_execution_start();
    // This function must be called at the end of any execution, even if an error was found
    // during the execution.
    // Returns `null`, or a string containing an error message if an error occurred.
    std::unique_ptr<std::string> handle_execution_end();

    /***** Functions for handling events encountered during program execution. *****/

    /**** Memory access handling ****/

    [[nodiscard]] LoadResult handle_atomic_load(
        ThreadId thread_id,
        uint64_t address,
        uint64_t size,
        MemOrdering ord,
        GenmcScalar old_val
    );
    [[nodiscard]] LoadResult
    handle_non_atomic_load(ThreadId thread_id, uint64_t address, uint64_t size);
    [[nodiscard]] ReadModifyWriteResult handle_read_modify_write(
        ThreadId thread_id,
        uint64_t address,
        uint64_t size,
        RMWBinOp rmw_op,
        MemOrdering ordering,
        GenmcScalar rhs_value,
        GenmcScalar old_val
    );
    [[nodiscard]] CompareExchangeResult handle_compare_exchange(
        ThreadId thread_id,
        uint64_t address,
        uint64_t size,
        GenmcScalar expected_value,
        GenmcScalar new_value,
        GenmcScalar old_val,
        MemOrdering success_ordering,
        MemOrdering fail_load_ordering,
        bool can_fail_spuriously
    );
    [[nodiscard]] StoreResult handle_atomic_store(
        ThreadId thread_id,
        uint64_t address,
        uint64_t size,
        GenmcScalar value,
        GenmcScalar old_val,
        MemOrdering ord
    );
    [[nodiscard]] StoreResult
    handle_non_atomic_store(ThreadId thread_id, uint64_t address, uint64_t size);

    void handle_fence(ThreadId thread_id, MemOrdering ord);

    /**** Memory (de)allocation ****/

    auto handle_malloc(ThreadId thread_id, uint64_t size, uint64_t alignment) -> MallocResult;

    /** Returns null on success, or an error string if an error occurs. */
    auto handle_free(ThreadId thread_id, uint64_t address) -> std::unique_ptr<std::string>;

    /**** Thread management ****/
    void handle_thread_create(ThreadId thread_id, ThreadId parent_id);
    void handle_thread_join(ThreadId thread_id, ThreadId child_id);
    void handle_thread_finish(ThreadId thread_id, uint64_t ret_val);
    void handle_thread_kill(ThreadId thread_id);

    /**** Blocking instructions ****/
    /// Inform GenMC that the thread should be blocked.
    void handle_assume_block(ThreadId thread_id, AssumeType assume_type);

    /**** Mutex handling ****/
    auto handle_mutex_lock(ThreadId thread_id, uint64_t address, uint64_t size) -> MutexLockResult;
    auto handle_mutex_try_lock(ThreadId thread_id, uint64_t address, uint64_t size)
        -> MutexLockResult;
    auto handle_mutex_unlock(ThreadId thread_id, uint64_t address, uint64_t size) -> StoreResult;

    /***** Exploration related functionality *****/

    /** Ask the GenMC scheduler for a new thread to schedule and return whether the execution is
     * finished, blocked, or can continue.
     * Updates the next instruction kind for the given thread id. */
    auto schedule_next(const int curr_thread_id, const ActionKind curr_thread_next_instr_kind)
        -> SchedulingResult;

    /**
     * Check whether there are more executions to explore.
     * If there are more executions, this method prepares for the next execution and returns
     * `true`. Returns true if there are no more executions to explore. */
    auto is_exploration_done() -> bool {
        return GenMCDriver::done();
    }

    /**** Result querying functionality. ****/

    // NOTE: We don't want to share the `VerificationResult` type with the Rust side, since it
    // is very large, uses features that CXX.rs doesn't support and may change as GenMC changes.
    // Instead, we only use the result on the C++ side, and only expose these getter function to
    // the Rust side.

    // Note that CXX.rs doesn't support returning a C++ string to Rust by value,
    // it must be behind an indirection like a `unique_ptr` (tested with CXX 1.0.170).

    /// Get the number of blocked executions encountered by GenMC (cast into a fixed with
    /// integer)
    auto get_blocked_execution_count() const -> uint64_t {
        return static_cast<uint64_t>(getResult().exploredBlocked);
    }

    /// Get the number of executions explored by GenMC (cast into a fixed with integer)
    auto get_explored_execution_count() const -> uint64_t {
        return static_cast<uint64_t>(getResult().explored);
    }

    /// Get all messages that GenMC produced (errors, warnings), combined into one string.
    auto get_result_message() const -> std::unique_ptr<std::string> {
        return std::make_unique<std::string>(getResult().message);
    }

    /// If an error occurred, return a string describing the error, otherwise, return `nullptr`.
    auto get_error_string() const -> std::unique_ptr<std::string> {
        const auto& result = GenMCDriver::getResult();
        if (result.status.has_value())
            return format_error(result.status.value());
        return nullptr;
    }

    /**** Printing and estimation mode functionality. ****/

    /// Get the results of a run in estimation mode.
    auto get_estimation_results() const -> EstimationResult;

  private:
    /** Returns the current event for a given thread. */
    inline auto curr_pos(ThreadId tid) -> Event {
        ERROR_ON(tid >= threads_action_.size(), "ThreadId out of bounds");
        return threads_action_[tid].event;
    }
    /** Increment the event index in the given thread by `count`. */
    inline void inc_pos(ThreadId tid, unsigned int count) {
        ERROR_ON(tid >= threads_action_.size(), "ThreadId out of bounds");
        threads_action_[tid].event.index += count;
    }

    /**
     * GenMC uses the term `Action` to refer to a struct of:
     * - `ActionKind`, storing whether the next instruction in a thread may be a load
     * - `Event`, storing the most recent event index added for a thread
     *
     * Here we store the "action" for each thread. In particular we use this to assign event
     * indices, since GenMC expects us to do that.
     */
    std::vector<Action> threads_action_;
};

/// Get the bit mask that GenMC expects for global memory allocations.
/// FIXME(genmc): currently we use `get_global_alloc_static_mask()` to ensure the
/// `SAddr::staticMask` constant is consistent between Miri and GenMC, but if
/// https://github.com/dtolnay/cxx/issues/1051 is fixed we could share the constant
///   directly.
constexpr auto get_global_alloc_static_mask() -> uint64_t {
    return SAddr::staticMask;
}

// CXX.rs generated headers:
// NOTE: this must be included *after* `MiriGenmcShim` and all the other types we use are defined,
// otherwise there will be compilation errors due to missing definitions.
#include "genmc-sys/src/lib.rs.h"

/**** Result handling ****/
// NOTE: these must come after the cxx_bridge generated code, since that contains the actual
// definitions of these types.

namespace GenmcScalarExt {
inline GenmcScalar uninit() {
    return GenmcScalar {
        .value = 0,
        .provenance = 0,
        .is_init = false,
    };
}

inline GenmcScalar from_sval(SVal sval) {
    return GenmcScalar {
        .value = sval.get(),
        .provenance = sval.getProvenance(),
        .is_init = true,
    };
}

inline SVal to_sval(GenmcScalar scalar) {
    ERROR_ON(!scalar.is_init, "Cannot convert an uninitialized `GenmcScalar` into an `SVal`\n");
    return SVal(scalar.value, scalar.provenance);
}

inline std::optional<SVal> try_to_sval(GenmcScalar scalar) {
    if (scalar.is_init)
        return { SVal(scalar.value, scalar.provenance) };
    return std::nullopt;
}
} // namespace GenmcScalarExt

namespace LoadResultExt {
inline LoadResult no_value() {
    return LoadResult {
        .invalid = false,
        .error = std::unique_ptr<std::string>(nullptr),
        .read_value = GenmcScalarExt::uninit(),
    };
}

inline LoadResult from_value(SVal read_value) {
    return LoadResult { .invalid = false,
                        .error = std::unique_ptr<std::string>(nullptr),
                        .read_value = GenmcScalarExt::from_sval(read_value) };
}

inline LoadResult from_error(std::unique_ptr<std::string> error) {
    return LoadResult { .invalid = false,
                        .error = std::move(error),
                        .read_value = GenmcScalarExt::uninit() };
}

inline LoadResult from_invalid() {
    return LoadResult { .invalid = true, .error = nullptr, .read_value = GenmcScalarExt::uninit() };
}

} // namespace LoadResultExt

namespace StoreResultExt {
inline StoreResult ok(bool is_coherence_order_maximal_write) {
    return StoreResult { .invalid = false,
                         .error = std::unique_ptr<std::string>(nullptr),
                         .is_coherence_order_maximal_write = is_coherence_order_maximal_write };
}

inline StoreResult from_error(std::unique_ptr<std::string> error) {
    return StoreResult { .invalid = false,
                         .error = std::move(error),
                         .is_coherence_order_maximal_write = false };
}

inline StoreResult from_invalid() {
    return StoreResult { .invalid = true,
                         .error = nullptr,
                         .is_coherence_order_maximal_write = false };
}
} // namespace StoreResultExt

namespace ReadModifyWriteResultExt {
inline ReadModifyWriteResult
ok(SVal old_value, SVal new_value, bool is_coherence_order_maximal_write) {
    return ReadModifyWriteResult { .invalid = false,
                                   .error = std::unique_ptr<std::string>(nullptr),
                                   .old_value = GenmcScalarExt::from_sval(old_value),
                                   .new_value = GenmcScalarExt::from_sval(new_value),
                                   .is_coherence_order_maximal_write =
                                       is_coherence_order_maximal_write };
}

inline ReadModifyWriteResult from_error(std::unique_ptr<std::string> error) {
    return ReadModifyWriteResult { .invalid = false,
                                   .error = std::move(error),
                                   .old_value = GenmcScalarExt::uninit(),
                                   .new_value = GenmcScalarExt::uninit(),
                                   .is_coherence_order_maximal_write = false };
}

inline ReadModifyWriteResult from_invalid() {
    return ReadModifyWriteResult { .invalid = true,
                                   .error = nullptr,
                                   .old_value = GenmcScalarExt::uninit(),
                                   .new_value = GenmcScalarExt::uninit(),
                                   .is_coherence_order_maximal_write = false };
}
} // namespace ReadModifyWriteResultExt

namespace CompareExchangeResultExt {
inline CompareExchangeResult success(SVal old_value, bool is_coherence_order_maximal_write) {
    return CompareExchangeResult { .invalid = false,
                                   .error = nullptr,
                                   .old_value = GenmcScalarExt::from_sval(old_value),
                                   .is_success = true,
                                   .is_coherence_order_maximal_write =
                                       is_coherence_order_maximal_write };
}

inline CompareExchangeResult failure(SVal old_value) {
    return CompareExchangeResult { .invalid = false,
                                   .error = nullptr,
                                   .old_value = GenmcScalarExt::from_sval(old_value),
                                   .is_success = false,
                                   .is_coherence_order_maximal_write = false };
}

inline CompareExchangeResult from_error(std::unique_ptr<std::string> error) {
    return CompareExchangeResult { .invalid = false,
                                   .error = std::move(error),
                                   .old_value = GenmcScalarExt::uninit(),
                                   .is_success = false,
                                   .is_coherence_order_maximal_write = false };
}

inline CompareExchangeResult from_invalid() {
    return CompareExchangeResult { .invalid = true,
                                   .error = nullptr,
                                   .old_value = GenmcScalarExt::uninit(),
                                   .is_success = false,
                                   .is_coherence_order_maximal_write = false };
}
} // namespace CompareExchangeResultExt

namespace MutexLockResultExt {
inline MutexLockResult ok(bool is_lock_acquired) {
    return MutexLockResult { .invalid = false,
                             .error = nullptr,
                             .is_reset = false,
                             .is_lock_acquired = is_lock_acquired };
}

inline MutexLockResult reset() {
    return MutexLockResult { .invalid = false,
                             .error = nullptr,
                             .is_reset = true,
                             .is_lock_acquired = false };
}

inline MutexLockResult from_error(std::unique_ptr<std::string> error) {
    return MutexLockResult { .invalid = false,
                             .error = std::move(error),
                             .is_reset = false,
                             .is_lock_acquired = false };
}

inline MutexLockResult from_invalid() {
    return MutexLockResult { .invalid = true,
                             .error = nullptr,
                             .is_reset = false,
                             .is_lock_acquired = false };
}
} // namespace MutexLockResultExt

namespace MallocResultExt {
inline MallocResult ok(SVal addr) {
    return MallocResult { .error = nullptr, .address = addr.get() };
}

inline MallocResult from_error(std::unique_ptr<std::string> error) {
    return MallocResult { .error = std::move(error), .address = 0UL };
}
} // namespace MallocResultExt

#endif /* GENMC_MIRI_INTERFACE_HPP */
