// Copyright (c) 2020-now by the Zeek Project. See LICENSE for details.

#pragma once

#include <csetjmp>
#include <exception>
#include <memory>
#include <optional>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>

#include <hilti/rt/any.h>
#include <hilti/rt/autogen/config.h>
#include <hilti/rt/configuration.h>
#include <hilti/rt/exception.h>
#include <hilti/rt/types/reference.h>
#include <hilti/rt/util.h>

struct Fiber;

// Fiber entry point for execution of fiber payload.
extern "C" void __fiber_run_trampoline(void* args);

// Fiber entry point for stack switch trampoline.
extern "C" void __fiber_switch_trampoline(void* args);

namespace hilti::rt {

namespace detail {
class Fiber;
} // namespace detail

namespace resumable {
/** Abstract handle providing access to a currently active function running inside a fiber.  */
using Handle = detail::Fiber;
} // namespace resumable

namespace detail {

/** Helper recording global stack resource usage. */
extern void trackStack();

/** Context-wide state for managing all fibers associated with that context. */
struct FiberContext {
    FiberContext();
    ~FiberContext();

    FiberContext(const FiberContext&) = delete;
    FiberContext(FiberContext&&) = default;

    FiberContext& operator=(const FiberContext&) = delete;
    FiberContext& operator=(FiberContext&&) = default;

    /** (Pseudo-)fiber representing the main function. */
    std::unique_ptr<detail::Fiber> main;

    /** Fiber implementing the switch trampoline. */
    std::unique_ptr<detail::Fiber> switch_trampoline;

    /** Currently executing fiber .*/
    detail::Fiber* current = nullptr;

    /** Fiber holding the shared stack (the fiber itself isn't used, just its stack memory) */
    std::unique_ptr<::Fiber> shared_stack;

    /** Cache of previously used fibers available for reuse. */
    std::vector<std::unique_ptr<Fiber>> cache;
};

/**
 * Helper retaining a fiber's saved stack content.
 */
struct StackBuffer {
    /**
     * Constructor.
     *
     * @param fiber fiber of which to track its current stack region
     */
    StackBuffer(const ::Fiber* fiber) : _fiber(fiber) {}

    /** Destructor. */
    ~StackBuffer();

    /**
     * Returns the lower/upper addresses of the memory region that is currently
     * actively in use by the fiber's stack. This value is only well-defined if
     * the fiber is *not* currently executing.
     **/
    std::pair<char*, char*> activeRegion() const;

    /**
     * Returns the lower/upper addresses of the memory region that is allocated
     * for the fiber's stack.
     **/
    std::pair<char*, char*> allocatedRegion() const;

    /**
     * Returns the size of the memory region that is currently actively in use
     * by the fiber's stack. This value is only well-defined if the fiber is
     * *not* currently executing.
     **/
    size_t activeSize() const;

    /** Returns the size of the memory region that's allocated for the fiber's stack. */
    size_t allocatedSize() const { return static_cast<size_t>(allocatedRegion().second - allocatedRegion().first); }

    /** Returns an approximate size of stack space left for a currently executing fiber. */
    size_t liveRemainingSize() const;

    /** Copies the fiber's stack out into an internally allocated buffer. */
    void save();

    /**
     * Copies previously saved stack content back into its original location.
     * This does nothing if no content has been saved so far.
     **/
    void restore() const;

private:
    const ::Fiber* _fiber;
    void* _buffer = nullptr; // allocated memory holding swapped out stack content
    size_t _buffer_size = 0; // amount currently allocated for `_buffer`
};

// Render stack region for use in debug output.
inline std::ostream& operator<<(std::ostream& out, const StackBuffer& s) {
    out << fmt("%p-%p:%zu", s.activeRegion().first, s.activeRegion().second, s.activeSize());
    return out;
}

/** Helper class to store stackless callbacks to be executed on fibers.*/
class Callback {
public:
    template<typename F>
    Callback(F f)
        : _f(std::move(f)), _invoke([](const hilti::rt::any& f, resumable::Handle* h) -> hilti::rt::any {
              return hilti::rt::any_cast<F>(f)(h);
          }) {}

    Callback(const Callback&) = default;
    Callback(Callback&&) = default;

    Callback& operator=(const Callback&) = default;
    Callback& operator=(Callback&&) = default;

    hilti::rt::any operator()(resumable::Handle* h) const { return _invoke(_f, h); }

private:
    hilti::rt::any _f; //< Type-erased storage for the concrete callback.
    hilti::rt::any (*_invoke)(const hilti::rt::any& f, resumable::Handle* h); //< Invoke type-erased callback.
};

/**
 * A fiber implements a co-routine that can at any time yield control back to
 * its caller, to be resumed later. This is the internal class implementing the
 * main functionality. It's used by `Resumable`, which provides the external
 * interface.
 */
class Fiber {
public:
    /** Type of fiber. */
    enum class Type : int64_t {
        IndividualStack, /**< Fiver using a dedicated local stack (needs more memory, but switching is fast) */
        SharedStack,     /**< Fiber sharing a global stack (needs less memory, but switching costs extra) */

        Main,             /**< Pseudo-fiber for the top-level process; for internally use only */
        SwitchTrampoline, /**< Fiber representing a trampoline for stack switching; for internal use only */
    };

    Fiber(Type type);
    ~Fiber();

    Fiber(const Fiber&) = delete;
    Fiber(Fiber&&) = delete;
    Fiber& operator=(const Fiber&) = delete;
    Fiber& operator=(Fiber&&) = delete;

    void init(Callback f) {
        _result = {};
        _exception = nullptr;
        _function = std::move(f);
    }

    /** Returns the fiber's type. */
    auto type() { return _type; }

    /** Returns the fiber's stack buffer. */
    const auto& stackBuffer() const { return _stack_buffer; }

    void run();
    void yield();
    void resume();
    void abort();

    bool isMain() const { return _type == Type::Main; }

    bool isDone() {
        switch ( _state ) {
            case State::Running:
            case State::Yielded: return false;

            case State::Aborting:
            case State::Finished:
            case State::Idle:
            case State::Init:
                // All these mean we didn't recently run a function that could have
                // produced a result still pending.
                return true;
        }
        cannot_be_reached(); // For you, GCC.
    }

    auto&& result() { return std::move(_result); }
    std::exception_ptr exception() const { return _exception; }

    /** Returns the current source code location if set, or null if not. */
    const char* location() const { return _location; }

    /**
     * Sets the current source code location or unsets it if argument is null.
     * @param l pointer to a statically allocated string that won't go out of scope.
     */
    void setLocation(const char* location = nullptr) { _location = location; }

    std::string tag() const;

    static std::unique_ptr<Fiber> create();
    static void destroy(std::unique_ptr<Fiber> f);
    static void primeCache();
    static void reset();

    struct Statistics {
        uint64_t total;
        uint64_t current;
        uint64_t cached;
        uint64_t max;
        uint64_t max_stack_size;
        uint64_t initialized;
    };

    static Statistics statistics();

private:
    friend void ::__fiber_run_trampoline(void* argsp);
    friend void ::__fiber_switch_trampoline(void* argsp);
    friend void detail::trackStack();

    enum class State { Init, Running, Aborting, Yielded, Idle, Finished };

    void _yield(const char* tag);
    void _activate(const char* tag);

    /** Code to run just before we switch to a fiber. */
    static void _startSwitchFiber(const char* tag, detail::Fiber* to);

    /** Code to run just after we have switched to a fiber. */
    static void _finishSwitchFiber(const char* tag);

    /** Low-level switch from one fiber to another. */
    static void _executeSwitch(const char* tag, detail::Fiber* from, detail::Fiber* to);

    Type _type;
    State _state{State::Init};
    std::optional<Callback> _function;
    std::optional<hilti::rt::any> _result;
    std::exception_ptr _exception;

    /** The underlying 3rdparty implementation of this fiber. */
    std::unique_ptr<::Fiber> _fiber;

    /** The coroutine this fiber will yield to. */
    Fiber* _caller = nullptr;

    /** Buffer for the fiber's stack when swapped out. */
    StackBuffer _stack_buffer;

    /** Current location for user-visible diagnostic messages; null if not set. */
    const char* _location = nullptr;

#ifdef _WIN32
    /** Saved TEB stack boundaries for proper exception handling during fiber switches. */
    struct {
        void* stack_base = nullptr;
        void* stack_limit = nullptr;
        void* deallocation_stack = nullptr;
    } _teb;
#endif

#ifdef HILTI_HAVE_ASAN
    /** Additional tracking state that ASAN needs. */
    struct {
        const void* stack = nullptr;
        size_t stack_size = 0;
        void* fake_stack = nullptr;
    } _asan;
#endif

    // TODO: Usage of these isn't thread-safe. Should become "atomic" and
    // move into global state.
    HILTI_JIT_IMPORT_OR_INLINE static uint64_t _total_fibers;
    HILTI_JIT_IMPORT_OR_INLINE static uint64_t _current_fibers;
    HILTI_JIT_IMPORT_OR_INLINE static uint64_t _cached_fibers;
    HILTI_JIT_IMPORT_OR_INLINE static uint64_t _max_fibers;
    HILTI_JIT_IMPORT_OR_INLINE static uint64_t _max_stack_size;
    HILTI_JIT_IMPORT_OR_INLINE static uint64_t _initialized; // number of trampolines run
};

std::ostream& operator<<(std::ostream& out, const Fiber& fiber);

extern void yield();

} // namespace detail

/**
 * Executor for a function that may yield control back to the caller even
 * before it's finished. The caller can then later resume the function to
 * continue its operation.
 */
class Resumable {
public:
    /**
     * Creates an instance initialized with a function to execute. The
     * function can then be started by calling `run()`.
     *
     * @param f function to be executed
     */
    template<typename Function>
    Resumable(Function f)
        requires(std::is_invocable_v<Function, resumable::Handle*>)
        : _fiber(detail::Fiber::create()) {
        _fiber->init(std::move(f));
    }

    Resumable() = default;
    Resumable(const Resumable& r) = delete;
    Resumable(Resumable&& r) noexcept = default;
    Resumable& operator=(const Resumable& other) = delete;
    Resumable& operator=(Resumable&& other) noexcept = default;

    ~Resumable() {
        if ( _fiber )
            try {
                detail::Fiber::destroy(std::move(_fiber));
            } catch ( ... ) {
                cannot_be_reached();
            }
    }

    /** Starts execution of the function. This must be called only once. */
    void run();

    /** When a function has yielded, resumes its operation. */
    void resume();

    /** When a function has yielded, abort its operation without resuming. */
    void abort();

    /** Returns a handle to the currently running function. */
    resumable::Handle* handle() { return _fiber.get(); }

    /**
     * Returns true if the function has completed orderly and provided a result.
     * If so, `get()` can be used to retrieve the result.
     */
    bool hasResult() const { return _done && _result.has_value(); }

    /**
     * Returns the function's result once it has completed. Must not be
     * called before completion; check with `hasResult()` first.
     */
    template<typename Result>
    const Result& get() const {
        assert(static_cast<bool>(_result));

        if constexpr ( std::is_same_v<Result, void> )
            return {};
        else {
            try {
                return hilti::rt::any_cast<const Result&>(*_result);
            } catch ( const hilti::rt::bad_any_cast& ) {
                throw InvalidArgument("mismatch in result type");
            }
        }
    }

    /** Returns true if the function has completed. **/
    explicit operator bool() const { return _done; }

private:
    void yielded();

    void checkFiber(const char* location) const {
        if ( ! _fiber )
            throw std::logic_error(std::string("fiber not set in ") + location);
    }

    std::unique_ptr<detail::Fiber> _fiber;
    bool _done = false;
    std::optional<hilti::rt::any> _result;
};

namespace resumable::detail {

/** Helper to deep-copy `Resumable` arguments in preparation for moving them to the heap. */
template<typename T>
auto copyArg(T t) {
    // In general, we can't move references to the heap.
    static_assert(! std::is_reference_v<T>, "copyArg() does not accept references other than ValueReference<T>.");
    return t;
}

// Special case: We don't want to (nor need to) deep-copy value references.
// Their payload already resides on the heap, so reuse that.
template<typename T>
ValueReference<T> copyArg(const ValueReference<T>& t) {
    return ValueReference<T>(t.asSharedPtr());
}

// Special case: We don't want to (nor need to) deep-copy value references.
// Their payload already resides on the heap, so reuse that.
template<typename T>
ValueReference<T> copyArg(ValueReference<T>& t) {
    return ValueReference<T>(t.asSharedPtr());
}

} // namespace resumable::detail

namespace fiber {

/**
 * Executes a resumable function. This is a utility wrapper around
 * `Resumable` that immediately starts the function.
 */
template<typename Function>
auto execute(Function f) {
    Resumable r(std::move(f));
    r.run();
    return r;
}

} // namespace fiber
} // namespace hilti::rt
